From 4c09570d510efa76c7d49cf316de3957ac1e8398 Mon Sep 17 00:00:00 2001 From: james-prysm Date: Wed, 29 Jul 2026 13:41:37 -0500 Subject: [PATCH 01/26] initial commit implementing https://github.com/ethereum/keymanager-APIs/pull/88 --- ...mes-prysm_keymanager-builders-endpoints.md | 17 + cmd/prysmctl/validator/proposer_settings.go | 3 +- config/proposer/BUILD.bazel | 7 +- .../proposer/effective_builder_config_test.go | 104 +++++ config/proposer/loader/BUILD.bazel | 1 + config/proposer/loader/loader.go | 117 +++-- config/proposer/loader/loader_test.go | 134 +++++- .../testdata/good-v2-proposer-config.json | 25 + config/proposer/settings.go | 268 +++++++++-- config/proposer/settings_test.go | 119 ++++- .../validator-client/keymanager.pb.go | 440 +++++++++++++----- .../validator-client/keymanager.proto | 44 +- validator/client/BUILD.bazel | 1 + validator/client/propose.go | 17 +- validator/client/runner_test.go | 12 +- validator/client/validator.go | 180 +++++-- validator/client/validator_test.go | 46 +- validator/db/BUILD.bazel | 1 + validator/db/convert_test.go | 6 +- validator/db/filesystem/BUILD.bazel | 2 + validator/db/filesystem/db.go | 17 - .../db/filesystem/proposer_settings_test.go | 38 ++ validator/db/kv/BUILD.bazel | 1 + validator/db/kv/proposer_settings_test.go | 5 +- validator/rpc/BUILD.bazel | 3 + validator/rpc/handlers_keymanager.go | 62 +-- validator/rpc/handlers_keymanager_test.go | 13 +- validator/rpc/handlers_validator_config.go | 325 +++++++++++++ .../rpc/handlers_validator_config_test.go | 211 +++++++++ validator/rpc/server.go | 5 + validator/rpc/structs.go | 23 + 31 files changed, 1868 insertions(+), 379 deletions(-) create mode 100644 changelog/james-prysm_keymanager-builders-endpoints.md create mode 100644 config/proposer/effective_builder_config_test.go create mode 100644 config/proposer/loader/testdata/good-v2-proposer-config.json create mode 100644 validator/rpc/handlers_validator_config.go create mode 100644 validator/rpc/handlers_validator_config_test.go diff --git a/changelog/james-prysm_keymanager-builders-endpoints.md b/changelog/james-prysm_keymanager-builders-endpoints.md new file mode 100644 index 000000000000..a839bed706f7 --- /dev/null +++ b/changelog/james-prysm_keymanager-builders-endpoints.md @@ -0,0 +1,17 @@ +### Added + +- Add `GET`/`POST`/`DELETE /eth/v1/validator/{pubkey}/builders` keymanager endpoints for managing a key's per-builder configuration (keymanager-APIs #88). + +### Changed + +- Resolve per-key builder settings with field-level inheritance from `default_config` on v2 proposer settings; registration uses the default fee recipient when a key has none, and `--enable-builder` only applies when `enabled` is unset. +- Merge file- or URL-loaded proposer settings per key: the file only overrides keys it names. +- Setting a fee recipient, gas limit, or graffiti for a new key no longer copies `default_config`'s builder settings onto that key; the key inherits them and tracks later changes. + +### Fixed + +- Decode unset builder settings fields identically across both validator DB backends. + +### Removed + +- Remove the unused `relays` field from the proposer settings builder config; a settings file that still contains it logs a warning and the field is ignored. diff --git a/cmd/prysmctl/validator/proposer_settings.go b/cmd/prysmctl/validator/proposer_settings.go index 0a97481eb9a5..8498f9f8c847 100644 --- a/cmd/prysmctl/validator/proposer_settings.go +++ b/cmd/prysmctl/validator/proposer_settings.go @@ -69,8 +69,9 @@ func getProposerSettings(c *cli.Context, r io.Reader) error { log.Infof("The default fee recipient is set to %s", defaultFeeRecipient) var builderSettings *validatorpb.BuilderConfig if c.Bool(WithBuilderFlag.Name) { + enabled := true builderSettings = &validatorpb.BuilderConfig{ - Enabled: true, + Enabled: &enabled, GasLimit: validatorType.Uint64(params.BeaconConfig().DefaultBuilderGasLimit), } } else { diff --git a/config/proposer/BUILD.bazel b/config/proposer/BUILD.bazel index 007baab9d477..7fe6d79d0076 100644 --- a/config/proposer/BUILD.bazel +++ b/config/proposer/BUILD.bazel @@ -24,17 +24,22 @@ go_library( go_test( name = "go_default_test", - srcs = ["settings_test.go"], + srcs = [ + "effective_builder_config_test.go", + "settings_test.go", + ], embed = [":go_default_library"], deps = [ "//config/fieldparams:go_default_library", "//config/params:go_default_library", "//consensus-types/validator:go_default_library", "//encoding/bytesutil:go_default_library", + "//proto/prysm/v1alpha1/validator-client:go_default_library", "//testing/assert:go_default_library", "//testing/require:go_default_library", "@com_github_ethereum_go_ethereum//common:go_default_library", "@com_github_ethereum_go_ethereum//common/hexutil:go_default_library", "@com_github_sirupsen_logrus//hooks/test:go_default_library", + "@org_golang_google_protobuf//proto:go_default_library", ], ) diff --git a/config/proposer/effective_builder_config_test.go b/config/proposer/effective_builder_config_test.go new file mode 100644 index 000000000000..c8441732609b --- /dev/null +++ b/config/proposer/effective_builder_config_test.go @@ -0,0 +1,104 @@ +package proposer + +import ( + "testing" + + "github.com/OffchainLabs/prysm/v7/consensus-types/validator" + "github.com/OffchainLabs/prysm/v7/testing/require" + "google.golang.org/protobuf/proto" +) + +// These cases mirror the test vectors proposed upstream on keymanager-APIs #87 +// for builder-config inheritance granularity. +func TestEffectiveBuilderConfig(t *testing.T) { + entryA := &BuilderEntry{URL: "https://a"} + entryB := &BuilderEntry{URL: "https://b"} + entryC := &BuilderEntry{URL: "https://c"} + + t.Run("nil per-key returns default", func(t *testing.T) { + def := &BuilderConfig{Enabled: true} + require.Equal(t, def, EffectiveBuilderConfig(nil, def)) + }) + t.Run("nil default returns per-key", func(t *testing.T) { + perKey := &BuilderConfig{Enabled: true} + require.Equal(t, perKey, EffectiveBuilderConfig(perKey, nil)) + }) + t.Run("min_bid inherits when per-key omits it", func(t *testing.T) { + def := &BuilderConfig{Enabled: true, Builders: []*BuilderEntry{entryA, entryB}, MinBid: uint64ValPtr(5000000)} + perKey := &BuilderConfig{Enabled: true, Builders: []*BuilderEntry{entryC}} + eff := EffectiveBuilderConfig(perKey, def) + require.NotNil(t, eff.MinBid) + require.Equal(t, validator.Uint64(5000000), *eff.MinBid) + require.Equal(t, 1, len(eff.Builders)) + require.Equal(t, "https://c", eff.Builders[0].URL) + }) + t.Run("explicit zero max payment is preserved, not inherited over", func(t *testing.T) { + def := &BuilderConfig{MaxExecutionPayment: uint64ValPtr(1000000000)} + perKey := &BuilderConfig{Enabled: true, MaxExecutionPayment: uint64ValPtr(0)} + eff := EffectiveBuilderConfig(perKey, def) + require.NotNil(t, eff.MaxExecutionPayment) + require.Equal(t, validator.Uint64(0), *eff.MaxExecutionPayment) + }) + t.Run("unset max payment inherits default", func(t *testing.T) { + def := &BuilderConfig{MaxExecutionPayment: uint64ValPtr(1000000000)} + perKey := &BuilderConfig{Enabled: true} + eff := EffectiveBuilderConfig(perKey, def) + require.NotNil(t, eff.MaxExecutionPayment) + require.Equal(t, validator.Uint64(1000000000), *eff.MaxExecutionPayment) + }) + t.Run("explicit per-key disable wins over enabled default", func(t *testing.T) { + def := &BuilderConfig{Enabled: true} + perKey := &BuilderConfig{Enabled: false, MinBid: uint64ValPtr(1)} + require.Equal(t, false, EffectiveBuilderConfig(perKey, def).IsEnabled()) + }) + t.Run("present per-key builder config is authoritative on enabled", func(t *testing.T) { + // A per-key config with enabled false does not inherit an enabled default; + // whole-config inheritance happens only when the per-key builder config is nil. + def := &BuilderConfig{Enabled: true} + perKey := &BuilderConfig{MinBid: uint64ValPtr(1)} + require.Equal(t, false, EffectiveBuilderConfig(perKey, def).IsEnabled()) + require.Equal(t, true, EffectiveBuilderConfig(nil, def).IsEnabled()) + }) + t.Run("present builders list replaces, never unions", func(t *testing.T) { + def := &BuilderConfig{Builders: []*BuilderEntry{entryA, entryB}} + perKey := &BuilderConfig{Builders: []*BuilderEntry{entryC}} + eff := EffectiveBuilderConfig(perKey, def) + require.Equal(t, 1, len(eff.Builders)) + require.Equal(t, "https://c", eff.Builders[0].URL) + }) + t.Run("absent builders list inherits default list", func(t *testing.T) { + def := &BuilderConfig{Builders: []*BuilderEntry{entryA, entryB}} + perKey := &BuilderConfig{Enabled: true} + require.Equal(t, 2, len(EffectiveBuilderConfig(perKey, def).Builders)) + }) + t.Run("zero gas limit inherits default gas limit", func(t *testing.T) { + def := &BuilderConfig{GasLimit: validator.Uint64(30000000)} + perKey := &BuilderConfig{Enabled: true} + require.Equal(t, validator.Uint64(30000000), EffectiveBuilderConfig(perKey, def).GasLimit) + }) + t.Run("proxy and boost inherit per field", func(t *testing.T) { + def := &BuilderConfig{Proxy: proto.String("http://side-car:9001"), BuilderBoostFactor: uint64ValPtr(90)} + perKey := &BuilderConfig{Enabled: true, BuilderBoostFactor: uint64ValPtr(120)} + eff := EffectiveBuilderConfig(perKey, def) + require.Equal(t, "http://side-car:9001", *eff.Proxy) + require.Equal(t, validator.Uint64(120), *eff.BuilderBoostFactor) + }) + t.Run("both-set min_bid: per-key wins", func(t *testing.T) { + def := &BuilderConfig{MinBid: uint64ValPtr(5000000)} + perKey := &BuilderConfig{MinBid: uint64ValPtr(7000000)} + require.Equal(t, validator.Uint64(7000000), *EffectiveBuilderConfig(perKey, def).MinBid) + }) + t.Run("both-set proxy: per-key wins", func(t *testing.T) { + def := &BuilderConfig{Proxy: proto.String("http://default:1")} + perKey := &BuilderConfig{Proxy: proto.String("http://mine:2")} + require.Equal(t, "http://mine:2", *EffectiveBuilderConfig(perKey, def).Proxy) + }) + t.Run("nonzero per-key gas limit wins over default", func(t *testing.T) { + def := &BuilderConfig{GasLimit: validator.Uint64(30000000)} + perKey := &BuilderConfig{GasLimit: validator.Uint64(45000000)} + require.Equal(t, validator.Uint64(45000000), EffectiveBuilderConfig(perKey, def).GasLimit) + }) + t.Run("nil nil is nil", func(t *testing.T) { + require.Equal(t, (*BuilderConfig)(nil), EffectiveBuilderConfig(nil, nil)) + }) +} diff --git a/config/proposer/loader/BUILD.bazel b/config/proposer/loader/BUILD.bazel index 401a0fbcb33c..53f04c5029e9 100644 --- a/config/proposer/loader/BUILD.bazel +++ b/config/proposer/loader/BUILD.bazel @@ -22,6 +22,7 @@ go_test( "@com_github_ethereum_go_ethereum//common/hexutil:go_default_library", "@com_github_sirupsen_logrus//hooks/test:go_default_library", "@com_github_urfave_cli_v2//:go_default_library", + "@org_golang_google_protobuf//proto:go_default_library", ], ) diff --git a/config/proposer/loader/loader.go b/config/proposer/loader/loader.go index 27ee4ef7a271..820ee63152ae 100644 --- a/config/proposer/loader/loader.go +++ b/config/proposer/loader/loader.go @@ -1,8 +1,11 @@ package loader import ( + "bytes" "encoding/json" "fmt" + "os" + "path/filepath" "strconv" "github.com/OffchainLabs/prysm/v7/cmd/validator/flags" @@ -186,18 +189,6 @@ func (psl *SettingsLoader) Load(cliCtx *cli.Context) (*proposer.Settings, error) return ps, nil } -func hasBuilderShape(p *validatorpb.ProposerSettingsPayload) bool { - if p.DefaultConfig != nil && p.DefaultConfig.Builder != nil { - return true - } - for _, o := range p.ProposerConfig { - if o != nil && o.Builder != nil { - return true - } - } - return false -} - func (psl *SettingsLoader) applyOverrides() { if psl.options.builderConfig != nil && psl.options.gasLimit != nil { psl.options.builderConfig.GasLimit = *psl.options.gasLimit @@ -231,6 +222,9 @@ func (psl *SettingsLoader) loadFromFile(cliCtx *cli.Context, dbSettings *validat if settingFromFile == nil { return nil, errors.Errorf("proposer settings is empty after unmarshalling from file specified by %s flag", flags.ProposerSettingsFlag.Name) } + if raw, err := os.ReadFile(filepath.Clean(cliCtx.String(flags.ProposerSettingsFlag.Name))); err == nil && bytes.Contains(raw, []byte("relays")) { + log.Warn("The relays field in proposer settings is no longer supported and was ignored; configure builders instead") + } log.WithField(flags.ProposerSettingsFlag.Name, cliCtx.String(flags.ProposerSettingsFlag.Name)).Info("Proposer settings loaded from file") return psl.processProposerSettings(settingFromFile, dbSettings), nil } @@ -268,13 +262,22 @@ func (psl *SettingsLoader) processProposerSettings(loadedSettings, dbSettings *v // through Builder; v2 lives on Option directly. func mergeProposerSettings(loaded, db *validatorpb.ProposerSettingsPayload, options *flagOptions) *validatorpb.ProposerSettingsPayload { merged := &validatorpb.ProposerSettingsPayload{} + // The schema version never regresses: the highest version wins and the + // lower-version side's content is promoted before merging. if db != nil { merged.Version = db.Version } - // v1-shaped source content must not inherit DB's v2, else the runtime upgrade is skipped. - if loaded != nil && (loaded.Version != 0 || hasBuilderShape(loaded)) { + if loaded != nil && loaded.Version > merged.Version { merged.Version = loaded.Version } + if merged.Version == proposer.SchemaV2 { + if db != nil && db.Version < proposer.SchemaV2 { + promotePayloadToV2(db) + } + if loaded != nil && loaded.Version < proposer.SchemaV2 { + promotePayloadToV2(loaded) + } + } var builderConfig *validatorpb.BuilderConfig var gasLimitOnly *validator.Uint64 @@ -286,11 +289,56 @@ func mergeProposerSettings(loaded, db *validatorpb.ProposerSettingsPayload, opti } if merged.Version == proposer.SchemaV2 { - return mergeProposerSettingsV2(merged, loaded, db, gasLimitOnly) + return mergeProposerSettingsV2(merged, loaded, db, builderConfig, gasLimitOnly) } return mergeProposerSettingsV1(merged, loaded, db, builderConfig, gasLimitOnly) } +// promotePayloadToV2 mirrors Settings.UpgradeToV2 plus legacy presence +// normalization, so v1 content merged into a v2 result reads correctly. +func promotePayloadToV2(p *validatorpb.ProposerSettingsPayload) { + promote := func(opt *validatorpb.ProposerOptionPayload) { + if opt == nil || opt.Builder == nil { + return + } + if opt.GasLimit == 0 { + opt.GasLimit = opt.Builder.GasLimit + } + if opt.Builder.Enabled == nil { + disabled := false + opt.Builder.Enabled = &disabled + } + if opt.Builder.MaxExecutionPayment != nil && *opt.Builder.MaxExecutionPayment == 0 { + opt.Builder.MaxExecutionPayment = nil + } + } + promote(p.DefaultConfig) + for _, opt := range p.ProposerConfig { + promote(opt) + } +} + +// overlayProposerConfig merges per-key options: the loaded source wins only for +// keys it names; DB-resident keys it does not name are retained. +func overlayProposerConfig(db, loaded *validatorpb.ProposerSettingsPayload) map[string]*validatorpb.ProposerOptionPayload { + var out map[string]*validatorpb.ProposerOptionPayload + if db != nil && len(db.ProposerConfig) > 0 { + out = make(map[string]*validatorpb.ProposerOptionPayload, len(db.ProposerConfig)) + for k, v := range db.ProposerConfig { + out[k] = v + } + } + if loaded != nil && len(loaded.ProposerConfig) > 0 { + if out == nil { + out = make(map[string]*validatorpb.ProposerOptionPayload, len(loaded.ProposerConfig)) + } + for k, v := range loaded.ProposerConfig { + out[k] = v + } + } + return out +} + func mergeProposerSettingsV1(merged, loaded, db *validatorpb.ProposerSettingsPayload, builderConfig *validatorpb.BuilderConfig, gasLimitOnly *validator.Uint64) *validatorpb.ProposerSettingsPayload { stripDBBuilder := builderConfig == nil @@ -304,17 +352,12 @@ func mergeProposerSettingsV1(merged, loaded, db *validatorpb.ProposerSettingsPay merged.DefaultConfig = loaded.DefaultConfig } - if db != nil && len(db.ProposerConfig) > 0 { - merged.ProposerConfig = db.ProposerConfig - if stripDBBuilder { - for _, option := range db.ProposerConfig { - option.Builder = nil - } + if db != nil && stripDBBuilder { + for _, option := range db.ProposerConfig { + option.Builder = nil } } - if loaded != nil && len(loaded.ProposerConfig) > 0 { - merged.ProposerConfig = loaded.ProposerConfig - } + merged.ProposerConfig = overlayProposerConfig(db, loaded) if merged.DefaultConfig != nil { merged.DefaultConfig.Builder = processBuilderConfig(merged.DefaultConfig.Builder, builderConfig, gasLimitOnly) @@ -331,25 +374,35 @@ func mergeProposerSettingsV1(merged, loaded, db *validatorpb.ProposerSettingsPay merged.DefaultConfig = &validatorpb.ProposerOptionPayload{Builder: builderConfig} case gasLimitOnly != nil: merged.DefaultConfig = &validatorpb.ProposerOptionPayload{ - Builder: &validatorpb.BuilderConfig{Enabled: false, GasLimit: *gasLimitOnly}, + Builder: &validatorpb.BuilderConfig{GasLimit: *gasLimitOnly}, } } } return merged } -func mergeProposerSettingsV2(merged, loaded, db *validatorpb.ProposerSettingsPayload, gasLimitOnly *validator.Uint64) *validatorpb.ProposerSettingsPayload { +func mergeProposerSettingsV2(merged, loaded, db *validatorpb.ProposerSettingsPayload, builderConfig *validatorpb.BuilderConfig, gasLimitOnly *validator.Uint64) *validatorpb.ProposerSettingsPayload { if db != nil && db.DefaultConfig != nil { merged.DefaultConfig = db.DefaultConfig } if loaded != nil && loaded.DefaultConfig != nil { merged.DefaultConfig = loaded.DefaultConfig } - if db != nil && len(db.ProposerConfig) > 0 { - merged.ProposerConfig = db.ProposerConfig - } - if loaded != nil && len(loaded.ProposerConfig) > 0 { - merged.ProposerConfig = loaded.ProposerConfig + merged.ProposerConfig = overlayProposerConfig(db, loaded) + + // --enable-builder fills in only the default builder toggle when unset; an + // explicit enabled and every per-key entry are left for field-level inheritance. + if builderConfig != nil { + if merged.DefaultConfig == nil { + merged.DefaultConfig = &validatorpb.ProposerOptionPayload{} + } + if merged.DefaultConfig.Builder == nil { + merged.DefaultConfig.Builder = &validatorpb.BuilderConfig{} + } + if merged.DefaultConfig.Builder.Enabled == nil { + enabled := true + merged.DefaultConfig.Builder.Enabled = &enabled + } } if gasLimitOnly == nil { @@ -383,7 +436,7 @@ func processBuilderConfig(current *validatorpb.BuilderConfig, override *validato return override } if gasLimitOnly != nil { - return &validatorpb.BuilderConfig{Enabled: false, GasLimit: *gasLimitOnly} + return &validatorpb.BuilderConfig{GasLimit: *gasLimitOnly} } return nil } diff --git a/config/proposer/loader/loader_test.go b/config/proposer/loader/loader_test.go index 3875358425bb..419b572a9081 100644 --- a/config/proposer/loader/loader_test.go +++ b/config/proposer/loader/loader_test.go @@ -8,6 +8,12 @@ import ( "os" "testing" + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/common/hexutil" + logtest "github.com/sirupsen/logrus/hooks/test" + "github.com/urfave/cli/v2" + "google.golang.org/protobuf/proto" + "github.com/OffchainLabs/prysm/v7/cmd/validator/flags" fieldparams "github.com/OffchainLabs/prysm/v7/config/fieldparams" "github.com/OffchainLabs/prysm/v7/config/params" @@ -19,10 +25,6 @@ import ( "github.com/OffchainLabs/prysm/v7/testing/require" "github.com/OffchainLabs/prysm/v7/validator/db/iface" dbTest "github.com/OffchainLabs/prysm/v7/validator/db/testing" - "github.com/ethereum/go-ethereum/common" - "github.com/ethereum/go-ethereum/common/hexutil" - logtest "github.com/sirupsen/logrus/hooks/test" - "github.com/urfave/cli/v2" ) func TestProposerSettingsLoader(t *testing.T) { @@ -404,6 +406,46 @@ func TestProposerSettingsLoader(t *testing.T) { }, wantErr: "", }, + { + name: "v2 file with builders list loads at v2 and dedups duplicate builder urls", + args: args{ + proposerSettingsFlagValues: &proposerSettingsFlag{ + dir: "./testdata/good-v2-proposer-config.json", + }, + }, + want: func() *proposer.Settings { + key1, err := hexutil.Decode("0xa057816155ad77931185101128655c0191bd0214c201ca48ed887f6c4c6adf334070efcd75140eada5ac83a92506dd7a") + require.NoError(t, err) + u64 := func(v uint64) *validator.Uint64 { u := validator.Uint64(v); return &u } + return &proposer.Settings{ + Version: proposer.SchemaV2, + ProposeConfig: map[[fieldparams.BLSPubkeyLength]byte]*proposer.Option{ + bytesutil.ToBytes48(key1): { + FeeRecipientConfig: &proposer.FeeRecipientConfig{ + FeeRecipient: common.HexToAddress("0x50155530FCE8a85ec7055A5F8b2bE214B3DaeFd3"), + }, + GasLimit: 40000000, + BuilderConfig: &proposer.BuilderConfig{ + Enabled: true, + MinBid: u64(500000000), + Builders: []*proposer.BuilderEntry{ + {URL: "https://builder-a.example", MaxExecutionPayment: u64(1000000000)}, + {URL: "https://builder-b.example"}, + }, + }, + }, + }, + DefaultConfig: &proposer.Option{ + FeeRecipientConfig: &proposer.FeeRecipientConfig{ + FeeRecipient: common.HexToAddress("0x6e35733c5af9B61374A128e6F85f553aF09ff89A"), + }, + GasLimit: 30000000, + BuilderConfig: &proposer.BuilderConfig{Enabled: false}, + }, + } + }, + wantErr: "", + }, { name: "Happy Path Suggested Fee ", args: args{ @@ -995,7 +1037,6 @@ func Test_ProposerSettingsLoaderWithOnlyBuilder_DoesNotSaveInDB(t *testing.T) { BuilderConfig: &proposer.BuilderConfig{ Enabled: true, GasLimit: validator.Uint64(params.BeaconConfig().DefaultBuilderGasLimit), - Relays: nil, }, }, } @@ -1027,7 +1068,7 @@ func Test_ProposerSettingsLoader_GasLimitWithoutBuilder(t *testing.T) { require.NotNil(t, got) require.NotNil(t, got.DefaultConfig) require.NotNil(t, got.DefaultConfig.BuilderConfig) - require.Equal(t, false, got.DefaultConfig.BuilderConfig.Enabled) + require.Equal(t, false, got.DefaultConfig.BuilderConfig.IsEnabled()) require.Equal(t, validator.Uint64(12345678), got.DefaultConfig.BuilderConfig.GasLimit) }) } @@ -1180,23 +1221,46 @@ func Test_mergeProposerSettings_VersionPrecedence(t *testing.T) { ) require.Equal(t, uint32(proposer.SchemaV2), merged.Version) }) - t.Run("unversioned v1 content does not inherit v2 from db", func(t *testing.T) { + t.Run("v1 content merged into a v2 db is promoted, version never regresses", func(t *testing.T) { merged := mergeProposerSettings( &validatorpb.ProposerSettingsPayload{ DefaultConfig: &validatorpb.ProposerOptionPayload{ - Builder: &validatorpb.BuilderConfig{Enabled: true, GasLimit: 30000000}, + Builder: &validatorpb.BuilderConfig{Enabled: proto.Bool(true), GasLimit: 30000000}, }, }, &validatorpb.ProposerSettingsPayload{Version: proposer.SchemaV2}, &flagOptions{}, ) - require.Equal(t, uint32(0), merged.Version) + require.Equal(t, uint32(proposer.SchemaV2), merged.Version) require.NotNil(t, merged.DefaultConfig.Builder) + // The builder gas limit is promoted so v2 reads see it at the top level. + require.Equal(t, validator.Uint64(30000000), merged.DefaultConfig.GasLimit) + }) + t.Run("file wins only for keys it names", func(t *testing.T) { + dbPayload := &validatorpb.ProposerSettingsPayload{ + Version: proposer.SchemaV2, + ProposerConfig: map[string]*validatorpb.ProposerOptionPayload{ + "0xaa": {FeeRecipient: "0x1111111111111111111111111111111111111111"}, + "0xbb": {FeeRecipient: "0x2222222222222222222222222222222222222222", GasLimit: 45000000}, + }, + } + filePayload := &validatorpb.ProposerSettingsPayload{ + Version: proposer.SchemaV2, + ProposerConfig: map[string]*validatorpb.ProposerOptionPayload{ + "0xaa": {FeeRecipient: "0x3333333333333333333333333333333333333333"}, + }, + } + merged := mergeProposerSettings(filePayload, dbPayload, &flagOptions{}) + require.Equal(t, 2, len(merged.ProposerConfig)) + require.Equal(t, "0x3333333333333333333333333333333333333333", merged.ProposerConfig["0xaa"].FeeRecipient) + // The key the file does not name keeps its DB-resident settings. + require.Equal(t, "0x2222222222222222222222222222222222222222", merged.ProposerConfig["0xbb"].FeeRecipient) + require.Equal(t, validator.Uint64(45000000), merged.ProposerConfig["0xbb"].GasLimit) }) } -// Restarting with the same v1 file after migration persisted v2 to the DB must -// reload in v1 form so the runtime upgrade re-applies the file's gas limits. +// Restarting with the same v1 file after migration persisted v2 to the DB keeps +// the v2 version and promotes the file's content so its gas limits stay readable. func TestSettingsLoader_V1FileAfterMigratedDB(t *testing.T) { params.SetupTestConfigCleanup(t) cfg := params.BeaconConfig().Copy() @@ -1223,12 +1287,13 @@ func TestSettingsLoader_V1FileAfterMigratedDB(t *testing.T) { require.NoError(t, err) require.NotNil(t, got) - require.Equal(t, uint32(0), got.Version) + require.Equal(t, proposer.SchemaV2, got.Version) require.NotNil(t, got.DefaultConfig.BuilderConfig) require.Equal(t, validator.Uint64(40000000), got.DefaultConfig.BuilderConfig.GasLimit) - assert.LogsContain(t, hook, "deprecated v1 schema") + assert.LogsDoNotContain(t, hook, "deprecated v1 schema") - require.Equal(t, true, got.UpgradeToV2()) + // Already v2: the file's builder gas limits were promoted at merge time. + require.Equal(t, false, got.UpgradeToV2()) key1, err := hexutil.Decode("0xa057816155ad77931185101128655c0191bd0214c201ca48ed887f6c4c6adf334070efcd75140eada5ac83a92506dd7a") require.NoError(t, err) require.Equal(t, validator.Uint64(60000000), got.GasLimit(bytesutil.ToBytes48(key1))) @@ -1243,7 +1308,7 @@ func Test_mergeProposerSettings_CreatesDefaultFromGasLimitFlag(t *testing.T) { ) require.NotNil(t, merged.DefaultConfig) require.NotNil(t, merged.DefaultConfig.Builder) - require.Equal(t, false, merged.DefaultConfig.Builder.Enabled) + require.Equal(t, false, merged.DefaultConfig.Builder.GetEnabled()) require.Equal(t, gl, merged.DefaultConfig.Builder.GasLimit) } @@ -1261,7 +1326,7 @@ func Test_mergeProposerSettings_V2GasLimitOnlyGoesToOption(t *testing.T) { func Test_mergeProposerSettings_VersionGatesBuilderReset(t *testing.T) { v1Builder := func() *validatorpb.BuilderConfig { - return &validatorpb.BuilderConfig{Enabled: true, GasLimit: 40000000, Relays: []string{"r"}} + return &validatorpb.BuilderConfig{Enabled: proto.Bool(true), GasLimit: 40000000} } t.Run("v1 db without enable-builder drops DB builder", func(t *testing.T) { db := &validatorpb.ProposerSettingsPayload{ @@ -1280,6 +1345,43 @@ func Test_mergeProposerSettings_VersionGatesBuilderReset(t *testing.T) { require.NotNil(t, merged.DefaultConfig.Builder) require.Equal(t, validator.Uint64(40000000), merged.DefaultConfig.Builder.GasLimit) }) + t.Run("v2 --enable-builder fills the default toggle only when unset", func(t *testing.T) { + opts := &flagOptions{builderConfig: &proposer.BuilderConfig{Enabled: true}} + + // No default builder: the flag creates it enabled. + db := &validatorpb.ProposerSettingsPayload{ + Version: proposer.SchemaV2, + DefaultConfig: &validatorpb.ProposerOptionPayload{FeeRecipient: "0x"}, + } + merged := mergeProposerSettings(nil, db, opts) + require.NotNil(t, merged.DefaultConfig.Builder) + require.NotNil(t, merged.DefaultConfig.Builder.Enabled) + require.Equal(t, true, *merged.DefaultConfig.Builder.Enabled) + + // An explicit default enabled=false is NOT overridden by the flag. + disabled := false + db2 := &validatorpb.ProposerSettingsPayload{ + Version: proposer.SchemaV2, + DefaultConfig: &validatorpb.ProposerOptionPayload{ + FeeRecipient: "0x", + Builder: &validatorpb.BuilderConfig{Enabled: &disabled}, + }, + } + merged2 := mergeProposerSettings(nil, db2, opts) + require.NotNil(t, merged2.DefaultConfig.Builder.Enabled) + require.Equal(t, false, *merged2.DefaultConfig.Builder.Enabled) + + // Per-key entries are untouched by the flag. + db3 := &validatorpb.ProposerSettingsPayload{ + Version: proposer.SchemaV2, + DefaultConfig: &validatorpb.ProposerOptionPayload{FeeRecipient: "0x"}, + ProposerConfig: map[string]*validatorpb.ProposerOptionPayload{ + "0xkey": {FeeRecipient: "0xk"}, + }, + } + merged3 := mergeProposerSettings(nil, db3, opts) + require.IsNil(t, merged3.ProposerConfig["0xkey"].Builder) + }) } func Test_mergeProposerSettings_V2LoadedOverridesDB(t *testing.T) { diff --git a/config/proposer/loader/testdata/good-v2-proposer-config.json b/config/proposer/loader/testdata/good-v2-proposer-config.json new file mode 100644 index 000000000000..06f9c133f5d9 --- /dev/null +++ b/config/proposer/loader/testdata/good-v2-proposer-config.json @@ -0,0 +1,25 @@ +{ + "proposer_config": { + "0xa057816155ad77931185101128655c0191bd0214c201ca48ed887f6c4c6adf334070efcd75140eada5ac83a92506dd7a": { + "fee_recipient": "0x50155530FCE8a85ec7055A5F8b2bE214B3DaeFd3", + "gas_limit": "40000000", + "builder": { + "enabled": true, + "min_bid": "500000000", + "builders": [ + { "url": "https://builder-a.example", "max_execution_payment": "1000000000" }, + { "url": "https://builder-b.example" }, + { "url": "https://builder-a.example", "max_execution_payment": "2" } + ] + } + } + }, + "default_config": { + "fee_recipient": "0x6e35733c5af9B61374A128e6F85f553aF09ff89A", + "gas_limit": "30000000", + "builder": { + "enabled": false + } + }, + "version": 2 +} diff --git a/config/proposer/settings.go b/config/proposer/settings.go index 0679a27e8e0e..5800c77201f5 100644 --- a/config/proposer/settings.go +++ b/config/proposer/settings.go @@ -63,9 +63,59 @@ func SettingFromConsensus(ps *validatorpb.ProposerSettingsPayload) (*Settings, e d.GasLimit = ps.DefaultConfig.GasLimit settings.DefaultConfig = d } + settings.normalizeLegacyPresence() + settings.dedupBuilders() return settings, nil } +// Persisted configs may predate duplicate-url rejection; the first entry wins, +// matching inline preference assembly. +func (ps *Settings) dedupBuilders() { + fix := func(opt *Option) { + if opt == nil || opt.BuilderConfig == nil || len(opt.BuilderConfig.Builders) < 2 { + return + } + seen := make(map[string]bool, len(opt.BuilderConfig.Builders)) + kept := opt.BuilderConfig.Builders[:0] + for _, e := range opt.BuilderConfig.Builders { + if e == nil || seen[e.URL] { + continue + } + seen[e.URL] = true + kept = append(kept, e) + } + if len(kept) != len(opt.BuilderConfig.Builders) { + log.Warn("Duplicate builder urls in proposer settings; keeping the first entry for each url") + opt.BuilderConfig.Builders = kept + } + } + fix(ps.DefaultConfig) + for _, opt := range ps.ProposeConfig { + fix(opt) + } +} + +// v1 payloads predate presence tracking: wire-absent enabled meant disabled, and +// the filesystem backend persisted a spurious explicit 0 max execution payment. +func (ps *Settings) normalizeLegacyPresence() { + if ps == nil || ps.isV2() { + return + } + normalize := func(opt *Option) { + if opt == nil || opt.BuilderConfig == nil { + return + } + bc := opt.BuilderConfig + if bc.MaxExecutionPayment != nil && *bc.MaxExecutionPayment == 0 { + bc.MaxExecutionPayment = nil + } + } + normalize(ps.DefaultConfig) + for _, opt := range ps.ProposeConfig { + normalize(opt) + } +} + func verifyOption(key string, option *validatorpb.ProposerOptionPayload) error { if option == nil { return fmt.Errorf("fee recipient is required for proposer %s", key) @@ -82,10 +132,73 @@ func verifyOption(key string, option *validatorpb.ProposerOptionPayload) error { // BuilderConfig is the struct representation of the JSON config file set in the validator through the CLI. // GasLimit is a number set to help the network decide on the maximum gas in each block. type BuilderConfig struct { - Enabled bool `json:"enabled" yaml:"enabled"` - GasLimit validator.Uint64 `json:"gas_limit,omitempty" yaml:"gas_limit,omitempty"` - Relays []string `json:"relays,omitempty" yaml:"relays,omitempty"` - MaxExecutionPayment validator.Uint64 `json:"max_execution_payment,omitempty" yaml:"max_execution_payment,omitempty"` + Enabled bool `json:"enabled,omitempty" yaml:"enabled,omitempty"` + GasLimit validator.Uint64 `json:"gas_limit,omitempty" yaml:"gas_limit,omitempty"` + MaxExecutionPayment *validator.Uint64 `json:"max_execution_payment,omitempty" yaml:"max_execution_payment,omitempty"` // explicit 0 = trustless-only; unset inherits + Builders []*BuilderEntry `json:"builders,omitempty" yaml:"builders,omitempty"` + Proxy *string `json:"proxy,omitempty" yaml:"proxy,omitempty"` + MinBid *validator.Uint64 `json:"min_bid,omitempty" yaml:"min_bid,omitempty"` + BuilderBoostFactor *validator.Uint64 `json:"builder_boost_factor,omitempty" yaml:"builder_boost_factor,omitempty"` +} + +// BuilderEntry is one builder in a proposer's per-key builder list. Unset fields +// fall back to the enclosing BuilderConfig, then default_config. +type BuilderEntry struct { + URL string `json:"url" yaml:"url"` + Pubkey []byte `json:"pubkey,omitempty" yaml:"pubkey,omitempty"` + AuthData []byte `json:"auth_data,omitempty" yaml:"auth_data,omitempty"` + Proxy *string `json:"proxy,omitempty" yaml:"proxy,omitempty"` + MinBid *validator.Uint64 `json:"min_bid,omitempty" yaml:"min_bid,omitempty"` + MaxExecutionPayment *validator.Uint64 `json:"max_execution_payment,omitempty" yaml:"max_execution_payment,omitempty"` + BuilderBoostFactor *validator.Uint64 `json:"builder_boost_factor,omitempty" yaml:"builder_boost_factor,omitempty"` +} + +// IsEnabled reports whether the builder path is explicitly enabled. A nil config +// or unset enabled field is treated as disabled. +func (bc *BuilderConfig) IsEnabled() bool { + return bc != nil && bc.Enabled +} + +// EffectiveBuilderConfig resolves per-key builder config with field-level +// inheritance; the builders list replaces rather than merges. +func EffectiveBuilderConfig(perKey, def *BuilderConfig) *BuilderConfig { + if perKey == nil { + return def + } + if def == nil { + return perKey + } + eff := &BuilderConfig{ + Enabled: perKey.Enabled, + GasLimit: perKey.GasLimit, + MaxExecutionPayment: coalesceUint64(perKey.MaxExecutionPayment, def.MaxExecutionPayment), + MinBid: coalesceUint64(perKey.MinBid, def.MinBid), + BuilderBoostFactor: coalesceUint64(perKey.BuilderBoostFactor, def.BuilderBoostFactor), + Proxy: coalesceString(perKey.Proxy, def.Proxy), + Builders: perKey.Builders, + } + if eff.GasLimit == 0 { + eff.GasLimit = def.GasLimit + } + // A nil list inherits the default's; a non-nil empty list means "use no builders". + if eff.Builders == nil { + eff.Builders = def.Builders + } + return eff +} + +func coalesceString(a, b *string) *string { + if a != nil { + return a + } + return b +} + +func coalesceUint64(a, b *validator.Uint64) *validator.Uint64 { + if a != nil { + return a + } + return b } // BuilderConfigFromConsensus converts protobuf to a builder config used in in-memory storage @@ -94,16 +207,41 @@ func BuilderConfigFromConsensus(from *validatorpb.BuilderConfig) *BuilderConfig return nil } c := &BuilderConfig{ - Enabled: from.Enabled, + Enabled: from.GetEnabled(), GasLimit: from.GasLimit, + MaxExecutionPayment: cloneUint64(from.MaxExecutionPayment), + Proxy: cloneString(from.Proxy), + MinBid: cloneUint64(from.MinBid), + BuilderBoostFactor: cloneUint64(from.BuilderBoostFactor), + } + if len(from.Builders) > 0 { + c.Builders = make([]*BuilderEntry, 0, len(from.Builders)) + for _, b := range from.Builders { + c.Builders = append(c.Builders, builderEntryFromConsensus(b)) + } + } + return c +} + +func builderEntryFromConsensus(from *validatorpb.BuilderEntry) *BuilderEntry { + if from == nil { + return nil + } + e := &BuilderEntry{ + URL: from.Url, + Proxy: from.Proxy, + MinBid: from.MinBid, MaxExecutionPayment: from.MaxExecutionPayment, + BuilderBoostFactor: from.BuilderBoostFactor, } - if from.Relays != nil { - relays := make([]string, len(from.Relays)) - copy(relays, from.Relays) - c.Relays = relays + // Treat empty as absent so bolt (nil) and filesystem (empty) round-trips agree. + if len(from.Pubkey) != 0 { + e.Pubkey = bytesutil.SafeCopyBytes(from.Pubkey) } - return c + if len(from.AuthData) != 0 { + e.AuthData = bytesutil.SafeCopyBytes(from.AuthData) + } + return e } // Schema versions for proposer settings. SchemaV1Unset is the proto3 zero @@ -241,16 +379,51 @@ func (bc *BuilderConfig) Clone() *BuilderConfig { c := &BuilderConfig{} c.Enabled = bc.Enabled c.GasLimit = bc.GasLimit - c.MaxExecutionPayment = bc.MaxExecutionPayment - var relays []string - if bc.Relays != nil { - relays = make([]string, len(bc.Relays)) - copy(relays, bc.Relays) - c.Relays = relays + c.MaxExecutionPayment = cloneUint64(bc.MaxExecutionPayment) + c.Proxy = cloneString(bc.Proxy) + c.MinBid = cloneUint64(bc.MinBid) + c.BuilderBoostFactor = cloneUint64(bc.BuilderBoostFactor) + if len(bc.Builders) > 0 { + c.Builders = make([]*BuilderEntry, 0, len(bc.Builders)) + for _, b := range bc.Builders { + c.Builders = append(c.Builders, b.Clone()) + } } return c } +// Clone creates a deep copy of a builder entry +func (be *BuilderEntry) Clone() *BuilderEntry { + if be == nil { + return nil + } + return &BuilderEntry{ + URL: be.URL, + Pubkey: bytesutil.SafeCopyBytes(be.Pubkey), + AuthData: bytesutil.SafeCopyBytes(be.AuthData), + Proxy: cloneString(be.Proxy), + MinBid: cloneUint64(be.MinBid), + MaxExecutionPayment: cloneUint64(be.MaxExecutionPayment), + BuilderBoostFactor: cloneUint64(be.BuilderBoostFactor), + } +} + +func cloneString(v *string) *string { + if v == nil { + return nil + } + c := *v + return &c +} + +func cloneUint64(v *validator.Uint64) *validator.Uint64 { + if v == nil { + return nil + } + c := *v + return &c +} + // Clone creates a deep copy of graffiti config func (gc *GraffitiConfig) Clone() *GraffitiConfig { if gc == nil { @@ -265,18 +438,37 @@ func (bc *BuilderConfig) ToConsensus() *validatorpb.BuilderConfig { return nil } c := &validatorpb.BuilderConfig{} - c.Enabled = bc.Enabled - var relays []string - if bc.Relays != nil { - relays = make([]string, len(bc.Relays)) - copy(relays, bc.Relays) - c.Relays = relays - } + enabled := bc.Enabled + c.Enabled = &enabled c.GasLimit = bc.GasLimit - c.MaxExecutionPayment = bc.MaxExecutionPayment + c.MaxExecutionPayment = cloneUint64(bc.MaxExecutionPayment) + c.Proxy = cloneString(bc.Proxy) + c.MinBid = cloneUint64(bc.MinBid) + c.BuilderBoostFactor = cloneUint64(bc.BuilderBoostFactor) + if len(bc.Builders) > 0 { + c.Builders = make([]*validatorpb.BuilderEntry, 0, len(bc.Builders)) + for _, b := range bc.Builders { + c.Builders = append(c.Builders, b.toConsensus()) + } + } return c } +func (be *BuilderEntry) toConsensus() *validatorpb.BuilderEntry { + if be == nil { + return nil + } + return &validatorpb.BuilderEntry{ + Url: be.URL, + Pubkey: bytesutil.SafeCopyBytes(be.Pubkey), + AuthData: bytesutil.SafeCopyBytes(be.AuthData), + Proxy: cloneString(be.Proxy), + MinBid: cloneUint64(be.MinBid), + MaxExecutionPayment: cloneUint64(be.MaxExecutionPayment), + BuilderBoostFactor: cloneUint64(be.BuilderBoostFactor), + } +} + func (ps *Settings) isV2() bool { return ps != nil && ps.Version == SchemaV2 } @@ -299,6 +491,7 @@ func (ps *Settings) UpgradeToV2() bool { if ps == nil || ps.isV2() { return false } + ps.normalizeLegacyPresence() migrate := func(opt *Option) { if opt == nil || opt.BuilderConfig == nil { return @@ -355,24 +548,31 @@ func (ps *Settings) GasLimit(pubkey [fieldparams.BLSPubkeyLength]byte) validator // SetGasLimit writes the per-pubkey gas limit. v1 requires existing settings // with builder enabled. +// UpsertProposeOption returns the per-key proposer option, creating the ProposeConfig +// map and the option if absent. A newly created option has a nil BuilderConfig so it +// inherits default_config rather than snapshotting it. +func (ps *Settings) UpsertProposeOption(pubkey [fieldparams.BLSPubkeyLength]byte) *Option { + if ps.ProposeConfig == nil { + ps.ProposeConfig = make(map[[fieldparams.BLSPubkeyLength]byte]*Option) + } + opt := ps.ProposeConfig[pubkey] + if opt == nil { + opt = &Option{} + ps.ProposeConfig[pubkey] = opt + } + return opt +} + func (ps *Settings) SetGasLimit(pubkey [fieldparams.BLSPubkeyLength]byte, gasLimit validator.Uint64) error { if ps == nil { return errors.New("No proposer settings were found to update") } if ps.isV2() { - if ps.ProposeConfig == nil { - ps.ProposeConfig = make(map[[fieldparams.BLSPubkeyLength]byte]*Option) - } - opt := ps.ProposeConfig[pubkey] - if opt == nil { - opt = &Option{} - ps.ProposeConfig[pubkey] = opt - } - opt.GasLimit = gasLimit + ps.UpsertProposeOption(pubkey).GasLimit = gasLimit return nil } builderEnabled := func(o *Option) bool { - return o != nil && o.BuilderConfig != nil && o.BuilderConfig.Enabled + return o != nil && o.BuilderConfig.IsEnabled() } if ps.ProposeConfig == nil { if !builderEnabled(ps.DefaultConfig) { diff --git a/config/proposer/settings_test.go b/config/proposer/settings_test.go index 0e1d912a25a1..5bdfe51b13d0 100644 --- a/config/proposer/settings_test.go +++ b/config/proposer/settings_test.go @@ -3,17 +3,25 @@ package proposer import ( "testing" + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/common/hexutil" + logtest "github.com/sirupsen/logrus/hooks/test" + "google.golang.org/protobuf/proto" + fieldparams "github.com/OffchainLabs/prysm/v7/config/fieldparams" "github.com/OffchainLabs/prysm/v7/config/params" "github.com/OffchainLabs/prysm/v7/consensus-types/validator" "github.com/OffchainLabs/prysm/v7/encoding/bytesutil" + validatorpb "github.com/OffchainLabs/prysm/v7/proto/prysm/v1alpha1/validator-client" "github.com/OffchainLabs/prysm/v7/testing/assert" "github.com/OffchainLabs/prysm/v7/testing/require" - "github.com/ethereum/go-ethereum/common" - "github.com/ethereum/go-ethereum/common/hexutil" - logtest "github.com/sirupsen/logrus/hooks/test" ) +func uint64ValPtr(v uint64) *validator.Uint64 { + u := validator.Uint64(v) + return &u +} + func Test_Proposer_Setting_Cloning(t *testing.T) { key1hex := "0xa057816155ad77931185101128655c0191bd0214c201ca48ed887f6c4c6adf334070efcd75140eada5ac83a92506dd7a" key1, err := hexutil.Decode(key1hex) @@ -27,8 +35,7 @@ func Test_Proposer_Setting_Cloning(t *testing.T) { BuilderConfig: &BuilderConfig{ Enabled: true, GasLimit: validator.Uint64(40000000), - Relays: []string{"https://example-relay.com"}, - MaxExecutionPayment: validator.Uint64(1000000000), + MaxExecutionPayment: uint64ValPtr(1000000000), }, }, }, @@ -39,8 +46,7 @@ func Test_Proposer_Setting_Cloning(t *testing.T) { BuilderConfig: &BuilderConfig{ Enabled: false, GasLimit: validator.Uint64(params.BeaconConfig().DefaultBuilderGasLimit), - Relays: []string{"https://example-relay.com"}, - MaxExecutionPayment: validator.Uint64(2000000000), + MaxExecutionPayment: uint64ValPtr(2000000000), }, }, } @@ -66,10 +72,9 @@ func Test_Proposer_Setting_Cloning(t *testing.T) { t.Run("Happy Path BuilderConfigFromConsensus", func(t *testing.T) { clone := settings.DefaultConfig.BuilderConfig.Clone() config := BuilderConfigFromConsensus(clone.ToConsensus()) - require.DeepEqual(t, config.Relays, clone.Relays) - require.Equal(t, config.Enabled, clone.Enabled) + require.DeepEqual(t, config.Enabled, clone.Enabled) require.Equal(t, config.GasLimit, clone.GasLimit) - require.Equal(t, config.MaxExecutionPayment, clone.MaxExecutionPayment) + require.DeepEqual(t, config.MaxExecutionPayment, clone.MaxExecutionPayment) }) t.Run("To Payload and SettingFromConsensus", func(t *testing.T) { payload := settings.ToConsensus() @@ -80,7 +85,7 @@ func Test_Proposer_Setting_Cloning(t *testing.T) { require.Equal(t, true, pok) require.Equal(t, option.FeeRecipientConfig.FeeRecipient.Hex(), potion.FeeRecipient) require.Equal(t, settings.DefaultConfig.FeeRecipientConfig.FeeRecipient.Hex(), payload.DefaultConfig.FeeRecipient) - require.Equal(t, settings.DefaultConfig.BuilderConfig.Enabled, payload.DefaultConfig.Builder.Enabled) + require.Equal(t, settings.DefaultConfig.BuilderConfig.Enabled, payload.DefaultConfig.Builder.GetEnabled()) potion.FeeRecipient = fee newSettings, err := SettingFromConsensus(payload) require.NoError(t, err) @@ -116,7 +121,6 @@ func TestProposerSettings_ShouldBeSaved(t *testing.T) { BuilderConfig: &BuilderConfig{ Enabled: true, GasLimit: validator.Uint64(40000000), - Relays: []string{"https://example-relay.com"}, }, }, }, @@ -135,7 +139,6 @@ func TestProposerSettings_ShouldBeSaved(t *testing.T) { BuilderConfig: &BuilderConfig{ Enabled: true, GasLimit: validator.Uint64(40000000), - Relays: []string{"https://example-relay.com"}, }, }, }, @@ -152,7 +155,6 @@ func TestProposerSettings_ShouldBeSaved(t *testing.T) { BuilderConfig: &BuilderConfig{ Enabled: true, GasLimit: validator.Uint64(40000000), - Relays: []string{"https://example-relay.com"}, }, }, }, @@ -163,7 +165,6 @@ func TestProposerSettings_ShouldBeSaved(t *testing.T) { BuilderConfig: &BuilderConfig{ Enabled: true, GasLimit: validator.Uint64(40000000), - Relays: []string{"https://example-relay.com"}, }, }, }, @@ -196,7 +197,6 @@ func TestProposerSettings_ShouldBeSaved(t *testing.T) { BuilderConfig: &BuilderConfig{ Enabled: true, GasLimit: validator.Uint64(40000000), - Relays: []string{"https://example-relay.com"}, }, }, }, @@ -507,7 +507,7 @@ func TestSettings_UpgradeToV2(t *testing.T) { t.Run("v1 default lifts BuilderConfig.GasLimit to top-level and retains builder relays", func(t *testing.T) { ps := &Settings{ DefaultConfig: &Option{ - BuilderConfig: &BuilderConfig{Enabled: true, GasLimit: validator.Uint64(42_000_000), Relays: []string{"http://b:8080"}}, + BuilderConfig: &BuilderConfig{Enabled: true, GasLimit: validator.Uint64(42_000_000)}, }, } require.Equal(t, true, ps.UpgradeToV2()) @@ -515,8 +515,6 @@ func TestSettings_UpgradeToV2(t *testing.T) { require.Equal(t, validator.Uint64(42_000_000), ps.DefaultConfig.GasLimit) // BuilderConfig is retained so the gloas builder-API relays/enabled survive the upgrade. require.NotNil(t, ps.DefaultConfig.BuilderConfig) - require.Equal(t, 1, len(ps.DefaultConfig.BuilderConfig.Relays)) - require.Equal(t, "http://b:8080", ps.DefaultConfig.BuilderConfig.Relays[0]) }) t.Run("v1 top-level GasLimit already set is preserved", func(t *testing.T) { @@ -614,3 +612,86 @@ func TestSettings_TargetGasLimit(t *testing.T) { require.Equal(t, chainDefault, ps.TargetGasLimit(pk)) }) } + +// Legacy (pre-v2) payloads can't express presence: wire-absent enabled meant +// disabled, and the filesystem backend wrote spurious explicit-0 max payments. +// Persisted payloads may predate duplicate-url rejection at the API. +func TestSettingFromConsensus_DedupsBuilders(t *testing.T) { + payload := &validatorpb.ProposerSettingsPayload{ + Version: SchemaV2, + DefaultConfig: &validatorpb.ProposerOptionPayload{ + Builder: &validatorpb.BuilderConfig{ + Enabled: proto.Bool(true), + Builders: []*validatorpb.BuilderEntry{ + {Url: "https://b.example", AuthData: []byte("first")}, + {Url: "https://b.example", AuthData: []byte("second")}, + {Url: "https://other.example"}, + }, + }, + }, + } + ps, err := SettingFromConsensus(payload) + require.NoError(t, err) + builders := ps.DefaultConfig.BuilderConfig.Builders + require.Equal(t, 2, len(builders)) + require.Equal(t, "https://b.example", builders[0].URL) + require.DeepEqual(t, []byte("first"), builders[0].AuthData) + require.Equal(t, "https://other.example", builders[1].URL) +} + +func TestSettingFromConsensus_NormalizesLegacyPresence(t *testing.T) { + key := [fieldparams.BLSPubkeyLength]byte{9} + legacy := &Settings{ + Version: SchemaV1, + DefaultConfig: &Option{BuilderConfig: &BuilderConfig{ + GasLimit: validator.Uint64(30000000), + MaxExecutionPayment: uint64ValPtr(0), + }}, + ProposeConfig: map[[fieldparams.BLSPubkeyLength]byte]*Option{ + key: {BuilderConfig: &BuilderConfig{GasLimit: validator.Uint64(25000000)}}, + }, + } + + got, err := SettingFromConsensus(legacy.ToConsensus()) + require.NoError(t, err) + + def := got.DefaultConfig.BuilderConfig + require.Equal(t, false, def.Enabled) + require.Equal(t, (*validator.Uint64)(nil), def.MaxExecutionPayment) + + perKey := got.ProposeConfig[key].BuilderConfig + require.Equal(t, false, perKey.Enabled) + + // A disabled per-key section can never silently activate under an enabled default: + // EffectiveBuilderConfig takes the per-key enabled, not the default's. + eff := EffectiveBuilderConfig(perKey, &BuilderConfig{Enabled: true}) + require.Equal(t, false, eff.IsEnabled()) +} + +func TestSettingFromConsensus_V2PresencePreserved(t *testing.T) { + v2 := &Settings{ + Version: SchemaV2, + DefaultConfig: &Option{BuilderConfig: &BuilderConfig{ + MaxExecutionPayment: uint64ValPtr(0), + }}, + } + got, err := SettingFromConsensus(v2.ToConsensus()) + require.NoError(t, err) + bc := got.DefaultConfig.BuilderConfig + require.Equal(t, false, bc.Enabled) + require.NotNil(t, bc.MaxExecutionPayment) + require.Equal(t, validator.Uint64(0), *bc.MaxExecutionPayment) +} + +func TestUpgradeToV2_NormalizesLegacyPresence(t *testing.T) { + ps := &Settings{ + Version: SchemaV1, + DefaultConfig: &Option{BuilderConfig: &BuilderConfig{MaxExecutionPayment: uint64ValPtr(0)}}, + } + require.Equal(t, true, ps.UpgradeToV2()) + bc := ps.DefaultConfig.BuilderConfig + require.Equal(t, false, bc.Enabled) + require.Equal(t, (*validator.Uint64)(nil), bc.MaxExecutionPayment) + require.Equal(t, SchemaV2, ps.Version) + require.Equal(t, false, ps.UpgradeToV2()) +} diff --git a/proto/prysm/v1alpha1/validator-client/keymanager.pb.go b/proto/prysm/v1alpha1/validator-client/keymanager.pb.go index c07eebf76de6..91c690c0816a 100755 --- a/proto/prysm/v1alpha1/validator-client/keymanager.pb.go +++ b/proto/prysm/v1alpha1/validator-client/keymanager.pb.go @@ -712,11 +712,14 @@ func (x *ProposerOptionPayload) GetGasLimit() github_com_OffchainLabs_prysm_v7_c } type BuilderConfig struct { - state protoimpl.MessageState `protogen:"open.v1"` - Enabled bool `protobuf:"varint,1,opt,name=enabled,proto3" json:"enabled,omitempty"` - GasLimit github_com_OffchainLabs_prysm_v7_consensus_types_validator.Uint64 `protobuf:"varint,2,opt,name=gas_limit,json=gasLimit,proto3" json:"gas_limit,omitempty" cast-type:"github.com/OffchainLabs/prysm/v7/consensus-types/validator.Uint64"` - Relays []string `protobuf:"bytes,3,rep,name=relays,proto3" json:"relays,omitempty"` - MaxExecutionPayment github_com_OffchainLabs_prysm_v7_consensus_types_validator.Uint64 `protobuf:"varint,4,opt,name=max_execution_payment,json=maxExecutionPayment,proto3" json:"max_execution_payment,omitempty" cast-type:"github.com/OffchainLabs/prysm/v7/consensus-types/validator.Uint64"` + state protoimpl.MessageState `protogen:"open.v1"` + Enabled *bool `protobuf:"varint,1,opt,name=enabled,proto3,oneof" json:"enabled,omitempty"` + GasLimit github_com_OffchainLabs_prysm_v7_consensus_types_validator.Uint64 `protobuf:"varint,2,opt,name=gas_limit,json=gasLimit,proto3" json:"gas_limit,omitempty" cast-type:"github.com/OffchainLabs/prysm/v7/consensus-types/validator.Uint64"` + MaxExecutionPayment *github_com_OffchainLabs_prysm_v7_consensus_types_validator.Uint64 `protobuf:"varint,4,opt,name=max_execution_payment,json=maxExecutionPayment,proto3,oneof" json:"max_execution_payment,omitempty" cast-type:"github.com/OffchainLabs/prysm/v7/consensus-types/validator.Uint64"` + Builders []*BuilderEntry `protobuf:"bytes,5,rep,name=builders,proto3" json:"builders,omitempty"` + Proxy *string `protobuf:"bytes,6,opt,name=proxy,proto3,oneof" json:"proxy,omitempty"` + MinBid *github_com_OffchainLabs_prysm_v7_consensus_types_validator.Uint64 `protobuf:"varint,7,opt,name=min_bid,json=minBid,proto3,oneof" json:"min_bid,omitempty" cast-type:"github.com/OffchainLabs/prysm/v7/consensus-types/validator.Uint64"` + BuilderBoostFactor *github_com_OffchainLabs_prysm_v7_consensus_types_validator.Uint64 `protobuf:"varint,8,opt,name=builder_boost_factor,json=builderBoostFactor,proto3,oneof" json:"builder_boost_factor,omitempty" cast-type:"github.com/OffchainLabs/prysm/v7/consensus-types/validator.Uint64"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } @@ -752,8 +755,8 @@ func (*BuilderConfig) Descriptor() ([]byte, []int) { } func (x *BuilderConfig) GetEnabled() bool { - if x != nil { - return x.Enabled + if x != nil && x.Enabled != nil { + return *x.Enabled } return false } @@ -765,16 +768,129 @@ func (x *BuilderConfig) GetGasLimit() github_com_OffchainLabs_prysm_v7_consensus return github_com_OffchainLabs_prysm_v7_consensus_types_validator.Uint64(0) } -func (x *BuilderConfig) GetRelays() []string { +func (x *BuilderConfig) GetMaxExecutionPayment() github_com_OffchainLabs_prysm_v7_consensus_types_validator.Uint64 { + if x != nil && x.MaxExecutionPayment != nil { + return *x.MaxExecutionPayment + } + return github_com_OffchainLabs_prysm_v7_consensus_types_validator.Uint64(0) +} + +func (x *BuilderConfig) GetBuilders() []*BuilderEntry { if x != nil { - return x.Relays + return x.Builders } return nil } -func (x *BuilderConfig) GetMaxExecutionPayment() github_com_OffchainLabs_prysm_v7_consensus_types_validator.Uint64 { +func (x *BuilderConfig) GetProxy() string { + if x != nil && x.Proxy != nil { + return *x.Proxy + } + return "" +} + +func (x *BuilderConfig) GetMinBid() github_com_OffchainLabs_prysm_v7_consensus_types_validator.Uint64 { + if x != nil && x.MinBid != nil { + return *x.MinBid + } + return github_com_OffchainLabs_prysm_v7_consensus_types_validator.Uint64(0) +} + +func (x *BuilderConfig) GetBuilderBoostFactor() github_com_OffchainLabs_prysm_v7_consensus_types_validator.Uint64 { + if x != nil && x.BuilderBoostFactor != nil { + return *x.BuilderBoostFactor + } + return github_com_OffchainLabs_prysm_v7_consensus_types_validator.Uint64(0) +} + +type BuilderEntry struct { + state protoimpl.MessageState `protogen:"open.v1"` + Url string `protobuf:"bytes,1,opt,name=url,proto3" json:"url,omitempty"` + Pubkey []byte `protobuf:"bytes,2,opt,name=pubkey,proto3,oneof" json:"pubkey,omitempty"` + AuthData []byte `protobuf:"bytes,3,opt,name=auth_data,json=authData,proto3,oneof" json:"auth_data,omitempty"` + Proxy *string `protobuf:"bytes,4,opt,name=proxy,proto3,oneof" json:"proxy,omitempty"` + MinBid *github_com_OffchainLabs_prysm_v7_consensus_types_validator.Uint64 `protobuf:"varint,5,opt,name=min_bid,json=minBid,proto3,oneof" json:"min_bid,omitempty" cast-type:"github.com/OffchainLabs/prysm/v7/consensus-types/validator.Uint64"` + MaxExecutionPayment *github_com_OffchainLabs_prysm_v7_consensus_types_validator.Uint64 `protobuf:"varint,6,opt,name=max_execution_payment,json=maxExecutionPayment,proto3,oneof" json:"max_execution_payment,omitempty" cast-type:"github.com/OffchainLabs/prysm/v7/consensus-types/validator.Uint64"` + BuilderBoostFactor *github_com_OffchainLabs_prysm_v7_consensus_types_validator.Uint64 `protobuf:"varint,7,opt,name=builder_boost_factor,json=builderBoostFactor,proto3,oneof" json:"builder_boost_factor,omitempty" cast-type:"github.com/OffchainLabs/prysm/v7/consensus-types/validator.Uint64"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *BuilderEntry) Reset() { + *x = BuilderEntry{} + mi := &file_proto_prysm_v1alpha1_validator_client_keymanager_proto_msgTypes[4] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *BuilderEntry) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*BuilderEntry) ProtoMessage() {} + +func (x *BuilderEntry) ProtoReflect() protoreflect.Message { + mi := &file_proto_prysm_v1alpha1_validator_client_keymanager_proto_msgTypes[4] if x != nil { - return x.MaxExecutionPayment + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use BuilderEntry.ProtoReflect.Descriptor instead. +func (*BuilderEntry) Descriptor() ([]byte, []int) { + return file_proto_prysm_v1alpha1_validator_client_keymanager_proto_rawDescGZIP(), []int{4} +} + +func (x *BuilderEntry) GetUrl() string { + if x != nil { + return x.Url + } + return "" +} + +func (x *BuilderEntry) GetPubkey() []byte { + if x != nil { + return x.Pubkey + } + return nil +} + +func (x *BuilderEntry) GetAuthData() []byte { + if x != nil { + return x.AuthData + } + return nil +} + +func (x *BuilderEntry) GetProxy() string { + if x != nil && x.Proxy != nil { + return *x.Proxy + } + return "" +} + +func (x *BuilderEntry) GetMinBid() github_com_OffchainLabs_prysm_v7_consensus_types_validator.Uint64 { + if x != nil && x.MinBid != nil { + return *x.MinBid + } + return github_com_OffchainLabs_prysm_v7_consensus_types_validator.Uint64(0) +} + +func (x *BuilderEntry) GetMaxExecutionPayment() github_com_OffchainLabs_prysm_v7_consensus_types_validator.Uint64 { + if x != nil && x.MaxExecutionPayment != nil { + return *x.MaxExecutionPayment + } + return github_com_OffchainLabs_prysm_v7_consensus_types_validator.Uint64(0) +} + +func (x *BuilderEntry) GetBuilderBoostFactor() github_com_OffchainLabs_prysm_v7_consensus_types_validator.Uint64 { + if x != nil && x.BuilderBoostFactor != nil { + return *x.BuilderBoostFactor } return github_com_OffchainLabs_prysm_v7_consensus_types_validator.Uint64(0) } @@ -790,7 +906,7 @@ type ProposerSettingsPayload struct { func (x *ProposerSettingsPayload) Reset() { *x = ProposerSettingsPayload{} - mi := &file_proto_prysm_v1alpha1_validator_client_keymanager_proto_msgTypes[4] + mi := &file_proto_prysm_v1alpha1_validator_client_keymanager_proto_msgTypes[5] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -802,7 +918,7 @@ func (x *ProposerSettingsPayload) String() string { func (*ProposerSettingsPayload) ProtoMessage() {} func (x *ProposerSettingsPayload) ProtoReflect() protoreflect.Message { - mi := &file_proto_prysm_v1alpha1_validator_client_keymanager_proto_msgTypes[4] + mi := &file_proto_prysm_v1alpha1_validator_client_keymanager_proto_msgTypes[5] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -815,7 +931,7 @@ func (x *ProposerSettingsPayload) ProtoReflect() protoreflect.Message { // Deprecated: Use ProposerSettingsPayload.ProtoReflect.Descriptor instead. func (*ProposerSettingsPayload) Descriptor() ([]byte, []int) { - return file_proto_prysm_v1alpha1_validator_client_keymanager_proto_rawDescGZIP(), []int{4} + return file_proto_prysm_v1alpha1_validator_client_keymanager_proto_rawDescGZIP(), []int{5} } func (x *ProposerSettingsPayload) GetProposerConfig() map[string]*ProposerOptionPayload { @@ -1068,63 +1184,125 @@ var file_proto_prysm_v1alpha1_validator_client_keymanager_proto_rawDesc = []byte 0x6f, 0x6e, 0x73, 0x65, 0x6e, 0x73, 0x75, 0x73, 0x2d, 0x74, 0x79, 0x70, 0x65, 0x73, 0x2f, 0x76, 0x61, 0x6c, 0x69, 0x64, 0x61, 0x74, 0x6f, 0x72, 0x2e, 0x55, 0x69, 0x6e, 0x74, 0x36, 0x34, 0x52, 0x08, 0x67, 0x61, 0x73, 0x4c, 0x69, 0x6d, 0x69, 0x74, 0x42, 0x0b, 0x0a, 0x09, 0x5f, 0x67, 0x72, - 0x61, 0x66, 0x66, 0x69, 0x74, 0x69, 0x22, 0xa0, 0x02, 0x0a, 0x0d, 0x42, 0x75, 0x69, 0x6c, 0x64, - 0x65, 0x72, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x18, 0x0a, 0x07, 0x65, 0x6e, 0x61, 0x62, - 0x6c, 0x65, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x08, 0x52, 0x07, 0x65, 0x6e, 0x61, 0x62, 0x6c, - 0x65, 0x64, 0x12, 0x62, 0x0a, 0x09, 0x67, 0x61, 0x73, 0x5f, 0x6c, 0x69, 0x6d, 0x69, 0x74, 0x18, - 0x02, 0x20, 0x01, 0x28, 0x04, 0x42, 0x45, 0x82, 0xb5, 0x18, 0x41, 0x67, 0x69, 0x74, 0x68, 0x75, - 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x4f, 0x66, 0x66, 0x63, 0x68, 0x61, 0x69, 0x6e, 0x4c, 0x61, - 0x62, 0x73, 0x2f, 0x70, 0x72, 0x79, 0x73, 0x6d, 0x2f, 0x76, 0x37, 0x2f, 0x63, 0x6f, 0x6e, 0x73, - 0x65, 0x6e, 0x73, 0x75, 0x73, 0x2d, 0x74, 0x79, 0x70, 0x65, 0x73, 0x2f, 0x76, 0x61, 0x6c, 0x69, - 0x64, 0x61, 0x74, 0x6f, 0x72, 0x2e, 0x55, 0x69, 0x6e, 0x74, 0x36, 0x34, 0x52, 0x08, 0x67, 0x61, - 0x73, 0x4c, 0x69, 0x6d, 0x69, 0x74, 0x12, 0x16, 0x0a, 0x06, 0x72, 0x65, 0x6c, 0x61, 0x79, 0x73, - 0x18, 0x03, 0x20, 0x03, 0x28, 0x09, 0x52, 0x06, 0x72, 0x65, 0x6c, 0x61, 0x79, 0x73, 0x12, 0x79, - 0x0a, 0x15, 0x6d, 0x61, 0x78, 0x5f, 0x65, 0x78, 0x65, 0x63, 0x75, 0x74, 0x69, 0x6f, 0x6e, 0x5f, - 0x70, 0x61, 0x79, 0x6d, 0x65, 0x6e, 0x74, 0x18, 0x04, 0x20, 0x01, 0x28, 0x04, 0x42, 0x45, 0x82, - 0xb5, 0x18, 0x41, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x4f, 0x66, - 0x66, 0x63, 0x68, 0x61, 0x69, 0x6e, 0x4c, 0x61, 0x62, 0x73, 0x2f, 0x70, 0x72, 0x79, 0x73, 0x6d, - 0x2f, 0x76, 0x37, 0x2f, 0x63, 0x6f, 0x6e, 0x73, 0x65, 0x6e, 0x73, 0x75, 0x73, 0x2d, 0x74, 0x79, - 0x70, 0x65, 0x73, 0x2f, 0x76, 0x61, 0x6c, 0x69, 0x64, 0x61, 0x74, 0x6f, 0x72, 0x2e, 0x55, 0x69, - 0x6e, 0x74, 0x36, 0x34, 0x52, 0x13, 0x6d, 0x61, 0x78, 0x45, 0x78, 0x65, 0x63, 0x75, 0x74, 0x69, - 0x6f, 0x6e, 0x50, 0x61, 0x79, 0x6d, 0x65, 0x6e, 0x74, 0x22, 0x81, 0x03, 0x0a, 0x17, 0x50, 0x72, - 0x6f, 0x70, 0x6f, 0x73, 0x65, 0x72, 0x53, 0x65, 0x74, 0x74, 0x69, 0x6e, 0x67, 0x73, 0x50, 0x61, - 0x79, 0x6c, 0x6f, 0x61, 0x64, 0x12, 0x74, 0x0a, 0x0f, 0x70, 0x72, 0x6f, 0x70, 0x6f, 0x73, 0x65, - 0x72, 0x5f, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x4b, - 0x2e, 0x65, 0x74, 0x68, 0x65, 0x72, 0x65, 0x75, 0x6d, 0x2e, 0x76, 0x61, 0x6c, 0x69, 0x64, 0x61, - 0x74, 0x6f, 0x72, 0x2e, 0x61, 0x63, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x73, 0x2e, 0x76, 0x32, 0x2e, - 0x50, 0x72, 0x6f, 0x70, 0x6f, 0x73, 0x65, 0x72, 0x53, 0x65, 0x74, 0x74, 0x69, 0x6e, 0x67, 0x73, - 0x50, 0x61, 0x79, 0x6c, 0x6f, 0x61, 0x64, 0x2e, 0x50, 0x72, 0x6f, 0x70, 0x6f, 0x73, 0x65, 0x72, - 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x52, 0x0e, 0x70, 0x72, 0x6f, - 0x70, 0x6f, 0x73, 0x65, 0x72, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x5c, 0x0a, 0x0e, 0x64, - 0x65, 0x66, 0x61, 0x75, 0x6c, 0x74, 0x5f, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x18, 0x02, 0x20, - 0x01, 0x28, 0x0b, 0x32, 0x35, 0x2e, 0x65, 0x74, 0x68, 0x65, 0x72, 0x65, 0x75, 0x6d, 0x2e, 0x76, - 0x61, 0x6c, 0x69, 0x64, 0x61, 0x74, 0x6f, 0x72, 0x2e, 0x61, 0x63, 0x63, 0x6f, 0x75, 0x6e, 0x74, - 0x73, 0x2e, 0x76, 0x32, 0x2e, 0x50, 0x72, 0x6f, 0x70, 0x6f, 0x73, 0x65, 0x72, 0x4f, 0x70, 0x74, - 0x69, 0x6f, 0x6e, 0x50, 0x61, 0x79, 0x6c, 0x6f, 0x61, 0x64, 0x52, 0x0d, 0x64, 0x65, 0x66, 0x61, - 0x75, 0x6c, 0x74, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x18, 0x0a, 0x07, 0x76, 0x65, 0x72, - 0x73, 0x69, 0x6f, 0x6e, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x07, 0x76, 0x65, 0x72, 0x73, - 0x69, 0x6f, 0x6e, 0x1a, 0x78, 0x0a, 0x13, 0x50, 0x72, 0x6f, 0x70, 0x6f, 0x73, 0x65, 0x72, 0x43, - 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, - 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x4b, 0x0a, 0x05, - 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x35, 0x2e, 0x65, 0x74, - 0x68, 0x65, 0x72, 0x65, 0x75, 0x6d, 0x2e, 0x76, 0x61, 0x6c, 0x69, 0x64, 0x61, 0x74, 0x6f, 0x72, - 0x2e, 0x61, 0x63, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x73, 0x2e, 0x76, 0x32, 0x2e, 0x50, 0x72, 0x6f, - 0x70, 0x6f, 0x73, 0x65, 0x72, 0x4f, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x50, 0x61, 0x79, 0x6c, 0x6f, - 0x61, 0x64, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x3a, 0x02, 0x38, 0x01, 0x42, 0xcd, 0x01, - 0x0a, 0x22, 0x6f, 0x72, 0x67, 0x2e, 0x65, 0x74, 0x68, 0x65, 0x72, 0x65, 0x75, 0x6d, 0x2e, 0x76, - 0x61, 0x6c, 0x69, 0x64, 0x61, 0x74, 0x6f, 0x72, 0x2e, 0x61, 0x63, 0x63, 0x6f, 0x75, 0x6e, 0x74, - 0x73, 0x2e, 0x76, 0x32, 0x42, 0x0f, 0x4b, 0x65, 0x79, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x72, - 0x50, 0x72, 0x6f, 0x74, 0x6f, 0x50, 0x01, 0x5a, 0x52, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, + 0x61, 0x66, 0x66, 0x69, 0x74, 0x69, 0x22, 0xbd, 0x05, 0x0a, 0x0d, 0x42, 0x75, 0x69, 0x6c, 0x64, + 0x65, 0x72, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x1d, 0x0a, 0x07, 0x65, 0x6e, 0x61, 0x62, + 0x6c, 0x65, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x08, 0x48, 0x00, 0x52, 0x07, 0x65, 0x6e, 0x61, + 0x62, 0x6c, 0x65, 0x64, 0x88, 0x01, 0x01, 0x12, 0x62, 0x0a, 0x09, 0x67, 0x61, 0x73, 0x5f, 0x6c, + 0x69, 0x6d, 0x69, 0x74, 0x18, 0x02, 0x20, 0x01, 0x28, 0x04, 0x42, 0x45, 0x82, 0xb5, 0x18, 0x41, + 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x4f, 0x66, 0x66, 0x63, 0x68, + 0x61, 0x69, 0x6e, 0x4c, 0x61, 0x62, 0x73, 0x2f, 0x70, 0x72, 0x79, 0x73, 0x6d, 0x2f, 0x76, 0x37, + 0x2f, 0x63, 0x6f, 0x6e, 0x73, 0x65, 0x6e, 0x73, 0x75, 0x73, 0x2d, 0x74, 0x79, 0x70, 0x65, 0x73, + 0x2f, 0x76, 0x61, 0x6c, 0x69, 0x64, 0x61, 0x74, 0x6f, 0x72, 0x2e, 0x55, 0x69, 0x6e, 0x74, 0x36, + 0x34, 0x52, 0x08, 0x67, 0x61, 0x73, 0x4c, 0x69, 0x6d, 0x69, 0x74, 0x12, 0x7e, 0x0a, 0x15, 0x6d, + 0x61, 0x78, 0x5f, 0x65, 0x78, 0x65, 0x63, 0x75, 0x74, 0x69, 0x6f, 0x6e, 0x5f, 0x70, 0x61, 0x79, + 0x6d, 0x65, 0x6e, 0x74, 0x18, 0x04, 0x20, 0x01, 0x28, 0x04, 0x42, 0x45, 0x82, 0xb5, 0x18, 0x41, + 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x4f, 0x66, 0x66, 0x63, 0x68, + 0x61, 0x69, 0x6e, 0x4c, 0x61, 0x62, 0x73, 0x2f, 0x70, 0x72, 0x79, 0x73, 0x6d, 0x2f, 0x76, 0x37, + 0x2f, 0x63, 0x6f, 0x6e, 0x73, 0x65, 0x6e, 0x73, 0x75, 0x73, 0x2d, 0x74, 0x79, 0x70, 0x65, 0x73, + 0x2f, 0x76, 0x61, 0x6c, 0x69, 0x64, 0x61, 0x74, 0x6f, 0x72, 0x2e, 0x55, 0x69, 0x6e, 0x74, 0x36, + 0x34, 0x48, 0x01, 0x52, 0x13, 0x6d, 0x61, 0x78, 0x45, 0x78, 0x65, 0x63, 0x75, 0x74, 0x69, 0x6f, + 0x6e, 0x50, 0x61, 0x79, 0x6d, 0x65, 0x6e, 0x74, 0x88, 0x01, 0x01, 0x12, 0x48, 0x0a, 0x08, 0x62, + 0x75, 0x69, 0x6c, 0x64, 0x65, 0x72, 0x73, 0x18, 0x05, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x2c, 0x2e, + 0x65, 0x74, 0x68, 0x65, 0x72, 0x65, 0x75, 0x6d, 0x2e, 0x76, 0x61, 0x6c, 0x69, 0x64, 0x61, 0x74, + 0x6f, 0x72, 0x2e, 0x61, 0x63, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x73, 0x2e, 0x76, 0x32, 0x2e, 0x42, + 0x75, 0x69, 0x6c, 0x64, 0x65, 0x72, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x52, 0x08, 0x62, 0x75, 0x69, + 0x6c, 0x64, 0x65, 0x72, 0x73, 0x12, 0x19, 0x0a, 0x05, 0x70, 0x72, 0x6f, 0x78, 0x79, 0x18, 0x06, + 0x20, 0x01, 0x28, 0x09, 0x48, 0x02, 0x52, 0x05, 0x70, 0x72, 0x6f, 0x78, 0x79, 0x88, 0x01, 0x01, + 0x12, 0x63, 0x0a, 0x07, 0x6d, 0x69, 0x6e, 0x5f, 0x62, 0x69, 0x64, 0x18, 0x07, 0x20, 0x01, 0x28, + 0x04, 0x42, 0x45, 0x82, 0xb5, 0x18, 0x41, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, + 0x6d, 0x2f, 0x4f, 0x66, 0x66, 0x63, 0x68, 0x61, 0x69, 0x6e, 0x4c, 0x61, 0x62, 0x73, 0x2f, 0x70, + 0x72, 0x79, 0x73, 0x6d, 0x2f, 0x76, 0x37, 0x2f, 0x63, 0x6f, 0x6e, 0x73, 0x65, 0x6e, 0x73, 0x75, + 0x73, 0x2d, 0x74, 0x79, 0x70, 0x65, 0x73, 0x2f, 0x76, 0x61, 0x6c, 0x69, 0x64, 0x61, 0x74, 0x6f, + 0x72, 0x2e, 0x55, 0x69, 0x6e, 0x74, 0x36, 0x34, 0x48, 0x03, 0x52, 0x06, 0x6d, 0x69, 0x6e, 0x42, + 0x69, 0x64, 0x88, 0x01, 0x01, 0x12, 0x7c, 0x0a, 0x14, 0x62, 0x75, 0x69, 0x6c, 0x64, 0x65, 0x72, + 0x5f, 0x62, 0x6f, 0x6f, 0x73, 0x74, 0x5f, 0x66, 0x61, 0x63, 0x74, 0x6f, 0x72, 0x18, 0x08, 0x20, + 0x01, 0x28, 0x04, 0x42, 0x45, 0x82, 0xb5, 0x18, 0x41, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x4f, 0x66, 0x66, 0x63, 0x68, 0x61, 0x69, 0x6e, 0x4c, 0x61, 0x62, 0x73, - 0x2f, 0x70, 0x72, 0x79, 0x73, 0x6d, 0x2f, 0x76, 0x37, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2f, - 0x70, 0x72, 0x79, 0x73, 0x6d, 0x2f, 0x76, 0x31, 0x61, 0x6c, 0x70, 0x68, 0x61, 0x31, 0x2f, 0x76, - 0x61, 0x6c, 0x69, 0x64, 0x61, 0x74, 0x6f, 0x72, 0x2d, 0x63, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x3b, - 0x76, 0x61, 0x6c, 0x69, 0x64, 0x61, 0x74, 0x6f, 0x72, 0x70, 0x62, 0xaa, 0x02, 0x1e, 0x45, 0x74, - 0x68, 0x65, 0x72, 0x65, 0x75, 0x6d, 0x2e, 0x56, 0x61, 0x6c, 0x69, 0x64, 0x61, 0x74, 0x6f, 0x72, - 0x2e, 0x41, 0x63, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x73, 0x2e, 0x56, 0x32, 0xca, 0x02, 0x1e, 0x45, - 0x74, 0x68, 0x65, 0x72, 0x65, 0x75, 0x6d, 0x5c, 0x56, 0x61, 0x6c, 0x69, 0x64, 0x61, 0x74, 0x6f, - 0x72, 0x5c, 0x41, 0x63, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x73, 0x5c, 0x56, 0x32, 0x62, 0x06, 0x70, - 0x72, 0x6f, 0x74, 0x6f, 0x33, + 0x2f, 0x70, 0x72, 0x79, 0x73, 0x6d, 0x2f, 0x76, 0x37, 0x2f, 0x63, 0x6f, 0x6e, 0x73, 0x65, 0x6e, + 0x73, 0x75, 0x73, 0x2d, 0x74, 0x79, 0x70, 0x65, 0x73, 0x2f, 0x76, 0x61, 0x6c, 0x69, 0x64, 0x61, + 0x74, 0x6f, 0x72, 0x2e, 0x55, 0x69, 0x6e, 0x74, 0x36, 0x34, 0x48, 0x04, 0x52, 0x12, 0x62, 0x75, + 0x69, 0x6c, 0x64, 0x65, 0x72, 0x42, 0x6f, 0x6f, 0x73, 0x74, 0x46, 0x61, 0x63, 0x74, 0x6f, 0x72, + 0x88, 0x01, 0x01, 0x42, 0x0a, 0x0a, 0x08, 0x5f, 0x65, 0x6e, 0x61, 0x62, 0x6c, 0x65, 0x64, 0x42, + 0x18, 0x0a, 0x16, 0x5f, 0x6d, 0x61, 0x78, 0x5f, 0x65, 0x78, 0x65, 0x63, 0x75, 0x74, 0x69, 0x6f, + 0x6e, 0x5f, 0x70, 0x61, 0x79, 0x6d, 0x65, 0x6e, 0x74, 0x42, 0x08, 0x0a, 0x06, 0x5f, 0x70, 0x72, + 0x6f, 0x78, 0x79, 0x42, 0x0a, 0x0a, 0x08, 0x5f, 0x6d, 0x69, 0x6e, 0x5f, 0x62, 0x69, 0x64, 0x42, + 0x17, 0x0a, 0x15, 0x5f, 0x62, 0x75, 0x69, 0x6c, 0x64, 0x65, 0x72, 0x5f, 0x62, 0x6f, 0x6f, 0x73, + 0x74, 0x5f, 0x66, 0x61, 0x63, 0x74, 0x6f, 0x72, 0x4a, 0x04, 0x08, 0x03, 0x10, 0x04, 0x52, 0x06, + 0x72, 0x65, 0x6c, 0x61, 0x79, 0x73, 0x22, 0xbf, 0x04, 0x0a, 0x0c, 0x42, 0x75, 0x69, 0x6c, 0x64, + 0x65, 0x72, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x12, 0x10, 0x0a, 0x03, 0x75, 0x72, 0x6c, 0x18, 0x01, + 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x75, 0x72, 0x6c, 0x12, 0x1b, 0x0a, 0x06, 0x70, 0x75, 0x62, + 0x6b, 0x65, 0x79, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0c, 0x48, 0x00, 0x52, 0x06, 0x70, 0x75, 0x62, + 0x6b, 0x65, 0x79, 0x88, 0x01, 0x01, 0x12, 0x20, 0x0a, 0x09, 0x61, 0x75, 0x74, 0x68, 0x5f, 0x64, + 0x61, 0x74, 0x61, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0c, 0x48, 0x01, 0x52, 0x08, 0x61, 0x75, 0x74, + 0x68, 0x44, 0x61, 0x74, 0x61, 0x88, 0x01, 0x01, 0x12, 0x19, 0x0a, 0x05, 0x70, 0x72, 0x6f, 0x78, + 0x79, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x48, 0x02, 0x52, 0x05, 0x70, 0x72, 0x6f, 0x78, 0x79, + 0x88, 0x01, 0x01, 0x12, 0x63, 0x0a, 0x07, 0x6d, 0x69, 0x6e, 0x5f, 0x62, 0x69, 0x64, 0x18, 0x05, + 0x20, 0x01, 0x28, 0x04, 0x42, 0x45, 0x82, 0xb5, 0x18, 0x41, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, + 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x4f, 0x66, 0x66, 0x63, 0x68, 0x61, 0x69, 0x6e, 0x4c, 0x61, 0x62, + 0x73, 0x2f, 0x70, 0x72, 0x79, 0x73, 0x6d, 0x2f, 0x76, 0x37, 0x2f, 0x63, 0x6f, 0x6e, 0x73, 0x65, + 0x6e, 0x73, 0x75, 0x73, 0x2d, 0x74, 0x79, 0x70, 0x65, 0x73, 0x2f, 0x76, 0x61, 0x6c, 0x69, 0x64, + 0x61, 0x74, 0x6f, 0x72, 0x2e, 0x55, 0x69, 0x6e, 0x74, 0x36, 0x34, 0x48, 0x03, 0x52, 0x06, 0x6d, + 0x69, 0x6e, 0x42, 0x69, 0x64, 0x88, 0x01, 0x01, 0x12, 0x7e, 0x0a, 0x15, 0x6d, 0x61, 0x78, 0x5f, + 0x65, 0x78, 0x65, 0x63, 0x75, 0x74, 0x69, 0x6f, 0x6e, 0x5f, 0x70, 0x61, 0x79, 0x6d, 0x65, 0x6e, + 0x74, 0x18, 0x06, 0x20, 0x01, 0x28, 0x04, 0x42, 0x45, 0x82, 0xb5, 0x18, 0x41, 0x67, 0x69, 0x74, + 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x4f, 0x66, 0x66, 0x63, 0x68, 0x61, 0x69, 0x6e, + 0x4c, 0x61, 0x62, 0x73, 0x2f, 0x70, 0x72, 0x79, 0x73, 0x6d, 0x2f, 0x76, 0x37, 0x2f, 0x63, 0x6f, + 0x6e, 0x73, 0x65, 0x6e, 0x73, 0x75, 0x73, 0x2d, 0x74, 0x79, 0x70, 0x65, 0x73, 0x2f, 0x76, 0x61, + 0x6c, 0x69, 0x64, 0x61, 0x74, 0x6f, 0x72, 0x2e, 0x55, 0x69, 0x6e, 0x74, 0x36, 0x34, 0x48, 0x04, + 0x52, 0x13, 0x6d, 0x61, 0x78, 0x45, 0x78, 0x65, 0x63, 0x75, 0x74, 0x69, 0x6f, 0x6e, 0x50, 0x61, + 0x79, 0x6d, 0x65, 0x6e, 0x74, 0x88, 0x01, 0x01, 0x12, 0x7c, 0x0a, 0x14, 0x62, 0x75, 0x69, 0x6c, + 0x64, 0x65, 0x72, 0x5f, 0x62, 0x6f, 0x6f, 0x73, 0x74, 0x5f, 0x66, 0x61, 0x63, 0x74, 0x6f, 0x72, + 0x18, 0x07, 0x20, 0x01, 0x28, 0x04, 0x42, 0x45, 0x82, 0xb5, 0x18, 0x41, 0x67, 0x69, 0x74, 0x68, + 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x4f, 0x66, 0x66, 0x63, 0x68, 0x61, 0x69, 0x6e, 0x4c, + 0x61, 0x62, 0x73, 0x2f, 0x70, 0x72, 0x79, 0x73, 0x6d, 0x2f, 0x76, 0x37, 0x2f, 0x63, 0x6f, 0x6e, + 0x73, 0x65, 0x6e, 0x73, 0x75, 0x73, 0x2d, 0x74, 0x79, 0x70, 0x65, 0x73, 0x2f, 0x76, 0x61, 0x6c, + 0x69, 0x64, 0x61, 0x74, 0x6f, 0x72, 0x2e, 0x55, 0x69, 0x6e, 0x74, 0x36, 0x34, 0x48, 0x05, 0x52, + 0x12, 0x62, 0x75, 0x69, 0x6c, 0x64, 0x65, 0x72, 0x42, 0x6f, 0x6f, 0x73, 0x74, 0x46, 0x61, 0x63, + 0x74, 0x6f, 0x72, 0x88, 0x01, 0x01, 0x42, 0x09, 0x0a, 0x07, 0x5f, 0x70, 0x75, 0x62, 0x6b, 0x65, + 0x79, 0x42, 0x0c, 0x0a, 0x0a, 0x5f, 0x61, 0x75, 0x74, 0x68, 0x5f, 0x64, 0x61, 0x74, 0x61, 0x42, + 0x08, 0x0a, 0x06, 0x5f, 0x70, 0x72, 0x6f, 0x78, 0x79, 0x42, 0x0a, 0x0a, 0x08, 0x5f, 0x6d, 0x69, + 0x6e, 0x5f, 0x62, 0x69, 0x64, 0x42, 0x18, 0x0a, 0x16, 0x5f, 0x6d, 0x61, 0x78, 0x5f, 0x65, 0x78, + 0x65, 0x63, 0x75, 0x74, 0x69, 0x6f, 0x6e, 0x5f, 0x70, 0x61, 0x79, 0x6d, 0x65, 0x6e, 0x74, 0x42, + 0x17, 0x0a, 0x15, 0x5f, 0x62, 0x75, 0x69, 0x6c, 0x64, 0x65, 0x72, 0x5f, 0x62, 0x6f, 0x6f, 0x73, + 0x74, 0x5f, 0x66, 0x61, 0x63, 0x74, 0x6f, 0x72, 0x22, 0x81, 0x03, 0x0a, 0x17, 0x50, 0x72, 0x6f, + 0x70, 0x6f, 0x73, 0x65, 0x72, 0x53, 0x65, 0x74, 0x74, 0x69, 0x6e, 0x67, 0x73, 0x50, 0x61, 0x79, + 0x6c, 0x6f, 0x61, 0x64, 0x12, 0x74, 0x0a, 0x0f, 0x70, 0x72, 0x6f, 0x70, 0x6f, 0x73, 0x65, 0x72, + 0x5f, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x4b, 0x2e, + 0x65, 0x74, 0x68, 0x65, 0x72, 0x65, 0x75, 0x6d, 0x2e, 0x76, 0x61, 0x6c, 0x69, 0x64, 0x61, 0x74, + 0x6f, 0x72, 0x2e, 0x61, 0x63, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x73, 0x2e, 0x76, 0x32, 0x2e, 0x50, + 0x72, 0x6f, 0x70, 0x6f, 0x73, 0x65, 0x72, 0x53, 0x65, 0x74, 0x74, 0x69, 0x6e, 0x67, 0x73, 0x50, + 0x61, 0x79, 0x6c, 0x6f, 0x61, 0x64, 0x2e, 0x50, 0x72, 0x6f, 0x70, 0x6f, 0x73, 0x65, 0x72, 0x43, + 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x52, 0x0e, 0x70, 0x72, 0x6f, 0x70, + 0x6f, 0x73, 0x65, 0x72, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x5c, 0x0a, 0x0e, 0x64, 0x65, + 0x66, 0x61, 0x75, 0x6c, 0x74, 0x5f, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x18, 0x02, 0x20, 0x01, + 0x28, 0x0b, 0x32, 0x35, 0x2e, 0x65, 0x74, 0x68, 0x65, 0x72, 0x65, 0x75, 0x6d, 0x2e, 0x76, 0x61, + 0x6c, 0x69, 0x64, 0x61, 0x74, 0x6f, 0x72, 0x2e, 0x61, 0x63, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x73, + 0x2e, 0x76, 0x32, 0x2e, 0x50, 0x72, 0x6f, 0x70, 0x6f, 0x73, 0x65, 0x72, 0x4f, 0x70, 0x74, 0x69, + 0x6f, 0x6e, 0x50, 0x61, 0x79, 0x6c, 0x6f, 0x61, 0x64, 0x52, 0x0d, 0x64, 0x65, 0x66, 0x61, 0x75, + 0x6c, 0x74, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x18, 0x0a, 0x07, 0x76, 0x65, 0x72, 0x73, + 0x69, 0x6f, 0x6e, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x07, 0x76, 0x65, 0x72, 0x73, 0x69, + 0x6f, 0x6e, 0x1a, 0x78, 0x0a, 0x13, 0x50, 0x72, 0x6f, 0x70, 0x6f, 0x73, 0x65, 0x72, 0x43, 0x6f, + 0x6e, 0x66, 0x69, 0x67, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, + 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x4b, 0x0a, 0x05, 0x76, + 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x35, 0x2e, 0x65, 0x74, 0x68, + 0x65, 0x72, 0x65, 0x75, 0x6d, 0x2e, 0x76, 0x61, 0x6c, 0x69, 0x64, 0x61, 0x74, 0x6f, 0x72, 0x2e, + 0x61, 0x63, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x73, 0x2e, 0x76, 0x32, 0x2e, 0x50, 0x72, 0x6f, 0x70, + 0x6f, 0x73, 0x65, 0x72, 0x4f, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x50, 0x61, 0x79, 0x6c, 0x6f, 0x61, + 0x64, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x3a, 0x02, 0x38, 0x01, 0x42, 0xcd, 0x01, 0x0a, + 0x22, 0x6f, 0x72, 0x67, 0x2e, 0x65, 0x74, 0x68, 0x65, 0x72, 0x65, 0x75, 0x6d, 0x2e, 0x76, 0x61, + 0x6c, 0x69, 0x64, 0x61, 0x74, 0x6f, 0x72, 0x2e, 0x61, 0x63, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x73, + 0x2e, 0x76, 0x32, 0x42, 0x0f, 0x4b, 0x65, 0x79, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x72, 0x50, + 0x72, 0x6f, 0x74, 0x6f, 0x50, 0x01, 0x5a, 0x52, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, + 0x6f, 0x6d, 0x2f, 0x4f, 0x66, 0x66, 0x63, 0x68, 0x61, 0x69, 0x6e, 0x4c, 0x61, 0x62, 0x73, 0x2f, + 0x70, 0x72, 0x79, 0x73, 0x6d, 0x2f, 0x76, 0x37, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2f, 0x70, + 0x72, 0x79, 0x73, 0x6d, 0x2f, 0x76, 0x31, 0x61, 0x6c, 0x70, 0x68, 0x61, 0x31, 0x2f, 0x76, 0x61, + 0x6c, 0x69, 0x64, 0x61, 0x74, 0x6f, 0x72, 0x2d, 0x63, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x3b, 0x76, + 0x61, 0x6c, 0x69, 0x64, 0x61, 0x74, 0x6f, 0x72, 0x70, 0x62, 0xaa, 0x02, 0x1e, 0x45, 0x74, 0x68, + 0x65, 0x72, 0x65, 0x75, 0x6d, 0x2e, 0x56, 0x61, 0x6c, 0x69, 0x64, 0x61, 0x74, 0x6f, 0x72, 0x2e, + 0x41, 0x63, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x73, 0x2e, 0x56, 0x32, 0xca, 0x02, 0x1e, 0x45, 0x74, + 0x68, 0x65, 0x72, 0x65, 0x75, 0x6d, 0x5c, 0x56, 0x61, 0x6c, 0x69, 0x64, 0x61, 0x74, 0x6f, 0x72, + 0x5c, 0x41, 0x63, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x73, 0x5c, 0x56, 0x32, 0x62, 0x06, 0x70, 0x72, + 0x6f, 0x74, 0x6f, 0x33, } var ( @@ -1140,74 +1318,76 @@ func file_proto_prysm_v1alpha1_validator_client_keymanager_proto_rawDescGZIP() [ } var file_proto_prysm_v1alpha1_validator_client_keymanager_proto_enumTypes = make([]protoimpl.EnumInfo, 1) -var file_proto_prysm_v1alpha1_validator_client_keymanager_proto_msgTypes = make([]protoimpl.MessageInfo, 6) +var file_proto_prysm_v1alpha1_validator_client_keymanager_proto_msgTypes = make([]protoimpl.MessageInfo, 7) var file_proto_prysm_v1alpha1_validator_client_keymanager_proto_goTypes = []any{ (SignResponse_Status)(0), // 0: ethereum.validator.accounts.v2.SignResponse.Status (*SignRequest)(nil), // 1: ethereum.validator.accounts.v2.SignRequest (*SignResponse)(nil), // 2: ethereum.validator.accounts.v2.SignResponse (*ProposerOptionPayload)(nil), // 3: ethereum.validator.accounts.v2.ProposerOptionPayload (*BuilderConfig)(nil), // 4: ethereum.validator.accounts.v2.BuilderConfig - (*ProposerSettingsPayload)(nil), // 5: ethereum.validator.accounts.v2.ProposerSettingsPayload - nil, // 6: ethereum.validator.accounts.v2.ProposerSettingsPayload.ProposerConfigEntry - (*v1alpha1.BeaconBlock)(nil), // 7: ethereum.eth.v1alpha1.BeaconBlock - (*v1alpha1.AttestationData)(nil), // 8: ethereum.eth.v1alpha1.AttestationData - (*v1alpha1.AggregateAttestationAndProof)(nil), // 9: ethereum.eth.v1alpha1.AggregateAttestationAndProof - (*v1alpha1.VoluntaryExit)(nil), // 10: ethereum.eth.v1alpha1.VoluntaryExit - (*v1alpha1.BeaconBlockAltair)(nil), // 11: ethereum.eth.v1alpha1.BeaconBlockAltair - (*v1alpha1.SyncAggregatorSelectionData)(nil), // 12: ethereum.eth.v1alpha1.SyncAggregatorSelectionData - (*v1alpha1.ContributionAndProof)(nil), // 13: ethereum.eth.v1alpha1.ContributionAndProof - (*v1alpha1.BeaconBlockBellatrix)(nil), // 14: ethereum.eth.v1alpha1.BeaconBlockBellatrix - (*v1alpha1.BlindedBeaconBlockBellatrix)(nil), // 15: ethereum.eth.v1alpha1.BlindedBeaconBlockBellatrix - (*v1alpha1.ValidatorRegistrationV1)(nil), // 16: ethereum.eth.v1alpha1.ValidatorRegistrationV1 - (*v1alpha1.BeaconBlockCapella)(nil), // 17: ethereum.eth.v1alpha1.BeaconBlockCapella - (*v1alpha1.BlindedBeaconBlockCapella)(nil), // 18: ethereum.eth.v1alpha1.BlindedBeaconBlockCapella - (*v1alpha1.BeaconBlockDeneb)(nil), // 19: ethereum.eth.v1alpha1.BeaconBlockDeneb - (*v1alpha1.BlindedBeaconBlockDeneb)(nil), // 20: ethereum.eth.v1alpha1.BlindedBeaconBlockDeneb - (*v1alpha1.BeaconBlockElectra)(nil), // 21: ethereum.eth.v1alpha1.BeaconBlockElectra - (*v1alpha1.BlindedBeaconBlockElectra)(nil), // 22: ethereum.eth.v1alpha1.BlindedBeaconBlockElectra - (*v1alpha1.AggregateAttestationAndProofElectra)(nil), // 23: ethereum.eth.v1alpha1.AggregateAttestationAndProofElectra - (*v1alpha1.BlindedBeaconBlockFulu)(nil), // 24: ethereum.eth.v1alpha1.BlindedBeaconBlockFulu - (*v1alpha1.BeaconBlockGloas)(nil), // 25: ethereum.eth.v1alpha1.BeaconBlockGloas - (*v1alpha1.ExecutionPayloadEnvelope)(nil), // 26: ethereum.eth.v1alpha1.ExecutionPayloadEnvelope - (*v1alpha1.PayloadAttestationData)(nil), // 27: ethereum.eth.v1alpha1.PayloadAttestationData - (*v1alpha1.ProposerPreferences)(nil), // 28: ethereum.eth.v1alpha1.ProposerPreferences - (*v1alpha1.RequestAuthV1)(nil), // 29: ethereum.eth.v1alpha1.RequestAuthV1 + (*BuilderEntry)(nil), // 5: ethereum.validator.accounts.v2.BuilderEntry + (*ProposerSettingsPayload)(nil), // 6: ethereum.validator.accounts.v2.ProposerSettingsPayload + nil, // 7: ethereum.validator.accounts.v2.ProposerSettingsPayload.ProposerConfigEntry + (*v1alpha1.BeaconBlock)(nil), // 8: ethereum.eth.v1alpha1.BeaconBlock + (*v1alpha1.AttestationData)(nil), // 9: ethereum.eth.v1alpha1.AttestationData + (*v1alpha1.AggregateAttestationAndProof)(nil), // 10: ethereum.eth.v1alpha1.AggregateAttestationAndProof + (*v1alpha1.VoluntaryExit)(nil), // 11: ethereum.eth.v1alpha1.VoluntaryExit + (*v1alpha1.BeaconBlockAltair)(nil), // 12: ethereum.eth.v1alpha1.BeaconBlockAltair + (*v1alpha1.SyncAggregatorSelectionData)(nil), // 13: ethereum.eth.v1alpha1.SyncAggregatorSelectionData + (*v1alpha1.ContributionAndProof)(nil), // 14: ethereum.eth.v1alpha1.ContributionAndProof + (*v1alpha1.BeaconBlockBellatrix)(nil), // 15: ethereum.eth.v1alpha1.BeaconBlockBellatrix + (*v1alpha1.BlindedBeaconBlockBellatrix)(nil), // 16: ethereum.eth.v1alpha1.BlindedBeaconBlockBellatrix + (*v1alpha1.ValidatorRegistrationV1)(nil), // 17: ethereum.eth.v1alpha1.ValidatorRegistrationV1 + (*v1alpha1.BeaconBlockCapella)(nil), // 18: ethereum.eth.v1alpha1.BeaconBlockCapella + (*v1alpha1.BlindedBeaconBlockCapella)(nil), // 19: ethereum.eth.v1alpha1.BlindedBeaconBlockCapella + (*v1alpha1.BeaconBlockDeneb)(nil), // 20: ethereum.eth.v1alpha1.BeaconBlockDeneb + (*v1alpha1.BlindedBeaconBlockDeneb)(nil), // 21: ethereum.eth.v1alpha1.BlindedBeaconBlockDeneb + (*v1alpha1.BeaconBlockElectra)(nil), // 22: ethereum.eth.v1alpha1.BeaconBlockElectra + (*v1alpha1.BlindedBeaconBlockElectra)(nil), // 23: ethereum.eth.v1alpha1.BlindedBeaconBlockElectra + (*v1alpha1.AggregateAttestationAndProofElectra)(nil), // 24: ethereum.eth.v1alpha1.AggregateAttestationAndProofElectra + (*v1alpha1.BlindedBeaconBlockFulu)(nil), // 25: ethereum.eth.v1alpha1.BlindedBeaconBlockFulu + (*v1alpha1.BeaconBlockGloas)(nil), // 26: ethereum.eth.v1alpha1.BeaconBlockGloas + (*v1alpha1.ExecutionPayloadEnvelope)(nil), // 27: ethereum.eth.v1alpha1.ExecutionPayloadEnvelope + (*v1alpha1.PayloadAttestationData)(nil), // 28: ethereum.eth.v1alpha1.PayloadAttestationData + (*v1alpha1.ProposerPreferences)(nil), // 29: ethereum.eth.v1alpha1.ProposerPreferences + (*v1alpha1.RequestAuthV1)(nil), // 30: ethereum.eth.v1alpha1.RequestAuthV1 } var file_proto_prysm_v1alpha1_validator_client_keymanager_proto_depIdxs = []int32{ - 7, // 0: ethereum.validator.accounts.v2.SignRequest.block:type_name -> ethereum.eth.v1alpha1.BeaconBlock - 8, // 1: ethereum.validator.accounts.v2.SignRequest.attestation_data:type_name -> ethereum.eth.v1alpha1.AttestationData - 9, // 2: ethereum.validator.accounts.v2.SignRequest.aggregate_attestation_and_proof:type_name -> ethereum.eth.v1alpha1.AggregateAttestationAndProof - 10, // 3: ethereum.validator.accounts.v2.SignRequest.exit:type_name -> ethereum.eth.v1alpha1.VoluntaryExit - 11, // 4: ethereum.validator.accounts.v2.SignRequest.block_altair:type_name -> ethereum.eth.v1alpha1.BeaconBlockAltair - 12, // 5: ethereum.validator.accounts.v2.SignRequest.sync_aggregator_selection_data:type_name -> ethereum.eth.v1alpha1.SyncAggregatorSelectionData - 13, // 6: ethereum.validator.accounts.v2.SignRequest.contribution_and_proof:type_name -> ethereum.eth.v1alpha1.ContributionAndProof - 14, // 7: ethereum.validator.accounts.v2.SignRequest.block_bellatrix:type_name -> ethereum.eth.v1alpha1.BeaconBlockBellatrix - 15, // 8: ethereum.validator.accounts.v2.SignRequest.blinded_block_bellatrix:type_name -> ethereum.eth.v1alpha1.BlindedBeaconBlockBellatrix - 16, // 9: ethereum.validator.accounts.v2.SignRequest.registration:type_name -> ethereum.eth.v1alpha1.ValidatorRegistrationV1 - 17, // 10: ethereum.validator.accounts.v2.SignRequest.block_capella:type_name -> ethereum.eth.v1alpha1.BeaconBlockCapella - 18, // 11: ethereum.validator.accounts.v2.SignRequest.blinded_block_capella:type_name -> ethereum.eth.v1alpha1.BlindedBeaconBlockCapella - 19, // 12: ethereum.validator.accounts.v2.SignRequest.block_deneb:type_name -> ethereum.eth.v1alpha1.BeaconBlockDeneb - 20, // 13: ethereum.validator.accounts.v2.SignRequest.blinded_block_deneb:type_name -> ethereum.eth.v1alpha1.BlindedBeaconBlockDeneb - 21, // 14: ethereum.validator.accounts.v2.SignRequest.block_electra:type_name -> ethereum.eth.v1alpha1.BeaconBlockElectra - 22, // 15: ethereum.validator.accounts.v2.SignRequest.blinded_block_electra:type_name -> ethereum.eth.v1alpha1.BlindedBeaconBlockElectra - 23, // 16: ethereum.validator.accounts.v2.SignRequest.aggregate_attestation_and_proof_electra:type_name -> ethereum.eth.v1alpha1.AggregateAttestationAndProofElectra - 21, // 17: ethereum.validator.accounts.v2.SignRequest.block_fulu:type_name -> ethereum.eth.v1alpha1.BeaconBlockElectra - 24, // 18: ethereum.validator.accounts.v2.SignRequest.blinded_block_fulu:type_name -> ethereum.eth.v1alpha1.BlindedBeaconBlockFulu - 25, // 19: ethereum.validator.accounts.v2.SignRequest.block_gloas:type_name -> ethereum.eth.v1alpha1.BeaconBlockGloas - 26, // 20: ethereum.validator.accounts.v2.SignRequest.execution_payload_envelope:type_name -> ethereum.eth.v1alpha1.ExecutionPayloadEnvelope - 27, // 21: ethereum.validator.accounts.v2.SignRequest.payload_attestation_data:type_name -> ethereum.eth.v1alpha1.PayloadAttestationData - 28, // 22: ethereum.validator.accounts.v2.SignRequest.proposer_preference:type_name -> ethereum.eth.v1alpha1.ProposerPreferences - 29, // 23: ethereum.validator.accounts.v2.SignRequest.request_auth:type_name -> ethereum.eth.v1alpha1.RequestAuthV1 + 8, // 0: ethereum.validator.accounts.v2.SignRequest.block:type_name -> ethereum.eth.v1alpha1.BeaconBlock + 9, // 1: ethereum.validator.accounts.v2.SignRequest.attestation_data:type_name -> ethereum.eth.v1alpha1.AttestationData + 10, // 2: ethereum.validator.accounts.v2.SignRequest.aggregate_attestation_and_proof:type_name -> ethereum.eth.v1alpha1.AggregateAttestationAndProof + 11, // 3: ethereum.validator.accounts.v2.SignRequest.exit:type_name -> ethereum.eth.v1alpha1.VoluntaryExit + 12, // 4: ethereum.validator.accounts.v2.SignRequest.block_altair:type_name -> ethereum.eth.v1alpha1.BeaconBlockAltair + 13, // 5: ethereum.validator.accounts.v2.SignRequest.sync_aggregator_selection_data:type_name -> ethereum.eth.v1alpha1.SyncAggregatorSelectionData + 14, // 6: ethereum.validator.accounts.v2.SignRequest.contribution_and_proof:type_name -> ethereum.eth.v1alpha1.ContributionAndProof + 15, // 7: ethereum.validator.accounts.v2.SignRequest.block_bellatrix:type_name -> ethereum.eth.v1alpha1.BeaconBlockBellatrix + 16, // 8: ethereum.validator.accounts.v2.SignRequest.blinded_block_bellatrix:type_name -> ethereum.eth.v1alpha1.BlindedBeaconBlockBellatrix + 17, // 9: ethereum.validator.accounts.v2.SignRequest.registration:type_name -> ethereum.eth.v1alpha1.ValidatorRegistrationV1 + 18, // 10: ethereum.validator.accounts.v2.SignRequest.block_capella:type_name -> ethereum.eth.v1alpha1.BeaconBlockCapella + 19, // 11: ethereum.validator.accounts.v2.SignRequest.blinded_block_capella:type_name -> ethereum.eth.v1alpha1.BlindedBeaconBlockCapella + 20, // 12: ethereum.validator.accounts.v2.SignRequest.block_deneb:type_name -> ethereum.eth.v1alpha1.BeaconBlockDeneb + 21, // 13: ethereum.validator.accounts.v2.SignRequest.blinded_block_deneb:type_name -> ethereum.eth.v1alpha1.BlindedBeaconBlockDeneb + 22, // 14: ethereum.validator.accounts.v2.SignRequest.block_electra:type_name -> ethereum.eth.v1alpha1.BeaconBlockElectra + 23, // 15: ethereum.validator.accounts.v2.SignRequest.blinded_block_electra:type_name -> ethereum.eth.v1alpha1.BlindedBeaconBlockElectra + 24, // 16: ethereum.validator.accounts.v2.SignRequest.aggregate_attestation_and_proof_electra:type_name -> ethereum.eth.v1alpha1.AggregateAttestationAndProofElectra + 22, // 17: ethereum.validator.accounts.v2.SignRequest.block_fulu:type_name -> ethereum.eth.v1alpha1.BeaconBlockElectra + 25, // 18: ethereum.validator.accounts.v2.SignRequest.blinded_block_fulu:type_name -> ethereum.eth.v1alpha1.BlindedBeaconBlockFulu + 26, // 19: ethereum.validator.accounts.v2.SignRequest.block_gloas:type_name -> ethereum.eth.v1alpha1.BeaconBlockGloas + 27, // 20: ethereum.validator.accounts.v2.SignRequest.execution_payload_envelope:type_name -> ethereum.eth.v1alpha1.ExecutionPayloadEnvelope + 28, // 21: ethereum.validator.accounts.v2.SignRequest.payload_attestation_data:type_name -> ethereum.eth.v1alpha1.PayloadAttestationData + 29, // 22: ethereum.validator.accounts.v2.SignRequest.proposer_preference:type_name -> ethereum.eth.v1alpha1.ProposerPreferences + 30, // 23: ethereum.validator.accounts.v2.SignRequest.request_auth:type_name -> ethereum.eth.v1alpha1.RequestAuthV1 0, // 24: ethereum.validator.accounts.v2.SignResponse.status:type_name -> ethereum.validator.accounts.v2.SignResponse.Status 4, // 25: ethereum.validator.accounts.v2.ProposerOptionPayload.builder:type_name -> ethereum.validator.accounts.v2.BuilderConfig - 6, // 26: ethereum.validator.accounts.v2.ProposerSettingsPayload.proposer_config:type_name -> ethereum.validator.accounts.v2.ProposerSettingsPayload.ProposerConfigEntry - 3, // 27: ethereum.validator.accounts.v2.ProposerSettingsPayload.default_config:type_name -> ethereum.validator.accounts.v2.ProposerOptionPayload - 3, // 28: ethereum.validator.accounts.v2.ProposerSettingsPayload.ProposerConfigEntry.value:type_name -> ethereum.validator.accounts.v2.ProposerOptionPayload - 29, // [29:29] is the sub-list for method output_type - 29, // [29:29] is the sub-list for method input_type - 29, // [29:29] is the sub-list for extension type_name - 29, // [29:29] is the sub-list for extension extendee - 0, // [0:29] is the sub-list for field type_name + 5, // 26: ethereum.validator.accounts.v2.BuilderConfig.builders:type_name -> ethereum.validator.accounts.v2.BuilderEntry + 7, // 27: ethereum.validator.accounts.v2.ProposerSettingsPayload.proposer_config:type_name -> ethereum.validator.accounts.v2.ProposerSettingsPayload.ProposerConfigEntry + 3, // 28: ethereum.validator.accounts.v2.ProposerSettingsPayload.default_config:type_name -> ethereum.validator.accounts.v2.ProposerOptionPayload + 3, // 29: ethereum.validator.accounts.v2.ProposerSettingsPayload.ProposerConfigEntry.value:type_name -> ethereum.validator.accounts.v2.ProposerOptionPayload + 30, // [30:30] is the sub-list for method output_type + 30, // [30:30] is the sub-list for method input_type + 30, // [30:30] is the sub-list for extension type_name + 30, // [30:30] is the sub-list for extension extendee + 0, // [0:30] is the sub-list for field type_name } func init() { file_proto_prysm_v1alpha1_validator_client_keymanager_proto_init() } @@ -1245,13 +1425,15 @@ func file_proto_prysm_v1alpha1_validator_client_keymanager_proto_init() { (*SignRequest_RequestAuth)(nil), } file_proto_prysm_v1alpha1_validator_client_keymanager_proto_msgTypes[2].OneofWrappers = []any{} + file_proto_prysm_v1alpha1_validator_client_keymanager_proto_msgTypes[3].OneofWrappers = []any{} + file_proto_prysm_v1alpha1_validator_client_keymanager_proto_msgTypes[4].OneofWrappers = []any{} type x struct{} out := protoimpl.TypeBuilder{ File: protoimpl.DescBuilder{ GoPackagePath: reflect.TypeOf(x{}).PkgPath(), RawDescriptor: file_proto_prysm_v1alpha1_validator_client_keymanager_proto_rawDesc, NumEnums: 1, - NumMessages: 6, + NumMessages: 7, NumExtensions: 0, NumServices: 0, }, diff --git a/proto/prysm/v1alpha1/validator-client/keymanager.proto b/proto/prysm/v1alpha1/validator-client/keymanager.proto index 688f1ce37bdb..4c55d25ae16a 100644 --- a/proto/prysm/v1alpha1/validator-client/keymanager.proto +++ b/proto/prysm/v1alpha1/validator-client/keymanager.proto @@ -129,16 +129,50 @@ message ProposerOptionPayload { // BuilderConfig is a property of ProposerOptionPayload message BuilderConfig { - bool enabled = 1; + optional bool enabled = 1; uint64 gas_limit = 2 [ (ethereum.eth.ext.cast_type) = "github.com/OffchainLabs/prysm/v7/consensus-types/validator.Uint64" ]; - repeated string relays = 3; + reserved 3; + reserved "relays"; // max_execution_payment is the maximum execution layer payment (in Gwei) the - // proposer will accept from a builder for the Gloas builder API. 0 means - // trustless-only; 2^64-1 means accept any amount. - uint64 max_execution_payment = 4 [ + // proposer will accept from a builder for the Gloas builder API. Explicit 0 + // means trustless-only; unset inherits; 2^64-1 means accept any amount. + optional uint64 max_execution_payment = 4 [ + (ethereum.eth.ext.cast_type) = + "github.com/OffchainLabs/prysm/v7/consensus-types/validator.Uint64" + ]; + // builders is the per-key builder list from the keymanager validator config + // endpoint. Each entry may override the builder-level fallbacks below. + repeated BuilderEntry builders = 5; + optional string proxy = 6; + optional uint64 min_bid = 7 [ + (ethereum.eth.ext.cast_type) = + "github.com/OffchainLabs/prysm/v7/consensus-types/validator.Uint64" + ]; + optional uint64 builder_boost_factor = 8 [ + (ethereum.eth.ext.cast_type) = + "github.com/OffchainLabs/prysm/v7/consensus-types/validator.Uint64" + ]; +} + +// BuilderEntry is a single builder in a proposer's per-key builder list. Fields +// left unset inherit from the enclosing BuilderConfig, then default_config. +message BuilderEntry { + string url = 1; + optional bytes pubkey = 2; + optional bytes auth_data = 3; + optional string proxy = 4; + optional uint64 min_bid = 5 [ + (ethereum.eth.ext.cast_type) = + "github.com/OffchainLabs/prysm/v7/consensus-types/validator.Uint64" + ]; + optional uint64 max_execution_payment = 6 [ + (ethereum.eth.ext.cast_type) = + "github.com/OffchainLabs/prysm/v7/consensus-types/validator.Uint64" + ]; + optional uint64 builder_boost_factor = 7 [ (ethereum.eth.ext.cast_type) = "github.com/OffchainLabs/prysm/v7/consensus-types/validator.Uint64" ]; diff --git a/validator/client/BUILD.bazel b/validator/client/BUILD.bazel index 4460d0f87a82..aea46f0955cf 100644 --- a/validator/client/BUILD.bazel +++ b/validator/client/BUILD.bazel @@ -52,6 +52,7 @@ go_library( "//consensus-types/blocks:go_default_library", "//consensus-types/interfaces:go_default_library", "//consensus-types/primitives:go_default_library", + "//consensus-types/validator:go_default_library", "//container/slice:go_default_library", "//crypto/bls:go_default_library", "//crypto/hash:go_default_library", diff --git a/validator/client/propose.go b/validator/client/propose.go index 9681cc7f41f2..70fb0de590a5 100644 --- a/validator/client/propose.go +++ b/validator/client/propose.go @@ -571,21 +571,8 @@ func (v *validator) SetGraffiti(ctx context.Context, pubkey [fieldparams.BLSPubk if v.proposerSettings != nil { settings = v.proposerSettings.Clone() } - if settings.ProposeConfig == nil { - settings.ProposeConfig = map[[48]byte]*proposer.Option{pubkey: {GraffitiConfig: &proposer.GraffitiConfig{Graffiti: string(graffiti)}}} - return v.SetProposerSettings(ctx, settings) - } - option, ok := settings.ProposeConfig[pubkey] - if !ok || option == nil { - settings.ProposeConfig[pubkey] = &proposer.Option{GraffitiConfig: &proposer.GraffitiConfig{ - Graffiti: string(graffiti), - }} - } else { - option.GraffitiConfig = &proposer.GraffitiConfig{ - Graffiti: string(graffiti), - } - } - return v.SetProposerSettings(ctx, settings) // save the proposer settings + settings.UpsertProposeOption(pubkey).GraffitiConfig = &proposer.GraffitiConfig{Graffiti: string(graffiti)} + return v.SetProposerSettings(ctx, settings) } func (v *validator) DeleteGraffiti(ctx context.Context, pubKey [fieldparams.BLSPubkeyLength]byte) error { diff --git a/validator/client/runner_test.go b/validator/client/runner_test.go index 7eab7a39af14..a5547c53179b 100644 --- a/validator/client/runner_test.go +++ b/validator/client/runner_test.go @@ -11,6 +11,12 @@ import ( "time" "github.com/OffchainLabs/go-bitfield" + "github.com/ethereum/go-ethereum/common" + "github.com/pkg/errors" + "github.com/sirupsen/logrus" + logTest "github.com/sirupsen/logrus/hooks/test" + "go.uber.org/mock/gomock" + "github.com/OffchainLabs/prysm/v7/async/event" fieldparams "github.com/OffchainLabs/prysm/v7/config/fieldparams" "github.com/OffchainLabs/prysm/v7/config/params" @@ -30,11 +36,6 @@ import ( testing2 "github.com/OffchainLabs/prysm/v7/validator/db/testing" "github.com/OffchainLabs/prysm/v7/validator/keymanager" "github.com/OffchainLabs/prysm/v7/validator/keymanager/local" - "github.com/ethereum/go-ethereum/common" - "github.com/pkg/errors" - "github.com/sirupsen/logrus" - logTest "github.com/sirupsen/logrus/hooks/test" - "go.uber.org/mock/gomock" ) func cancelledContext() context.Context { @@ -570,7 +571,6 @@ func TestRunnerPushesProposerSettings_ValidContext(t *testing.T) { BuilderConfig: &proposer.BuilderConfig{ Enabled: true, GasLimit: 60_000_000, - Relays: []string{"https://example.com"}, }, GraffitiConfig: &proposer.GraffitiConfig{ Graffiti: "foobar", diff --git a/validator/client/validator.go b/validator/client/validator.go index 61513898e44f..4299b091a67b 100644 --- a/validator/client/validator.go +++ b/validator/client/validator.go @@ -28,6 +28,7 @@ import ( "github.com/OffchainLabs/prysm/v7/config/params" "github.com/OffchainLabs/prysm/v7/config/proposer" "github.com/OffchainLabs/prysm/v7/consensus-types/primitives" + validatortypes "github.com/OffchainLabs/prysm/v7/consensus-types/validator" "github.com/OffchainLabs/prysm/v7/crypto/hash" "github.com/OffchainLabs/prysm/v7/encoding/bytesutil" "github.com/OffchainLabs/prysm/v7/monitoring/tracing/trace" @@ -887,7 +888,7 @@ func (v *validator) PushProposerSettings(ctx context.Context, slot primitives.Sl v.connTracker.confirm(proposerPrefsPush, connGen) } - if reqs := v.buildBuilderPreferenceRequests(ctx, km, slot); len(reqs) > 0 { + if reqs := v.warmBuilderRequestAuths(ctx, km, slot); len(reqs) > 0 { delay := params.BeaconConfig().SlotDuration() / 2 time.AfterFunc(delay, func() { // Detached from the slot context, which may expire before the delay elapses. @@ -1136,8 +1137,10 @@ func (v *validator) buildProposerSettingsRequests( // upgradeProposerSettingsToV2 migrates v1 proposer settings to v2 and persists // them. Callers must gate this on gloas-active so the pre-gloas registration // path still sees BuilderConfig. +// Upgrades a clone then swaps, so concurrent readers never observe a +// half-migrated settings object. func (v *validator) upgradeProposerSettingsToV2(ctx context.Context) { - ps := v.ProposerSettings() + ps := v.ProposerSettings().Clone() if !ps.UpgradeToV2() { return } @@ -1358,28 +1361,86 @@ func (v *validator) submittedPrefSlotsCount() int { return len(v.submittedPrefSlots) } -func (v *validator) builderConfigForKey(pk pubkey) ([]string, uint64, bool) { +// builderTarget is one resolved builder for a proposer: a URL plus the +// preferences to submit for it, with per-entry overrides already applied. +type builderTarget struct { + url string + authData []byte + pubkey []byte + maxPayment uint64 + minBid *uint64 + boostFactor *uint64 +} + +// builderConfigForKey returns pk's effective builder config: per-key fields win, +// unset fields inherit from default_config (field-level inheritance). +func (v *validator) builderConfigForKey(pk pubkey) *proposer.BuilderConfig { ps := v.ProposerSettings() if ps == nil { - return nil, 0, false + return nil } - var bc *proposer.BuilderConfig + var def, perKey *proposer.BuilderConfig if ps.DefaultConfig != nil { - bc = ps.DefaultConfig.BuilderConfig + def = ps.DefaultConfig.BuilderConfig } if ps.ProposeConfig != nil { - if c, ok := ps.ProposeConfig[pk]; ok && c != nil && c.BuilderConfig != nil { - bc = c.BuilderConfig + if c, ok := ps.ProposeConfig[pk]; ok && c != nil { + perKey = c.BuilderConfig } } - if bc == nil { - return nil, 0, false + return proposer.EffectiveBuilderConfig(perKey, def) +} + +// builderTargetsForKey resolves the enabled builder list for pk; each entry +// overrides the config-level fallbacks. +// TODO(gloas): resolved minBid/boostFactor/pubkey/authData and url-less entries +// take effect with the inline produce-block preferences wire (beacon-APIs #630). +func (v *validator) builderTargetsForKey(pk pubkey) []builderTarget { + bc := v.builderConfigForKey(pk) + if !bc.IsEnabled() { + return nil + } + // Unset resolves to 0, trustless-only. + var fbMax uint64 + if bc.MaxExecutionPayment != nil { + fbMax = uint64(*bc.MaxExecutionPayment) + } + fbMin := uint64Ptr(bc.MinBid) + fbBoost := uint64Ptr(bc.BuilderBoostFactor) + + targets := make([]builderTarget, 0, len(bc.Builders)) + seen := make(map[string]bool, len(bc.Builders)) + for _, e := range bc.Builders { + if e == nil || e.URL == "" || seen[e.URL] { + continue + } + seen[e.URL] = true + t := builderTarget{url: e.URL, authData: e.AuthData, pubkey: e.Pubkey, maxPayment: fbMax, minBid: fbMin, boostFactor: fbBoost} + if e.MaxExecutionPayment != nil { + t.maxPayment = uint64(*e.MaxExecutionPayment) + } + if e.MinBid != nil { + t.minBid = uint64Ptr(e.MinBid) + } + if e.BuilderBoostFactor != nil { + t.boostFactor = uint64Ptr(e.BuilderBoostFactor) + } + targets = append(targets, t) + } + return targets +} + +func uint64Ptr(v *validatortypes.Uint64) *uint64 { + if v == nil { + return nil } - return bc.Relays, uint64(bc.MaxExecutionPayment), bc.Enabled + u := uint64(*v) + return &u } -// Resubmitted every push to repopulate a restarted beacon node, using cached auths to avoid re-signing. -func (v *validator) buildBuilderPreferenceRequests(ctx context.Context, km keymanager.IKeymanager, slot primitives.Slot) []*ethpb.SubmitBuilderPreferencesRequest { +// warmBuilderRequestAuths pre-signs request auths for upcoming proposal slots and +// returns the ahead-of-time preference submissions, rebuilt every push. +func (v *validator) warmBuilderRequestAuths(ctx context.Context, km keymanager.IKeymanager, slot primitives.Slot) []*ethpb.SubmitBuilderPreferencesRequest { if slots.ToEpoch(slot)+1 < params.BeaconConfig().GloasForkEpoch { return nil } @@ -1388,24 +1449,23 @@ func (v *validator) buildBuilderPreferenceRequests(ctx context.Context, km keyma return nil } v.pruneSignedRequestAuths(slot) - - reqs := v.builderPreferenceRequestsForDuties(ctx, km, slot, snap.currentDuties()) - return append(reqs, v.builderPreferenceRequestsForDuties(ctx, km, slot, snap.nextDuties())...) + reqs := v.warmBuilderRequestAuthsForDuties(ctx, km, slot, snap.currentDuties()) + return append(reqs, v.warmBuilderRequestAuthsForDuties(ctx, km, slot, snap.nextDuties())...) } -func (v *validator) builderPreferenceRequestsForDuties(ctx context.Context, km keymanager.IKeymanager, slot primitives.Slot, duties iter.Seq2[pubkey, *ethpb.ValidatorDuty]) []*ethpb.SubmitBuilderPreferencesRequest { +func (v *validator) warmBuilderRequestAuthsForDuties(ctx context.Context, km keymanager.IKeymanager, slot primitives.Slot, duties iter.Seq2[pubkey, *ethpb.ValidatorDuty]) []*ethpb.SubmitBuilderPreferencesRequest { var reqs []*ethpb.SubmitBuilderPreferencesRequest for pk, duty := range duties { - relays, maxPayment, enabled := v.builderConfigForKey(pk) - if !enabled || len(relays) == 0 { + targets := v.builderTargetsForKey(pk) + if len(targets) == 0 { continue } for _, proposalSlot := range duty.ProposerSlots { if proposalSlot <= slot { continue } - for _, relay := range relays { - signed, err := v.signRequestAuthCached(ctx, km, pk, relay, proposalSlot) + for _, t := range targets { + signed, err := v.signRequestAuthCached(ctx, km, pk, t.url, proposalSlot) if err != nil { log.WithError(err).Warn("Failed to sign builder request auth") continue @@ -1413,7 +1473,7 @@ func (v *validator) builderPreferenceRequestsForDuties(ctx context.Context, km k reqs = append(reqs, ðpb.SubmitBuilderPreferencesRequest{ ValidatorPubkey: pk[:], Request: ðpb.BuilderPreferencesRequestV1{ - Preferences: ðpb.BuilderPreferencesV1{MaxExecutionPayment: primitives.Gwei(maxPayment)}, + Preferences: ðpb.BuilderPreferencesV1{MaxExecutionPayment: primitives.Gwei(t.maxPayment)}, Auth: signed, }, }) @@ -1485,8 +1545,13 @@ func (v *validator) buildSignedRegReqs( return signedValRegRequests } + isV2 := v.ProposerSettings().Version == proposer.SchemaV2 if v.ProposerSettings().DefaultConfig != nil && v.ProposerSettings().DefaultConfig.FeeRecipientConfig == nil && v.ProposerSettings().DefaultConfig.BuilderConfig != nil { - log.Warn("Builder is `enabled` in default config but will be ignored because no fee recipient was provided!") + if isV2 { + log.Warn("Default builder config has no default fee recipient; only keys with their own fee recipient can register") + } else { + log.Warn("Builder is `enabled` in default config but will be ignored because no fee recipient was provided!") + } } for i, k := range activePubkeys { @@ -1496,37 +1561,60 @@ func (v *validator) buildSignedRegReqs( continue } + var enabled bool feeRecipient := common.HexToAddress(params.BeaconConfig().EthBurnAddressHex) gasLimit := params.BeaconConfig().DefaultBuilderGasLimit - enabled := false - if v.ProposerSettings().DefaultConfig != nil && v.ProposerSettings().DefaultConfig.FeeRecipientConfig != nil { - defaultConfig := v.ProposerSettings().DefaultConfig - feeRecipient = defaultConfig.FeeRecipientConfig.FeeRecipient // Use cli defaultBuilderConfig for fee recipient. - defaultBuilderConfig := defaultConfig.BuilderConfig - - if defaultBuilderConfig != nil && defaultBuilderConfig.Enabled { - gasLimit = uint64(defaultBuilderConfig.GasLimit) // Use cli config for gas limit. - enabled = true + if isV2 { + hasFeeRecipient := false + var defBC, perKeyBC *proposer.BuilderConfig + if defaultConfig := v.ProposerSettings().DefaultConfig; defaultConfig != nil { + defBC = defaultConfig.BuilderConfig + if defaultConfig.FeeRecipientConfig != nil { + feeRecipient = defaultConfig.FeeRecipientConfig.FeeRecipient + hasFeeRecipient = true + } } - } - - if v.ProposerSettings().ProposeConfig != nil { - config, ok := v.ProposerSettings().ProposeConfig[k] - if ok && config != nil && config.FeeRecipientConfig != nil { - feeRecipient = config.FeeRecipientConfig.FeeRecipient // Use file config for fee recipient. - builderConfig := config.BuilderConfig - if builderConfig != nil { - if builderConfig.Enabled { - gasLimit = uint64(builderConfig.GasLimit) // Use file config for gas limit. - enabled = true - } else { - enabled = false // Custom config can disable validator from register. + if v.ProposerSettings().ProposeConfig != nil { + if config, ok := v.ProposerSettings().ProposeConfig[k]; ok && config != nil { + perKeyBC = config.BuilderConfig + if config.FeeRecipientConfig != nil { + feeRecipient = config.FeeRecipientConfig.FeeRecipient + hasFeeRecipient = true + } + } + } + // Field-level resolution: per-key builder fields inherit from the default; + // registration still requires a fee recipient configured at some level. + bc := proposer.EffectiveBuilderConfig(perKeyBC, defBC) + enabled = bc.IsEnabled() && hasFeeRecipient + if bc.GasLimit != 0 { + gasLimit = uint64(bc.GasLimit) + } + } else { + // V1 settings keep the pre-v2 object-level semantics so existing + // registrations are unchanged; inheritance starts at SchemaV2. + if defaultConfig := v.ProposerSettings().DefaultConfig; defaultConfig != nil && defaultConfig.FeeRecipientConfig != nil { + feeRecipient = defaultConfig.FeeRecipientConfig.FeeRecipient + if defaultConfig.BuilderConfig.IsEnabled() { + gasLimit = uint64(defaultConfig.BuilderConfig.GasLimit) + enabled = true + } + } + if v.ProposerSettings().ProposeConfig != nil { + if config, ok := v.ProposerSettings().ProposeConfig[k]; ok && config != nil && config.FeeRecipientConfig != nil { + feeRecipient = config.FeeRecipientConfig.FeeRecipient + if bc := config.BuilderConfig; bc != nil { + if bc.IsEnabled() { + gasLimit = uint64(bc.GasLimit) + enabled = true + } else { + enabled = false + } } } } } - if !enabled { continue } diff --git a/validator/client/validator_test.go b/validator/client/validator_test.go index fb3fe9d643c1..7e314da2deb1 100644 --- a/validator/client/validator_test.go +++ b/validator/client/validator_test.go @@ -3264,7 +3264,7 @@ func TestValidator_buildSignedRegReqs_DefaultConfigDisabled(t *testing.T) { client := validatormock.NewMockValidatorClient(ctrl) signature := blsmock.NewMockSignature(ctrl) - signature.EXPECT().Marshal().Return([]byte{}) + signature.EXPECT().Marshal().Return([]byte{}).AnyTimes() v := validator{ signedValidatorRegistrations: map[[48]byte]*ethpb.SignedValidatorRegistrationV1{}, @@ -3336,7 +3336,51 @@ func TestValidator_buildSignedRegReqs_DefaultConfigDisabled(t *testing.T) { assert.DeepEqual(t, feeRecipient1[:], actual[0].Message.FeeRecipient) assert.Equal(t, uint64(1111), actual[0].Message.GasLimit) assert.DeepEqual(t, pubkey1[:], actual[0].Message.Pubkey) +} + +// The same settings shape registers differently by schema version: v1 keeps the +// legacy object-level semantics, v2 applies field-level inheritance. +func TestValidator_buildSignedRegReqs_VersionGatesInheritance(t *testing.T) { + pubkey1 := pubkeyFromString(t, "0x111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111") + defaultFeeRecipient := feeRecipientFromString(t, "0xdddddddddddddddddddddddddddddddddddddddd") + + ctrl := gomock.NewController(t) + defer ctrl.Finish() + ctx := t.Context() + + signature := blsmock.NewMockSignature(ctrl) + signature.EXPECT().Marshal().Return([]byte{}).AnyTimes() + signer := func(_ context.Context, _ *validatorpb.SignRequest) (bls.Signature, error) { + return signature, nil + } + + newSettings := func(version uint32) *proposer.Settings { + return &proposer.Settings{ + Version: version, + DefaultConfig: &proposer.Option{ + FeeRecipientConfig: &proposer.FeeRecipientConfig{FeeRecipient: defaultFeeRecipient}, + BuilderConfig: &proposer.BuilderConfig{Enabled: true, GasLimit: 9999}, + }, + ProposeConfig: map[[48]byte]*proposer.Option{ + // Per-key disable without a per-key fee recipient. + pubkey1: {BuilderConfig: &proposer.BuilderConfig{Enabled: false}}, + }, + } + } + v := validator{ + signedValidatorRegistrations: map[[48]byte]*ethpb.SignedValidatorRegistrationV1{}, + pubkeyToStatus: map[[48]byte]*validatorStatus{ + pubkey1: {publicKey: pubkey1[:], status: ðpb.ValidatorStatusResponse{Status: ethpb.ValidatorStatus_ACTIVE}}, + }, + } + pubkeys := [][fieldparams.BLSPubkeyLength]byte{pubkey1} + + v.proposerSettings = newSettings(0) + assert.Equal(t, 1, len(v.buildSignedRegReqs(ctx, pubkeys, signer, 0, false))) + v.proposerSettings = newSettings(proposer.SchemaV2) + v.signedValidatorRegistrations = map[[48]byte]*ethpb.SignedValidatorRegistrationV1{} + assert.Equal(t, 0, len(v.buildSignedRegReqs(ctx, pubkeys, signer, 0, false))) } func TestValidator_buildSignedRegReqs_DefaultConfigEnabled(t *testing.T) { diff --git a/validator/db/BUILD.bazel b/validator/db/BUILD.bazel index ad4ae74a13f4..131cb9964289 100644 --- a/validator/db/BUILD.bazel +++ b/validator/db/BUILD.bazel @@ -57,5 +57,6 @@ go_test( "@com_github_ethereum_go_ethereum//common/hexutil:go_default_library", "@com_github_sirupsen_logrus//hooks/test:go_default_library", "@com_github_urfave_cli_v2//:go_default_library", + "@org_golang_google_protobuf//proto:go_default_library", ], ) diff --git a/validator/db/convert_test.go b/validator/db/convert_test.go index 059a7172654d..2e4fbb933ba1 100644 --- a/validator/db/convert_test.go +++ b/validator/db/convert_test.go @@ -5,6 +5,8 @@ import ( "path/filepath" "testing" + "github.com/ethereum/go-ethereum/common/hexutil" + fieldparams "github.com/OffchainLabs/prysm/v7/config/fieldparams" "github.com/OffchainLabs/prysm/v7/config/proposer" "github.com/OffchainLabs/prysm/v7/consensus-types/primitives" @@ -15,7 +17,6 @@ import ( "github.com/OffchainLabs/prysm/v7/validator/db/filesystem" "github.com/OffchainLabs/prysm/v7/validator/db/iface" "github.com/OffchainLabs/prysm/v7/validator/db/kv" - "github.com/ethereum/go-ethereum/common/hexutil" ) func getPubkeyFromString(t *testing.T, pubkeyString string) [fieldparams.BLSPubkeyLength]byte { @@ -97,7 +98,6 @@ func TestDB_ConvertDatabase(t *testing.T) { require.NoError(t, err, "could not save graffiti ordered index") // Save the proposer settings. - var relays []string = nil expectedProposerSettings := &proposer.Settings{} if withProposerSettings { @@ -110,7 +110,6 @@ func TestDB_ConvertDatabase(t *testing.T) { BuilderConfig: &proposer.BuilderConfig{ Enabled: true, GasLimit: 42, - Relays: relays, }, }, }, @@ -121,7 +120,6 @@ func TestDB_ConvertDatabase(t *testing.T) { BuilderConfig: &proposer.BuilderConfig{ Enabled: false, GasLimit: 43, - Relays: relays, }, }, } diff --git a/validator/db/filesystem/BUILD.bazel b/validator/db/filesystem/BUILD.bazel index e09134c5dca6..798db1eeea80 100644 --- a/validator/db/filesystem/BUILD.bazel +++ b/validator/db/filesystem/BUILD.bazel @@ -55,6 +55,7 @@ go_test( "//config/proposer:go_default_library", "//consensus-types/blocks:go_default_library", "//consensus-types/primitives:go_default_library", + "//consensus-types/validator:go_default_library", "//crypto/bls:go_default_library", "//io/file:go_default_library", "//proto/prysm/v1alpha1:go_default_library", @@ -67,5 +68,6 @@ go_test( "//validator/testing:go_default_library", "@com_github_ethereum_go_ethereum//common:go_default_library", "@com_github_ethereum_go_ethereum//common/hexutil:go_default_library", + "@org_golang_google_protobuf//proto:go_default_library", ], ) diff --git a/validator/db/filesystem/db.go b/validator/db/filesystem/db.go index 1ec43f30a282..9eac85b02ac4 100644 --- a/validator/db/filesystem/db.go +++ b/validator/db/filesystem/db.go @@ -239,23 +239,6 @@ func (s *Store) configuration() (*Configuration, error) { return nil, errors.Wrapf(err, "could not unmarshal %s", cleanedConfigFilePath) } - // yaml.Unmarshal converts nil array to empty array. - // To get the same behavior as the BoltDB implementation, we need to convert empty array to nil. - if config.ProposerSettings != nil && - config.ProposerSettings.DefaultConfig != nil && - config.ProposerSettings.DefaultConfig.Builder != nil && - len(config.ProposerSettings.DefaultConfig.Builder.Relays) == 0 { - config.ProposerSettings.DefaultConfig.Builder.Relays = nil - } - - if config.ProposerSettings != nil && config.ProposerSettings.ProposerConfig != nil { - for _, option := range config.ProposerSettings.ProposerConfig { - if option.Builder != nil && len(option.Builder.Relays) == 0 { - option.Builder.Relays = nil - } - } - } - return config, nil } diff --git a/validator/db/filesystem/proposer_settings_test.go b/validator/db/filesystem/proposer_settings_test.go index 1e3de64924ac..ad68c57c722c 100644 --- a/validator/db/filesystem/proposer_settings_test.go +++ b/validator/db/filesystem/proposer_settings_test.go @@ -5,8 +5,10 @@ import ( fieldparams "github.com/OffchainLabs/prysm/v7/config/fieldparams" "github.com/OffchainLabs/prysm/v7/config/proposer" + "github.com/OffchainLabs/prysm/v7/consensus-types/validator" validatorpb "github.com/OffchainLabs/prysm/v7/proto/prysm/v1alpha1/validator-client" "github.com/OffchainLabs/prysm/v7/testing/require" + "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/common/hexutil" ) @@ -226,3 +228,39 @@ func TestStore_SaveProposerSettings(t *testing.T) { }) } } + +// v2 settings with a builders list must survive a filesystem round-trip: Version +// preserved and optional entry bytes normalized to nil (matching the bolt backend). +func TestStore_ProposerSettings_V2BuildersRoundTrip(t *testing.T) { + ctx := t.Context() + u64 := func(v uint64) *validator.Uint64 { u := validator.Uint64(v); return &u } + key := getPubkeyFromString(t, "0xa057816155ad77931185101128655c0191bd0214c201ca48ed887f6c4c6adf334070efcd75140eada5ac83a92506dd7a") + + in := &proposer.Settings{ + Version: proposer.SchemaV2, + ProposeConfig: map[[fieldparams.BLSPubkeyLength]byte]*proposer.Option{ + key: { + FeeRecipientConfig: &proposer.FeeRecipientConfig{FeeRecipient: common.HexToAddress("0x50155530FCE8a85ec7055A5F8b2bE214B3DaeFd3")}, + GasLimit: 40000000, + BuilderConfig: &proposer.BuilderConfig{ + Enabled: true, + Builders: []*proposer.BuilderEntry{ + {URL: "https://builder-a.example", AuthData: []byte("secret"), MaxExecutionPayment: u64(1000000000)}, + {URL: "https://builder-b.example"}, + }, + }, + }, + }, + } + + store, err := NewStore(t.TempDir(), nil) + require.NoError(t, err) + require.NoError(t, store.SaveProposerSettings(ctx, in)) + + got, err := store.ProposerSettings(ctx) + require.NoError(t, err) + require.DeepEqual(t, in, got) + + // The entry without auth_data must load back as nil, not empty-non-nil. + require.IsNil(t, got.ProposeConfig[key].BuilderConfig.Builders[1].AuthData) +} diff --git a/validator/db/kv/BUILD.bazel b/validator/db/kv/BUILD.bazel index 7c085773e8f1..89f09b7ac5be 100644 --- a/validator/db/kv/BUILD.bazel +++ b/validator/db/kv/BUILD.bazel @@ -95,5 +95,6 @@ go_test( "@com_github_sirupsen_logrus//:go_default_library", "@com_github_sirupsen_logrus//hooks/test:go_default_library", "@io_etcd_go_bbolt//:go_default_library", + "@org_golang_google_protobuf//proto:go_default_library", ], ) diff --git a/validator/db/kv/proposer_settings_test.go b/validator/db/kv/proposer_settings_test.go index b32a3d87267c..7e31529a972e 100644 --- a/validator/db/kv/proposer_settings_test.go +++ b/validator/db/kv/proposer_settings_test.go @@ -3,14 +3,15 @@ package kv import ( "testing" + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/common/hexutil" + fieldparams "github.com/OffchainLabs/prysm/v7/config/fieldparams" "github.com/OffchainLabs/prysm/v7/config/params" "github.com/OffchainLabs/prysm/v7/config/proposer" "github.com/OffchainLabs/prysm/v7/consensus-types/validator" "github.com/OffchainLabs/prysm/v7/encoding/bytesutil" "github.com/OffchainLabs/prysm/v7/testing/require" - "github.com/ethereum/go-ethereum/common" - "github.com/ethereum/go-ethereum/common/hexutil" ) func TestStore_ProposerSettings_ReadAndWrite(t *testing.T) { diff --git a/validator/rpc/BUILD.bazel b/validator/rpc/BUILD.bazel index 5b08ca61782c..a0dc809979e8 100644 --- a/validator/rpc/BUILD.bazel +++ b/validator/rpc/BUILD.bazel @@ -12,6 +12,7 @@ go_library( "handlers_health.go", "handlers_keymanager.go", "handlers_slashing.go", + "handlers_validator_config.go", "intercepter.go", "log.go", "server.go", @@ -94,6 +95,7 @@ go_test( "handlers_health_test.go", "handlers_keymanager_test.go", "handlers_slashing_test.go", + "handlers_validator_config_test.go", "intercepter_test.go", "server_test.go", ], @@ -148,6 +150,7 @@ go_test( "@org_golang_google_grpc//codes:go_default_library", "@org_golang_google_grpc//metadata:go_default_library", "@org_golang_google_grpc//status:go_default_library", + "@org_golang_google_protobuf//proto:go_default_library", "@org_golang_google_protobuf//types/known/emptypb:go_default_library", "@org_golang_google_protobuf//types/known/timestamppb:go_default_library", "@org_uber_go_mock//gomock:go_default_library", diff --git a/validator/rpc/handlers_keymanager.go b/validator/rpc/handlers_keymanager.go index 87adc93c9ef0..dce5d4d99e23 100644 --- a/validator/rpc/handlers_keymanager.go +++ b/validator/rpc/handlers_keymanager.go @@ -624,53 +624,14 @@ func (s *Server) SetFeeRecipientByPubkey(w http.ResponseWriter, r *http.Request) return } feeRecipient := common.BytesToAddress(ethAddress) + s.proposerSettingsLock.Lock() + defer s.proposerSettingsLock.Unlock() settings := s.validatorService.ProposerSettings() - switch { - case settings == nil: - settings = &proposer.Settings{ - ProposeConfig: map[[fieldparams.BLSPubkeyLength]byte]*proposer.Option{ - bytesutil.ToBytes48(pubkey): { - FeeRecipientConfig: &proposer.FeeRecipientConfig{ - FeeRecipient: feeRecipient, - }, - BuilderConfig: nil, - }, - }, - DefaultConfig: nil, - } - case settings.ProposeConfig == nil: - var builderConfig *proposer.BuilderConfig - if settings.DefaultConfig != nil && settings.DefaultConfig.BuilderConfig != nil { - builderConfig = settings.DefaultConfig.BuilderConfig.Clone() - } - settings.ProposeConfig = map[[fieldparams.BLSPubkeyLength]byte]*proposer.Option{ - bytesutil.ToBytes48(pubkey): { - FeeRecipientConfig: &proposer.FeeRecipientConfig{ - FeeRecipient: feeRecipient, - }, - BuilderConfig: builderConfig, - }, - } - default: - proposerOption, found := settings.ProposeConfig[bytesutil.ToBytes48(pubkey)] - if found && proposerOption != nil { - proposerOption.FeeRecipientConfig = &proposer.FeeRecipientConfig{ - FeeRecipient: feeRecipient, - } - } else { - var builderConfig = &proposer.BuilderConfig{} - if settings.DefaultConfig != nil && settings.DefaultConfig.BuilderConfig != nil { - builderConfig = settings.DefaultConfig.BuilderConfig.Clone() - } - settings.ProposeConfig[bytesutil.ToBytes48(pubkey)] = &proposer.Option{ - FeeRecipientConfig: &proposer.FeeRecipientConfig{ - FeeRecipient: feeRecipient, - }, - BuilderConfig: builderConfig, - } - } + if settings == nil { + settings = &proposer.Settings{} } - // save the settings + // A newly created option leaves BuilderConfig nil so the key inherits default_config. + settings.UpsertProposeOption(bytesutil.ToBytes48(pubkey)).FeeRecipientConfig = &proposer.FeeRecipientConfig{FeeRecipient: feeRecipient} if err := s.validatorService.SetProposerSettings(ctx, settings); err != nil { httputil.HandleError(w, "Could not set proposer settings: "+err.Error(), http.StatusInternalServerError) return @@ -693,6 +654,8 @@ func (s *Server) DeleteFeeRecipientByPubkey(w http.ResponseWriter, r *http.Reque return } + s.proposerSettingsLock.Lock() + defer s.proposerSettingsLock.Unlock() settings := s.validatorService.ProposerSettings() if settings != nil && settings.ProposeConfig != nil { proposerOption, found := settings.ProposeConfig[bytesutil.ToBytes48(pubkey)] @@ -764,6 +727,8 @@ func (s *Server) SetGasLimit(w http.ResponseWriter, r *http.Request) { return } + s.proposerSettingsLock.Lock() + defer s.proposerSettingsLock.Unlock() settings := s.validatorService.ProposerSettings() if err := settings.SetGasLimit(bytesutil.ToBytes48(pubkey), validator.Uint64(gasLimit)); err != nil { httputil.HandleError(w, err.Error(), http.StatusInternalServerError) @@ -791,6 +756,8 @@ func (s *Server) DeleteGasLimit(w http.ResponseWriter, r *http.Request) { return } + s.proposerSettingsLock.Lock() + defer s.proposerSettingsLock.Unlock() settings := s.validatorService.ProposerSettings() if !settings.ResetGasLimit(bytesutil.ToBytes48(pubkey)) { httputil.HandleError(w, fmt.Sprintf("No gas limit found for pubkey %q", rawPubkey), http.StatusNotFound) @@ -860,6 +827,9 @@ func (s *Server) SetGraffiti(w http.ResponseWriter, r *http.Request) { return } + // Graffiti writes proposer settings, so it serializes with the other writers. + s.proposerSettingsLock.Lock() + defer s.proposerSettingsLock.Unlock() if err := s.validatorService.SetGraffiti(ctx, bytesutil.ToBytes48(pubkey), []byte(req.Graffiti)); err != nil { httputil.HandleError(w, err.Error(), http.StatusInternalServerError) return @@ -880,6 +850,8 @@ func (s *Server) DeleteGraffiti(w http.ResponseWriter, r *http.Request) { return } + s.proposerSettingsLock.Lock() + defer s.proposerSettingsLock.Unlock() if err := s.validatorService.DeleteGraffiti(ctx, bytesutil.ToBytes48(pubkey)); err != nil { httputil.HandleError(w, err.Error(), http.StatusNotFound) return diff --git a/validator/rpc/handlers_keymanager_test.go b/validator/rpc/handlers_keymanager_test.go index d01e742bf271..51db40cfe5ba 100644 --- a/validator/rpc/handlers_keymanager_test.go +++ b/validator/rpc/handlers_keymanager_test.go @@ -13,6 +13,13 @@ import ( "testing" "time" + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/common/hexutil" + "github.com/urfave/cli/v2" + "go.uber.org/mock/gomock" + "google.golang.org/protobuf/types/known/emptypb" + "google.golang.org/protobuf/types/known/timestamppb" + "github.com/OffchainLabs/prysm/v7/cmd/validator/flags" fieldparams "github.com/OffchainLabs/prysm/v7/config/fieldparams" "github.com/OffchainLabs/prysm/v7/config/params" @@ -39,12 +46,6 @@ import ( remoteweb3signer "github.com/OffchainLabs/prysm/v7/validator/keymanager/remote-web3signer" "github.com/OffchainLabs/prysm/v7/validator/slashing-protection-history/format" mocks "github.com/OffchainLabs/prysm/v7/validator/testing" - "github.com/ethereum/go-ethereum/common" - "github.com/ethereum/go-ethereum/common/hexutil" - "github.com/urfave/cli/v2" - "go.uber.org/mock/gomock" - "google.golang.org/protobuf/types/known/emptypb" - "google.golang.org/protobuf/types/known/timestamppb" ) func TestServer_ListKeystores(t *testing.T) { diff --git a/validator/rpc/handlers_validator_config.go b/validator/rpc/handlers_validator_config.go new file mode 100644 index 000000000000..48b571f4e165 --- /dev/null +++ b/validator/rpc/handlers_validator_config.go @@ -0,0 +1,325 @@ +package rpc + +import ( + "encoding/json" + "fmt" + "net/http" + "net/url" + "strconv" + + "github.com/OffchainLabs/prysm/v7/beacon-chain/rpc/eth/shared" + fieldparams "github.com/OffchainLabs/prysm/v7/config/fieldparams" + "github.com/OffchainLabs/prysm/v7/config/proposer" + "github.com/OffchainLabs/prysm/v7/consensus-types/validator" + "github.com/OffchainLabs/prysm/v7/encoding/bytesutil" + "github.com/OffchainLabs/prysm/v7/monitoring/tracing/trace" + "github.com/OffchainLabs/prysm/v7/network/httputil" + "github.com/ethereum/go-ethereum/common/hexutil" + "github.com/pkg/errors" +) + +const maxBuilderEntries = 64 + +// GetBuilders implements GET /eth/v1/validator/{pubkey}/builders (keymanager-APIs #88). +// It returns the key's builder configuration resolved against default_config so +// re-submitting the response reproduces the same behavior. +func (s *Server) GetBuilders(w http.ResponseWriter, r *http.Request) { + _, span := trace.StartSpan(r.Context(), "validator.keymanagerAPI.GetBuilders") + defer span.End() + + if s.validatorService == nil { + httputil.HandleError(w, "Validator service not ready.", http.StatusServiceUnavailable) + return + } + _, pubkey, ok := shared.HexFromRoute(w, r, "pubkey", fieldparams.BLSPubkeyLength) + if !ok { + return + } + + eff := resolveBuilders(s.validatorService.ProposerSettings(), bytesutil.ToBytes48(pubkey)) + out := builderConfigJSONFromConsensus(eff) + // The resolved response always states a concrete builders list. + if out.Builders == nil { + out.Builders = []*BuilderEntryJson{} + } + for _, e := range out.Builders { + if e.MinBid == nil { + e.MinBid = out.MinBid + } + if e.BuilderBoostFactor == nil { + e.BuilderBoostFactor = out.BuilderBoostFactor + } + } + httputil.WriteJson(w, out) +} + +// SetBuilders implements POST /eth/v1/validator/{pubkey}/builders. It replaces the +// key's builder configuration in full, leaving fee recipient, gas limit and graffiti +// untouched. +func (s *Server) SetBuilders(w http.ResponseWriter, r *http.Request) { + ctx, span := trace.StartSpan(r.Context(), "validator.keymanagerAPI.SetBuilders") + defer span.End() + + if s.validatorService == nil { + httputil.HandleError(w, "Validator service not ready.", http.StatusServiceUnavailable) + return + } + _, pubkey, ok := shared.HexFromRoute(w, r, "pubkey", fieldparams.BLSPubkeyLength) + if !ok { + return + } + + var body BuilderConfigJson + if err := json.NewDecoder(r.Body).Decode(&body); err != nil { + httputil.HandleError(w, "Could not decode builder config: "+err.Error(), http.StatusBadRequest) + return + } + if body.Enabled == nil { + httputil.HandleError(w, "enabled is required", http.StatusBadRequest) + return + } + bc, err := builderConfigFromJSON(&body) + if err != nil { + httputil.HandleError(w, err.Error(), http.StatusBadRequest) + return + } + + s.proposerSettingsLock.Lock() + defer s.proposerSettingsLock.Unlock() + + settings := s.validatorService.ProposerSettings() + if settings == nil { + settings = &proposer.Settings{Version: proposer.SchemaV2} + } else { + settings = settings.Clone() + settings.UpgradeToV2() + } + if settings.ProposeConfig == nil { + settings.ProposeConfig = make(map[[fieldparams.BLSPubkeyLength]byte]*proposer.Option) + } + key := bytesutil.ToBytes48(pubkey) + opt := settings.ProposeConfig[key] + if opt == nil { + opt = &proposer.Option{} + settings.ProposeConfig[key] = opt + } + opt.BuilderConfig = bc + + if err := s.validatorService.SetProposerSettings(ctx, settings); err != nil { + httputil.HandleError(w, "Could not set proposer settings: "+err.Error(), http.StatusInternalServerError) + return + } + w.WriteHeader(http.StatusAccepted) +} + +// DeleteBuilders implements DELETE /eth/v1/validator/{pubkey}/builders. It removes +// the key's builder configuration so the key follows the validator client defaults; +// this differs from enabled:false. +func (s *Server) DeleteBuilders(w http.ResponseWriter, r *http.Request) { + ctx, span := trace.StartSpan(r.Context(), "validator.keymanagerAPI.DeleteBuilders") + defer span.End() + + if s.validatorService == nil { + httputil.HandleError(w, "Validator service not ready.", http.StatusServiceUnavailable) + return + } + rawPubkey, pubkey, ok := shared.HexFromRoute(w, r, "pubkey", fieldparams.BLSPubkeyLength) + if !ok { + return + } + + s.proposerSettingsLock.Lock() + defer s.proposerSettingsLock.Unlock() + + settings := s.validatorService.ProposerSettings() + key := bytesutil.ToBytes48(pubkey) + if settings == nil || settings.ProposeConfig == nil { + httputil.HandleError(w, fmt.Sprintf("No builder configuration found for pubkey %q", rawPubkey), http.StatusNotFound) + return + } + opt, found := settings.ProposeConfig[key] + if !found || opt == nil || opt.BuilderConfig == nil { + httputil.HandleError(w, fmt.Sprintf("No builder configuration found for pubkey %q", rawPubkey), http.StatusNotFound) + return + } + + settings = settings.Clone() + settings.ProposeConfig[key].BuilderConfig = nil + if err := s.validatorService.SetProposerSettings(ctx, settings); err != nil { + httputil.HandleError(w, "Could not set proposer settings: "+err.Error(), http.StatusInternalServerError) + return + } + w.WriteHeader(http.StatusNoContent) +} + +func resolveBuilders(settings *proposer.Settings, key [fieldparams.BLSPubkeyLength]byte) *proposer.BuilderConfig { + var perKey, def *proposer.BuilderConfig + if settings != nil { + if settings.DefaultConfig != nil { + def = settings.DefaultConfig.BuilderConfig + } + if opt, ok := settings.ProposeConfig[key]; ok && opt != nil { + perKey = opt.BuilderConfig + } + } + if eff := proposer.EffectiveBuilderConfig(perKey, def); eff != nil { + return eff + } + return &proposer.BuilderConfig{} +} + +func builderConfigJSONFromConsensus(bc *proposer.BuilderConfig) *BuilderConfigJson { + enabled := bc.Enabled + out := &BuilderConfigJson{ + Enabled: &enabled, + MinBid: optUintStrPtr(bc.MinBid), + BuilderBoostFactor: optUintStrPtr(bc.BuilderBoostFactor), + } + if bc.Builders != nil { + out.Builders = make([]*BuilderEntryJson, 0, len(bc.Builders)) + for _, b := range bc.Builders { + out.Builders = append(out.Builders, builderEntryJSONFromConsensus(b)) + } + } + return out +} + +func builderEntryJSONFromConsensus(be *proposer.BuilderEntry) *BuilderEntryJson { + out := &BuilderEntryJson{ + MaxExecutionPayment: optUintStrPtr(be.MaxExecutionPayment), + MinBid: optUintStrPtr(be.MinBid), + BuilderBoostFactor: optUintStrPtr(be.BuilderBoostFactor), + } + if be.URL != "" { + out.Url = strPtr(be.URL) + } + if len(be.Pubkey) != 0 { + out.BuilderPubkey = strPtr(hexutil.Encode(be.Pubkey)) + } + if len(be.AuthData) != 0 { + out.AuthData = strPtr(hexutil.Encode(be.AuthData)) + } + return out +} + +func builderConfigFromJSON(in *BuilderConfigJson) (*proposer.BuilderConfig, error) { + bc := &proposer.BuilderConfig{Enabled: in.Enabled != nil && *in.Enabled} + if in.MinBid != nil { + v, err := parseUint(*in.MinBid, "min_bid") + if err != nil { + return nil, err + } + bc.MinBid = &v + } + if in.BuilderBoostFactor != nil { + v, err := parseUint(*in.BuilderBoostFactor, "builder_boost_factor") + if err != nil { + return nil, err + } + bc.BuilderBoostFactor = &v + } + if in.Builders == nil { + return bc, nil + } + if len(in.Builders) > maxBuilderEntries { + return nil, errors.Errorf("builders exceeds %d entries", maxBuilderEntries) + } + // Non-nil (possibly empty) list means "use exactly these builders", not "inherit". + // Uniqueness is scoped by role: url entries by (url, auth_data), + // url-less p2p-policy entries by builder_pubkey. + bc.Builders = make([]*proposer.BuilderEntry, 0, len(in.Builders)) + seenURL := make(map[string]bool, len(in.Builders)) + seenP2P := make(map[string]bool, len(in.Builders)) + for i, entry := range in.Builders { + be, err := builderEntryFromJSON(entry, i) + if err != nil { + return nil, err + } + if be.URL != "" { + key := fmt.Sprintf("%s|%x", be.URL, be.AuthData) + if seenURL[key] { + return nil, errors.Errorf("builders[%d]: two entries share the same url and auth_data", i) + } + seenURL[key] = true + } else if seenP2P[string(be.Pubkey)] { + return nil, errors.Errorf("builders[%d]: two p2p-policy entries share the same builder_pubkey", i) + } else { + seenP2P[string(be.Pubkey)] = true + } + bc.Builders = append(bc.Builders, be) + } + return bc, nil +} + +func builderEntryFromJSON(in *BuilderEntryJson, i int) (*proposer.BuilderEntry, error) { + be := &proposer.BuilderEntry{} + if in.Url != nil && *in.Url != "" { + if u, err := url.Parse(*in.Url); err != nil || u.Scheme == "" || u.Host == "" { + return nil, errors.Errorf("builders[%d].url is not a valid URL", i) + } + be.URL = *in.Url + } + if in.BuilderPubkey != nil { + pk, err := hexutil.Decode(*in.BuilderPubkey) + if err != nil || len(pk) != fieldparams.BLSPubkeyLength { + return nil, errors.Errorf("builders[%d].builder_pubkey is not a valid BLS public key", i) + } + be.Pubkey = pk + } + if be.URL == "" && len(be.Pubkey) == 0 { + return nil, errors.Errorf("builders[%d]: at least one of url and builder_pubkey is required", i) + } + if in.AuthData != nil { + ad, err := hexutil.Decode(*in.AuthData) + if err != nil { + return nil, errors.Errorf("builders[%d].auth_data is not valid hex", i) + } + if len(ad) > 4096 { + return nil, errors.Errorf("builders[%d].auth_data exceeds 4096 bytes", i) + } + be.AuthData = ad + } + if in.MaxExecutionPayment != nil { + v, err := parseUint(*in.MaxExecutionPayment, "builders[].max_execution_payment") + if err != nil { + return nil, err + } + be.MaxExecutionPayment = &v + } + if in.MinBid != nil { + v, err := parseUint(*in.MinBid, "builders[].min_bid") + if err != nil { + return nil, err + } + be.MinBid = &v + } + if in.BuilderBoostFactor != nil { + v, err := parseUint(*in.BuilderBoostFactor, "builders[].builder_boost_factor") + if err != nil { + return nil, err + } + be.BuilderBoostFactor = &v + } + return be, nil +} + +func parseUint(s, field string) (validator.Uint64, error) { + v, err := strconv.ParseUint(s, 10, 64) + if err != nil { + return 0, errors.Errorf("%s is not a valid uint64", field) + } + return validator.Uint64(v), nil +} + +func formatUint(v validator.Uint64) string { + return strconv.FormatUint(uint64(v), 10) +} + +func strPtr(s string) *string { return &s } + +func optUintStrPtr(v *validator.Uint64) *string { + if v == nil { + return nil + } + return strPtr(formatUint(*v)) +} diff --git a/validator/rpc/handlers_validator_config_test.go b/validator/rpc/handlers_validator_config_test.go new file mode 100644 index 000000000000..bcc458492dc2 --- /dev/null +++ b/validator/rpc/handlers_validator_config_test.go @@ -0,0 +1,211 @@ +package rpc + +import ( + "bytes" + "encoding/json" + "net/http" + "net/http/httptest" + "strconv" + "strings" + "testing" + + "github.com/OffchainLabs/prysm/v7/config/proposer" + "github.com/OffchainLabs/prysm/v7/testing/require" + "github.com/OffchainLabs/prysm/v7/validator/keymanager/derived" + mocks "github.com/OffchainLabs/prysm/v7/validator/testing" + "github.com/ethereum/go-ethereum/common/hexutil" +) + +// setupConfigServer builds a Server with a derived keymanager holding numKeys +// recovered accounts, and returns the server plus the known validating pubkeys. +func setupConfigServer(t *testing.T, numKeys int) (*Server, [][48]byte) { + ctx := t.Context() + srv := setupServerWithWallet(t) + km, err := srv.validatorService.Keymanager() + require.NoError(t, err) + dr, ok := km.(*derived.Keymanager) + require.Equal(t, true, ok) + require.NoError(t, dr.RecoverAccountsFromMnemonic(ctx, mocks.TestMnemonic, derived.DefaultMnemonicLanguage, "", numKeys)) + keys, err := dr.FetchValidatingPublicKeys(ctx) + require.NoError(t, err) + require.Equal(t, numKeys, len(keys)) + return srv, keys +} + +func postBuilders(t *testing.T, s *Server, pubkey, body string) *httptest.ResponseRecorder { + req := httptest.NewRequest(http.MethodPost, "/eth/v1/validator/"+pubkey+"/builders", bytes.NewBufferString(body)) + req.SetPathValue("pubkey", pubkey) + w := httptest.NewRecorder() + w.Body = &bytes.Buffer{} + s.SetBuilders(w, req) + return w +} + +func getBuilders(t *testing.T, s *Server, pubkey string) (*httptest.ResponseRecorder, *BuilderConfigJson) { + req := httptest.NewRequest(http.MethodGet, "/eth/v1/validator/"+pubkey+"/builders", nil) + req.SetPathValue("pubkey", pubkey) + w := httptest.NewRecorder() + w.Body = &bytes.Buffer{} + s.GetBuilders(w, req) + cfg := &BuilderConfigJson{} + if w.Code == http.StatusOK { + require.NoError(t, json.Unmarshal(w.Body.Bytes(), cfg)) + } + return w, cfg +} + +func deleteBuilders(t *testing.T, s *Server, pubkey string) *httptest.ResponseRecorder { + req := httptest.NewRequest(http.MethodDelete, "/eth/v1/validator/"+pubkey+"/builders", nil) + req.SetPathValue("pubkey", pubkey) + w := httptest.NewRecorder() + w.Body = &bytes.Buffer{} + s.DeleteBuilders(w, req) + return w +} + +func TestServer_SetGetBuilders_RoundTrip(t *testing.T) { + srv, keys := setupConfigServer(t, 1) + pk := hexutil.Encode(keys[0][:]) + body := `{"enabled":true,"min_bid":"5","builder_boost_factor":"120",` + + `"builders":[{"url":"https://b.example","auth_data":"0x0102","max_execution_payment":"1000"}]}` + + w := postBuilders(t, srv, pk, body) + require.Equal(t, http.StatusAccepted, w.Code) + + _, cfg := getBuilders(t, srv, pk) + require.NotNil(t, cfg.Enabled) + require.Equal(t, true, *cfg.Enabled) + require.Equal(t, "5", *cfg.MinBid) + require.Equal(t, "120", *cfg.BuilderBoostFactor) + require.Equal(t, 1, len(cfg.Builders)) + require.Equal(t, "https://b.example", *cfg.Builders[0].Url) + require.Equal(t, "1000", *cfg.Builders[0].MaxExecutionPayment) + // Resolved: the entry omitted min_bid/boost, so GET fills them from the defaults. + require.Equal(t, "5", *cfg.Builders[0].MinBid) + require.Equal(t, "120", *cfg.Builders[0].BuilderBoostFactor) +} + +func TestServer_SetBuilders_FullReplace(t *testing.T) { + srv, keys := setupConfigServer(t, 1) + pk := hexutil.Encode(keys[0][:]) + + require.Equal(t, http.StatusAccepted, postBuilders(t, srv, pk, + `{"enabled":true,"builders":[{"url":"https://a.example"},{"url":"https://b.example"}]}`).Code) + require.Equal(t, http.StatusAccepted, postBuilders(t, srv, pk, + `{"enabled":true,"builders":[{"url":"https://c.example"}]}`).Code) + + _, cfg := getBuilders(t, srv, pk) + require.Equal(t, 1, len(cfg.Builders)) + require.Equal(t, "https://c.example", *cfg.Builders[0].Url) +} + +func TestServer_SetBuilders_EmptyArrayIsUseNone(t *testing.T) { + srv, keys := setupConfigServer(t, 1) + pk := hexutil.Encode(keys[0][:]) + require.Equal(t, http.StatusAccepted, postBuilders(t, srv, pk, `{"enabled":true,"builders":[]}`).Code) + + _, cfg := getBuilders(t, srv, pk) + require.NotNil(t, cfg.Builders) + require.Equal(t, 0, len(cfg.Builders)) +} + +func TestServer_SetBuilders_BuilderPubkeyOnlyEntry(t *testing.T) { + srv, keys := setupConfigServer(t, 1) + pk := hexutil.Encode(keys[0][:]) + bpk := "0x" + strings.Repeat("ab", 48) + body := `{"enabled":true,"builders":[{"builder_pubkey":"` + bpk + `","min_bid":"9"}]}` + + require.Equal(t, http.StatusAccepted, postBuilders(t, srv, pk, body).Code) + _, cfg := getBuilders(t, srv, pk) + require.Equal(t, 1, len(cfg.Builders)) + require.IsNil(t, cfg.Builders[0].Url) + require.Equal(t, bpk, *cfg.Builders[0].BuilderPubkey) +} + +func TestServer_SetBuilders_Rejects(t *testing.T) { + srv, keys := setupConfigServer(t, 1) + pk := hexutil.Encode(keys[0][:]) + bpk := "0x" + strings.Repeat("ab", 48) + + cases := map[string]struct { + body, contains string + }{ + "missing enabled": {`{"builders":[{"url":"https://a"}]}`, "enabled is required"}, + "entry has neither": {`{"enabled":true,"builders":[{"min_bid":"1"}]}`, "at least one of url and builder_pubkey"}, + "same url and auth_data": {`{"enabled":true,"builders":[{"url":"https://a"},{"url":"https://a"}]}`, "share the same url and auth_data"}, + "p2p pubkey repeated": {`{"enabled":true,"builders":[{"builder_pubkey":"` + bpk + `"},{"builder_pubkey":"` + bpk + `"}]}`, "two p2p-policy entries share the same builder_pubkey"}, + "invalid url": {`{"enabled":true,"builders":[{"url":"not a url"}]}`, "url is not a valid URL"}, + } + for name, tc := range cases { + t.Run(name, func(t *testing.T) { + w := postBuilders(t, srv, pk, tc.body) + require.Equal(t, http.StatusBadRequest, w.Code) + require.Equal(t, true, strings.Contains(w.Body.String(), tc.contains), "body: %s", w.Body.String()) + }) + } +} + +// Same url with distinct auth_data is a legal pair, not a duplicate. +func TestServer_SetBuilders_SharedURLDistinctAuth(t *testing.T) { + srv, keys := setupConfigServer(t, 1) + pk := hexutil.Encode(keys[0][:]) + body := `{"enabled":true,"builders":[{"url":"https://a","auth_data":"0x01"},{"url":"https://a","auth_data":"0x02"}]}` + require.Equal(t, http.StatusAccepted, postBuilders(t, srv, pk, body).Code) + _, cfg := getBuilders(t, srv, pk) + require.Equal(t, 2, len(cfg.Builders)) +} + +func TestServer_SetBuilders_MaxEntries(t *testing.T) { + srv, keys := setupConfigServer(t, 1) + pk := hexutil.Encode(keys[0][:]) + entries := make([]string, 0, maxBuilderEntries+1) + for i := 0; i <= maxBuilderEntries; i++ { + entries = append(entries, `{"url":"https://b`+strconv.Itoa(i)+`.example"}`) + } + body := `{"enabled":true,"builders":[` + strings.Join(entries, ",") + `]}` + w := postBuilders(t, srv, pk, body) + require.Equal(t, http.StatusBadRequest, w.Code) + require.Equal(t, true, strings.Contains(w.Body.String(), "exceeds 64 entries")) +} + +func TestServer_DeleteBuilders(t *testing.T) { + srv, keys := setupConfigServer(t, 1) + pk := hexutil.Encode(keys[0][:]) + + // Nothing configured yet -> 404. + require.Equal(t, http.StatusNotFound, deleteBuilders(t, srv, pk).Code) + + require.Equal(t, http.StatusAccepted, postBuilders(t, srv, pk, `{"enabled":true,"builders":[{"url":"https://a"}]}`).Code) + require.Equal(t, http.StatusNoContent, deleteBuilders(t, srv, pk).Code) + + // After delete the key follows defaults (no per-key builders). + _, cfg := getBuilders(t, srv, pk) + require.NotNil(t, cfg.Enabled) + require.Equal(t, false, *cfg.Enabled) + require.Equal(t, 0, len(cfg.Builders)) +} + +// GET on an unconfigured key resolves against default_config. +func TestServer_GetBuilders_ResolvesDefault(t *testing.T) { + srv, keys := setupConfigServer(t, 1) + pk := hexutil.Encode(keys[0][:]) + require.NoError(t, srv.validatorService.SetProposerSettings(t.Context(), &proposer.Settings{ + Version: proposer.SchemaV2, + DefaultConfig: &proposer.Option{ + BuilderConfig: &proposer.BuilderConfig{Enabled: true, Builders: []*proposer.BuilderEntry{{URL: "https://default.example"}}}, + }, + })) + + _, cfg := getBuilders(t, srv, pk) + require.NotNil(t, cfg.Enabled) + require.Equal(t, true, *cfg.Enabled) + require.Equal(t, 1, len(cfg.Builders)) + require.Equal(t, "https://default.example", *cfg.Builders[0].Url) +} + +func TestServer_SetBuilders_ValidatorServiceNil(t *testing.T) { + srv, keys := setupConfigServer(t, 1) + srv.validatorService = nil + w := postBuilders(t, srv, hexutil.Encode(keys[0][:]), `{"enabled":true}`) + require.Equal(t, http.StatusServiceUnavailable, w.Code) +} diff --git a/validator/rpc/server.go b/validator/rpc/server.go index a718799cc81f..52ec2e37a92b 100644 --- a/validator/rpc/server.go +++ b/validator/rpc/server.go @@ -7,6 +7,7 @@ import ( "net/http" "path/filepath" "strings" + "sync" "time" "github.com/OffchainLabs/prysm/v7/api" @@ -80,6 +81,7 @@ type Server struct { walletDir string jwtSecret []byte grpcHeaders []string + proposerSettingsLock sync.Mutex } // NewServer instantiates a new HTTP server. @@ -198,6 +200,9 @@ func (s *Server) InitializeRoutes() error { s.router.HandleFunc("GET /eth/v1/validator/{pubkey}/graffiti", s.GetGraffiti) s.router.HandleFunc("POST /eth/v1/validator/{pubkey}/graffiti", s.SetGraffiti) s.router.HandleFunc("DELETE /eth/v1/validator/{pubkey}/graffiti", s.DeleteGraffiti) + s.router.HandleFunc("GET /eth/v1/validator/{pubkey}/builders", s.GetBuilders) + s.router.HandleFunc("POST /eth/v1/validator/{pubkey}/builders", s.SetBuilders) + s.router.HandleFunc("DELETE /eth/v1/validator/{pubkey}/builders", s.DeleteBuilders) // auth endpoint s.router.HandleFunc("GET "+api.WebUrlPrefix+"initialize", s.Initialize) diff --git a/validator/rpc/structs.go b/validator/rpc/structs.go index 77bfabff8f94..a4f9e100ff33 100644 --- a/validator/rpc/structs.go +++ b/validator/rpc/structs.go @@ -109,6 +109,29 @@ type GraffitiData struct { Graffiti string `json:"graffiti"` } +// Per-key builders keymanager api (keymanager-APIs #88). All integers are decimal +// strings and all byte values are 0x-prefixed hex, per keymanager conventions. +// A nil Builders means "use the validator client's builders"; a non-nil empty +// Builders means "use none" (p2p bids only). +// Builders has no omitempty: on the resolved GET response an empty list ("use no +// builders") must serialize as [] rather than be dropped. On POST input an absent +// field still decodes to nil, which means "inherit the validator client's builders". +type BuilderConfigJson struct { + Enabled *bool `json:"enabled"` + Builders []*BuilderEntryJson `json:"builders"` + MinBid *string `json:"min_bid,omitempty"` + BuilderBoostFactor *string `json:"builder_boost_factor,omitempty"` +} + +type BuilderEntryJson struct { + Url *string `json:"url,omitempty"` + AuthData *string `json:"auth_data,omitempty"` + BuilderPubkey *string `json:"builder_pubkey,omitempty"` + MaxExecutionPayment *string `json:"max_execution_payment,omitempty"` + MinBid *string `json:"min_bid,omitempty"` + BuilderBoostFactor *string `json:"builder_boost_factor,omitempty"` +} + type BeaconStatusResponse struct { BeaconNodeEndpoint string `json:"beacon_node_endpoint"` Connected bool `json:"connected"` From 27698ca272d10e0f927a543bc7125c91c0177b25 Mon Sep 17 00:00:00 2001 From: james-prysm Date: Thu, 30 Jul 2026 15:23:10 -0500 Subject: [PATCH 02/26] gaz --- validator/db/BUILD.bazel | 1 - validator/db/filesystem/BUILD.bazel | 1 - validator/db/kv/BUILD.bazel | 1 - validator/rpc/BUILD.bazel | 1 - 4 files changed, 4 deletions(-) diff --git a/validator/db/BUILD.bazel b/validator/db/BUILD.bazel index 131cb9964289..ad4ae74a13f4 100644 --- a/validator/db/BUILD.bazel +++ b/validator/db/BUILD.bazel @@ -57,6 +57,5 @@ go_test( "@com_github_ethereum_go_ethereum//common/hexutil:go_default_library", "@com_github_sirupsen_logrus//hooks/test:go_default_library", "@com_github_urfave_cli_v2//:go_default_library", - "@org_golang_google_protobuf//proto:go_default_library", ], ) diff --git a/validator/db/filesystem/BUILD.bazel b/validator/db/filesystem/BUILD.bazel index 798db1eeea80..3b23b61d5790 100644 --- a/validator/db/filesystem/BUILD.bazel +++ b/validator/db/filesystem/BUILD.bazel @@ -68,6 +68,5 @@ go_test( "//validator/testing:go_default_library", "@com_github_ethereum_go_ethereum//common:go_default_library", "@com_github_ethereum_go_ethereum//common/hexutil:go_default_library", - "@org_golang_google_protobuf//proto:go_default_library", ], ) diff --git a/validator/db/kv/BUILD.bazel b/validator/db/kv/BUILD.bazel index 89f09b7ac5be..7c085773e8f1 100644 --- a/validator/db/kv/BUILD.bazel +++ b/validator/db/kv/BUILD.bazel @@ -95,6 +95,5 @@ go_test( "@com_github_sirupsen_logrus//:go_default_library", "@com_github_sirupsen_logrus//hooks/test:go_default_library", "@io_etcd_go_bbolt//:go_default_library", - "@org_golang_google_protobuf//proto:go_default_library", ], ) diff --git a/validator/rpc/BUILD.bazel b/validator/rpc/BUILD.bazel index a0dc809979e8..f6478b39dae6 100644 --- a/validator/rpc/BUILD.bazel +++ b/validator/rpc/BUILD.bazel @@ -150,7 +150,6 @@ go_test( "@org_golang_google_grpc//codes:go_default_library", "@org_golang_google_grpc//metadata:go_default_library", "@org_golang_google_grpc//status:go_default_library", - "@org_golang_google_protobuf//proto:go_default_library", "@org_golang_google_protobuf//types/known/emptypb:go_default_library", "@org_golang_google_protobuf//types/known/timestamppb:go_default_library", "@org_uber_go_mock//gomock:go_default_library", From f4c33541b6d2a74ba543ad3a0320dc3b2ccf35bc Mon Sep 17 00:00:00 2001 From: james-prysm Date: Mon, 3 Aug 2026 16:54:40 -0500 Subject: [PATCH 03/26] updating based on removal of p2p boost config --- config/proposer/settings.go | 89 ++++++++++++++++-- config/proposer/settings_test.go | 20 ++-- validator/client/validator.go | 22 ++--- validator/rpc/handlers_validator_config.go | 93 ++++++++----------- .../rpc/handlers_validator_config_test.go | 61 +++++++++--- validator/rpc/structs.go | 4 +- 6 files changed, 195 insertions(+), 94 deletions(-) diff --git a/config/proposer/settings.go b/config/proposer/settings.go index 5800c77201f5..73084119e142 100644 --- a/config/proposer/settings.go +++ b/config/proposer/settings.go @@ -68,24 +68,24 @@ func SettingFromConsensus(ps *validatorpb.ProposerSettingsPayload) (*Settings, e return settings, nil } -// Persisted configs may predate duplicate-url rejection; the first entry wins, -// matching inline preference assembly. +// Persisted configs may predate url-required and (url, auth_data) uniqueness; +// url-less entries are dropped and the first entry wins, matching POST validation. func (ps *Settings) dedupBuilders() { fix := func(opt *Option) { - if opt == nil || opt.BuilderConfig == nil || len(opt.BuilderConfig.Builders) < 2 { + if opt == nil || opt.BuilderConfig == nil || len(opt.BuilderConfig.Builders) == 0 { return } - seen := make(map[string]bool, len(opt.BuilderConfig.Builders)) + seen := make(map[EntryIdentity]bool, len(opt.BuilderConfig.Builders)) kept := opt.BuilderConfig.Builders[:0] for _, e := range opt.BuilderConfig.Builders { - if e == nil || seen[e.URL] { + if e == nil || e.URL == "" || seen[e.Identity()] { continue } - seen[e.URL] = true + seen[e.Identity()] = true kept = append(kept, e) } if len(kept) != len(opt.BuilderConfig.Builders) { - log.Warn("Duplicate builder urls in proposer settings; keeping the first entry for each url") + log.Warn("Removed url-less or duplicate builder entries from proposer settings") opt.BuilderConfig.Builders = kept } } @@ -153,6 +153,78 @@ type BuilderEntry struct { BuilderBoostFactor *validator.Uint64 `json:"builder_boost_factor,omitempty" yaml:"builder_boost_factor,omitempty"` } +// EffectiveAuthData resolves omitted auth_data to the spec convention: +// the UTF-8 bytes of the builder's URL. +func (be *BuilderEntry) EffectiveAuthData() []byte { + if len(be.AuthData) != 0 { + return be.AuthData + } + return []byte(be.URL) +} + +// EntryIdentity is what makes a builder entry unique: its url compared as the +// exact string and its auth_data as the resolved bytes. +type EntryIdentity struct { + URL string + Auth string +} + +func (be *BuilderEntry) Identity() EntryIdentity { + return EntryIdentity{URL: be.URL, Auth: string(be.EffectiveAuthData())} +} + +// NeutralBuilderBoostFactor is the resolved boost when none is configured: +// pure profit maximization between builder and local payloads. +const NeutralBuilderBoostFactor = 100 + +// EffectiveMinBid resolves the config-level floor; unset means no floor. +func (bc *BuilderConfig) EffectiveMinBid() validator.Uint64 { + if bc == nil || bc.MinBid == nil { + return 0 + } + return *bc.MinBid +} + +// EffectiveBuilderBoostFactor resolves the config-level boost; unset means neutral. +func (bc *BuilderConfig) EffectiveBuilderBoostFactor() validator.Uint64 { + if bc == nil || bc.BuilderBoostFactor == nil { + return NeutralBuilderBoostFactor + } + return *bc.BuilderBoostFactor +} + +// EffectiveMaxExecutionPayment resolves the ceiling; unset means trustless-only. +func (bc *BuilderConfig) EffectiveMaxExecutionPayment() validator.Uint64 { + if bc == nil || bc.MaxExecutionPayment == nil { + return 0 + } + return *bc.MaxExecutionPayment +} + +// EffectiveMinBid resolves this entry's floor, falling back to the enclosing config. +func (be *BuilderEntry) EffectiveMinBid(bc *BuilderConfig) validator.Uint64 { + if be.MinBid != nil { + return *be.MinBid + } + return bc.EffectiveMinBid() +} + +// EffectiveBuilderBoostFactor resolves this entry's boost, falling back to the enclosing config. +func (be *BuilderEntry) EffectiveBuilderBoostFactor(bc *BuilderConfig) validator.Uint64 { + if be.BuilderBoostFactor != nil { + return *be.BuilderBoostFactor + } + return bc.EffectiveBuilderBoostFactor() +} + +// EffectiveMaxExecutionPayment resolves this entry's ceiling, falling back to the enclosing config. +func (be *BuilderEntry) EffectiveMaxExecutionPayment(bc *BuilderConfig) validator.Uint64 { + if be.MaxExecutionPayment != nil { + return *be.MaxExecutionPayment + } + return bc.EffectiveMaxExecutionPayment() +} + // IsEnabled reports whether the builder path is explicitly enabled. A nil config // or unset enabled field is treated as disabled. func (bc *BuilderConfig) IsEnabled() bool { @@ -383,7 +455,8 @@ func (bc *BuilderConfig) Clone() *BuilderConfig { c.Proxy = cloneString(bc.Proxy) c.MinBid = cloneUint64(bc.MinBid) c.BuilderBoostFactor = cloneUint64(bc.BuilderBoostFactor) - if len(bc.Builders) > 0 { + // Preserve nil vs empty: an empty list is the "use no builders" marker. + if bc.Builders != nil { c.Builders = make([]*BuilderEntry, 0, len(bc.Builders)) for _, b := range bc.Builders { c.Builders = append(c.Builders, b.Clone()) diff --git a/config/proposer/settings_test.go b/config/proposer/settings_test.go index 5bdfe51b13d0..855cc28dbe63 100644 --- a/config/proposer/settings_test.go +++ b/config/proposer/settings_test.go @@ -68,6 +68,11 @@ func Test_Proposer_Setting_Cloning(t *testing.T) { settings.DefaultConfig.BuilderConfig.GasLimit = 1 require.NotEqual(t, settings.DefaultConfig.BuilderConfig.GasLimit, clone.GasLimit) }) + t.Run("Cloning preserves the use-none builders marker", func(t *testing.T) { + clone := (&BuilderConfig{Enabled: true, Builders: []*BuilderEntry{}}).Clone() + require.NotNil(t, clone.Builders) + require.Equal(t, 0, len(clone.Builders)) + }) t.Run("Happy Path BuilderConfigFromConsensus", func(t *testing.T) { clone := settings.DefaultConfig.BuilderConfig.Clone() @@ -613,9 +618,9 @@ func TestSettings_TargetGasLimit(t *testing.T) { }) } -// Legacy (pre-v2) payloads can't express presence: wire-absent enabled meant -// disabled, and the filesystem backend wrote spurious explicit-0 max payments. -// Persisted payloads may predate duplicate-url rejection at the API. +// Persisted payloads may predate url-required and (url, auth_data) uniqueness: +// url-less entries drop, (url, auth) duplicates keep the first, and an omitted +// auth_data compares as its derived value (the url's UTF-8 bytes). func TestSettingFromConsensus_DedupsBuilders(t *testing.T) { payload := &validatorpb.ProposerSettingsPayload{ Version: SchemaV2, @@ -625,7 +630,10 @@ func TestSettingFromConsensus_DedupsBuilders(t *testing.T) { Builders: []*validatorpb.BuilderEntry{ {Url: "https://b.example", AuthData: []byte("first")}, {Url: "https://b.example", AuthData: []byte("second")}, + {Url: "https://b.example", AuthData: []byte("first")}, {Url: "https://other.example"}, + {Url: "https://other.example", AuthData: []byte("https://other.example")}, + {AuthData: []byte("url-less")}, }, }, }, @@ -633,10 +641,10 @@ func TestSettingFromConsensus_DedupsBuilders(t *testing.T) { ps, err := SettingFromConsensus(payload) require.NoError(t, err) builders := ps.DefaultConfig.BuilderConfig.Builders - require.Equal(t, 2, len(builders)) - require.Equal(t, "https://b.example", builders[0].URL) + require.Equal(t, 3, len(builders)) require.DeepEqual(t, []byte("first"), builders[0].AuthData) - require.Equal(t, "https://other.example", builders[1].URL) + require.DeepEqual(t, []byte("second"), builders[1].AuthData) + require.Equal(t, "https://other.example", builders[2].URL) } func TestSettingFromConsensus_NormalizesLegacyPresence(t *testing.T) { diff --git a/validator/client/validator.go b/validator/client/validator.go index ce66d74639c4..ae48f02fb428 100644 --- a/validator/client/validator.go +++ b/validator/client/validator.go @@ -1398,29 +1398,29 @@ func (v *validator) builderConfigForKey(pk pubkey) *proposer.BuilderConfig { // builderTargetsForKey resolves the enabled builder list for pk; each entry // overrides the config-level fallbacks. -// TODO(gloas): resolved minBid/boostFactor/pubkey/authData and url-less entries -// take effect with the inline produce-block preferences wire (beacon-APIs #630). +// TODO(gloas): resolved minBid/boostFactor/pubkey take effect with the inline +// produce-block preferences wire (beacon-APIs #630). func (v *validator) builderTargetsForKey(pk pubkey) []builderTarget { bc := v.builderConfigForKey(pk) if !bc.IsEnabled() { return nil } - // Unset resolves to 0, trustless-only. - var fbMax uint64 - if bc.MaxExecutionPayment != nil { - fbMax = uint64(*bc.MaxExecutionPayment) - } + fbMax := uint64(bc.EffectiveMaxExecutionPayment()) fbMin := uint64Ptr(bc.MinBid) fbBoost := uint64Ptr(bc.BuilderBoostFactor) targets := make([]builderTarget, 0, len(bc.Builders)) - seen := make(map[string]bool, len(bc.Builders)) + seen := make(map[proposer.EntryIdentity]bool, len(bc.Builders)) for _, e := range bc.Builders { - if e == nil || e.URL == "" || seen[e.URL] { + if e == nil || e.URL == "" { + continue + } + // One request per entry: entries may share a url with different auth_data. + if seen[e.Identity()] { continue } - seen[e.URL] = true - t := builderTarget{url: e.URL, authData: e.AuthData, pubkey: e.Pubkey, maxPayment: fbMax, minBid: fbMin, boostFactor: fbBoost} + seen[e.Identity()] = true + t := builderTarget{url: e.URL, authData: e.EffectiveAuthData(), pubkey: e.Pubkey, maxPayment: fbMax, minBid: fbMin, boostFactor: fbBoost} if e.MaxExecutionPayment != nil { t.maxPayment = uint64(*e.MaxExecutionPayment) } diff --git a/validator/rpc/handlers_validator_config.go b/validator/rpc/handlers_validator_config.go index 48b571f4e165..6fa2b774af5c 100644 --- a/validator/rpc/handlers_validator_config.go +++ b/validator/rpc/handlers_validator_config.go @@ -2,7 +2,6 @@ package rpc import ( "encoding/json" - "fmt" "net/http" "net/url" "strconv" @@ -18,7 +17,11 @@ import ( "github.com/pkg/errors" ) -const maxBuilderEntries = 64 +const ( + maxBuilderEntries = 64 // MAX_BUILDER_ENTRIES + maxBuilderURLSize = 2048 // MAX_BUILDER_URL_SIZE + maxAuthDataSize = 4096 // MAX_DATA_SIZE +) // GetBuilders implements GET /eth/v1/validator/{pubkey}/builders (keymanager-APIs #88). // It returns the key's builder configuration resolved against default_config so @@ -42,14 +45,6 @@ func (s *Server) GetBuilders(w http.ResponseWriter, r *http.Request) { if out.Builders == nil { out.Builders = []*BuilderEntryJson{} } - for _, e := range out.Builders { - if e.MinBid == nil { - e.MinBid = out.MinBid - } - if e.BuilderBoostFactor == nil { - e.BuilderBoostFactor = out.BuilderBoostFactor - } - } httputil.WriteJson(w, out) } @@ -123,7 +118,7 @@ func (s *Server) DeleteBuilders(w http.ResponseWriter, r *http.Request) { httputil.HandleError(w, "Validator service not ready.", http.StatusServiceUnavailable) return } - rawPubkey, pubkey, ok := shared.HexFromRoute(w, r, "pubkey", fieldparams.BLSPubkeyLength) + _, pubkey, ok := shared.HexFromRoute(w, r, "pubkey", fieldparams.BLSPubkeyLength) if !ok { return } @@ -133,13 +128,14 @@ func (s *Server) DeleteBuilders(w http.ResponseWriter, r *http.Request) { settings := s.validatorService.ProposerSettings() key := bytesutil.ToBytes48(pubkey) + // Removing an absent configuration succeeds; the key already follows the defaults. if settings == nil || settings.ProposeConfig == nil { - httputil.HandleError(w, fmt.Sprintf("No builder configuration found for pubkey %q", rawPubkey), http.StatusNotFound) + w.WriteHeader(http.StatusNoContent) return } opt, found := settings.ProposeConfig[key] if !found || opt == nil || opt.BuilderConfig == nil { - httputil.HandleError(w, fmt.Sprintf("No builder configuration found for pubkey %q", rawPubkey), http.StatusNotFound) + w.WriteHeader(http.StatusNoContent) return } @@ -152,6 +148,8 @@ func (s *Server) DeleteBuilders(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusNoContent) } +// resolveBuilders returns the key's effective builder config; the Effective* +// getters resolve unset values (no floor, neutral boost, trustless-only). func resolveBuilders(settings *proposer.Settings, key [fieldparams.BLSPubkeyLength]byte) *proposer.BuilderConfig { var perKey, def *proposer.BuilderConfig if settings != nil { @@ -172,32 +170,30 @@ func builderConfigJSONFromConsensus(bc *proposer.BuilderConfig) *BuilderConfigJs enabled := bc.Enabled out := &BuilderConfigJson{ Enabled: &enabled, - MinBid: optUintStrPtr(bc.MinBid), - BuilderBoostFactor: optUintStrPtr(bc.BuilderBoostFactor), + MinBid: new(formatUint(bc.EffectiveMinBid())), + BuilderBoostFactor: new(formatUint(bc.EffectiveBuilderBoostFactor())), } if bc.Builders != nil { out.Builders = make([]*BuilderEntryJson, 0, len(bc.Builders)) for _, b := range bc.Builders { - out.Builders = append(out.Builders, builderEntryJSONFromConsensus(b)) + out.Builders = append(out.Builders, builderEntryJSONFromConsensus(b, bc)) } } return out } -func builderEntryJSONFromConsensus(be *proposer.BuilderEntry) *BuilderEntryJson { +func builderEntryJSONFromConsensus(be *proposer.BuilderEntry, bc *proposer.BuilderConfig) *BuilderEntryJson { out := &BuilderEntryJson{ - MaxExecutionPayment: optUintStrPtr(be.MaxExecutionPayment), - MinBid: optUintStrPtr(be.MinBid), - BuilderBoostFactor: optUintStrPtr(be.BuilderBoostFactor), + MaxExecutionPayment: new(formatUint(be.EffectiveMaxExecutionPayment(bc))), + MinBid: new(formatUint(be.EffectiveMinBid(bc))), + BuilderBoostFactor: new(formatUint(be.EffectiveBuilderBoostFactor(bc))), } if be.URL != "" { - out.Url = strPtr(be.URL) + out.Url = be.URL + out.AuthData = new(hexutil.Encode(be.EffectiveAuthData())) } if len(be.Pubkey) != 0 { - out.BuilderPubkey = strPtr(hexutil.Encode(be.Pubkey)) - } - if len(be.AuthData) != 0 { - out.AuthData = strPtr(hexutil.Encode(be.AuthData)) + out.BuilderPubkey = new(hexutil.Encode(be.Pubkey)) } return out } @@ -225,27 +221,18 @@ func builderConfigFromJSON(in *BuilderConfigJson) (*proposer.BuilderConfig, erro return nil, errors.Errorf("builders exceeds %d entries", maxBuilderEntries) } // Non-nil (possibly empty) list means "use exactly these builders", not "inherit". - // Uniqueness is scoped by role: url entries by (url, auth_data), - // url-less p2p-policy entries by builder_pubkey. + // Omitted auth_data compares as its derived value, so it collides with the explicit form. bc.Builders = make([]*proposer.BuilderEntry, 0, len(in.Builders)) - seenURL := make(map[string]bool, len(in.Builders)) - seenP2P := make(map[string]bool, len(in.Builders)) + seen := make(map[proposer.EntryIdentity]bool, len(in.Builders)) for i, entry := range in.Builders { be, err := builderEntryFromJSON(entry, i) if err != nil { return nil, err } - if be.URL != "" { - key := fmt.Sprintf("%s|%x", be.URL, be.AuthData) - if seenURL[key] { - return nil, errors.Errorf("builders[%d]: two entries share the same url and auth_data", i) - } - seenURL[key] = true - } else if seenP2P[string(be.Pubkey)] { - return nil, errors.Errorf("builders[%d]: two p2p-policy entries share the same builder_pubkey", i) - } else { - seenP2P[string(be.Pubkey)] = true + if seen[be.Identity()] { + return nil, errors.Errorf("builders[%d]: two entries share the same url and auth_data", i) } + seen[be.Identity()] = true bc.Builders = append(bc.Builders, be) } return bc, nil @@ -253,12 +240,16 @@ func builderConfigFromJSON(in *BuilderConfigJson) (*proposer.BuilderConfig, erro func builderEntryFromJSON(in *BuilderEntryJson, i int) (*proposer.BuilderEntry, error) { be := &proposer.BuilderEntry{} - if in.Url != nil && *in.Url != "" { - if u, err := url.Parse(*in.Url); err != nil || u.Scheme == "" || u.Host == "" { - return nil, errors.Errorf("builders[%d].url is not a valid URL", i) - } - be.URL = *in.Url + if in.Url == "" { + return nil, errors.Errorf("builders[%d].url is required", i) } + if len(in.Url) > maxBuilderURLSize { + return nil, errors.Errorf("builders[%d].url exceeds %d bytes", i, maxBuilderURLSize) + } + if u, err := url.Parse(in.Url); err != nil || u.Scheme == "" || u.Host == "" { + return nil, errors.Errorf("builders[%d].url is not a valid URL", i) + } + be.URL = in.Url if in.BuilderPubkey != nil { pk, err := hexutil.Decode(*in.BuilderPubkey) if err != nil || len(pk) != fieldparams.BLSPubkeyLength { @@ -266,16 +257,13 @@ func builderEntryFromJSON(in *BuilderEntryJson, i int) (*proposer.BuilderEntry, } be.Pubkey = pk } - if be.URL == "" && len(be.Pubkey) == 0 { - return nil, errors.Errorf("builders[%d]: at least one of url and builder_pubkey is required", i) - } if in.AuthData != nil { ad, err := hexutil.Decode(*in.AuthData) if err != nil { return nil, errors.Errorf("builders[%d].auth_data is not valid hex", i) } - if len(ad) > 4096 { - return nil, errors.Errorf("builders[%d].auth_data exceeds 4096 bytes", i) + if len(ad) > maxAuthDataSize { + return nil, errors.Errorf("builders[%d].auth_data exceeds %d bytes", i, maxAuthDataSize) } be.AuthData = ad } @@ -316,10 +304,3 @@ func formatUint(v validator.Uint64) string { } func strPtr(s string) *string { return &s } - -func optUintStrPtr(v *validator.Uint64) *string { - if v == nil { - return nil - } - return strPtr(formatUint(*v)) -} diff --git a/validator/rpc/handlers_validator_config_test.go b/validator/rpc/handlers_validator_config_test.go index bcc458492dc2..8b17e4d68eb5 100644 --- a/validator/rpc/handlers_validator_config_test.go +++ b/validator/rpc/handlers_validator_config_test.go @@ -78,7 +78,7 @@ func TestServer_SetGetBuilders_RoundTrip(t *testing.T) { require.Equal(t, "5", *cfg.MinBid) require.Equal(t, "120", *cfg.BuilderBoostFactor) require.Equal(t, 1, len(cfg.Builders)) - require.Equal(t, "https://b.example", *cfg.Builders[0].Url) + require.Equal(t, "https://b.example", cfg.Builders[0].Url) require.Equal(t, "1000", *cfg.Builders[0].MaxExecutionPayment) // Resolved: the entry omitted min_bid/boost, so GET fills them from the defaults. require.Equal(t, "5", *cfg.Builders[0].MinBid) @@ -96,7 +96,7 @@ func TestServer_SetBuilders_FullReplace(t *testing.T) { _, cfg := getBuilders(t, srv, pk) require.Equal(t, 1, len(cfg.Builders)) - require.Equal(t, "https://c.example", *cfg.Builders[0].Url) + require.Equal(t, "https://c.example", cfg.Builders[0].Url) } func TestServer_SetBuilders_EmptyArrayIsUseNone(t *testing.T) { @@ -109,16 +109,35 @@ func TestServer_SetBuilders_EmptyArrayIsUseNone(t *testing.T) { require.Equal(t, 0, len(cfg.Builders)) } -func TestServer_SetBuilders_BuilderPubkeyOnlyEntry(t *testing.T) { +// A POST for one key must not flip another key's use-none marker back to inherit. +func TestServer_SetBuilders_PreservesOtherKeysUseNone(t *testing.T) { + srv, keys := setupConfigServer(t, 2) + pkA := hexutil.Encode(keys[0][:]) + pkB := hexutil.Encode(keys[1][:]) + require.NoError(t, srv.validatorService.SetProposerSettings(t.Context(), &proposer.Settings{ + Version: proposer.SchemaV2, + DefaultConfig: &proposer.Option{ + BuilderConfig: &proposer.BuilderConfig{Enabled: true, Builders: []*proposer.BuilderEntry{{URL: "https://default.example"}}}, + }, + })) + require.Equal(t, http.StatusAccepted, postBuilders(t, srv, pkA, `{"enabled":true,"builders":[]}`).Code) + require.Equal(t, http.StatusAccepted, postBuilders(t, srv, pkB, `{"enabled":true,"builders":[{"url":"https://b.example"}]}`).Code) + + _, cfg := getBuilders(t, srv, pkA) + require.Equal(t, 0, len(cfg.Builders)) +} + +// builder_pubkey rides along as the entry's response filter. +func TestServer_SetBuilders_BuilderPubkeyFilter(t *testing.T) { srv, keys := setupConfigServer(t, 1) pk := hexutil.Encode(keys[0][:]) bpk := "0x" + strings.Repeat("ab", 48) - body := `{"enabled":true,"builders":[{"builder_pubkey":"` + bpk + `","min_bid":"9"}]}` + body := `{"enabled":true,"builders":[{"url":"https://a.example","builder_pubkey":"` + bpk + `"}]}` require.Equal(t, http.StatusAccepted, postBuilders(t, srv, pk, body).Code) _, cfg := getBuilders(t, srv, pk) require.Equal(t, 1, len(cfg.Builders)) - require.IsNil(t, cfg.Builders[0].Url) + require.Equal(t, "https://a.example", cfg.Builders[0].Url) require.Equal(t, bpk, *cfg.Builders[0].BuilderPubkey) } @@ -127,14 +146,17 @@ func TestServer_SetBuilders_Rejects(t *testing.T) { pk := hexutil.Encode(keys[0][:]) bpk := "0x" + strings.Repeat("ab", 48) + longURL := "https://a.example/" + strings.Repeat("x", 2048) cases := map[string]struct { body, contains string }{ "missing enabled": {`{"builders":[{"url":"https://a"}]}`, "enabled is required"}, - "entry has neither": {`{"enabled":true,"builders":[{"min_bid":"1"}]}`, "at least one of url and builder_pubkey"}, + "entry without url": {`{"enabled":true,"builders":[{"min_bid":"1"}]}`, "url is required"}, + "pubkey-only entry": {`{"enabled":true,"builders":[{"builder_pubkey":"` + bpk + `"}]}`, "url is required"}, "same url and auth_data": {`{"enabled":true,"builders":[{"url":"https://a"},{"url":"https://a"}]}`, "share the same url and auth_data"}, - "p2p pubkey repeated": {`{"enabled":true,"builders":[{"builder_pubkey":"` + bpk + `"},{"builder_pubkey":"` + bpk + `"}]}`, "two p2p-policy entries share the same builder_pubkey"}, - "invalid url": {`{"enabled":true,"builders":[{"url":"not a url"}]}`, "url is not a valid URL"}, + "omitted auth_data collides with its derived value": {`{"enabled":true,"builders":[{"url":"https://a"},{"url":"https://a","auth_data":"` + hexutil.Encode([]byte("https://a")) + `"}]}`, "share the same url and auth_data"}, + "invalid url": {`{"enabled":true,"builders":[{"url":"not a url"}]}`, "url is not a valid URL"}, + "url too long": {`{"enabled":true,"builders":[{"url":"` + longURL + `"}]}`, "url exceeds 2048 bytes"}, } for name, tc := range cases { t.Run(name, func(t *testing.T) { @@ -155,6 +177,23 @@ func TestServer_SetBuilders_SharedURLDistinctAuth(t *testing.T) { require.Equal(t, 2, len(cfg.Builders)) } +// GET is fully resolved: omitted auth_data becomes the url's UTF-8 bytes, and +// unset values become the runtime fallbacks (no floor, neutral boost, trustless-only). +func TestServer_GetBuilders_ResolvesOmittedValues(t *testing.T) { + srv, keys := setupConfigServer(t, 1) + pk := hexutil.Encode(keys[0][:]) + require.Equal(t, http.StatusAccepted, postBuilders(t, srv, pk, + `{"enabled":true,"builders":[{"url":"https://a.example"}]}`).Code) + _, cfg := getBuilders(t, srv, pk) + require.Equal(t, "0", *cfg.MinBid) + require.Equal(t, "100", *cfg.BuilderBoostFactor) + require.Equal(t, 1, len(cfg.Builders)) + require.Equal(t, hexutil.Encode([]byte("https://a.example")), *cfg.Builders[0].AuthData) + require.Equal(t, "0", *cfg.Builders[0].MinBid) + require.Equal(t, "100", *cfg.Builders[0].BuilderBoostFactor) + require.Equal(t, "0", *cfg.Builders[0].MaxExecutionPayment) +} + func TestServer_SetBuilders_MaxEntries(t *testing.T) { srv, keys := setupConfigServer(t, 1) pk := hexutil.Encode(keys[0][:]) @@ -172,8 +211,8 @@ func TestServer_DeleteBuilders(t *testing.T) { srv, keys := setupConfigServer(t, 1) pk := hexutil.Encode(keys[0][:]) - // Nothing configured yet -> 404. - require.Equal(t, http.StatusNotFound, deleteBuilders(t, srv, pk).Code) + // Removing an absent configuration succeeds. + require.Equal(t, http.StatusNoContent, deleteBuilders(t, srv, pk).Code) require.Equal(t, http.StatusAccepted, postBuilders(t, srv, pk, `{"enabled":true,"builders":[{"url":"https://a"}]}`).Code) require.Equal(t, http.StatusNoContent, deleteBuilders(t, srv, pk).Code) @@ -200,7 +239,7 @@ func TestServer_GetBuilders_ResolvesDefault(t *testing.T) { require.NotNil(t, cfg.Enabled) require.Equal(t, true, *cfg.Enabled) require.Equal(t, 1, len(cfg.Builders)) - require.Equal(t, "https://default.example", *cfg.Builders[0].Url) + require.Equal(t, "https://default.example", cfg.Builders[0].Url) } func TestServer_SetBuilders_ValidatorServiceNil(t *testing.T) { diff --git a/validator/rpc/structs.go b/validator/rpc/structs.go index a4f9e100ff33..2f728641888e 100644 --- a/validator/rpc/structs.go +++ b/validator/rpc/structs.go @@ -118,13 +118,13 @@ type GraffitiData struct { // field still decodes to nil, which means "inherit the validator client's builders". type BuilderConfigJson struct { Enabled *bool `json:"enabled"` - Builders []*BuilderEntryJson `json:"builders"` MinBid *string `json:"min_bid,omitempty"` BuilderBoostFactor *string `json:"builder_boost_factor,omitempty"` + Builders []*BuilderEntryJson `json:"builders"` } type BuilderEntryJson struct { - Url *string `json:"url,omitempty"` + Url string `json:"url,omitempty"` AuthData *string `json:"auth_data,omitempty"` BuilderPubkey *string `json:"builder_pubkey,omitempty"` MaxExecutionPayment *string `json:"max_execution_payment,omitempty"` From df8afee4565b9c1dfa9a35e3f7a309002d89626a Mon Sep 17 00:00:00 2001 From: james-prysm Date: Tue, 4 Aug 2026 22:49:19 -0500 Subject: [PATCH 04/26] self review --- .../proposer/effective_builder_config_test.go | 32 +++++++------- config/proposer/loader/loader.go | 2 + config/proposer/loader/loader_test.go | 14 ++++++ config/proposer/settings.go | 22 ++++++++-- config/proposer/settings_test.go | 2 +- validator/client/validator.go | 44 ++++--------------- validator/rpc/handlers_validator_config.go | 24 +++------- .../rpc/handlers_validator_config_test.go | 30 ++++++++++++- 8 files changed, 93 insertions(+), 77 deletions(-) diff --git a/config/proposer/effective_builder_config_test.go b/config/proposer/effective_builder_config_test.go index c8441732609b..9061c1613b53 100644 --- a/config/proposer/effective_builder_config_test.go +++ b/config/proposer/effective_builder_config_test.go @@ -17,16 +17,16 @@ func TestEffectiveBuilderConfig(t *testing.T) { t.Run("nil per-key returns default", func(t *testing.T) { def := &BuilderConfig{Enabled: true} - require.Equal(t, def, EffectiveBuilderConfig(nil, def)) + require.Equal(t, def, effectiveBuilderConfig(nil, def)) }) t.Run("nil default returns per-key", func(t *testing.T) { perKey := &BuilderConfig{Enabled: true} - require.Equal(t, perKey, EffectiveBuilderConfig(perKey, nil)) + require.Equal(t, perKey, effectiveBuilderConfig(perKey, nil)) }) t.Run("min_bid inherits when per-key omits it", func(t *testing.T) { def := &BuilderConfig{Enabled: true, Builders: []*BuilderEntry{entryA, entryB}, MinBid: uint64ValPtr(5000000)} perKey := &BuilderConfig{Enabled: true, Builders: []*BuilderEntry{entryC}} - eff := EffectiveBuilderConfig(perKey, def) + eff := effectiveBuilderConfig(perKey, def) require.NotNil(t, eff.MinBid) require.Equal(t, validator.Uint64(5000000), *eff.MinBid) require.Equal(t, 1, len(eff.Builders)) @@ -35,70 +35,70 @@ func TestEffectiveBuilderConfig(t *testing.T) { t.Run("explicit zero max payment is preserved, not inherited over", func(t *testing.T) { def := &BuilderConfig{MaxExecutionPayment: uint64ValPtr(1000000000)} perKey := &BuilderConfig{Enabled: true, MaxExecutionPayment: uint64ValPtr(0)} - eff := EffectiveBuilderConfig(perKey, def) + eff := effectiveBuilderConfig(perKey, def) require.NotNil(t, eff.MaxExecutionPayment) require.Equal(t, validator.Uint64(0), *eff.MaxExecutionPayment) }) t.Run("unset max payment inherits default", func(t *testing.T) { def := &BuilderConfig{MaxExecutionPayment: uint64ValPtr(1000000000)} perKey := &BuilderConfig{Enabled: true} - eff := EffectiveBuilderConfig(perKey, def) + eff := effectiveBuilderConfig(perKey, def) require.NotNil(t, eff.MaxExecutionPayment) require.Equal(t, validator.Uint64(1000000000), *eff.MaxExecutionPayment) }) t.Run("explicit per-key disable wins over enabled default", func(t *testing.T) { def := &BuilderConfig{Enabled: true} perKey := &BuilderConfig{Enabled: false, MinBid: uint64ValPtr(1)} - require.Equal(t, false, EffectiveBuilderConfig(perKey, def).IsEnabled()) + require.Equal(t, false, effectiveBuilderConfig(perKey, def).IsEnabled()) }) t.Run("present per-key builder config is authoritative on enabled", func(t *testing.T) { // A per-key config with enabled false does not inherit an enabled default; // whole-config inheritance happens only when the per-key builder config is nil. def := &BuilderConfig{Enabled: true} perKey := &BuilderConfig{MinBid: uint64ValPtr(1)} - require.Equal(t, false, EffectiveBuilderConfig(perKey, def).IsEnabled()) - require.Equal(t, true, EffectiveBuilderConfig(nil, def).IsEnabled()) + require.Equal(t, false, effectiveBuilderConfig(perKey, def).IsEnabled()) + require.Equal(t, true, effectiveBuilderConfig(nil, def).IsEnabled()) }) t.Run("present builders list replaces, never unions", func(t *testing.T) { def := &BuilderConfig{Builders: []*BuilderEntry{entryA, entryB}} perKey := &BuilderConfig{Builders: []*BuilderEntry{entryC}} - eff := EffectiveBuilderConfig(perKey, def) + eff := effectiveBuilderConfig(perKey, def) require.Equal(t, 1, len(eff.Builders)) require.Equal(t, "https://c", eff.Builders[0].URL) }) t.Run("absent builders list inherits default list", func(t *testing.T) { def := &BuilderConfig{Builders: []*BuilderEntry{entryA, entryB}} perKey := &BuilderConfig{Enabled: true} - require.Equal(t, 2, len(EffectiveBuilderConfig(perKey, def).Builders)) + require.Equal(t, 2, len(effectiveBuilderConfig(perKey, def).Builders)) }) t.Run("zero gas limit inherits default gas limit", func(t *testing.T) { def := &BuilderConfig{GasLimit: validator.Uint64(30000000)} perKey := &BuilderConfig{Enabled: true} - require.Equal(t, validator.Uint64(30000000), EffectiveBuilderConfig(perKey, def).GasLimit) + require.Equal(t, validator.Uint64(30000000), effectiveBuilderConfig(perKey, def).GasLimit) }) t.Run("proxy and boost inherit per field", func(t *testing.T) { def := &BuilderConfig{Proxy: proto.String("http://side-car:9001"), BuilderBoostFactor: uint64ValPtr(90)} perKey := &BuilderConfig{Enabled: true, BuilderBoostFactor: uint64ValPtr(120)} - eff := EffectiveBuilderConfig(perKey, def) + eff := effectiveBuilderConfig(perKey, def) require.Equal(t, "http://side-car:9001", *eff.Proxy) require.Equal(t, validator.Uint64(120), *eff.BuilderBoostFactor) }) t.Run("both-set min_bid: per-key wins", func(t *testing.T) { def := &BuilderConfig{MinBid: uint64ValPtr(5000000)} perKey := &BuilderConfig{MinBid: uint64ValPtr(7000000)} - require.Equal(t, validator.Uint64(7000000), *EffectiveBuilderConfig(perKey, def).MinBid) + require.Equal(t, validator.Uint64(7000000), *effectiveBuilderConfig(perKey, def).MinBid) }) t.Run("both-set proxy: per-key wins", func(t *testing.T) { def := &BuilderConfig{Proxy: proto.String("http://default:1")} perKey := &BuilderConfig{Proxy: proto.String("http://mine:2")} - require.Equal(t, "http://mine:2", *EffectiveBuilderConfig(perKey, def).Proxy) + require.Equal(t, "http://mine:2", *effectiveBuilderConfig(perKey, def).Proxy) }) t.Run("nonzero per-key gas limit wins over default", func(t *testing.T) { def := &BuilderConfig{GasLimit: validator.Uint64(30000000)} perKey := &BuilderConfig{GasLimit: validator.Uint64(45000000)} - require.Equal(t, validator.Uint64(45000000), EffectiveBuilderConfig(perKey, def).GasLimit) + require.Equal(t, validator.Uint64(45000000), effectiveBuilderConfig(perKey, def).GasLimit) }) t.Run("nil nil is nil", func(t *testing.T) { - require.Equal(t, (*BuilderConfig)(nil), EffectiveBuilderConfig(nil, nil)) + require.Equal(t, (*BuilderConfig)(nil), effectiveBuilderConfig(nil, nil)) }) } diff --git a/config/proposer/loader/loader.go b/config/proposer/loader/loader.go index 820ee63152ae..955b6295ee8e 100644 --- a/config/proposer/loader/loader.go +++ b/config/proposer/loader/loader.go @@ -304,6 +304,8 @@ func promotePayloadToV2(p *validatorpb.ProposerSettingsPayload) { if opt.GasLimit == 0 { opt.GasLimit = opt.Builder.GasLimit } + // In v1 an absent enabled meant disabled; make that explicit, else the + // --enable-builder fill (which only applies when unset) would turn it on. if opt.Builder.Enabled == nil { disabled := false opt.Builder.Enabled = &disabled diff --git a/config/proposer/loader/loader_test.go b/config/proposer/loader/loader_test.go index 419b572a9081..cc9d927c60d1 100644 --- a/config/proposer/loader/loader_test.go +++ b/config/proposer/loader/loader_test.go @@ -1382,6 +1382,20 @@ func Test_mergeProposerSettings_VersionGatesBuilderReset(t *testing.T) { merged3 := mergeProposerSettings(nil, db3, opts) require.IsNil(t, merged3.ProposerConfig["0xkey"].Builder) }) + t.Run("v1-promoted wire-absent enabled stays disabled under --enable-builder", func(t *testing.T) { + opts := &flagOptions{builderConfig: &proposer.BuilderConfig{Enabled: true}} + // v1 file: no enabled key in the builder block meant disabled. + file := &validatorpb.ProposerSettingsPayload{ + DefaultConfig: &validatorpb.ProposerOptionPayload{ + FeeRecipient: "0x", + Builder: &validatorpb.BuilderConfig{GasLimit: 30000000}, + }, + } + db := &validatorpb.ProposerSettingsPayload{Version: proposer.SchemaV2} + merged := mergeProposerSettings(file, db, opts) + require.NotNil(t, merged.DefaultConfig.Builder.Enabled) + require.Equal(t, false, *merged.DefaultConfig.Builder.Enabled) + }) } func Test_mergeProposerSettings_V2LoadedOverridesDB(t *testing.T) { diff --git a/config/proposer/settings.go b/config/proposer/settings.go index 73084119e142..23db057a518a 100644 --- a/config/proposer/settings.go +++ b/config/proposer/settings.go @@ -162,6 +162,22 @@ func (be *BuilderEntry) EffectiveAuthData() []byte { return []byte(be.URL) } +// EffectiveBuilderConfig resolves key's builder config against default_config; +// nil when neither level configures a builder. +func (ps *Settings) EffectiveBuilderConfig(key [fieldparams.BLSPubkeyLength]byte) *BuilderConfig { + if ps == nil { + return nil + } + var perKey, def *BuilderConfig + if ps.DefaultConfig != nil { + def = ps.DefaultConfig.BuilderConfig + } + if opt, ok := ps.ProposeConfig[key]; ok && opt != nil { + perKey = opt.BuilderConfig + } + return effectiveBuilderConfig(perKey, def) +} + // EntryIdentity is what makes a builder entry unique: its url compared as the // exact string and its auth_data as the resolved bytes. type EntryIdentity struct { @@ -231,9 +247,9 @@ func (bc *BuilderConfig) IsEnabled() bool { return bc != nil && bc.Enabled } -// EffectiveBuilderConfig resolves per-key builder config with field-level -// inheritance; the builders list replaces rather than merges. -func EffectiveBuilderConfig(perKey, def *BuilderConfig) *BuilderConfig { +// effectiveBuilderConfig merges two config levels with field-level inheritance; +// the builders list replaces rather than merges. +func effectiveBuilderConfig(perKey, def *BuilderConfig) *BuilderConfig { if perKey == nil { return def } diff --git a/config/proposer/settings_test.go b/config/proposer/settings_test.go index 855cc28dbe63..67ddb515efa5 100644 --- a/config/proposer/settings_test.go +++ b/config/proposer/settings_test.go @@ -672,7 +672,7 @@ func TestSettingFromConsensus_NormalizesLegacyPresence(t *testing.T) { // A disabled per-key section can never silently activate under an enabled default: // EffectiveBuilderConfig takes the per-key enabled, not the default's. - eff := EffectiveBuilderConfig(perKey, &BuilderConfig{Enabled: true}) + eff := effectiveBuilderConfig(perKey, &BuilderConfig{Enabled: true}) require.Equal(t, false, eff.IsEnabled()) } diff --git a/validator/client/validator.go b/validator/client/validator.go index ae48f02fb428..890c062d4352 100644 --- a/validator/client/validator.go +++ b/validator/client/validator.go @@ -1377,31 +1377,12 @@ type builderTarget struct { boostFactor *uint64 } -// builderConfigForKey returns pk's effective builder config: per-key fields win, -// unset fields inherit from default_config (field-level inheritance). -func (v *validator) builderConfigForKey(pk pubkey) *proposer.BuilderConfig { - ps := v.ProposerSettings() - if ps == nil { - return nil - } - var def, perKey *proposer.BuilderConfig - if ps.DefaultConfig != nil { - def = ps.DefaultConfig.BuilderConfig - } - if ps.ProposeConfig != nil { - if c, ok := ps.ProposeConfig[pk]; ok && c != nil { - perKey = c.BuilderConfig - } - } - return proposer.EffectiveBuilderConfig(perKey, def) -} - // builderTargetsForKey resolves the enabled builder list for pk; each entry // overrides the config-level fallbacks. // TODO(gloas): resolved minBid/boostFactor/pubkey take effect with the inline // produce-block preferences wire (beacon-APIs #630). func (v *validator) builderTargetsForKey(pk pubkey) []builderTarget { - bc := v.builderConfigForKey(pk) + bc := v.ProposerSettings().EffectiveBuilderConfig(pk) if !bc.IsEnabled() { return nil } @@ -1572,26 +1553,17 @@ func (v *validator) buildSignedRegReqs( if isV2 { hasFeeRecipient := false - var defBC, perKeyBC *proposer.BuilderConfig - if defaultConfig := v.ProposerSettings().DefaultConfig; defaultConfig != nil { - defBC = defaultConfig.BuilderConfig - if defaultConfig.FeeRecipientConfig != nil { - feeRecipient = defaultConfig.FeeRecipientConfig.FeeRecipient - hasFeeRecipient = true - } + if defaultConfig := v.ProposerSettings().DefaultConfig; defaultConfig != nil && defaultConfig.FeeRecipientConfig != nil { + feeRecipient = defaultConfig.FeeRecipientConfig.FeeRecipient + hasFeeRecipient = true } - if v.ProposerSettings().ProposeConfig != nil { - if config, ok := v.ProposerSettings().ProposeConfig[k]; ok && config != nil { - perKeyBC = config.BuilderConfig - if config.FeeRecipientConfig != nil { - feeRecipient = config.FeeRecipientConfig.FeeRecipient - hasFeeRecipient = true - } - } + if config, ok := v.ProposerSettings().ProposeConfig[k]; ok && config != nil && config.FeeRecipientConfig != nil { + feeRecipient = config.FeeRecipientConfig.FeeRecipient + hasFeeRecipient = true } // Field-level resolution: per-key builder fields inherit from the default; // registration still requires a fee recipient configured at some level. - bc := proposer.EffectiveBuilderConfig(perKeyBC, defBC) + bc := v.ProposerSettings().EffectiveBuilderConfig(k) enabled = bc.IsEnabled() && hasFeeRecipient if bc.GasLimit != 0 { gasLimit = uint64(bc.GasLimit) diff --git a/validator/rpc/handlers_validator_config.go b/validator/rpc/handlers_validator_config.go index 6fa2b774af5c..4159352f574d 100644 --- a/validator/rpc/handlers_validator_config.go +++ b/validator/rpc/handlers_validator_config.go @@ -39,7 +39,11 @@ func (s *Server) GetBuilders(w http.ResponseWriter, r *http.Request) { return } - eff := resolveBuilders(s.validatorService.ProposerSettings(), bytesutil.ToBytes48(pubkey)) + // The Effective* getters resolve unset values (no floor, neutral boost, trustless-only). + eff := s.validatorService.ProposerSettings().EffectiveBuilderConfig(bytesutil.ToBytes48(pubkey)) + if eff == nil { + eff = &proposer.BuilderConfig{} + } out := builderConfigJSONFromConsensus(eff) // The resolved response always states a concrete builders list. if out.Builders == nil { @@ -148,24 +152,6 @@ func (s *Server) DeleteBuilders(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusNoContent) } -// resolveBuilders returns the key's effective builder config; the Effective* -// getters resolve unset values (no floor, neutral boost, trustless-only). -func resolveBuilders(settings *proposer.Settings, key [fieldparams.BLSPubkeyLength]byte) *proposer.BuilderConfig { - var perKey, def *proposer.BuilderConfig - if settings != nil { - if settings.DefaultConfig != nil { - def = settings.DefaultConfig.BuilderConfig - } - if opt, ok := settings.ProposeConfig[key]; ok && opt != nil { - perKey = opt.BuilderConfig - } - } - if eff := proposer.EffectiveBuilderConfig(perKey, def); eff != nil { - return eff - } - return &proposer.BuilderConfig{} -} - func builderConfigJSONFromConsensus(bc *proposer.BuilderConfig) *BuilderConfigJson { enabled := bc.Enabled out := &BuilderConfigJson{ diff --git a/validator/rpc/handlers_validator_config_test.go b/validator/rpc/handlers_validator_config_test.go index 8b17e4d68eb5..dfc4fc803c03 100644 --- a/validator/rpc/handlers_validator_config_test.go +++ b/validator/rpc/handlers_validator_config_test.go @@ -9,7 +9,9 @@ import ( "strings" "testing" + fieldparams "github.com/OffchainLabs/prysm/v7/config/fieldparams" "github.com/OffchainLabs/prysm/v7/config/proposer" + "github.com/OffchainLabs/prysm/v7/consensus-types/validator" "github.com/OffchainLabs/prysm/v7/testing/require" "github.com/OffchainLabs/prysm/v7/validator/keymanager/derived" mocks "github.com/OffchainLabs/prysm/v7/validator/testing" @@ -127,6 +129,26 @@ func TestServer_SetBuilders_PreservesOtherKeysUseNone(t *testing.T) { require.Equal(t, 0, len(cfg.Builders)) } +// POST upgrades v1 settings in place: the version bumps and builder gas limits +// migrate to the option before the key's builder config is replaced. +func TestServer_SetBuilders_UpgradesV1Settings(t *testing.T) { + srv, keys := setupConfigServer(t, 1) + pk := hexutil.Encode(keys[0][:]) + require.NoError(t, srv.validatorService.SetProposerSettings(t.Context(), &proposer.Settings{ + Version: proposer.SchemaV1, + ProposeConfig: map[[fieldparams.BLSPubkeyLength]byte]*proposer.Option{ + keys[0]: {BuilderConfig: &proposer.BuilderConfig{Enabled: true, GasLimit: 999}}, + }, + })) + require.Equal(t, http.StatusAccepted, postBuilders(t, srv, pk, `{"enabled":true,"builders":[{"url":"https://a.example"}]}`).Code) + + got := srv.validatorService.ProposerSettings() + require.Equal(t, proposer.SchemaV2, got.Version) + opt := got.ProposeConfig[keys[0]] + require.Equal(t, validator.Uint64(999), opt.GasLimit) + require.Equal(t, 1, len(opt.BuilderConfig.Builders)) +} + // builder_pubkey rides along as the entry's response filter. func TestServer_SetBuilders_BuilderPubkeyFilter(t *testing.T) { srv, keys := setupConfigServer(t, 1) @@ -155,8 +177,12 @@ func TestServer_SetBuilders_Rejects(t *testing.T) { "pubkey-only entry": {`{"enabled":true,"builders":[{"builder_pubkey":"` + bpk + `"}]}`, "url is required"}, "same url and auth_data": {`{"enabled":true,"builders":[{"url":"https://a"},{"url":"https://a"}]}`, "share the same url and auth_data"}, "omitted auth_data collides with its derived value": {`{"enabled":true,"builders":[{"url":"https://a"},{"url":"https://a","auth_data":"` + hexutil.Encode([]byte("https://a")) + `"}]}`, "share the same url and auth_data"}, - "invalid url": {`{"enabled":true,"builders":[{"url":"not a url"}]}`, "url is not a valid URL"}, - "url too long": {`{"enabled":true,"builders":[{"url":"` + longURL + `"}]}`, "url exceeds 2048 bytes"}, + "invalid url": {`{"enabled":true,"builders":[{"url":"not a url"}]}`, "url is not a valid URL"}, + "url too long": {`{"enabled":true,"builders":[{"url":"` + longURL + `"}]}`, "url exceeds 2048 bytes"}, + "invalid builder_pubkey": {`{"enabled":true,"builders":[{"url":"https://a","builder_pubkey":"0x1234"}]}`, "builder_pubkey is not a valid BLS public key"}, + "invalid auth_data hex": {`{"enabled":true,"builders":[{"url":"https://a","auth_data":"0xzz"}]}`, "auth_data is not valid hex"}, + "auth_data too long": {`{"enabled":true,"builders":[{"url":"https://a","auth_data":"0x` + strings.Repeat("ab", 4097) + `"}]}`, "auth_data exceeds 4096 bytes"}, + "non-numeric min_bid": {`{"enabled":true,"min_bid":"abc","builders":[]}`, "min_bid is not a valid uint64"}, } for name, tc := range cases { t.Run(name, func(t *testing.T) { From ea814aa3ef927f8c9ab5054ec47019f74526164f Mon Sep 17 00:00:00 2001 From: james-prysm Date: Thu, 6 Aug 2026 09:04:47 -0500 Subject: [PATCH 05/26] make changelog more concise --- changelog/james-prysm_keymanager-builders-endpoints.md | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/changelog/james-prysm_keymanager-builders-endpoints.md b/changelog/james-prysm_keymanager-builders-endpoints.md index a839bed706f7..be8a917e53b4 100644 --- a/changelog/james-prysm_keymanager-builders-endpoints.md +++ b/changelog/james-prysm_keymanager-builders-endpoints.md @@ -1,12 +1,12 @@ ### Added -- Add `GET`/`POST`/`DELETE /eth/v1/validator/{pubkey}/builders` keymanager endpoints for managing a key's per-builder configuration (keymanager-APIs #88). +- Add `GET`/`POST`/`DELETE /eth/v1/validator/{pubkey}/builders` keymanager endpoints (keymanager-APIs #88). ### Changed -- Resolve per-key builder settings with field-level inheritance from `default_config` on v2 proposer settings; registration uses the default fee recipient when a key has none, and `--enable-builder` only applies when `enabled` is unset. -- Merge file- or URL-loaded proposer settings per key: the file only overrides keys it names. -- Setting a fee recipient, gas limit, or graffiti for a new key no longer copies `default_config`'s builder settings onto that key; the key inherits them and tracks later changes. +- Add v2 proposer settings: per-key builder fields inherit from `default_config`, and `--enable-builder` fills the default toggle only when unset. +- Merge file- or URL-loaded proposer settings per key; the file only overrides keys it names. +- Setting a fee recipient, gas limit, or graffiti no longer copies `default_config`'s builder settings onto the key. ### Fixed @@ -14,4 +14,4 @@ ### Removed -- Remove the unused `relays` field from the proposer settings builder config; a settings file that still contains it logs a warning and the field is ignored. +- Remove the unused `relays` field from the builder config; a file that still contains it logs a warning and the field is ignored. From 4f25f607e444427bf0cada411b94439887cab673 Mon Sep 17 00:00:00 2001 From: james-prysm Date: Fri, 7 Aug 2026 09:39:57 -0500 Subject: [PATCH 06/26] rolling back some flag overrides --- ...mes-prysm_keymanager-builders-endpoints.md | 2 +- config/proposer/loader/loader.go | 16 ++++------------ config/proposer/loader/loader_test.go | 19 +++++++++++-------- 3 files changed, 16 insertions(+), 21 deletions(-) diff --git a/changelog/james-prysm_keymanager-builders-endpoints.md b/changelog/james-prysm_keymanager-builders-endpoints.md index be8a917e53b4..aed8b9eaa4a0 100644 --- a/changelog/james-prysm_keymanager-builders-endpoints.md +++ b/changelog/james-prysm_keymanager-builders-endpoints.md @@ -4,7 +4,7 @@ ### Changed -- Add v2 proposer settings: per-key builder fields inherit from `default_config`, and `--enable-builder` fills the default toggle only when unset. +- Add v2 proposer settings: per-key builder fields inherit from `default_config`; `--enable-builder` forces the default builder on, and per-key `enabled: false` still opts a key out. - Merge file- or URL-loaded proposer settings per key; the file only overrides keys it names. - Setting a fee recipient, gas limit, or graffiti no longer copies `default_config`'s builder settings onto the key. diff --git a/config/proposer/loader/loader.go b/config/proposer/loader/loader.go index 955b6295ee8e..95e1e4d4e60a 100644 --- a/config/proposer/loader/loader.go +++ b/config/proposer/loader/loader.go @@ -304,12 +304,6 @@ func promotePayloadToV2(p *validatorpb.ProposerSettingsPayload) { if opt.GasLimit == 0 { opt.GasLimit = opt.Builder.GasLimit } - // In v1 an absent enabled meant disabled; make that explicit, else the - // --enable-builder fill (which only applies when unset) would turn it on. - if opt.Builder.Enabled == nil { - disabled := false - opt.Builder.Enabled = &disabled - } if opt.Builder.MaxExecutionPayment != nil && *opt.Builder.MaxExecutionPayment == 0 { opt.Builder.MaxExecutionPayment = nil } @@ -392,8 +386,8 @@ func mergeProposerSettingsV2(merged, loaded, db *validatorpb.ProposerSettingsPay } merged.ProposerConfig = overlayProposerConfig(db, loaded) - // --enable-builder fills in only the default builder toggle when unset; an - // explicit enabled and every per-key entry are left for field-level inheritance. + // --enable-builder forces the default toggle on, matching v1; per-key + // enabled is untouched so a single key can still opt out (keymanager #88). if builderConfig != nil { if merged.DefaultConfig == nil { merged.DefaultConfig = &validatorpb.ProposerOptionPayload{} @@ -401,10 +395,8 @@ func mergeProposerSettingsV2(merged, loaded, db *validatorpb.ProposerSettingsPay if merged.DefaultConfig.Builder == nil { merged.DefaultConfig.Builder = &validatorpb.BuilderConfig{} } - if merged.DefaultConfig.Builder.Enabled == nil { - enabled := true - merged.DefaultConfig.Builder.Enabled = &enabled - } + enabled := true + merged.DefaultConfig.Builder.Enabled = &enabled } if gasLimitOnly == nil { diff --git a/config/proposer/loader/loader_test.go b/config/proposer/loader/loader_test.go index cc9d927c60d1..6317d6fff715 100644 --- a/config/proposer/loader/loader_test.go +++ b/config/proposer/loader/loader_test.go @@ -1345,7 +1345,7 @@ func Test_mergeProposerSettings_VersionGatesBuilderReset(t *testing.T) { require.NotNil(t, merged.DefaultConfig.Builder) require.Equal(t, validator.Uint64(40000000), merged.DefaultConfig.Builder.GasLimit) }) - t.Run("v2 --enable-builder fills the default toggle only when unset", func(t *testing.T) { + t.Run("v2 --enable-builder forces the default toggle on", func(t *testing.T) { opts := &flagOptions{builderConfig: &proposer.BuilderConfig{Enabled: true}} // No default builder: the flag creates it enabled. @@ -1358,7 +1358,7 @@ func Test_mergeProposerSettings_VersionGatesBuilderReset(t *testing.T) { require.NotNil(t, merged.DefaultConfig.Builder.Enabled) require.Equal(t, true, *merged.DefaultConfig.Builder.Enabled) - // An explicit default enabled=false is NOT overridden by the flag. + // An explicit default enabled=false IS overridden by the flag, matching v1. disabled := false db2 := &validatorpb.ProposerSettingsPayload{ Version: proposer.SchemaV2, @@ -1369,22 +1369,25 @@ func Test_mergeProposerSettings_VersionGatesBuilderReset(t *testing.T) { } merged2 := mergeProposerSettings(nil, db2, opts) require.NotNil(t, merged2.DefaultConfig.Builder.Enabled) - require.Equal(t, false, *merged2.DefaultConfig.Builder.Enabled) + require.Equal(t, true, *merged2.DefaultConfig.Builder.Enabled) - // Per-key entries are untouched by the flag. + // Per-key entries are untouched: explicit enabled=false still opts the key out. + keyDisabled := false db3 := &validatorpb.ProposerSettingsPayload{ Version: proposer.SchemaV2, DefaultConfig: &validatorpb.ProposerOptionPayload{FeeRecipient: "0x"}, ProposerConfig: map[string]*validatorpb.ProposerOptionPayload{ - "0xkey": {FeeRecipient: "0xk"}, + "0xkey": {FeeRecipient: "0xk"}, + "0xkey2": {FeeRecipient: "0xk2", Builder: &validatorpb.BuilderConfig{Enabled: &keyDisabled}}, }, } merged3 := mergeProposerSettings(nil, db3, opts) require.IsNil(t, merged3.ProposerConfig["0xkey"].Builder) + require.Equal(t, false, *merged3.ProposerConfig["0xkey2"].Builder.Enabled) }) - t.Run("v1-promoted wire-absent enabled stays disabled under --enable-builder", func(t *testing.T) { + t.Run("v1-promoted default under --enable-builder stays enabled, matching v1", func(t *testing.T) { opts := &flagOptions{builderConfig: &proposer.BuilderConfig{Enabled: true}} - // v1 file: no enabled key in the builder block meant disabled. + // v1 file with a builder block and no enabled key: the v1 flag turned it on. file := &validatorpb.ProposerSettingsPayload{ DefaultConfig: &validatorpb.ProposerOptionPayload{ FeeRecipient: "0x", @@ -1394,7 +1397,7 @@ func Test_mergeProposerSettings_VersionGatesBuilderReset(t *testing.T) { db := &validatorpb.ProposerSettingsPayload{Version: proposer.SchemaV2} merged := mergeProposerSettings(file, db, opts) require.NotNil(t, merged.DefaultConfig.Builder.Enabled) - require.Equal(t, false, *merged.DefaultConfig.Builder.Enabled) + require.Equal(t, true, *merged.DefaultConfig.Builder.Enabled) }) } From 40486bdf0e11a356ae292986ba0db01f2d2f81e0 Mon Sep 17 00:00:00 2001 From: james-prysm Date: Fri, 7 Aug 2026 09:44:07 -0500 Subject: [PATCH 07/26] reverting change --- ...mes-prysm_keymanager-builders-endpoints.md | 1 - config/proposer/loader/loader.go | 29 +++++++------------ config/proposer/loader/loader_test.go | 24 +++++++++++---- 3 files changed, 29 insertions(+), 25 deletions(-) diff --git a/changelog/james-prysm_keymanager-builders-endpoints.md b/changelog/james-prysm_keymanager-builders-endpoints.md index aed8b9eaa4a0..50f6d030b2a0 100644 --- a/changelog/james-prysm_keymanager-builders-endpoints.md +++ b/changelog/james-prysm_keymanager-builders-endpoints.md @@ -5,7 +5,6 @@ ### Changed - Add v2 proposer settings: per-key builder fields inherit from `default_config`; `--enable-builder` forces the default builder on, and per-key `enabled: false` still opts a key out. -- Merge file- or URL-loaded proposer settings per key; the file only overrides keys it names. - Setting a fee recipient, gas limit, or graffiti no longer copies `default_config`'s builder settings onto the key. ### Fixed diff --git a/config/proposer/loader/loader.go b/config/proposer/loader/loader.go index 95e1e4d4e60a..0d94b05dfaf2 100644 --- a/config/proposer/loader/loader.go +++ b/config/proposer/loader/loader.go @@ -314,25 +314,16 @@ func promotePayloadToV2(p *validatorpb.ProposerSettingsPayload) { } } -// overlayProposerConfig merges per-key options: the loaded source wins only for -// keys it names; DB-resident keys it does not name are retained. -func overlayProposerConfig(db, loaded *validatorpb.ProposerSettingsPayload) map[string]*validatorpb.ProposerOptionPayload { - var out map[string]*validatorpb.ProposerOptionPayload - if db != nil && len(db.ProposerConfig) > 0 { - out = make(map[string]*validatorpb.ProposerOptionPayload, len(db.ProposerConfig)) - for k, v := range db.ProposerConfig { - out[k] = v - } - } +// selectProposerConfig keeps the pre-v2 source precedence: a loaded per-key +// section replaces the DB's entirely, so restarting with a file resets the DB. +func selectProposerConfig(db, loaded *validatorpb.ProposerSettingsPayload) map[string]*validatorpb.ProposerOptionPayload { if loaded != nil && len(loaded.ProposerConfig) > 0 { - if out == nil { - out = make(map[string]*validatorpb.ProposerOptionPayload, len(loaded.ProposerConfig)) - } - for k, v := range loaded.ProposerConfig { - out[k] = v - } + return loaded.ProposerConfig + } + if db != nil && len(db.ProposerConfig) > 0 { + return db.ProposerConfig } - return out + return nil } func mergeProposerSettingsV1(merged, loaded, db *validatorpb.ProposerSettingsPayload, builderConfig *validatorpb.BuilderConfig, gasLimitOnly *validator.Uint64) *validatorpb.ProposerSettingsPayload { @@ -353,7 +344,7 @@ func mergeProposerSettingsV1(merged, loaded, db *validatorpb.ProposerSettingsPay option.Builder = nil } } - merged.ProposerConfig = overlayProposerConfig(db, loaded) + merged.ProposerConfig = selectProposerConfig(db, loaded) if merged.DefaultConfig != nil { merged.DefaultConfig.Builder = processBuilderConfig(merged.DefaultConfig.Builder, builderConfig, gasLimitOnly) @@ -384,7 +375,7 @@ func mergeProposerSettingsV2(merged, loaded, db *validatorpb.ProposerSettingsPay if loaded != nil && loaded.DefaultConfig != nil { merged.DefaultConfig = loaded.DefaultConfig } - merged.ProposerConfig = overlayProposerConfig(db, loaded) + merged.ProposerConfig = selectProposerConfig(db, loaded) // --enable-builder forces the default toggle on, matching v1; per-key // enabled is untouched so a single key can still opt out (keymanager #88). diff --git a/config/proposer/loader/loader_test.go b/config/proposer/loader/loader_test.go index 6317d6fff715..73a56a74b03f 100644 --- a/config/proposer/loader/loader_test.go +++ b/config/proposer/loader/loader_test.go @@ -1236,7 +1236,7 @@ func Test_mergeProposerSettings_VersionPrecedence(t *testing.T) { // The builder gas limit is promoted so v2 reads see it at the top level. require.Equal(t, validator.Uint64(30000000), merged.DefaultConfig.GasLimit) }) - t.Run("file wins only for keys it names", func(t *testing.T) { + t.Run("file per-key section replaces the DB's entirely", func(t *testing.T) { dbPayload := &validatorpb.ProposerSettingsPayload{ Version: proposer.SchemaV2, ProposerConfig: map[string]*validatorpb.ProposerOptionPayload{ @@ -1251,11 +1251,25 @@ func Test_mergeProposerSettings_VersionPrecedence(t *testing.T) { }, } merged := mergeProposerSettings(filePayload, dbPayload, &flagOptions{}) - require.Equal(t, 2, len(merged.ProposerConfig)) + require.Equal(t, 1, len(merged.ProposerConfig)) require.Equal(t, "0x3333333333333333333333333333333333333333", merged.ProposerConfig["0xaa"].FeeRecipient) - // The key the file does not name keeps its DB-resident settings. - require.Equal(t, "0x2222222222222222222222222222222222222222", merged.ProposerConfig["0xbb"].FeeRecipient) - require.Equal(t, validator.Uint64(45000000), merged.ProposerConfig["0xbb"].GasLimit) + // Restarting with a file resets DB-resident keys the file does not name. + require.IsNil(t, merged.ProposerConfig["0xbb"]) + }) + t.Run("db per-key section kept when the file has none", func(t *testing.T) { + dbPayload := &validatorpb.ProposerSettingsPayload{ + Version: proposer.SchemaV2, + ProposerConfig: map[string]*validatorpb.ProposerOptionPayload{ + "0xaa": {FeeRecipient: "0x1111111111111111111111111111111111111111"}, + }, + } + filePayload := &validatorpb.ProposerSettingsPayload{ + Version: proposer.SchemaV2, + DefaultConfig: &validatorpb.ProposerOptionPayload{FeeRecipient: "0x4444444444444444444444444444444444444444"}, + } + merged := mergeProposerSettings(filePayload, dbPayload, &flagOptions{}) + require.Equal(t, 1, len(merged.ProposerConfig)) + require.Equal(t, "0x1111111111111111111111111111111111111111", merged.ProposerConfig["0xaa"].FeeRecipient) }) } From aa6247b9412e661b1befd7136e0f55ae5f1a2d75 Mon Sep 17 00:00:00 2001 From: james-prysm Date: Fri, 7 Aug 2026 09:50:23 -0500 Subject: [PATCH 08/26] clarifying changelog --- changelog/james-prysm_keymanager-builders-endpoints.md | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/changelog/james-prysm_keymanager-builders-endpoints.md b/changelog/james-prysm_keymanager-builders-endpoints.md index 50f6d030b2a0..05f491808e9b 100644 --- a/changelog/james-prysm_keymanager-builders-endpoints.md +++ b/changelog/james-prysm_keymanager-builders-endpoints.md @@ -1,16 +1,16 @@ ### Added -- Add `GET`/`POST`/`DELETE /eth/v1/validator/{pubkey}/builders` keymanager endpoints (keymanager-APIs #88). +- Add `GET`/`POST`/`DELETE /eth/v1/validator/{pubkey}/builders` keymanager endpoints for per-key builder configuration (keymanager-APIs #88). ### Changed -- Add v2 proposer settings: per-key builder fields inherit from `default_config`; `--enable-builder` forces the default builder on, and per-key `enabled: false` still opts a key out. -- Setting a fee recipient, gas limit, or graffiti no longer copies `default_config`'s builder settings onto the key. +- Add v2 proposer settings (`"version": 2`): builder fields a key does not set inherit from `default_config`. `--enable-builder` still forces the default builder on; a key with explicit `enabled: false` stays opted out. +- Setting a fee recipient, gas limit, or graffiti no longer snapshots `default_config`'s builder settings onto that key; the key keeps following the default as it changes. ### Fixed -- Decode unset builder settings fields identically across both validator DB backends. +- Fix the minimal slashing protection database decoding unset builder settings fields as explicit zero values; both validator DB backends now read the same stored settings identically. ### Removed -- Remove the unused `relays` field from the builder config; a file that still contains it logs a warning and the field is ignored. +- Remove the unused `relays` field from the builder config; a settings file that still contains it logs a warning and is otherwise unaffected. From 586904e2bc6e40e770220c451d0b1b4180cd190f Mon Sep 17 00:00:00 2001 From: james-prysm Date: Fri, 7 Aug 2026 10:12:08 -0500 Subject: [PATCH 09/26] self review on comments --- config/proposer/settings.go | 32 ++++++++-------------- validator/client/validator.go | 13 +++------ validator/rpc/handlers_validator_config.go | 15 ++++------ validator/rpc/structs.go | 9 ++---- 4 files changed, 24 insertions(+), 45 deletions(-) diff --git a/config/proposer/settings.go b/config/proposer/settings.go index 23db057a518a..ea8578da3818 100644 --- a/config/proposer/settings.go +++ b/config/proposer/settings.go @@ -332,9 +332,8 @@ func builderEntryFromConsensus(from *validatorpb.BuilderEntry) *BuilderEntry { return e } -// Schema versions for proposer settings. SchemaV1Unset is the proto3 zero -// value — every existing v1 user has it, since the version field is new. -// Both SchemaV1Unset and SchemaV1 are legacy v1 inputs to the migration. +// Schema versions for proposer settings. SchemaV1Unset is the proto3 zero value +// every pre-versioning user has; both it and SchemaV1 are legacy v1 inputs. const ( SchemaV1Unset uint32 = 0 SchemaV1 uint32 = 1 @@ -571,11 +570,8 @@ func (ps *Settings) WarnDeprecatedSchema() { log.Warn("Proposer settings use the deprecated v1 schema; they are upgraded automatically at the gloas fork. Please migrate your settings source to v2.") } -// UpgradeToV2 migrates v1 settings to v2 in place: builder gas limits are -// promoted to the top-level preferences gas limit (unless one is already set). -// BuilderConfig is retained because it carries the gloas builder-API relays / -// enabled / max_execution_payment, which have no top-level v2 field. Settings -// already on v2 are left untouched. Returns true if changed. +// UpgradeToV2 migrates v1 settings in place: builder gas limits are promoted to +// the option level unless one is set. Returns true if anything changed. func (ps *Settings) UpgradeToV2() bool { if ps == nil || ps.isV2() { return false @@ -597,10 +593,8 @@ func (ps *Settings) UpgradeToV2() bool { return true } -// TargetGasLimit returns the proposer preferences gas limit for pubkey from -// the top-level fields only: the per-pubkey override, else the default config -// value, else the chain default. Builder gas limits are registration-only and -// intentionally not consulted. +// TargetGasLimit resolves pubkey's gas limit from top-level fields only: per-key, +// else default config, else chain default. Builder gas limits are registration-only. func (ps *Settings) TargetGasLimit(pubkey [fieldparams.BLSPubkeyLength]byte) validator.Uint64 { chainDefault := validator.Uint64(params.BeaconConfig().DefaultBuilderGasLimit) if ps == nil { @@ -615,9 +609,8 @@ func (ps *Settings) TargetGasLimit(pubkey [fieldparams.BLSPubkeyLength]byte) val return chainDefault } -// GasLimit returns the gas limit (gwei) for pubkey: the per-pubkey override, -// else the default config value, else the chain default. v1 reads the builder -// gas limit; v2 reads the top-level fields. +// GasLimit resolves pubkey's gas limit: per-key, else default config, else chain +// default. v1 reads builder gas limits; v2 reads the top-level fields. func (ps *Settings) GasLimit(pubkey [fieldparams.BLSPubkeyLength]byte) validator.Uint64 { chainDefault := validator.Uint64(params.BeaconConfig().DefaultBuilderGasLimit) if ps == nil { @@ -635,11 +628,8 @@ func (ps *Settings) GasLimit(pubkey [fieldparams.BLSPubkeyLength]byte) validator return chainDefault } -// SetGasLimit writes the per-pubkey gas limit. v1 requires existing settings -// with builder enabled. -// UpsertProposeOption returns the per-key proposer option, creating the ProposeConfig -// map and the option if absent. A newly created option has a nil BuilderConfig so it -// inherits default_config rather than snapshotting it. +// UpsertProposeOption returns pubkey's option, creating it if absent. A new +// option keeps BuilderConfig nil so it inherits default_config. func (ps *Settings) UpsertProposeOption(pubkey [fieldparams.BLSPubkeyLength]byte) *Option { if ps.ProposeConfig == nil { ps.ProposeConfig = make(map[[fieldparams.BLSPubkeyLength]byte]*Option) @@ -652,6 +642,8 @@ func (ps *Settings) UpsertProposeOption(pubkey [fieldparams.BLSPubkeyLength]byte return opt } +// SetGasLimit writes the per-pubkey gas limit. v1 requires existing settings +// with builder enabled. func (ps *Settings) SetGasLimit(pubkey [fieldparams.BLSPubkeyLength]byte, gasLimit validator.Uint64) error { if ps == nil { return errors.New("No proposer settings were found to update") diff --git a/validator/client/validator.go b/validator/client/validator.go index 890c062d4352..7eaa476f5750 100644 --- a/validator/client/validator.go +++ b/validator/client/validator.go @@ -1139,11 +1139,8 @@ func (v *validator) buildProposerSettingsRequests( return prepareProposerReqs } -// upgradeProposerSettingsToV2 migrates v1 proposer settings to v2 and persists -// them. Callers must gate this on gloas-active so the pre-gloas registration -// path still sees BuilderConfig. -// Upgrades a clone then swaps, so concurrent readers never observe a -// half-migrated settings object. +// upgradeProposerSettingsToV2 migrates v1 settings to v2 and persists them; callers +// gate on gloas-active. Upgrades a clone then swaps so readers never see a half state. func (v *validator) upgradeProposerSettingsToV2(ctx context.Context) { ps := v.ProposerSettings().Clone() if !ps.UpgradeToV2() { @@ -1377,10 +1374,8 @@ type builderTarget struct { boostFactor *uint64 } -// builderTargetsForKey resolves the enabled builder list for pk; each entry -// overrides the config-level fallbacks. -// TODO(gloas): resolved minBid/boostFactor/pubkey take effect with the inline -// produce-block preferences wire (beacon-APIs #630). +// builderTargetsForKey resolves the enabled builder list for pk; entries override +// config-level fallbacks. TODO(gloas): minBid/boost/pubkey ride the #630 wire. func (v *validator) builderTargetsForKey(pk pubkey) []builderTarget { bc := v.ProposerSettings().EffectiveBuilderConfig(pk) if !bc.IsEnabled() { diff --git a/validator/rpc/handlers_validator_config.go b/validator/rpc/handlers_validator_config.go index 4159352f574d..a5b937705ff2 100644 --- a/validator/rpc/handlers_validator_config.go +++ b/validator/rpc/handlers_validator_config.go @@ -23,9 +23,8 @@ const ( maxAuthDataSize = 4096 // MAX_DATA_SIZE ) -// GetBuilders implements GET /eth/v1/validator/{pubkey}/builders (keymanager-APIs #88). -// It returns the key's builder configuration resolved against default_config so -// re-submitting the response reproduces the same behavior. +// GetBuilders implements GET /eth/v1/validator/{pubkey}/builders (keymanager-APIs +// #88): the key's config resolved against default_config, safe to re-submit. func (s *Server) GetBuilders(w http.ResponseWriter, r *http.Request) { _, span := trace.StartSpan(r.Context(), "validator.keymanagerAPI.GetBuilders") defer span.End() @@ -52,9 +51,8 @@ func (s *Server) GetBuilders(w http.ResponseWriter, r *http.Request) { httputil.WriteJson(w, out) } -// SetBuilders implements POST /eth/v1/validator/{pubkey}/builders. It replaces the -// key's builder configuration in full, leaving fee recipient, gas limit and graffiti -// untouched. +// SetBuilders implements POST /eth/v1/validator/{pubkey}/builders, replacing the +// key's builder config in full; fee recipient, gas limit and graffiti are untouched. func (s *Server) SetBuilders(w http.ResponseWriter, r *http.Request) { ctx, span := trace.StartSpan(r.Context(), "validator.keymanagerAPI.SetBuilders") defer span.End() @@ -111,9 +109,8 @@ func (s *Server) SetBuilders(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusAccepted) } -// DeleteBuilders implements DELETE /eth/v1/validator/{pubkey}/builders. It removes -// the key's builder configuration so the key follows the validator client defaults; -// this differs from enabled:false. +// DeleteBuilders implements DELETE /eth/v1/validator/{pubkey}/builders: the key +// follows the validator client defaults again; not the same as enabled:false. func (s *Server) DeleteBuilders(w http.ResponseWriter, r *http.Request) { ctx, span := trace.StartSpan(r.Context(), "validator.keymanagerAPI.DeleteBuilders") defer span.End() diff --git a/validator/rpc/structs.go b/validator/rpc/structs.go index 2f728641888e..4a8ddafe56ed 100644 --- a/validator/rpc/structs.go +++ b/validator/rpc/structs.go @@ -109,13 +109,8 @@ type GraffitiData struct { Graffiti string `json:"graffiti"` } -// Per-key builders keymanager api (keymanager-APIs #88). All integers are decimal -// strings and all byte values are 0x-prefixed hex, per keymanager conventions. -// A nil Builders means "use the validator client's builders"; a non-nil empty -// Builders means "use none" (p2p bids only). -// Builders has no omitempty: on the resolved GET response an empty list ("use no -// builders") must serialize as [] rather than be dropped. On POST input an absent -// field still decodes to nil, which means "inherit the validator client's builders". +// BuilderConfigJson is the keymanager-APIs #88 wire form: integers are decimal +// strings, bytes 0x-hex. Builders nil = inherit, [] = use none (kept by no omitempty). type BuilderConfigJson struct { Enabled *bool `json:"enabled"` MinBid *string `json:"min_bid,omitempty"` From ba99fc3a2f4da3f4ae426f6e793de378bc7fba0c Mon Sep 17 00:00:00 2001 From: james-prysm Date: Fri, 7 Aug 2026 11:08:35 -0500 Subject: [PATCH 10/26] moving functions around after self review --- config/proposer/settings_test.go | 130 +++--- validator/rpc/handlers_validator_config.go | 158 +------- .../rpc/handlers_validator_config_test.go | 381 +++++++++--------- validator/rpc/structs.go | 157 +++++++- 4 files changed, 410 insertions(+), 416 deletions(-) diff --git a/config/proposer/settings_test.go b/config/proposer/settings_test.go index 67ddb515efa5..d248e75d28d1 100644 --- a/config/proposer/settings_test.go +++ b/config/proposer/settings_test.go @@ -618,77 +618,79 @@ func TestSettings_TargetGasLimit(t *testing.T) { }) } -// Persisted payloads may predate url-required and (url, auth_data) uniqueness: -// url-less entries drop, (url, auth) duplicates keep the first, and an omitted -// auth_data compares as its derived value (the url's UTF-8 bytes). -func TestSettingFromConsensus_DedupsBuilders(t *testing.T) { - payload := &validatorpb.ProposerSettingsPayload{ - Version: SchemaV2, - DefaultConfig: &validatorpb.ProposerOptionPayload{ - Builder: &validatorpb.BuilderConfig{ - Enabled: proto.Bool(true), - Builders: []*validatorpb.BuilderEntry{ - {Url: "https://b.example", AuthData: []byte("first")}, - {Url: "https://b.example", AuthData: []byte("second")}, - {Url: "https://b.example", AuthData: []byte("first")}, - {Url: "https://other.example"}, - {Url: "https://other.example", AuthData: []byte("https://other.example")}, - {AuthData: []byte("url-less")}, +func TestSettingFromConsensus(t *testing.T) { + // Persisted payloads may predate url-required and (url, auth_data) uniqueness: + // url-less entries drop, (url, auth) duplicates keep the first, and an omitted + // auth_data compares as its derived value (the url's UTF-8 bytes). + t.Run("dedups builders", func(t *testing.T) { + payload := &validatorpb.ProposerSettingsPayload{ + Version: SchemaV2, + DefaultConfig: &validatorpb.ProposerOptionPayload{ + Builder: &validatorpb.BuilderConfig{ + Enabled: proto.Bool(true), + Builders: []*validatorpb.BuilderEntry{ + {Url: "https://b.example", AuthData: []byte("first")}, + {Url: "https://b.example", AuthData: []byte("second")}, + {Url: "https://b.example", AuthData: []byte("first")}, + {Url: "https://other.example"}, + {Url: "https://other.example", AuthData: []byte("https://other.example")}, + {AuthData: []byte("url-less")}, + }, }, }, - }, - } - ps, err := SettingFromConsensus(payload) - require.NoError(t, err) - builders := ps.DefaultConfig.BuilderConfig.Builders - require.Equal(t, 3, len(builders)) - require.DeepEqual(t, []byte("first"), builders[0].AuthData) - require.DeepEqual(t, []byte("second"), builders[1].AuthData) - require.Equal(t, "https://other.example", builders[2].URL) -} - -func TestSettingFromConsensus_NormalizesLegacyPresence(t *testing.T) { - key := [fieldparams.BLSPubkeyLength]byte{9} - legacy := &Settings{ - Version: SchemaV1, - DefaultConfig: &Option{BuilderConfig: &BuilderConfig{ - GasLimit: validator.Uint64(30000000), - MaxExecutionPayment: uint64ValPtr(0), - }}, - ProposeConfig: map[[fieldparams.BLSPubkeyLength]byte]*Option{ - key: {BuilderConfig: &BuilderConfig{GasLimit: validator.Uint64(25000000)}}, - }, - } + } + ps, err := SettingFromConsensus(payload) + require.NoError(t, err) + builders := ps.DefaultConfig.BuilderConfig.Builders + require.Equal(t, 3, len(builders)) + require.DeepEqual(t, []byte("first"), builders[0].AuthData) + require.DeepEqual(t, []byte("second"), builders[1].AuthData) + require.Equal(t, "https://other.example", builders[2].URL) + }) + + t.Run("normalizes legacy v1 presence", func(t *testing.T) { + key := [fieldparams.BLSPubkeyLength]byte{9} + legacy := &Settings{ + Version: SchemaV1, + DefaultConfig: &Option{BuilderConfig: &BuilderConfig{ + GasLimit: validator.Uint64(30000000), + MaxExecutionPayment: uint64ValPtr(0), + }}, + ProposeConfig: map[[fieldparams.BLSPubkeyLength]byte]*Option{ + key: {BuilderConfig: &BuilderConfig{GasLimit: validator.Uint64(25000000)}}, + }, + } - got, err := SettingFromConsensus(legacy.ToConsensus()) - require.NoError(t, err) + got, err := SettingFromConsensus(legacy.ToConsensus()) + require.NoError(t, err) - def := got.DefaultConfig.BuilderConfig - require.Equal(t, false, def.Enabled) - require.Equal(t, (*validator.Uint64)(nil), def.MaxExecutionPayment) + def := got.DefaultConfig.BuilderConfig + require.Equal(t, false, def.Enabled) + require.Equal(t, (*validator.Uint64)(nil), def.MaxExecutionPayment) - perKey := got.ProposeConfig[key].BuilderConfig - require.Equal(t, false, perKey.Enabled) + perKey := got.ProposeConfig[key].BuilderConfig + require.Equal(t, false, perKey.Enabled) - // A disabled per-key section can never silently activate under an enabled default: - // EffectiveBuilderConfig takes the per-key enabled, not the default's. - eff := effectiveBuilderConfig(perKey, &BuilderConfig{Enabled: true}) - require.Equal(t, false, eff.IsEnabled()) -} + // A disabled per-key section can never silently activate under an enabled default: + // EffectiveBuilderConfig takes the per-key enabled, not the default's. + eff := effectiveBuilderConfig(perKey, &BuilderConfig{Enabled: true}) + require.Equal(t, false, eff.IsEnabled()) + }) -func TestSettingFromConsensus_V2PresencePreserved(t *testing.T) { - v2 := &Settings{ - Version: SchemaV2, - DefaultConfig: &Option{BuilderConfig: &BuilderConfig{ - MaxExecutionPayment: uint64ValPtr(0), - }}, - } - got, err := SettingFromConsensus(v2.ToConsensus()) - require.NoError(t, err) - bc := got.DefaultConfig.BuilderConfig - require.Equal(t, false, bc.Enabled) - require.NotNil(t, bc.MaxExecutionPayment) - require.Equal(t, validator.Uint64(0), *bc.MaxExecutionPayment) + t.Run("v2 presence preserved", func(t *testing.T) { + v2 := &Settings{ + Version: SchemaV2, + DefaultConfig: &Option{BuilderConfig: &BuilderConfig{ + MaxExecutionPayment: uint64ValPtr(0), + }}, + } + got, err := SettingFromConsensus(v2.ToConsensus()) + require.NoError(t, err) + bc := got.DefaultConfig.BuilderConfig + require.Equal(t, false, bc.Enabled) + require.NotNil(t, bc.MaxExecutionPayment) + require.Equal(t, validator.Uint64(0), *bc.MaxExecutionPayment) + }) } func TestUpgradeToV2_NormalizesLegacyPresence(t *testing.T) { diff --git a/validator/rpc/handlers_validator_config.go b/validator/rpc/handlers_validator_config.go index a5b937705ff2..86886536d1ad 100644 --- a/validator/rpc/handlers_validator_config.go +++ b/validator/rpc/handlers_validator_config.go @@ -3,24 +3,13 @@ package rpc import ( "encoding/json" "net/http" - "net/url" - "strconv" "github.com/OffchainLabs/prysm/v7/beacon-chain/rpc/eth/shared" fieldparams "github.com/OffchainLabs/prysm/v7/config/fieldparams" "github.com/OffchainLabs/prysm/v7/config/proposer" - "github.com/OffchainLabs/prysm/v7/consensus-types/validator" "github.com/OffchainLabs/prysm/v7/encoding/bytesutil" "github.com/OffchainLabs/prysm/v7/monitoring/tracing/trace" "github.com/OffchainLabs/prysm/v7/network/httputil" - "github.com/ethereum/go-ethereum/common/hexutil" - "github.com/pkg/errors" -) - -const ( - maxBuilderEntries = 64 // MAX_BUILDER_ENTRIES - maxBuilderURLSize = 2048 // MAX_BUILDER_URL_SIZE - maxAuthDataSize = 4096 // MAX_DATA_SIZE ) // GetBuilders implements GET /eth/v1/validator/{pubkey}/builders (keymanager-APIs @@ -43,10 +32,10 @@ func (s *Server) GetBuilders(w http.ResponseWriter, r *http.Request) { if eff == nil { eff = &proposer.BuilderConfig{} } - out := builderConfigJSONFromConsensus(eff) + out := builderConfigFromConsensus(eff) // The resolved response always states a concrete builders list. if out.Builders == nil { - out.Builders = []*BuilderEntryJson{} + out.Builders = []*BuilderEntry{} } httputil.WriteJson(w, out) } @@ -66,7 +55,7 @@ func (s *Server) SetBuilders(w http.ResponseWriter, r *http.Request) { return } - var body BuilderConfigJson + var body BuilderConfig if err := json.NewDecoder(r.Body).Decode(&body); err != nil { httputil.HandleError(w, "Could not decode builder config: "+err.Error(), http.StatusBadRequest) return @@ -75,7 +64,7 @@ func (s *Server) SetBuilders(w http.ResponseWriter, r *http.Request) { httputil.HandleError(w, "enabled is required", http.StatusBadRequest) return } - bc, err := builderConfigFromJSON(&body) + bc, err := body.ToConsensus() if err != nil { httputil.HandleError(w, err.Error(), http.StatusBadRequest) return @@ -148,142 +137,3 @@ func (s *Server) DeleteBuilders(w http.ResponseWriter, r *http.Request) { } w.WriteHeader(http.StatusNoContent) } - -func builderConfigJSONFromConsensus(bc *proposer.BuilderConfig) *BuilderConfigJson { - enabled := bc.Enabled - out := &BuilderConfigJson{ - Enabled: &enabled, - MinBid: new(formatUint(bc.EffectiveMinBid())), - BuilderBoostFactor: new(formatUint(bc.EffectiveBuilderBoostFactor())), - } - if bc.Builders != nil { - out.Builders = make([]*BuilderEntryJson, 0, len(bc.Builders)) - for _, b := range bc.Builders { - out.Builders = append(out.Builders, builderEntryJSONFromConsensus(b, bc)) - } - } - return out -} - -func builderEntryJSONFromConsensus(be *proposer.BuilderEntry, bc *proposer.BuilderConfig) *BuilderEntryJson { - out := &BuilderEntryJson{ - MaxExecutionPayment: new(formatUint(be.EffectiveMaxExecutionPayment(bc))), - MinBid: new(formatUint(be.EffectiveMinBid(bc))), - BuilderBoostFactor: new(formatUint(be.EffectiveBuilderBoostFactor(bc))), - } - if be.URL != "" { - out.Url = be.URL - out.AuthData = new(hexutil.Encode(be.EffectiveAuthData())) - } - if len(be.Pubkey) != 0 { - out.BuilderPubkey = new(hexutil.Encode(be.Pubkey)) - } - return out -} - -func builderConfigFromJSON(in *BuilderConfigJson) (*proposer.BuilderConfig, error) { - bc := &proposer.BuilderConfig{Enabled: in.Enabled != nil && *in.Enabled} - if in.MinBid != nil { - v, err := parseUint(*in.MinBid, "min_bid") - if err != nil { - return nil, err - } - bc.MinBid = &v - } - if in.BuilderBoostFactor != nil { - v, err := parseUint(*in.BuilderBoostFactor, "builder_boost_factor") - if err != nil { - return nil, err - } - bc.BuilderBoostFactor = &v - } - if in.Builders == nil { - return bc, nil - } - if len(in.Builders) > maxBuilderEntries { - return nil, errors.Errorf("builders exceeds %d entries", maxBuilderEntries) - } - // Non-nil (possibly empty) list means "use exactly these builders", not "inherit". - // Omitted auth_data compares as its derived value, so it collides with the explicit form. - bc.Builders = make([]*proposer.BuilderEntry, 0, len(in.Builders)) - seen := make(map[proposer.EntryIdentity]bool, len(in.Builders)) - for i, entry := range in.Builders { - be, err := builderEntryFromJSON(entry, i) - if err != nil { - return nil, err - } - if seen[be.Identity()] { - return nil, errors.Errorf("builders[%d]: two entries share the same url and auth_data", i) - } - seen[be.Identity()] = true - bc.Builders = append(bc.Builders, be) - } - return bc, nil -} - -func builderEntryFromJSON(in *BuilderEntryJson, i int) (*proposer.BuilderEntry, error) { - be := &proposer.BuilderEntry{} - if in.Url == "" { - return nil, errors.Errorf("builders[%d].url is required", i) - } - if len(in.Url) > maxBuilderURLSize { - return nil, errors.Errorf("builders[%d].url exceeds %d bytes", i, maxBuilderURLSize) - } - if u, err := url.Parse(in.Url); err != nil || u.Scheme == "" || u.Host == "" { - return nil, errors.Errorf("builders[%d].url is not a valid URL", i) - } - be.URL = in.Url - if in.BuilderPubkey != nil { - pk, err := hexutil.Decode(*in.BuilderPubkey) - if err != nil || len(pk) != fieldparams.BLSPubkeyLength { - return nil, errors.Errorf("builders[%d].builder_pubkey is not a valid BLS public key", i) - } - be.Pubkey = pk - } - if in.AuthData != nil { - ad, err := hexutil.Decode(*in.AuthData) - if err != nil { - return nil, errors.Errorf("builders[%d].auth_data is not valid hex", i) - } - if len(ad) > maxAuthDataSize { - return nil, errors.Errorf("builders[%d].auth_data exceeds %d bytes", i, maxAuthDataSize) - } - be.AuthData = ad - } - if in.MaxExecutionPayment != nil { - v, err := parseUint(*in.MaxExecutionPayment, "builders[].max_execution_payment") - if err != nil { - return nil, err - } - be.MaxExecutionPayment = &v - } - if in.MinBid != nil { - v, err := parseUint(*in.MinBid, "builders[].min_bid") - if err != nil { - return nil, err - } - be.MinBid = &v - } - if in.BuilderBoostFactor != nil { - v, err := parseUint(*in.BuilderBoostFactor, "builders[].builder_boost_factor") - if err != nil { - return nil, err - } - be.BuilderBoostFactor = &v - } - return be, nil -} - -func parseUint(s, field string) (validator.Uint64, error) { - v, err := strconv.ParseUint(s, 10, 64) - if err != nil { - return 0, errors.Errorf("%s is not a valid uint64", field) - } - return validator.Uint64(v), nil -} - -func formatUint(v validator.Uint64) string { - return strconv.FormatUint(uint64(v), 10) -} - -func strPtr(s string) *string { return &s } diff --git a/validator/rpc/handlers_validator_config_test.go b/validator/rpc/handlers_validator_config_test.go index dfc4fc803c03..bb9fa608b9d2 100644 --- a/validator/rpc/handlers_validator_config_test.go +++ b/validator/rpc/handlers_validator_config_test.go @@ -43,13 +43,13 @@ func postBuilders(t *testing.T, s *Server, pubkey, body string) *httptest.Respon return w } -func getBuilders(t *testing.T, s *Server, pubkey string) (*httptest.ResponseRecorder, *BuilderConfigJson) { +func getBuilders(t *testing.T, s *Server, pubkey string) (*httptest.ResponseRecorder, *BuilderConfig) { req := httptest.NewRequest(http.MethodGet, "/eth/v1/validator/"+pubkey+"/builders", nil) req.SetPathValue("pubkey", pubkey) w := httptest.NewRecorder() w.Body = &bytes.Buffer{} s.GetBuilders(w, req) - cfg := &BuilderConfigJson{} + cfg := &BuilderConfig{} if w.Code == http.StatusOK { require.NoError(t, json.Unmarshal(w.Body.Bytes(), cfg)) } @@ -65,172 +65,196 @@ func deleteBuilders(t *testing.T, s *Server, pubkey string) *httptest.ResponseRe return w } -func TestServer_SetGetBuilders_RoundTrip(t *testing.T) { - srv, keys := setupConfigServer(t, 1) - pk := hexutil.Encode(keys[0][:]) - body := `{"enabled":true,"min_bid":"5","builder_boost_factor":"120",` + - `"builders":[{"url":"https://b.example","auth_data":"0x0102","max_execution_payment":"1000"}]}` - - w := postBuilders(t, srv, pk, body) - require.Equal(t, http.StatusAccepted, w.Code) - - _, cfg := getBuilders(t, srv, pk) - require.NotNil(t, cfg.Enabled) - require.Equal(t, true, *cfg.Enabled) - require.Equal(t, "5", *cfg.MinBid) - require.Equal(t, "120", *cfg.BuilderBoostFactor) - require.Equal(t, 1, len(cfg.Builders)) - require.Equal(t, "https://b.example", cfg.Builders[0].Url) - require.Equal(t, "1000", *cfg.Builders[0].MaxExecutionPayment) - // Resolved: the entry omitted min_bid/boost, so GET fills them from the defaults. - require.Equal(t, "5", *cfg.Builders[0].MinBid) - require.Equal(t, "120", *cfg.Builders[0].BuilderBoostFactor) -} - -func TestServer_SetBuilders_FullReplace(t *testing.T) { - srv, keys := setupConfigServer(t, 1) - pk := hexutil.Encode(keys[0][:]) - - require.Equal(t, http.StatusAccepted, postBuilders(t, srv, pk, - `{"enabled":true,"builders":[{"url":"https://a.example"},{"url":"https://b.example"}]}`).Code) - require.Equal(t, http.StatusAccepted, postBuilders(t, srv, pk, - `{"enabled":true,"builders":[{"url":"https://c.example"}]}`).Code) - - _, cfg := getBuilders(t, srv, pk) - require.Equal(t, 1, len(cfg.Builders)) - require.Equal(t, "https://c.example", cfg.Builders[0].Url) -} - -func TestServer_SetBuilders_EmptyArrayIsUseNone(t *testing.T) { - srv, keys := setupConfigServer(t, 1) - pk := hexutil.Encode(keys[0][:]) - require.Equal(t, http.StatusAccepted, postBuilders(t, srv, pk, `{"enabled":true,"builders":[]}`).Code) - - _, cfg := getBuilders(t, srv, pk) - require.NotNil(t, cfg.Builders) - require.Equal(t, 0, len(cfg.Builders)) -} - -// A POST for one key must not flip another key's use-none marker back to inherit. -func TestServer_SetBuilders_PreservesOtherKeysUseNone(t *testing.T) { - srv, keys := setupConfigServer(t, 2) - pkA := hexutil.Encode(keys[0][:]) - pkB := hexutil.Encode(keys[1][:]) - require.NoError(t, srv.validatorService.SetProposerSettings(t.Context(), &proposer.Settings{ - Version: proposer.SchemaV2, - DefaultConfig: &proposer.Option{ - BuilderConfig: &proposer.BuilderConfig{Enabled: true, Builders: []*proposer.BuilderEntry{{URL: "https://default.example"}}}, - }, - })) - require.Equal(t, http.StatusAccepted, postBuilders(t, srv, pkA, `{"enabled":true,"builders":[]}`).Code) - require.Equal(t, http.StatusAccepted, postBuilders(t, srv, pkB, `{"enabled":true,"builders":[{"url":"https://b.example"}]}`).Code) - - _, cfg := getBuilders(t, srv, pkA) - require.Equal(t, 0, len(cfg.Builders)) -} - -// POST upgrades v1 settings in place: the version bumps and builder gas limits -// migrate to the option before the key's builder config is replaced. -func TestServer_SetBuilders_UpgradesV1Settings(t *testing.T) { - srv, keys := setupConfigServer(t, 1) - pk := hexutil.Encode(keys[0][:]) - require.NoError(t, srv.validatorService.SetProposerSettings(t.Context(), &proposer.Settings{ - Version: proposer.SchemaV1, - ProposeConfig: map[[fieldparams.BLSPubkeyLength]byte]*proposer.Option{ - keys[0]: {BuilderConfig: &proposer.BuilderConfig{Enabled: true, GasLimit: 999}}, - }, - })) - require.Equal(t, http.StatusAccepted, postBuilders(t, srv, pk, `{"enabled":true,"builders":[{"url":"https://a.example"}]}`).Code) - - got := srv.validatorService.ProposerSettings() - require.Equal(t, proposer.SchemaV2, got.Version) - opt := got.ProposeConfig[keys[0]] - require.Equal(t, validator.Uint64(999), opt.GasLimit) - require.Equal(t, 1, len(opt.BuilderConfig.Builders)) -} - -// builder_pubkey rides along as the entry's response filter. -func TestServer_SetBuilders_BuilderPubkeyFilter(t *testing.T) { - srv, keys := setupConfigServer(t, 1) - pk := hexutil.Encode(keys[0][:]) - bpk := "0x" + strings.Repeat("ab", 48) - body := `{"enabled":true,"builders":[{"url":"https://a.example","builder_pubkey":"` + bpk + `"}]}` - - require.Equal(t, http.StatusAccepted, postBuilders(t, srv, pk, body).Code) - _, cfg := getBuilders(t, srv, pk) - require.Equal(t, 1, len(cfg.Builders)) - require.Equal(t, "https://a.example", cfg.Builders[0].Url) - require.Equal(t, bpk, *cfg.Builders[0].BuilderPubkey) -} - -func TestServer_SetBuilders_Rejects(t *testing.T) { - srv, keys := setupConfigServer(t, 1) - pk := hexutil.Encode(keys[0][:]) - bpk := "0x" + strings.Repeat("ab", 48) - - longURL := "https://a.example/" + strings.Repeat("x", 2048) - cases := map[string]struct { - body, contains string - }{ - "missing enabled": {`{"builders":[{"url":"https://a"}]}`, "enabled is required"}, - "entry without url": {`{"enabled":true,"builders":[{"min_bid":"1"}]}`, "url is required"}, - "pubkey-only entry": {`{"enabled":true,"builders":[{"builder_pubkey":"` + bpk + `"}]}`, "url is required"}, - "same url and auth_data": {`{"enabled":true,"builders":[{"url":"https://a"},{"url":"https://a"}]}`, "share the same url and auth_data"}, - "omitted auth_data collides with its derived value": {`{"enabled":true,"builders":[{"url":"https://a"},{"url":"https://a","auth_data":"` + hexutil.Encode([]byte("https://a")) + `"}]}`, "share the same url and auth_data"}, - "invalid url": {`{"enabled":true,"builders":[{"url":"not a url"}]}`, "url is not a valid URL"}, - "url too long": {`{"enabled":true,"builders":[{"url":"` + longURL + `"}]}`, "url exceeds 2048 bytes"}, - "invalid builder_pubkey": {`{"enabled":true,"builders":[{"url":"https://a","builder_pubkey":"0x1234"}]}`, "builder_pubkey is not a valid BLS public key"}, - "invalid auth_data hex": {`{"enabled":true,"builders":[{"url":"https://a","auth_data":"0xzz"}]}`, "auth_data is not valid hex"}, - "auth_data too long": {`{"enabled":true,"builders":[{"url":"https://a","auth_data":"0x` + strings.Repeat("ab", 4097) + `"}]}`, "auth_data exceeds 4096 bytes"}, - "non-numeric min_bid": {`{"enabled":true,"min_bid":"abc","builders":[]}`, "min_bid is not a valid uint64"}, - } - for name, tc := range cases { - t.Run(name, func(t *testing.T) { - w := postBuilders(t, srv, pk, tc.body) - require.Equal(t, http.StatusBadRequest, w.Code) - require.Equal(t, true, strings.Contains(w.Body.String(), tc.contains), "body: %s", w.Body.String()) - }) - } +func TestServer_SetBuilders(t *testing.T) { + t.Run("round trip via GET", func(t *testing.T) { + srv, keys := setupConfigServer(t, 1) + pk := hexutil.Encode(keys[0][:]) + body := `{"enabled":true,"min_bid":"5","builder_boost_factor":"120",` + + `"builders":[{"url":"https://b.example","auth_data":"0x0102","max_execution_payment":"1000"}]}` + + w := postBuilders(t, srv, pk, body) + require.Equal(t, http.StatusAccepted, w.Code) + + _, cfg := getBuilders(t, srv, pk) + require.NotNil(t, cfg.Enabled) + require.Equal(t, true, *cfg.Enabled) + require.Equal(t, "5", *cfg.MinBid) + require.Equal(t, "120", *cfg.BuilderBoostFactor) + require.Equal(t, 1, len(cfg.Builders)) + require.Equal(t, "https://b.example", cfg.Builders[0].Url) + require.Equal(t, "1000", *cfg.Builders[0].MaxExecutionPayment) + // Resolved: the entry omitted min_bid/boost, so GET fills them from the defaults. + require.Equal(t, "5", *cfg.Builders[0].MinBid) + require.Equal(t, "120", *cfg.Builders[0].BuilderBoostFactor) + }) + + t.Run("full replace", func(t *testing.T) { + srv, keys := setupConfigServer(t, 1) + pk := hexutil.Encode(keys[0][:]) + + require.Equal(t, http.StatusAccepted, postBuilders(t, srv, pk, + `{"enabled":true,"builders":[{"url":"https://a.example"},{"url":"https://b.example"}]}`).Code) + require.Equal(t, http.StatusAccepted, postBuilders(t, srv, pk, + `{"enabled":true,"builders":[{"url":"https://c.example"}]}`).Code) + + _, cfg := getBuilders(t, srv, pk) + require.Equal(t, 1, len(cfg.Builders)) + require.Equal(t, "https://c.example", cfg.Builders[0].Url) + }) + + t.Run("empty array is use-none", func(t *testing.T) { + srv, keys := setupConfigServer(t, 1) + pk := hexutil.Encode(keys[0][:]) + require.Equal(t, http.StatusAccepted, postBuilders(t, srv, pk, `{"enabled":true,"builders":[]}`).Code) + + _, cfg := getBuilders(t, srv, pk) + require.NotNil(t, cfg.Builders) + require.Equal(t, 0, len(cfg.Builders)) + }) + + t.Run("preserves another key's use-none marker", func(t *testing.T) { + srv, keys := setupConfigServer(t, 2) + pkA := hexutil.Encode(keys[0][:]) + pkB := hexutil.Encode(keys[1][:]) + require.NoError(t, srv.validatorService.SetProposerSettings(t.Context(), &proposer.Settings{ + Version: proposer.SchemaV2, + DefaultConfig: &proposer.Option{ + BuilderConfig: &proposer.BuilderConfig{Enabled: true, Builders: []*proposer.BuilderEntry{{URL: "https://default.example"}}}, + }, + })) + require.Equal(t, http.StatusAccepted, postBuilders(t, srv, pkA, `{"enabled":true,"builders":[]}`).Code) + require.Equal(t, http.StatusAccepted, postBuilders(t, srv, pkB, `{"enabled":true,"builders":[{"url":"https://b.example"}]}`).Code) + + _, cfg := getBuilders(t, srv, pkA) + require.Equal(t, 0, len(cfg.Builders)) + }) + + t.Run("upgrades v1 settings in place", func(t *testing.T) { + srv, keys := setupConfigServer(t, 1) + pk := hexutil.Encode(keys[0][:]) + require.NoError(t, srv.validatorService.SetProposerSettings(t.Context(), &proposer.Settings{ + Version: proposer.SchemaV1, + ProposeConfig: map[[fieldparams.BLSPubkeyLength]byte]*proposer.Option{ + keys[0]: {BuilderConfig: &proposer.BuilderConfig{Enabled: true, GasLimit: 999}}, + }, + })) + require.Equal(t, http.StatusAccepted, postBuilders(t, srv, pk, `{"enabled":true,"builders":[{"url":"https://a.example"}]}`).Code) + + got := srv.validatorService.ProposerSettings() + require.Equal(t, proposer.SchemaV2, got.Version) + opt := got.ProposeConfig[keys[0]] + // The version bumps and the builder gas limit migrates to the option. + require.Equal(t, validator.Uint64(999), opt.GasLimit) + require.Equal(t, 1, len(opt.BuilderConfig.Builders)) + }) + + t.Run("builder_pubkey rides along as the entry's response filter", func(t *testing.T) { + srv, keys := setupConfigServer(t, 1) + pk := hexutil.Encode(keys[0][:]) + bpk := "0x" + strings.Repeat("ab", 48) + body := `{"enabled":true,"builders":[{"url":"https://a.example","builder_pubkey":"` + bpk + `"}]}` + + require.Equal(t, http.StatusAccepted, postBuilders(t, srv, pk, body).Code) + _, cfg := getBuilders(t, srv, pk) + require.Equal(t, 1, len(cfg.Builders)) + require.Equal(t, "https://a.example", cfg.Builders[0].Url) + require.Equal(t, bpk, *cfg.Builders[0].BuilderPubkey) + }) + + t.Run("rejects invalid input", func(t *testing.T) { + srv, keys := setupConfigServer(t, 1) + pk := hexutil.Encode(keys[0][:]) + bpk := "0x" + strings.Repeat("ab", 48) + + longURL := "https://a.example/" + strings.Repeat("x", 2048) + cases := map[string]struct { + body, contains string + }{ + "missing enabled": {`{"builders":[{"url":"https://a"}]}`, "enabled is required"}, + "entry without url": {`{"enabled":true,"builders":[{"min_bid":"1"}]}`, "url is required"}, + "pubkey-only entry": {`{"enabled":true,"builders":[{"builder_pubkey":"` + bpk + `"}]}`, "url is required"}, + "same url and auth_data": {`{"enabled":true,"builders":[{"url":"https://a"},{"url":"https://a"}]}`, "share the same url and auth_data"}, + "omitted auth_data collides with its derived value": {`{"enabled":true,"builders":[{"url":"https://a"},{"url":"https://a","auth_data":"` + hexutil.Encode([]byte("https://a")) + `"}]}`, "share the same url and auth_data"}, + "invalid url": {`{"enabled":true,"builders":[{"url":"not a url"}]}`, "url is not a valid URL"}, + "url too long": {`{"enabled":true,"builders":[{"url":"` + longURL + `"}]}`, "url exceeds 2048 bytes"}, + "invalid builder_pubkey": {`{"enabled":true,"builders":[{"url":"https://a","builder_pubkey":"0x1234"}]}`, "builder_pubkey is not a valid BLS public key"}, + "invalid auth_data hex": {`{"enabled":true,"builders":[{"url":"https://a","auth_data":"0xzz"}]}`, "auth_data is not valid hex"}, + "auth_data too long": {`{"enabled":true,"builders":[{"url":"https://a","auth_data":"0x` + strings.Repeat("ab", 4097) + `"}]}`, "auth_data exceeds 4096 bytes"}, + "non-numeric min_bid": {`{"enabled":true,"min_bid":"abc","builders":[]}`, "min_bid is not a valid uint64"}, + } + for name, tc := range cases { + t.Run(name, func(t *testing.T) { + w := postBuilders(t, srv, pk, tc.body) + require.Equal(t, http.StatusBadRequest, w.Code) + require.Equal(t, true, strings.Contains(w.Body.String(), tc.contains), "body: %s", w.Body.String()) + }) + } + }) + + t.Run("same url with distinct auth_data is a legal pair", func(t *testing.T) { + srv, keys := setupConfigServer(t, 1) + pk := hexutil.Encode(keys[0][:]) + body := `{"enabled":true,"builders":[{"url":"https://a","auth_data":"0x01"},{"url":"https://a","auth_data":"0x02"}]}` + require.Equal(t, http.StatusAccepted, postBuilders(t, srv, pk, body).Code) + _, cfg := getBuilders(t, srv, pk) + require.Equal(t, 2, len(cfg.Builders)) + }) + + t.Run("max entries", func(t *testing.T) { + srv, keys := setupConfigServer(t, 1) + pk := hexutil.Encode(keys[0][:]) + entries := make([]string, 0, maxBuilderEntries+1) + for i := 0; i <= maxBuilderEntries; i++ { + entries = append(entries, `{"url":"https://b`+strconv.Itoa(i)+`.example"}`) + } + body := `{"enabled":true,"builders":[` + strings.Join(entries, ",") + `]}` + w := postBuilders(t, srv, pk, body) + require.Equal(t, http.StatusBadRequest, w.Code) + require.Equal(t, true, strings.Contains(w.Body.String(), "exceeds 64 entries")) + }) + + t.Run("validator service nil", func(t *testing.T) { + srv, keys := setupConfigServer(t, 1) + srv.validatorService = nil + w := postBuilders(t, srv, hexutil.Encode(keys[0][:]), `{"enabled":true}`) + require.Equal(t, http.StatusServiceUnavailable, w.Code) + }) } -// Same url with distinct auth_data is a legal pair, not a duplicate. -func TestServer_SetBuilders_SharedURLDistinctAuth(t *testing.T) { - srv, keys := setupConfigServer(t, 1) - pk := hexutil.Encode(keys[0][:]) - body := `{"enabled":true,"builders":[{"url":"https://a","auth_data":"0x01"},{"url":"https://a","auth_data":"0x02"}]}` - require.Equal(t, http.StatusAccepted, postBuilders(t, srv, pk, body).Code) - _, cfg := getBuilders(t, srv, pk) - require.Equal(t, 2, len(cfg.Builders)) -} - -// GET is fully resolved: omitted auth_data becomes the url's UTF-8 bytes, and -// unset values become the runtime fallbacks (no floor, neutral boost, trustless-only). -func TestServer_GetBuilders_ResolvesOmittedValues(t *testing.T) { - srv, keys := setupConfigServer(t, 1) - pk := hexutil.Encode(keys[0][:]) - require.Equal(t, http.StatusAccepted, postBuilders(t, srv, pk, - `{"enabled":true,"builders":[{"url":"https://a.example"}]}`).Code) - _, cfg := getBuilders(t, srv, pk) - require.Equal(t, "0", *cfg.MinBid) - require.Equal(t, "100", *cfg.BuilderBoostFactor) - require.Equal(t, 1, len(cfg.Builders)) - require.Equal(t, hexutil.Encode([]byte("https://a.example")), *cfg.Builders[0].AuthData) - require.Equal(t, "0", *cfg.Builders[0].MinBid) - require.Equal(t, "100", *cfg.Builders[0].BuilderBoostFactor) - require.Equal(t, "0", *cfg.Builders[0].MaxExecutionPayment) -} - -func TestServer_SetBuilders_MaxEntries(t *testing.T) { - srv, keys := setupConfigServer(t, 1) - pk := hexutil.Encode(keys[0][:]) - entries := make([]string, 0, maxBuilderEntries+1) - for i := 0; i <= maxBuilderEntries; i++ { - entries = append(entries, `{"url":"https://b`+strconv.Itoa(i)+`.example"}`) - } - body := `{"enabled":true,"builders":[` + strings.Join(entries, ",") + `]}` - w := postBuilders(t, srv, pk, body) - require.Equal(t, http.StatusBadRequest, w.Code) - require.Equal(t, true, strings.Contains(w.Body.String(), "exceeds 64 entries")) +func TestServer_GetBuilders(t *testing.T) { + // GET is fully resolved: omitted auth_data becomes the url's UTF-8 bytes, and + // unset values become the runtime fallbacks (no floor, neutral boost, trustless-only). + t.Run("resolves omitted values", func(t *testing.T) { + srv, keys := setupConfigServer(t, 1) + pk := hexutil.Encode(keys[0][:]) + require.Equal(t, http.StatusAccepted, postBuilders(t, srv, pk, + `{"enabled":true,"builders":[{"url":"https://a.example"}]}`).Code) + _, cfg := getBuilders(t, srv, pk) + require.Equal(t, "0", *cfg.MinBid) + require.Equal(t, "100", *cfg.BuilderBoostFactor) + require.Equal(t, 1, len(cfg.Builders)) + require.Equal(t, hexutil.Encode([]byte("https://a.example")), *cfg.Builders[0].AuthData) + require.Equal(t, "0", *cfg.Builders[0].MinBid) + require.Equal(t, "100", *cfg.Builders[0].BuilderBoostFactor) + require.Equal(t, "0", *cfg.Builders[0].MaxExecutionPayment) + }) + + t.Run("resolves default_config for an unconfigured key", func(t *testing.T) { + srv, keys := setupConfigServer(t, 1) + pk := hexutil.Encode(keys[0][:]) + require.NoError(t, srv.validatorService.SetProposerSettings(t.Context(), &proposer.Settings{ + Version: proposer.SchemaV2, + DefaultConfig: &proposer.Option{ + BuilderConfig: &proposer.BuilderConfig{Enabled: true, Builders: []*proposer.BuilderEntry{{URL: "https://default.example"}}}, + }, + })) + + _, cfg := getBuilders(t, srv, pk) + require.NotNil(t, cfg.Enabled) + require.Equal(t, true, *cfg.Enabled) + require.Equal(t, 1, len(cfg.Builders)) + require.Equal(t, "https://default.example", cfg.Builders[0].Url) + }) } func TestServer_DeleteBuilders(t *testing.T) { @@ -249,28 +273,3 @@ func TestServer_DeleteBuilders(t *testing.T) { require.Equal(t, false, *cfg.Enabled) require.Equal(t, 0, len(cfg.Builders)) } - -// GET on an unconfigured key resolves against default_config. -func TestServer_GetBuilders_ResolvesDefault(t *testing.T) { - srv, keys := setupConfigServer(t, 1) - pk := hexutil.Encode(keys[0][:]) - require.NoError(t, srv.validatorService.SetProposerSettings(t.Context(), &proposer.Settings{ - Version: proposer.SchemaV2, - DefaultConfig: &proposer.Option{ - BuilderConfig: &proposer.BuilderConfig{Enabled: true, Builders: []*proposer.BuilderEntry{{URL: "https://default.example"}}}, - }, - })) - - _, cfg := getBuilders(t, srv, pk) - require.NotNil(t, cfg.Enabled) - require.Equal(t, true, *cfg.Enabled) - require.Equal(t, 1, len(cfg.Builders)) - require.Equal(t, "https://default.example", cfg.Builders[0].Url) -} - -func TestServer_SetBuilders_ValidatorServiceNil(t *testing.T) { - srv, keys := setupConfigServer(t, 1) - srv.validatorService = nil - w := postBuilders(t, srv, hexutil.Encode(keys[0][:]), `{"enabled":true}`) - require.Equal(t, http.StatusServiceUnavailable, w.Code) -} diff --git a/validator/rpc/structs.go b/validator/rpc/structs.go index 4a8ddafe56ed..9ee6166c5f46 100644 --- a/validator/rpc/structs.go +++ b/validator/rpc/structs.go @@ -2,16 +2,20 @@ package rpc import ( "fmt" + "net/url" "strconv" "github.com/OffchainLabs/prysm/v7/api/server" "github.com/OffchainLabs/prysm/v7/api/server/structs" fieldparams "github.com/OffchainLabs/prysm/v7/config/fieldparams" + "github.com/OffchainLabs/prysm/v7/config/proposer" "github.com/OffchainLabs/prysm/v7/consensus-types/primitives" + "github.com/OffchainLabs/prysm/v7/consensus-types/validator" "github.com/OffchainLabs/prysm/v7/encoding/bytesutil" eth "github.com/OffchainLabs/prysm/v7/proto/prysm/v1alpha1" "github.com/OffchainLabs/prysm/v7/validator/keymanager" "github.com/ethereum/go-ethereum/common/hexutil" + "github.com/pkg/errors" ) // local keymanager api @@ -109,16 +113,16 @@ type GraffitiData struct { Graffiti string `json:"graffiti"` } -// BuilderConfigJson is the keymanager-APIs #88 wire form: integers are decimal +// BuilderConfig is the keymanager-APIs #88 wire form: integers are decimal // strings, bytes 0x-hex. Builders nil = inherit, [] = use none (kept by no omitempty). -type BuilderConfigJson struct { - Enabled *bool `json:"enabled"` - MinBid *string `json:"min_bid,omitempty"` - BuilderBoostFactor *string `json:"builder_boost_factor,omitempty"` - Builders []*BuilderEntryJson `json:"builders"` +type BuilderConfig struct { + Enabled *bool `json:"enabled"` + MinBid *string `json:"min_bid,omitempty"` + BuilderBoostFactor *string `json:"builder_boost_factor,omitempty"` + Builders []*BuilderEntry `json:"builders"` } -type BuilderEntryJson struct { +type BuilderEntry struct { Url string `json:"url,omitempty"` AuthData *string `json:"auth_data,omitempty"` BuilderPubkey *string `json:"builder_pubkey,omitempty"` @@ -127,6 +131,145 @@ type BuilderEntryJson struct { BuilderBoostFactor *string `json:"builder_boost_factor,omitempty"` } +const ( + maxBuilderEntries = 64 // MAX_BUILDER_ENTRIES + maxBuilderURLSize = 2048 // MAX_BUILDER_URL_SIZE + maxAuthDataSize = 4096 // MAX_DATA_SIZE +) + +func builderConfigFromConsensus(bc *proposer.BuilderConfig) *BuilderConfig { + enabled := bc.Enabled + out := &BuilderConfig{ + Enabled: &enabled, + MinBid: new(strconv.FormatUint(uint64(bc.EffectiveMinBid()), 10)), + BuilderBoostFactor: new(strconv.FormatUint(uint64(bc.EffectiveBuilderBoostFactor()), 10)), + } + if bc.Builders != nil { + out.Builders = make([]*BuilderEntry, 0, len(bc.Builders)) + for _, b := range bc.Builders { + out.Builders = append(out.Builders, builderEntryFromConsensus(b, bc)) + } + } + return out +} + +func builderEntryFromConsensus(be *proposer.BuilderEntry, bc *proposer.BuilderConfig) *BuilderEntry { + out := &BuilderEntry{ + MaxExecutionPayment: new(strconv.FormatUint(uint64(be.EffectiveMaxExecutionPayment(bc)), 10)), + MinBid: new(strconv.FormatUint(uint64(be.EffectiveMinBid(bc)), 10)), + BuilderBoostFactor: new(strconv.FormatUint(uint64(be.EffectiveBuilderBoostFactor(bc)), 10)), + } + if be.URL != "" { + out.Url = be.URL + out.AuthData = new(hexutil.Encode(be.EffectiveAuthData())) + } + if len(be.Pubkey) != 0 { + out.BuilderPubkey = new(hexutil.Encode(be.Pubkey)) + } + return out +} + +func (in *BuilderConfig) ToConsensus() (*proposer.BuilderConfig, error) { + bc := &proposer.BuilderConfig{Enabled: in.Enabled != nil && *in.Enabled} + if in.MinBid != nil { + v, err := parseUint(*in.MinBid, "min_bid") + if err != nil { + return nil, err + } + bc.MinBid = &v + } + if in.BuilderBoostFactor != nil { + v, err := parseUint(*in.BuilderBoostFactor, "builder_boost_factor") + if err != nil { + return nil, err + } + bc.BuilderBoostFactor = &v + } + if in.Builders == nil { + return bc, nil + } + if len(in.Builders) > maxBuilderEntries { + return nil, errors.Errorf("builders exceeds %d entries", maxBuilderEntries) + } + // Non-nil (possibly empty) list means "use exactly these builders", not "inherit". + // Omitted auth_data compares as its derived value, so it collides with the explicit form. + bc.Builders = make([]*proposer.BuilderEntry, 0, len(in.Builders)) + seen := make(map[proposer.EntryIdentity]bool, len(in.Builders)) + for i, entry := range in.Builders { + be, err := entry.ToConsensus(i) + if err != nil { + return nil, err + } + if seen[be.Identity()] { + return nil, errors.Errorf("builders[%d]: two entries share the same url and auth_data", i) + } + seen[be.Identity()] = true + bc.Builders = append(bc.Builders, be) + } + return bc, nil +} + +func (in *BuilderEntry) ToConsensus(i int) (*proposer.BuilderEntry, error) { + be := &proposer.BuilderEntry{} + if in.Url == "" { + return nil, errors.Errorf("builders[%d].url is required", i) + } + if len(in.Url) > maxBuilderURLSize { + return nil, errors.Errorf("builders[%d].url exceeds %d bytes", i, maxBuilderURLSize) + } + if u, err := url.Parse(in.Url); err != nil || u.Scheme == "" || u.Host == "" { + return nil, errors.Errorf("builders[%d].url is not a valid URL", i) + } + be.URL = in.Url + if in.BuilderPubkey != nil { + pk, err := hexutil.Decode(*in.BuilderPubkey) + if err != nil || len(pk) != fieldparams.BLSPubkeyLength { + return nil, errors.Errorf("builders[%d].builder_pubkey is not a valid BLS public key", i) + } + be.Pubkey = pk + } + if in.AuthData != nil { + ad, err := hexutil.Decode(*in.AuthData) + if err != nil { + return nil, errors.Errorf("builders[%d].auth_data is not valid hex", i) + } + if len(ad) > maxAuthDataSize { + return nil, errors.Errorf("builders[%d].auth_data exceeds %d bytes", i, maxAuthDataSize) + } + be.AuthData = ad + } + if in.MaxExecutionPayment != nil { + v, err := parseUint(*in.MaxExecutionPayment, "builders[].max_execution_payment") + if err != nil { + return nil, err + } + be.MaxExecutionPayment = &v + } + if in.MinBid != nil { + v, err := parseUint(*in.MinBid, "builders[].min_bid") + if err != nil { + return nil, err + } + be.MinBid = &v + } + if in.BuilderBoostFactor != nil { + v, err := parseUint(*in.BuilderBoostFactor, "builders[].builder_boost_factor") + if err != nil { + return nil, err + } + be.BuilderBoostFactor = &v + } + return be, nil +} + +func parseUint(s, field string) (validator.Uint64, error) { + v, err := strconv.ParseUint(s, 10, 64) + if err != nil { + return 0, errors.Errorf("%s is not a valid uint64", field) + } + return validator.Uint64(v), nil +} + type BeaconStatusResponse struct { BeaconNodeEndpoint string `json:"beacon_node_endpoint"` Connected bool `json:"connected"` From d9b6989bb7a215d6d134b8f3db68ec6e07d39685 Mon Sep 17 00:00:00 2001 From: james-prysm Date: Fri, 7 Aug 2026 16:00:30 -0500 Subject: [PATCH 11/26] more bug fixes and missing items after self review --- ...mes-prysm_keymanager-builders-endpoints.md | 2 +- config/proposer/loader/loader.go | 20 +- config/proposer/settings.go | 109 +++++++-- config/proposer/settings_test.go | 213 +++++++++++++++++- .../validator-client/keymanager.pb.go | 188 ++++++++-------- .../validator-client/keymanager.proto | 9 +- validator/client/validator.go | 82 +++---- validator/client/validator_test.go | 72 +++++- .../db/filesystem/proposer_settings_test.go | 9 + validator/rpc/handlers_validator_config.go | 2 + .../rpc/handlers_validator_config_test.go | 5 +- validator/rpc/structs.go | 9 +- 12 files changed, 521 insertions(+), 199 deletions(-) diff --git a/changelog/james-prysm_keymanager-builders-endpoints.md b/changelog/james-prysm_keymanager-builders-endpoints.md index 05f491808e9b..d3c69f6e99a9 100644 --- a/changelog/james-prysm_keymanager-builders-endpoints.md +++ b/changelog/james-prysm_keymanager-builders-endpoints.md @@ -13,4 +13,4 @@ ### Removed -- Remove the unused `relays` field from the builder config; a settings file that still contains it logs a warning and is otherwise unaffected. +- Remove the unused `relays` field from the builder config; a settings file that still contains it is unaffected. diff --git a/config/proposer/loader/loader.go b/config/proposer/loader/loader.go index 0d94b05dfaf2..f52a6a80986f 100644 --- a/config/proposer/loader/loader.go +++ b/config/proposer/loader/loader.go @@ -1,11 +1,7 @@ package loader import ( - "bytes" - "encoding/json" "fmt" - "os" - "path/filepath" "strconv" "github.com/OffchainLabs/prysm/v7/cmd/validator/flags" @@ -135,13 +131,9 @@ func (psl *SettingsLoader) Load(cliCtx *cli.Context) (*proposer.Settings, error) return nil, err } dbSettings = dbps.ToConsensus() - log.Debugf("DB loaded proposer settings: %s", func() string { - b, err := json.Marshal(dbSettings) - if err != nil { - return err.Error() - } - return string(b) - }()) + log.WithField("version", dbSettings.Version). + WithField("proposerKeys", len(dbSettings.ProposerConfig)). + Debug("Loaded proposer settings from DB") } // start to process based on load method @@ -222,9 +214,6 @@ func (psl *SettingsLoader) loadFromFile(cliCtx *cli.Context, dbSettings *validat if settingFromFile == nil { return nil, errors.Errorf("proposer settings is empty after unmarshalling from file specified by %s flag", flags.ProposerSettingsFlag.Name) } - if raw, err := os.ReadFile(filepath.Clean(cliCtx.String(flags.ProposerSettingsFlag.Name))); err == nil && bytes.Contains(raw, []byte("relays")) { - log.Warn("The relays field in proposer settings is no longer supported and was ignored; configure builders instead") - } log.WithField(flags.ProposerSettingsFlag.Name, cliCtx.String(flags.ProposerSettingsFlag.Name)).Info("Proposer settings loaded from file") return psl.processProposerSettings(settingFromFile, dbSettings), nil } @@ -304,9 +293,6 @@ func promotePayloadToV2(p *validatorpb.ProposerSettingsPayload) { if opt.GasLimit == 0 { opt.GasLimit = opt.Builder.GasLimit } - if opt.Builder.MaxExecutionPayment != nil && *opt.Builder.MaxExecutionPayment == 0 { - opt.Builder.MaxExecutionPayment = nil - } } promote(p.DefaultConfig) for _, opt := range p.ProposerConfig { diff --git a/config/proposer/settings.go b/config/proposer/settings.go index ea8578da3818..6757a6d5e152 100644 --- a/config/proposer/settings.go +++ b/config/proposer/settings.go @@ -63,7 +63,6 @@ func SettingFromConsensus(ps *validatorpb.ProposerSettingsPayload) (*Settings, e d.GasLimit = ps.DefaultConfig.GasLimit settings.DefaultConfig = d } - settings.normalizeLegacyPresence() settings.dedupBuilders() return settings, nil } @@ -95,27 +94,6 @@ func (ps *Settings) dedupBuilders() { } } -// v1 payloads predate presence tracking: wire-absent enabled meant disabled, and -// the filesystem backend persisted a spurious explicit 0 max execution payment. -func (ps *Settings) normalizeLegacyPresence() { - if ps == nil || ps.isV2() { - return - } - normalize := func(opt *Option) { - if opt == nil || opt.BuilderConfig == nil { - return - } - bc := opt.BuilderConfig - if bc.MaxExecutionPayment != nil && *bc.MaxExecutionPayment == 0 { - bc.MaxExecutionPayment = nil - } - } - normalize(ps.DefaultConfig) - for _, opt := range ps.ProposeConfig { - normalize(opt) - } -} - func verifyOption(key string, option *validatorpb.ProposerOptionPayload) error { if option == nil { return fmt.Errorf("fee recipient is required for proposer %s", key) @@ -178,6 +156,69 @@ func (ps *Settings) EffectiveBuilderConfig(key [fieldparams.BLSPubkeyLength]byte return effectiveBuilderConfig(perKey, def) } +// RegistrationFor resolves pubkey's validator registration: fee recipient, gas +// limit, and whether to register with the builder at all. +func (ps *Settings) RegistrationFor(pubkey [fieldparams.BLSPubkeyLength]byte) (common.Address, validator.Uint64, bool) { + feeRecipient := common.HexToAddress(params.BeaconConfig().EthBurnAddressHex) + gasLimit := validator.Uint64(params.BeaconConfig().DefaultBuilderGasLimit) + if ps == nil { + return feeRecipient, gasLimit, false + } + if ps.isV2() { + hasFeeRecipient := false + if ps.DefaultConfig != nil && ps.DefaultConfig.FeeRecipientConfig != nil { + feeRecipient = ps.DefaultConfig.FeeRecipientConfig.FeeRecipient + hasFeeRecipient = true + } + if opt, ok := ps.ProposeConfig[pubkey]; ok && opt != nil && opt.FeeRecipientConfig != nil { + feeRecipient = opt.FeeRecipientConfig.FeeRecipient + hasFeeRecipient = true + } + // Per-key builder fields inherit from the default, but registration still + // requires a fee recipient configured at some level. + bc := ps.EffectiveBuilderConfig(pubkey) + // v2 gas limits live on the option (where UpgradeToV2 and the gas-limit + // API write them); the builder-level value is a legacy fallback. + switch { + case ps.ProposeConfig[pubkey] != nil && ps.ProposeConfig[pubkey].GasLimit != 0: + gasLimit = ps.ProposeConfig[pubkey].GasLimit + case ps.DefaultConfig != nil && ps.DefaultConfig.GasLimit != 0: + gasLimit = ps.DefaultConfig.GasLimit + case bc != nil && bc.GasLimit != 0: + gasLimit = bc.GasLimit + } + return feeRecipient, gasLimit, bc.IsEnabled() && hasFeeRecipient + } + // v1 keeps object-level semantics: a per-key builder config wins wholesale, + // so a disabled one opts the key out even under an enabled default. + enabled := false + if ps.DefaultConfig != nil && ps.DefaultConfig.FeeRecipientConfig != nil { + feeRecipient = ps.DefaultConfig.FeeRecipientConfig.FeeRecipient + if ps.DefaultConfig.BuilderConfig.IsEnabled() { + // Zero means unset (API-created configs): keep the chain default, + // matching the loader's reviewGasLimit normalization. + if ps.DefaultConfig.BuilderConfig.GasLimit != 0 { + gasLimit = ps.DefaultConfig.BuilderConfig.GasLimit + } + enabled = true + } + } + if opt, ok := ps.ProposeConfig[pubkey]; ok && opt != nil && opt.FeeRecipientConfig != nil { + feeRecipient = opt.FeeRecipientConfig.FeeRecipient + if bc := opt.BuilderConfig; bc != nil { + if bc.IsEnabled() { + if bc.GasLimit != 0 { + gasLimit = bc.GasLimit + } + enabled = true + } else { + enabled = false + } + } + } + return feeRecipient, gasLimit, enabled +} + // EntryIdentity is what makes a builder entry unique: its url compared as the // exact string and its auth_data as the resolved bytes. type EntryIdentity struct { @@ -307,6 +348,9 @@ func BuilderConfigFromConsensus(from *validatorpb.BuilderConfig) *BuilderConfig for _, b := range from.Builders { c.Builders = append(c.Builders, builderEntryFromConsensus(b)) } + } else if from.GetBuildersSet() { + // An explicitly configured empty list means "use no builders". + c.Builders = []*BuilderEntry{} } return c } @@ -533,6 +577,9 @@ func (bc *BuilderConfig) ToConsensus() *validatorpb.BuilderConfig { c.Proxy = cloneString(bc.Proxy) c.MinBid = cloneUint64(bc.MinBid) c.BuilderBoostFactor = cloneUint64(bc.BuilderBoostFactor) + // BuildersSet preserves nil-vs-empty across the wire: an explicit empty + // list is the "use no builders" marker and must survive persistence. + c.BuildersSet = bc.Builders != nil if len(bc.Builders) > 0 { c.Builders = make([]*validatorpb.BuilderEntry, 0, len(bc.Builders)) for _, b := range bc.Builders { @@ -567,16 +614,32 @@ func (ps *Settings) WarnDeprecatedSchema() { if ps == nil || ps.Version == SchemaV2 || !params.GloasEnabled() { return } + // Fee recipients and graffiti behave identically across schemas; only + // builder content gives the migration something to change. + if !ps.hasBuilderContent() { + return + } log.Warn("Proposer settings use the deprecated v1 schema; they are upgraded automatically at the gloas fork. Please migrate your settings source to v2.") } +func (ps *Settings) hasBuilderContent() bool { + if ps.DefaultConfig != nil && ps.DefaultConfig.BuilderConfig != nil { + return true + } + for _, opt := range ps.ProposeConfig { + if opt != nil && opt.BuilderConfig != nil { + return true + } + } + return false +} + // UpgradeToV2 migrates v1 settings in place: builder gas limits are promoted to // the option level unless one is set. Returns true if anything changed. func (ps *Settings) UpgradeToV2() bool { if ps == nil || ps.isV2() { return false } - ps.normalizeLegacyPresence() migrate := func(opt *Option) { if opt == nil || opt.BuilderConfig == nil { return diff --git a/config/proposer/settings_test.go b/config/proposer/settings_test.go index d248e75d28d1..0e77b07e6e62 100644 --- a/config/proposer/settings_test.go +++ b/config/proposer/settings_test.go @@ -73,6 +73,13 @@ func Test_Proposer_Setting_Cloning(t *testing.T) { require.NotNil(t, clone.Builders) require.Equal(t, 0, len(clone.Builders)) }) + t.Run("Consensus round-trip preserves the use-none builders marker", func(t *testing.T) { + got := BuilderConfigFromConsensus((&BuilderConfig{Enabled: true, Builders: []*BuilderEntry{}}).ToConsensus()) + require.NotNil(t, got.Builders) + require.Equal(t, 0, len(got.Builders)) + // And nil stays nil, meaning "inherit". + require.IsNil(t, BuilderConfigFromConsensus((&BuilderConfig{Enabled: true}).ToConsensus()).Builders) + }) t.Run("Happy Path BuilderConfigFromConsensus", func(t *testing.T) { clone := settings.DefaultConfig.BuilderConfig.Clone() @@ -648,7 +655,7 @@ func TestSettingFromConsensus(t *testing.T) { require.Equal(t, "https://other.example", builders[2].URL) }) - t.Run("normalizes legacy v1 presence", func(t *testing.T) { + t.Run("v1 explicit max_execution_payment survives ingest", func(t *testing.T) { key := [fieldparams.BLSPubkeyLength]byte{9} legacy := &Settings{ Version: SchemaV1, @@ -666,10 +673,12 @@ func TestSettingFromConsensus(t *testing.T) { def := got.DefaultConfig.BuilderConfig require.Equal(t, false, def.Enabled) - require.Equal(t, (*validator.Uint64)(nil), def.MaxExecutionPayment) + // The explicit trustless-only ceiling is preserved, not stripped. + require.Equal(t, validator.Uint64(0), *def.MaxExecutionPayment) perKey := got.ProposeConfig[key].BuilderConfig require.Equal(t, false, perKey.Enabled) + require.Equal(t, (*validator.Uint64)(nil), perKey.MaxExecutionPayment) // A disabled per-key section can never silently activate under an enabled default: // EffectiveBuilderConfig takes the per-key enabled, not the default's. @@ -693,15 +702,207 @@ func TestSettingFromConsensus(t *testing.T) { }) } -func TestUpgradeToV2_NormalizesLegacyPresence(t *testing.T) { +func TestRegistrationFor(t *testing.T) { + key := [fieldparams.BLSPubkeyLength]byte{7} + recipient := common.HexToAddress("0x50155530FCE8a85ec7055A5F8b2bE214B3DaeFd3") + + t.Run("v2 with no builder config anywhere does not register", func(t *testing.T) { + ps := &Settings{ + Version: SchemaV2, + DefaultConfig: &Option{FeeRecipientConfig: &FeeRecipientConfig{FeeRecipient: recipient}}, + } + fr, _, enabled := ps.RegistrationFor(key) + require.Equal(t, false, enabled) + require.Equal(t, recipient, fr) + }) + + t.Run("v2 inherits the default builder and fee recipient", func(t *testing.T) { + ps := &Settings{ + Version: SchemaV2, + DefaultConfig: &Option{ + FeeRecipientConfig: &FeeRecipientConfig{FeeRecipient: recipient}, + BuilderConfig: &BuilderConfig{Enabled: true, GasLimit: 123}, + }, + } + fr, gl, enabled := ps.RegistrationFor(key) + require.Equal(t, true, enabled) + require.Equal(t, recipient, fr) + require.Equal(t, validator.Uint64(123), gl) + }) + + t.Run("v2 without a fee recipient at any level does not register", func(t *testing.T) { + ps := &Settings{ + Version: SchemaV2, + DefaultConfig: &Option{BuilderConfig: &BuilderConfig{Enabled: true}}, + } + _, _, enabled := ps.RegistrationFor(key) + require.Equal(t, false, enabled) + }) + + t.Run("v1 per-key disabled builder opts the key out", func(t *testing.T) { + ps := &Settings{ + Version: SchemaV1, + DefaultConfig: &Option{ + FeeRecipientConfig: &FeeRecipientConfig{FeeRecipient: recipient}, + BuilderConfig: &BuilderConfig{Enabled: true, GasLimit: 123}, + }, + ProposeConfig: map[[fieldparams.BLSPubkeyLength]byte]*Option{ + key: { + FeeRecipientConfig: &FeeRecipientConfig{FeeRecipient: recipient}, + BuilderConfig: &BuilderConfig{Enabled: false}, + }, + }, + } + _, _, enabled := ps.RegistrationFor(key) + require.Equal(t, false, enabled) + }) + + perKeyRecipient := common.HexToAddress("0x6e35733c5af9B61374A128e6F85f553aF09ff89A") + + t.Run("v2 per-key fee recipient and enabled:false both win over the default", func(t *testing.T) { + ps := &Settings{ + Version: SchemaV2, + DefaultConfig: &Option{ + FeeRecipientConfig: &FeeRecipientConfig{FeeRecipient: recipient}, + BuilderConfig: &BuilderConfig{Enabled: true}, + }, + ProposeConfig: map[[fieldparams.BLSPubkeyLength]byte]*Option{ + key: { + FeeRecipientConfig: &FeeRecipientConfig{FeeRecipient: perKeyRecipient}, + BuilderConfig: &BuilderConfig{Enabled: false}, + }, + }, + } + fr, _, enabled := ps.RegistrationFor(key) + require.Equal(t, false, enabled) + require.Equal(t, perKeyRecipient, fr) + }) + + t.Run("v2 option-level gas limit wins over builder-level", func(t *testing.T) { + ps := &Settings{ + Version: SchemaV2, + DefaultConfig: &Option{ + FeeRecipientConfig: &FeeRecipientConfig{FeeRecipient: recipient}, + BuilderConfig: &BuilderConfig{Enabled: true, GasLimit: 123}, + }, + ProposeConfig: map[[fieldparams.BLSPubkeyLength]byte]*Option{ + key: {GasLimit: 999}, + }, + } + _, gl, enabled := ps.RegistrationFor(key) + require.Equal(t, true, enabled) + // The option-level value (written by UpgradeToV2 and the gas-limit API) wins. + require.Equal(t, validator.Uint64(999), gl) + }) + + t.Run("v2 unset gas limit falls back to the chain default", func(t *testing.T) { + ps := &Settings{ + Version: SchemaV2, + DefaultConfig: &Option{ + FeeRecipientConfig: &FeeRecipientConfig{FeeRecipient: recipient}, + BuilderConfig: &BuilderConfig{Enabled: true}, + }, + } + _, gl, enabled := ps.RegistrationFor(key) + require.Equal(t, true, enabled) + require.Equal(t, validator.Uint64(params.BeaconConfig().DefaultBuilderGasLimit), gl) + }) + + t.Run("v1 default enabled registers with the default gas limit", func(t *testing.T) { + ps := &Settings{ + Version: SchemaV1, + DefaultConfig: &Option{ + FeeRecipientConfig: &FeeRecipientConfig{FeeRecipient: recipient}, + BuilderConfig: &BuilderConfig{Enabled: true, GasLimit: 456}, + }, + } + fr, gl, enabled := ps.RegistrationFor(key) + require.Equal(t, true, enabled) + require.Equal(t, recipient, fr) + require.Equal(t, validator.Uint64(456), gl) + }) + + t.Run("v1 per-key fee recipient without builder config keeps the default's toggle", func(t *testing.T) { + ps := &Settings{ + Version: SchemaV1, + DefaultConfig: &Option{ + FeeRecipientConfig: &FeeRecipientConfig{FeeRecipient: recipient}, + BuilderConfig: &BuilderConfig{Enabled: true, GasLimit: 456}, + }, + ProposeConfig: map[[fieldparams.BLSPubkeyLength]byte]*Option{ + key: {FeeRecipientConfig: &FeeRecipientConfig{FeeRecipient: perKeyRecipient}}, + }, + } + fr, gl, enabled := ps.RegistrationFor(key) + require.Equal(t, true, enabled) + require.Equal(t, perKeyRecipient, fr) + require.Equal(t, validator.Uint64(456), gl) + }) + + t.Run("v1 zero builder gas limit falls back to the chain default", func(t *testing.T) { + // API-created builder configs have no builder-level gas limit; a v1 + // registration must not advertise gas limit 0. + ps := &Settings{ + Version: SchemaV1, + ProposeConfig: map[[fieldparams.BLSPubkeyLength]byte]*Option{ + key: { + FeeRecipientConfig: &FeeRecipientConfig{FeeRecipient: recipient}, + BuilderConfig: &BuilderConfig{Enabled: true}, + }, + }, + } + _, gl, enabled := ps.RegistrationFor(key) + require.Equal(t, true, enabled) + require.Equal(t, validator.Uint64(params.BeaconConfig().DefaultBuilderGasLimit), gl) + }) + + t.Run("unset version uses v1 semantics", func(t *testing.T) { + ps := &Settings{ + Version: SchemaV1Unset, + DefaultConfig: &Option{FeeRecipientConfig: &FeeRecipientConfig{FeeRecipient: recipient}}, + } + _, _, enabled := ps.RegistrationFor(key) + require.Equal(t, false, enabled) + }) + + t.Run("nil settings never register", func(t *testing.T) { + var ps *Settings + fr, _, enabled := ps.RegistrationFor(key) + require.Equal(t, false, enabled) + require.Equal(t, common.HexToAddress(params.BeaconConfig().EthBurnAddressHex), fr) + }) +} + +// Version-neutral settings (fee recipient, graffiti) give the deprecation +// warning nothing to say; only builder content triggers it. +func TestHasBuilderContent(t *testing.T) { + key := [fieldparams.BLSPubkeyLength]byte{9} + neutral := &Settings{ + Version: SchemaV1, + DefaultConfig: &Option{FeeRecipientConfig: &FeeRecipientConfig{}}, + ProposeConfig: map[[fieldparams.BLSPubkeyLength]byte]*Option{ + key: {GraffitiConfig: &GraffitiConfig{Graffiti: "hi"}}, + }, + } + require.Equal(t, false, neutral.hasBuilderContent()) + neutral.ProposeConfig[key].BuilderConfig = &BuilderConfig{} + require.Equal(t, true, neutral.hasBuilderContent()) +} + +// An explicit trustless-only ceiling survives the v2 upgrade untouched; an +// absent one stays absent and follows v2 inheritance. +func TestUpgradeToV2_PreservesExplicitMaxPayment(t *testing.T) { + key := [fieldparams.BLSPubkeyLength]byte{9} ps := &Settings{ Version: SchemaV1, DefaultConfig: &Option{BuilderConfig: &BuilderConfig{MaxExecutionPayment: uint64ValPtr(0)}}, + ProposeConfig: map[[fieldparams.BLSPubkeyLength]byte]*Option{ + key: {BuilderConfig: &BuilderConfig{Enabled: true}}, + }, } require.Equal(t, true, ps.UpgradeToV2()) - bc := ps.DefaultConfig.BuilderConfig - require.Equal(t, false, bc.Enabled) - require.Equal(t, (*validator.Uint64)(nil), bc.MaxExecutionPayment) require.Equal(t, SchemaV2, ps.Version) + require.Equal(t, validator.Uint64(0), *ps.DefaultConfig.BuilderConfig.MaxExecutionPayment) + require.IsNil(t, ps.ProposeConfig[key].BuilderConfig.MaxExecutionPayment) require.Equal(t, false, ps.UpgradeToV2()) } diff --git a/proto/prysm/v1alpha1/validator-client/keymanager.pb.go b/proto/prysm/v1alpha1/validator-client/keymanager.pb.go index 91c690c0816a..8919142bf51d 100755 --- a/proto/prysm/v1alpha1/validator-client/keymanager.pb.go +++ b/proto/prysm/v1alpha1/validator-client/keymanager.pb.go @@ -717,9 +717,10 @@ type BuilderConfig struct { GasLimit github_com_OffchainLabs_prysm_v7_consensus_types_validator.Uint64 `protobuf:"varint,2,opt,name=gas_limit,json=gasLimit,proto3" json:"gas_limit,omitempty" cast-type:"github.com/OffchainLabs/prysm/v7/consensus-types/validator.Uint64"` MaxExecutionPayment *github_com_OffchainLabs_prysm_v7_consensus_types_validator.Uint64 `protobuf:"varint,4,opt,name=max_execution_payment,json=maxExecutionPayment,proto3,oneof" json:"max_execution_payment,omitempty" cast-type:"github.com/OffchainLabs/prysm/v7/consensus-types/validator.Uint64"` Builders []*BuilderEntry `protobuf:"bytes,5,rep,name=builders,proto3" json:"builders,omitempty"` - Proxy *string `protobuf:"bytes,6,opt,name=proxy,proto3,oneof" json:"proxy,omitempty"` - MinBid *github_com_OffchainLabs_prysm_v7_consensus_types_validator.Uint64 `protobuf:"varint,7,opt,name=min_bid,json=minBid,proto3,oneof" json:"min_bid,omitempty" cast-type:"github.com/OffchainLabs/prysm/v7/consensus-types/validator.Uint64"` - BuilderBoostFactor *github_com_OffchainLabs_prysm_v7_consensus_types_validator.Uint64 `protobuf:"varint,8,opt,name=builder_boost_factor,json=builderBoostFactor,proto3,oneof" json:"builder_boost_factor,omitempty" cast-type:"github.com/OffchainLabs/prysm/v7/consensus-types/validator.Uint64"` + BuildersSet bool `protobuf:"varint,6,opt,name=builders_set,json=buildersSet,proto3" json:"builders_set,omitempty"` + Proxy *string `protobuf:"bytes,7,opt,name=proxy,proto3,oneof" json:"proxy,omitempty"` + MinBid *github_com_OffchainLabs_prysm_v7_consensus_types_validator.Uint64 `protobuf:"varint,8,opt,name=min_bid,json=minBid,proto3,oneof" json:"min_bid,omitempty" cast-type:"github.com/OffchainLabs/prysm/v7/consensus-types/validator.Uint64"` + BuilderBoostFactor *github_com_OffchainLabs_prysm_v7_consensus_types_validator.Uint64 `protobuf:"varint,9,opt,name=builder_boost_factor,json=builderBoostFactor,proto3,oneof" json:"builder_boost_factor,omitempty" cast-type:"github.com/OffchainLabs/prysm/v7/consensus-types/validator.Uint64"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } @@ -782,6 +783,13 @@ func (x *BuilderConfig) GetBuilders() []*BuilderEntry { return nil } +func (x *BuilderConfig) GetBuildersSet() bool { + if x != nil { + return x.BuildersSet + } + return false +} + func (x *BuilderConfig) GetProxy() string { if x != nil && x.Proxy != nil { return *x.Proxy @@ -1184,7 +1192,7 @@ var file_proto_prysm_v1alpha1_validator_client_keymanager_proto_rawDesc = []byte 0x6f, 0x6e, 0x73, 0x65, 0x6e, 0x73, 0x75, 0x73, 0x2d, 0x74, 0x79, 0x70, 0x65, 0x73, 0x2f, 0x76, 0x61, 0x6c, 0x69, 0x64, 0x61, 0x74, 0x6f, 0x72, 0x2e, 0x55, 0x69, 0x6e, 0x74, 0x36, 0x34, 0x52, 0x08, 0x67, 0x61, 0x73, 0x4c, 0x69, 0x6d, 0x69, 0x74, 0x42, 0x0b, 0x0a, 0x09, 0x5f, 0x67, 0x72, - 0x61, 0x66, 0x66, 0x69, 0x74, 0x69, 0x22, 0xbd, 0x05, 0x0a, 0x0d, 0x42, 0x75, 0x69, 0x6c, 0x64, + 0x61, 0x66, 0x66, 0x69, 0x74, 0x69, 0x22, 0xe0, 0x05, 0x0a, 0x0d, 0x42, 0x75, 0x69, 0x6c, 0x64, 0x65, 0x72, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x1d, 0x0a, 0x07, 0x65, 0x6e, 0x61, 0x62, 0x6c, 0x65, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x08, 0x48, 0x00, 0x52, 0x07, 0x65, 0x6e, 0x61, 0x62, 0x6c, 0x65, 0x64, 0x88, 0x01, 0x01, 0x12, 0x62, 0x0a, 0x09, 0x67, 0x61, 0x73, 0x5f, 0x6c, @@ -1206,103 +1214,105 @@ var file_proto_prysm_v1alpha1_validator_client_keymanager_proto_rawDesc = []byte 0x65, 0x74, 0x68, 0x65, 0x72, 0x65, 0x75, 0x6d, 0x2e, 0x76, 0x61, 0x6c, 0x69, 0x64, 0x61, 0x74, 0x6f, 0x72, 0x2e, 0x61, 0x63, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x73, 0x2e, 0x76, 0x32, 0x2e, 0x42, 0x75, 0x69, 0x6c, 0x64, 0x65, 0x72, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x52, 0x08, 0x62, 0x75, 0x69, - 0x6c, 0x64, 0x65, 0x72, 0x73, 0x12, 0x19, 0x0a, 0x05, 0x70, 0x72, 0x6f, 0x78, 0x79, 0x18, 0x06, - 0x20, 0x01, 0x28, 0x09, 0x48, 0x02, 0x52, 0x05, 0x70, 0x72, 0x6f, 0x78, 0x79, 0x88, 0x01, 0x01, - 0x12, 0x63, 0x0a, 0x07, 0x6d, 0x69, 0x6e, 0x5f, 0x62, 0x69, 0x64, 0x18, 0x07, 0x20, 0x01, 0x28, - 0x04, 0x42, 0x45, 0x82, 0xb5, 0x18, 0x41, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, - 0x6d, 0x2f, 0x4f, 0x66, 0x66, 0x63, 0x68, 0x61, 0x69, 0x6e, 0x4c, 0x61, 0x62, 0x73, 0x2f, 0x70, - 0x72, 0x79, 0x73, 0x6d, 0x2f, 0x76, 0x37, 0x2f, 0x63, 0x6f, 0x6e, 0x73, 0x65, 0x6e, 0x73, 0x75, - 0x73, 0x2d, 0x74, 0x79, 0x70, 0x65, 0x73, 0x2f, 0x76, 0x61, 0x6c, 0x69, 0x64, 0x61, 0x74, 0x6f, - 0x72, 0x2e, 0x55, 0x69, 0x6e, 0x74, 0x36, 0x34, 0x48, 0x03, 0x52, 0x06, 0x6d, 0x69, 0x6e, 0x42, - 0x69, 0x64, 0x88, 0x01, 0x01, 0x12, 0x7c, 0x0a, 0x14, 0x62, 0x75, 0x69, 0x6c, 0x64, 0x65, 0x72, - 0x5f, 0x62, 0x6f, 0x6f, 0x73, 0x74, 0x5f, 0x66, 0x61, 0x63, 0x74, 0x6f, 0x72, 0x18, 0x08, 0x20, - 0x01, 0x28, 0x04, 0x42, 0x45, 0x82, 0xb5, 0x18, 0x41, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, - 0x63, 0x6f, 0x6d, 0x2f, 0x4f, 0x66, 0x66, 0x63, 0x68, 0x61, 0x69, 0x6e, 0x4c, 0x61, 0x62, 0x73, - 0x2f, 0x70, 0x72, 0x79, 0x73, 0x6d, 0x2f, 0x76, 0x37, 0x2f, 0x63, 0x6f, 0x6e, 0x73, 0x65, 0x6e, - 0x73, 0x75, 0x73, 0x2d, 0x74, 0x79, 0x70, 0x65, 0x73, 0x2f, 0x76, 0x61, 0x6c, 0x69, 0x64, 0x61, - 0x74, 0x6f, 0x72, 0x2e, 0x55, 0x69, 0x6e, 0x74, 0x36, 0x34, 0x48, 0x04, 0x52, 0x12, 0x62, 0x75, - 0x69, 0x6c, 0x64, 0x65, 0x72, 0x42, 0x6f, 0x6f, 0x73, 0x74, 0x46, 0x61, 0x63, 0x74, 0x6f, 0x72, - 0x88, 0x01, 0x01, 0x42, 0x0a, 0x0a, 0x08, 0x5f, 0x65, 0x6e, 0x61, 0x62, 0x6c, 0x65, 0x64, 0x42, - 0x18, 0x0a, 0x16, 0x5f, 0x6d, 0x61, 0x78, 0x5f, 0x65, 0x78, 0x65, 0x63, 0x75, 0x74, 0x69, 0x6f, - 0x6e, 0x5f, 0x70, 0x61, 0x79, 0x6d, 0x65, 0x6e, 0x74, 0x42, 0x08, 0x0a, 0x06, 0x5f, 0x70, 0x72, - 0x6f, 0x78, 0x79, 0x42, 0x0a, 0x0a, 0x08, 0x5f, 0x6d, 0x69, 0x6e, 0x5f, 0x62, 0x69, 0x64, 0x42, - 0x17, 0x0a, 0x15, 0x5f, 0x62, 0x75, 0x69, 0x6c, 0x64, 0x65, 0x72, 0x5f, 0x62, 0x6f, 0x6f, 0x73, - 0x74, 0x5f, 0x66, 0x61, 0x63, 0x74, 0x6f, 0x72, 0x4a, 0x04, 0x08, 0x03, 0x10, 0x04, 0x52, 0x06, - 0x72, 0x65, 0x6c, 0x61, 0x79, 0x73, 0x22, 0xbf, 0x04, 0x0a, 0x0c, 0x42, 0x75, 0x69, 0x6c, 0x64, - 0x65, 0x72, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x12, 0x10, 0x0a, 0x03, 0x75, 0x72, 0x6c, 0x18, 0x01, - 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x75, 0x72, 0x6c, 0x12, 0x1b, 0x0a, 0x06, 0x70, 0x75, 0x62, - 0x6b, 0x65, 0x79, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0c, 0x48, 0x00, 0x52, 0x06, 0x70, 0x75, 0x62, - 0x6b, 0x65, 0x79, 0x88, 0x01, 0x01, 0x12, 0x20, 0x0a, 0x09, 0x61, 0x75, 0x74, 0x68, 0x5f, 0x64, - 0x61, 0x74, 0x61, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0c, 0x48, 0x01, 0x52, 0x08, 0x61, 0x75, 0x74, - 0x68, 0x44, 0x61, 0x74, 0x61, 0x88, 0x01, 0x01, 0x12, 0x19, 0x0a, 0x05, 0x70, 0x72, 0x6f, 0x78, - 0x79, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x48, 0x02, 0x52, 0x05, 0x70, 0x72, 0x6f, 0x78, 0x79, - 0x88, 0x01, 0x01, 0x12, 0x63, 0x0a, 0x07, 0x6d, 0x69, 0x6e, 0x5f, 0x62, 0x69, 0x64, 0x18, 0x05, + 0x6c, 0x64, 0x65, 0x72, 0x73, 0x12, 0x21, 0x0a, 0x0c, 0x62, 0x75, 0x69, 0x6c, 0x64, 0x65, 0x72, + 0x73, 0x5f, 0x73, 0x65, 0x74, 0x18, 0x06, 0x20, 0x01, 0x28, 0x08, 0x52, 0x0b, 0x62, 0x75, 0x69, + 0x6c, 0x64, 0x65, 0x72, 0x73, 0x53, 0x65, 0x74, 0x12, 0x19, 0x0a, 0x05, 0x70, 0x72, 0x6f, 0x78, + 0x79, 0x18, 0x07, 0x20, 0x01, 0x28, 0x09, 0x48, 0x02, 0x52, 0x05, 0x70, 0x72, 0x6f, 0x78, 0x79, + 0x88, 0x01, 0x01, 0x12, 0x63, 0x0a, 0x07, 0x6d, 0x69, 0x6e, 0x5f, 0x62, 0x69, 0x64, 0x18, 0x08, 0x20, 0x01, 0x28, 0x04, 0x42, 0x45, 0x82, 0xb5, 0x18, 0x41, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x4f, 0x66, 0x66, 0x63, 0x68, 0x61, 0x69, 0x6e, 0x4c, 0x61, 0x62, 0x73, 0x2f, 0x70, 0x72, 0x79, 0x73, 0x6d, 0x2f, 0x76, 0x37, 0x2f, 0x63, 0x6f, 0x6e, 0x73, 0x65, 0x6e, 0x73, 0x75, 0x73, 0x2d, 0x74, 0x79, 0x70, 0x65, 0x73, 0x2f, 0x76, 0x61, 0x6c, 0x69, 0x64, 0x61, 0x74, 0x6f, 0x72, 0x2e, 0x55, 0x69, 0x6e, 0x74, 0x36, 0x34, 0x48, 0x03, 0x52, 0x06, 0x6d, - 0x69, 0x6e, 0x42, 0x69, 0x64, 0x88, 0x01, 0x01, 0x12, 0x7e, 0x0a, 0x15, 0x6d, 0x61, 0x78, 0x5f, - 0x65, 0x78, 0x65, 0x63, 0x75, 0x74, 0x69, 0x6f, 0x6e, 0x5f, 0x70, 0x61, 0x79, 0x6d, 0x65, 0x6e, - 0x74, 0x18, 0x06, 0x20, 0x01, 0x28, 0x04, 0x42, 0x45, 0x82, 0xb5, 0x18, 0x41, 0x67, 0x69, 0x74, - 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x4f, 0x66, 0x66, 0x63, 0x68, 0x61, 0x69, 0x6e, - 0x4c, 0x61, 0x62, 0x73, 0x2f, 0x70, 0x72, 0x79, 0x73, 0x6d, 0x2f, 0x76, 0x37, 0x2f, 0x63, 0x6f, - 0x6e, 0x73, 0x65, 0x6e, 0x73, 0x75, 0x73, 0x2d, 0x74, 0x79, 0x70, 0x65, 0x73, 0x2f, 0x76, 0x61, - 0x6c, 0x69, 0x64, 0x61, 0x74, 0x6f, 0x72, 0x2e, 0x55, 0x69, 0x6e, 0x74, 0x36, 0x34, 0x48, 0x04, - 0x52, 0x13, 0x6d, 0x61, 0x78, 0x45, 0x78, 0x65, 0x63, 0x75, 0x74, 0x69, 0x6f, 0x6e, 0x50, 0x61, - 0x79, 0x6d, 0x65, 0x6e, 0x74, 0x88, 0x01, 0x01, 0x12, 0x7c, 0x0a, 0x14, 0x62, 0x75, 0x69, 0x6c, + 0x69, 0x6e, 0x42, 0x69, 0x64, 0x88, 0x01, 0x01, 0x12, 0x7c, 0x0a, 0x14, 0x62, 0x75, 0x69, 0x6c, 0x64, 0x65, 0x72, 0x5f, 0x62, 0x6f, 0x6f, 0x73, 0x74, 0x5f, 0x66, 0x61, 0x63, 0x74, 0x6f, 0x72, - 0x18, 0x07, 0x20, 0x01, 0x28, 0x04, 0x42, 0x45, 0x82, 0xb5, 0x18, 0x41, 0x67, 0x69, 0x74, 0x68, + 0x18, 0x09, 0x20, 0x01, 0x28, 0x04, 0x42, 0x45, 0x82, 0xb5, 0x18, 0x41, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x4f, 0x66, 0x66, 0x63, 0x68, 0x61, 0x69, 0x6e, 0x4c, 0x61, 0x62, 0x73, 0x2f, 0x70, 0x72, 0x79, 0x73, 0x6d, 0x2f, 0x76, 0x37, 0x2f, 0x63, 0x6f, 0x6e, 0x73, 0x65, 0x6e, 0x73, 0x75, 0x73, 0x2d, 0x74, 0x79, 0x70, 0x65, 0x73, 0x2f, 0x76, 0x61, 0x6c, - 0x69, 0x64, 0x61, 0x74, 0x6f, 0x72, 0x2e, 0x55, 0x69, 0x6e, 0x74, 0x36, 0x34, 0x48, 0x05, 0x52, + 0x69, 0x64, 0x61, 0x74, 0x6f, 0x72, 0x2e, 0x55, 0x69, 0x6e, 0x74, 0x36, 0x34, 0x48, 0x04, 0x52, 0x12, 0x62, 0x75, 0x69, 0x6c, 0x64, 0x65, 0x72, 0x42, 0x6f, 0x6f, 0x73, 0x74, 0x46, 0x61, 0x63, - 0x74, 0x6f, 0x72, 0x88, 0x01, 0x01, 0x42, 0x09, 0x0a, 0x07, 0x5f, 0x70, 0x75, 0x62, 0x6b, 0x65, - 0x79, 0x42, 0x0c, 0x0a, 0x0a, 0x5f, 0x61, 0x75, 0x74, 0x68, 0x5f, 0x64, 0x61, 0x74, 0x61, 0x42, - 0x08, 0x0a, 0x06, 0x5f, 0x70, 0x72, 0x6f, 0x78, 0x79, 0x42, 0x0a, 0x0a, 0x08, 0x5f, 0x6d, 0x69, - 0x6e, 0x5f, 0x62, 0x69, 0x64, 0x42, 0x18, 0x0a, 0x16, 0x5f, 0x6d, 0x61, 0x78, 0x5f, 0x65, 0x78, - 0x65, 0x63, 0x75, 0x74, 0x69, 0x6f, 0x6e, 0x5f, 0x70, 0x61, 0x79, 0x6d, 0x65, 0x6e, 0x74, 0x42, - 0x17, 0x0a, 0x15, 0x5f, 0x62, 0x75, 0x69, 0x6c, 0x64, 0x65, 0x72, 0x5f, 0x62, 0x6f, 0x6f, 0x73, - 0x74, 0x5f, 0x66, 0x61, 0x63, 0x74, 0x6f, 0x72, 0x22, 0x81, 0x03, 0x0a, 0x17, 0x50, 0x72, 0x6f, - 0x70, 0x6f, 0x73, 0x65, 0x72, 0x53, 0x65, 0x74, 0x74, 0x69, 0x6e, 0x67, 0x73, 0x50, 0x61, 0x79, - 0x6c, 0x6f, 0x61, 0x64, 0x12, 0x74, 0x0a, 0x0f, 0x70, 0x72, 0x6f, 0x70, 0x6f, 0x73, 0x65, 0x72, - 0x5f, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x4b, 0x2e, + 0x74, 0x6f, 0x72, 0x88, 0x01, 0x01, 0x42, 0x0a, 0x0a, 0x08, 0x5f, 0x65, 0x6e, 0x61, 0x62, 0x6c, + 0x65, 0x64, 0x42, 0x18, 0x0a, 0x16, 0x5f, 0x6d, 0x61, 0x78, 0x5f, 0x65, 0x78, 0x65, 0x63, 0x75, + 0x74, 0x69, 0x6f, 0x6e, 0x5f, 0x70, 0x61, 0x79, 0x6d, 0x65, 0x6e, 0x74, 0x42, 0x08, 0x0a, 0x06, + 0x5f, 0x70, 0x72, 0x6f, 0x78, 0x79, 0x42, 0x0a, 0x0a, 0x08, 0x5f, 0x6d, 0x69, 0x6e, 0x5f, 0x62, + 0x69, 0x64, 0x42, 0x17, 0x0a, 0x15, 0x5f, 0x62, 0x75, 0x69, 0x6c, 0x64, 0x65, 0x72, 0x5f, 0x62, + 0x6f, 0x6f, 0x73, 0x74, 0x5f, 0x66, 0x61, 0x63, 0x74, 0x6f, 0x72, 0x4a, 0x04, 0x08, 0x03, 0x10, + 0x04, 0x52, 0x06, 0x72, 0x65, 0x6c, 0x61, 0x79, 0x73, 0x22, 0xbf, 0x04, 0x0a, 0x0c, 0x42, 0x75, + 0x69, 0x6c, 0x64, 0x65, 0x72, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x12, 0x10, 0x0a, 0x03, 0x75, 0x72, + 0x6c, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x75, 0x72, 0x6c, 0x12, 0x1b, 0x0a, 0x06, + 0x70, 0x75, 0x62, 0x6b, 0x65, 0x79, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0c, 0x48, 0x00, 0x52, 0x06, + 0x70, 0x75, 0x62, 0x6b, 0x65, 0x79, 0x88, 0x01, 0x01, 0x12, 0x20, 0x0a, 0x09, 0x61, 0x75, 0x74, + 0x68, 0x5f, 0x64, 0x61, 0x74, 0x61, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0c, 0x48, 0x01, 0x52, 0x08, + 0x61, 0x75, 0x74, 0x68, 0x44, 0x61, 0x74, 0x61, 0x88, 0x01, 0x01, 0x12, 0x19, 0x0a, 0x05, 0x70, + 0x72, 0x6f, 0x78, 0x79, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x48, 0x02, 0x52, 0x05, 0x70, 0x72, + 0x6f, 0x78, 0x79, 0x88, 0x01, 0x01, 0x12, 0x63, 0x0a, 0x07, 0x6d, 0x69, 0x6e, 0x5f, 0x62, 0x69, + 0x64, 0x18, 0x05, 0x20, 0x01, 0x28, 0x04, 0x42, 0x45, 0x82, 0xb5, 0x18, 0x41, 0x67, 0x69, 0x74, + 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x4f, 0x66, 0x66, 0x63, 0x68, 0x61, 0x69, 0x6e, + 0x4c, 0x61, 0x62, 0x73, 0x2f, 0x70, 0x72, 0x79, 0x73, 0x6d, 0x2f, 0x76, 0x37, 0x2f, 0x63, 0x6f, + 0x6e, 0x73, 0x65, 0x6e, 0x73, 0x75, 0x73, 0x2d, 0x74, 0x79, 0x70, 0x65, 0x73, 0x2f, 0x76, 0x61, + 0x6c, 0x69, 0x64, 0x61, 0x74, 0x6f, 0x72, 0x2e, 0x55, 0x69, 0x6e, 0x74, 0x36, 0x34, 0x48, 0x03, + 0x52, 0x06, 0x6d, 0x69, 0x6e, 0x42, 0x69, 0x64, 0x88, 0x01, 0x01, 0x12, 0x7e, 0x0a, 0x15, 0x6d, + 0x61, 0x78, 0x5f, 0x65, 0x78, 0x65, 0x63, 0x75, 0x74, 0x69, 0x6f, 0x6e, 0x5f, 0x70, 0x61, 0x79, + 0x6d, 0x65, 0x6e, 0x74, 0x18, 0x06, 0x20, 0x01, 0x28, 0x04, 0x42, 0x45, 0x82, 0xb5, 0x18, 0x41, + 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x4f, 0x66, 0x66, 0x63, 0x68, + 0x61, 0x69, 0x6e, 0x4c, 0x61, 0x62, 0x73, 0x2f, 0x70, 0x72, 0x79, 0x73, 0x6d, 0x2f, 0x76, 0x37, + 0x2f, 0x63, 0x6f, 0x6e, 0x73, 0x65, 0x6e, 0x73, 0x75, 0x73, 0x2d, 0x74, 0x79, 0x70, 0x65, 0x73, + 0x2f, 0x76, 0x61, 0x6c, 0x69, 0x64, 0x61, 0x74, 0x6f, 0x72, 0x2e, 0x55, 0x69, 0x6e, 0x74, 0x36, + 0x34, 0x48, 0x04, 0x52, 0x13, 0x6d, 0x61, 0x78, 0x45, 0x78, 0x65, 0x63, 0x75, 0x74, 0x69, 0x6f, + 0x6e, 0x50, 0x61, 0x79, 0x6d, 0x65, 0x6e, 0x74, 0x88, 0x01, 0x01, 0x12, 0x7c, 0x0a, 0x14, 0x62, + 0x75, 0x69, 0x6c, 0x64, 0x65, 0x72, 0x5f, 0x62, 0x6f, 0x6f, 0x73, 0x74, 0x5f, 0x66, 0x61, 0x63, + 0x74, 0x6f, 0x72, 0x18, 0x07, 0x20, 0x01, 0x28, 0x04, 0x42, 0x45, 0x82, 0xb5, 0x18, 0x41, 0x67, + 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x4f, 0x66, 0x66, 0x63, 0x68, 0x61, + 0x69, 0x6e, 0x4c, 0x61, 0x62, 0x73, 0x2f, 0x70, 0x72, 0x79, 0x73, 0x6d, 0x2f, 0x76, 0x37, 0x2f, + 0x63, 0x6f, 0x6e, 0x73, 0x65, 0x6e, 0x73, 0x75, 0x73, 0x2d, 0x74, 0x79, 0x70, 0x65, 0x73, 0x2f, + 0x76, 0x61, 0x6c, 0x69, 0x64, 0x61, 0x74, 0x6f, 0x72, 0x2e, 0x55, 0x69, 0x6e, 0x74, 0x36, 0x34, + 0x48, 0x05, 0x52, 0x12, 0x62, 0x75, 0x69, 0x6c, 0x64, 0x65, 0x72, 0x42, 0x6f, 0x6f, 0x73, 0x74, + 0x46, 0x61, 0x63, 0x74, 0x6f, 0x72, 0x88, 0x01, 0x01, 0x42, 0x09, 0x0a, 0x07, 0x5f, 0x70, 0x75, + 0x62, 0x6b, 0x65, 0x79, 0x42, 0x0c, 0x0a, 0x0a, 0x5f, 0x61, 0x75, 0x74, 0x68, 0x5f, 0x64, 0x61, + 0x74, 0x61, 0x42, 0x08, 0x0a, 0x06, 0x5f, 0x70, 0x72, 0x6f, 0x78, 0x79, 0x42, 0x0a, 0x0a, 0x08, + 0x5f, 0x6d, 0x69, 0x6e, 0x5f, 0x62, 0x69, 0x64, 0x42, 0x18, 0x0a, 0x16, 0x5f, 0x6d, 0x61, 0x78, + 0x5f, 0x65, 0x78, 0x65, 0x63, 0x75, 0x74, 0x69, 0x6f, 0x6e, 0x5f, 0x70, 0x61, 0x79, 0x6d, 0x65, + 0x6e, 0x74, 0x42, 0x17, 0x0a, 0x15, 0x5f, 0x62, 0x75, 0x69, 0x6c, 0x64, 0x65, 0x72, 0x5f, 0x62, + 0x6f, 0x6f, 0x73, 0x74, 0x5f, 0x66, 0x61, 0x63, 0x74, 0x6f, 0x72, 0x22, 0x81, 0x03, 0x0a, 0x17, + 0x50, 0x72, 0x6f, 0x70, 0x6f, 0x73, 0x65, 0x72, 0x53, 0x65, 0x74, 0x74, 0x69, 0x6e, 0x67, 0x73, + 0x50, 0x61, 0x79, 0x6c, 0x6f, 0x61, 0x64, 0x12, 0x74, 0x0a, 0x0f, 0x70, 0x72, 0x6f, 0x70, 0x6f, + 0x73, 0x65, 0x72, 0x5f, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, + 0x32, 0x4b, 0x2e, 0x65, 0x74, 0x68, 0x65, 0x72, 0x65, 0x75, 0x6d, 0x2e, 0x76, 0x61, 0x6c, 0x69, + 0x64, 0x61, 0x74, 0x6f, 0x72, 0x2e, 0x61, 0x63, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x73, 0x2e, 0x76, + 0x32, 0x2e, 0x50, 0x72, 0x6f, 0x70, 0x6f, 0x73, 0x65, 0x72, 0x53, 0x65, 0x74, 0x74, 0x69, 0x6e, + 0x67, 0x73, 0x50, 0x61, 0x79, 0x6c, 0x6f, 0x61, 0x64, 0x2e, 0x50, 0x72, 0x6f, 0x70, 0x6f, 0x73, + 0x65, 0x72, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x52, 0x0e, 0x70, + 0x72, 0x6f, 0x70, 0x6f, 0x73, 0x65, 0x72, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x5c, 0x0a, + 0x0e, 0x64, 0x65, 0x66, 0x61, 0x75, 0x6c, 0x74, 0x5f, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x18, + 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x35, 0x2e, 0x65, 0x74, 0x68, 0x65, 0x72, 0x65, 0x75, 0x6d, + 0x2e, 0x76, 0x61, 0x6c, 0x69, 0x64, 0x61, 0x74, 0x6f, 0x72, 0x2e, 0x61, 0x63, 0x63, 0x6f, 0x75, + 0x6e, 0x74, 0x73, 0x2e, 0x76, 0x32, 0x2e, 0x50, 0x72, 0x6f, 0x70, 0x6f, 0x73, 0x65, 0x72, 0x4f, + 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x50, 0x61, 0x79, 0x6c, 0x6f, 0x61, 0x64, 0x52, 0x0d, 0x64, 0x65, + 0x66, 0x61, 0x75, 0x6c, 0x74, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x18, 0x0a, 0x07, 0x76, + 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x07, 0x76, 0x65, + 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x1a, 0x78, 0x0a, 0x13, 0x50, 0x72, 0x6f, 0x70, 0x6f, 0x73, 0x65, + 0x72, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x12, 0x10, 0x0a, 0x03, + 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x4b, + 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x35, 0x2e, 0x65, 0x74, 0x68, 0x65, 0x72, 0x65, 0x75, 0x6d, 0x2e, 0x76, 0x61, 0x6c, 0x69, 0x64, 0x61, 0x74, 0x6f, 0x72, 0x2e, 0x61, 0x63, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x73, 0x2e, 0x76, 0x32, 0x2e, 0x50, - 0x72, 0x6f, 0x70, 0x6f, 0x73, 0x65, 0x72, 0x53, 0x65, 0x74, 0x74, 0x69, 0x6e, 0x67, 0x73, 0x50, - 0x61, 0x79, 0x6c, 0x6f, 0x61, 0x64, 0x2e, 0x50, 0x72, 0x6f, 0x70, 0x6f, 0x73, 0x65, 0x72, 0x43, - 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x52, 0x0e, 0x70, 0x72, 0x6f, 0x70, - 0x6f, 0x73, 0x65, 0x72, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x5c, 0x0a, 0x0e, 0x64, 0x65, - 0x66, 0x61, 0x75, 0x6c, 0x74, 0x5f, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x18, 0x02, 0x20, 0x01, - 0x28, 0x0b, 0x32, 0x35, 0x2e, 0x65, 0x74, 0x68, 0x65, 0x72, 0x65, 0x75, 0x6d, 0x2e, 0x76, 0x61, - 0x6c, 0x69, 0x64, 0x61, 0x74, 0x6f, 0x72, 0x2e, 0x61, 0x63, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x73, - 0x2e, 0x76, 0x32, 0x2e, 0x50, 0x72, 0x6f, 0x70, 0x6f, 0x73, 0x65, 0x72, 0x4f, 0x70, 0x74, 0x69, - 0x6f, 0x6e, 0x50, 0x61, 0x79, 0x6c, 0x6f, 0x61, 0x64, 0x52, 0x0d, 0x64, 0x65, 0x66, 0x61, 0x75, - 0x6c, 0x74, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x18, 0x0a, 0x07, 0x76, 0x65, 0x72, 0x73, - 0x69, 0x6f, 0x6e, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x07, 0x76, 0x65, 0x72, 0x73, 0x69, - 0x6f, 0x6e, 0x1a, 0x78, 0x0a, 0x13, 0x50, 0x72, 0x6f, 0x70, 0x6f, 0x73, 0x65, 0x72, 0x43, 0x6f, - 0x6e, 0x66, 0x69, 0x67, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, - 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x4b, 0x0a, 0x05, 0x76, - 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x35, 0x2e, 0x65, 0x74, 0x68, - 0x65, 0x72, 0x65, 0x75, 0x6d, 0x2e, 0x76, 0x61, 0x6c, 0x69, 0x64, 0x61, 0x74, 0x6f, 0x72, 0x2e, - 0x61, 0x63, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x73, 0x2e, 0x76, 0x32, 0x2e, 0x50, 0x72, 0x6f, 0x70, - 0x6f, 0x73, 0x65, 0x72, 0x4f, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x50, 0x61, 0x79, 0x6c, 0x6f, 0x61, - 0x64, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x3a, 0x02, 0x38, 0x01, 0x42, 0xcd, 0x01, 0x0a, - 0x22, 0x6f, 0x72, 0x67, 0x2e, 0x65, 0x74, 0x68, 0x65, 0x72, 0x65, 0x75, 0x6d, 0x2e, 0x76, 0x61, - 0x6c, 0x69, 0x64, 0x61, 0x74, 0x6f, 0x72, 0x2e, 0x61, 0x63, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x73, - 0x2e, 0x76, 0x32, 0x42, 0x0f, 0x4b, 0x65, 0x79, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x72, 0x50, - 0x72, 0x6f, 0x74, 0x6f, 0x50, 0x01, 0x5a, 0x52, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, - 0x6f, 0x6d, 0x2f, 0x4f, 0x66, 0x66, 0x63, 0x68, 0x61, 0x69, 0x6e, 0x4c, 0x61, 0x62, 0x73, 0x2f, - 0x70, 0x72, 0x79, 0x73, 0x6d, 0x2f, 0x76, 0x37, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2f, 0x70, - 0x72, 0x79, 0x73, 0x6d, 0x2f, 0x76, 0x31, 0x61, 0x6c, 0x70, 0x68, 0x61, 0x31, 0x2f, 0x76, 0x61, - 0x6c, 0x69, 0x64, 0x61, 0x74, 0x6f, 0x72, 0x2d, 0x63, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x3b, 0x76, - 0x61, 0x6c, 0x69, 0x64, 0x61, 0x74, 0x6f, 0x72, 0x70, 0x62, 0xaa, 0x02, 0x1e, 0x45, 0x74, 0x68, - 0x65, 0x72, 0x65, 0x75, 0x6d, 0x2e, 0x56, 0x61, 0x6c, 0x69, 0x64, 0x61, 0x74, 0x6f, 0x72, 0x2e, - 0x41, 0x63, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x73, 0x2e, 0x56, 0x32, 0xca, 0x02, 0x1e, 0x45, 0x74, - 0x68, 0x65, 0x72, 0x65, 0x75, 0x6d, 0x5c, 0x56, 0x61, 0x6c, 0x69, 0x64, 0x61, 0x74, 0x6f, 0x72, - 0x5c, 0x41, 0x63, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x73, 0x5c, 0x56, 0x32, 0x62, 0x06, 0x70, 0x72, - 0x6f, 0x74, 0x6f, 0x33, + 0x72, 0x6f, 0x70, 0x6f, 0x73, 0x65, 0x72, 0x4f, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x50, 0x61, 0x79, + 0x6c, 0x6f, 0x61, 0x64, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x3a, 0x02, 0x38, 0x01, 0x42, + 0xcd, 0x01, 0x0a, 0x22, 0x6f, 0x72, 0x67, 0x2e, 0x65, 0x74, 0x68, 0x65, 0x72, 0x65, 0x75, 0x6d, + 0x2e, 0x76, 0x61, 0x6c, 0x69, 0x64, 0x61, 0x74, 0x6f, 0x72, 0x2e, 0x61, 0x63, 0x63, 0x6f, 0x75, + 0x6e, 0x74, 0x73, 0x2e, 0x76, 0x32, 0x42, 0x0f, 0x4b, 0x65, 0x79, 0x6d, 0x61, 0x6e, 0x61, 0x67, + 0x65, 0x72, 0x50, 0x72, 0x6f, 0x74, 0x6f, 0x50, 0x01, 0x5a, 0x52, 0x67, 0x69, 0x74, 0x68, 0x75, + 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x4f, 0x66, 0x66, 0x63, 0x68, 0x61, 0x69, 0x6e, 0x4c, 0x61, + 0x62, 0x73, 0x2f, 0x70, 0x72, 0x79, 0x73, 0x6d, 0x2f, 0x76, 0x37, 0x2f, 0x70, 0x72, 0x6f, 0x74, + 0x6f, 0x2f, 0x70, 0x72, 0x79, 0x73, 0x6d, 0x2f, 0x76, 0x31, 0x61, 0x6c, 0x70, 0x68, 0x61, 0x31, + 0x2f, 0x76, 0x61, 0x6c, 0x69, 0x64, 0x61, 0x74, 0x6f, 0x72, 0x2d, 0x63, 0x6c, 0x69, 0x65, 0x6e, + 0x74, 0x3b, 0x76, 0x61, 0x6c, 0x69, 0x64, 0x61, 0x74, 0x6f, 0x72, 0x70, 0x62, 0xaa, 0x02, 0x1e, + 0x45, 0x74, 0x68, 0x65, 0x72, 0x65, 0x75, 0x6d, 0x2e, 0x56, 0x61, 0x6c, 0x69, 0x64, 0x61, 0x74, + 0x6f, 0x72, 0x2e, 0x41, 0x63, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x73, 0x2e, 0x56, 0x32, 0xca, 0x02, + 0x1e, 0x45, 0x74, 0x68, 0x65, 0x72, 0x65, 0x75, 0x6d, 0x5c, 0x56, 0x61, 0x6c, 0x69, 0x64, 0x61, + 0x74, 0x6f, 0x72, 0x5c, 0x41, 0x63, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x73, 0x5c, 0x56, 0x32, 0x62, + 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, } var ( diff --git a/proto/prysm/v1alpha1/validator-client/keymanager.proto b/proto/prysm/v1alpha1/validator-client/keymanager.proto index 4c55d25ae16a..ebe7da3bab7d 100644 --- a/proto/prysm/v1alpha1/validator-client/keymanager.proto +++ b/proto/prysm/v1alpha1/validator-client/keymanager.proto @@ -146,12 +146,15 @@ message BuilderConfig { // builders is the per-key builder list from the keymanager validator config // endpoint. Each entry may override the builder-level fallbacks below. repeated BuilderEntry builders = 5; - optional string proxy = 6; - optional uint64 min_bid = 7 [ + // builders_set preserves an explicitly configured empty builders list + // ("use no builders"), which proto3 cannot distinguish from absent. + bool builders_set = 6; + optional string proxy = 7; + optional uint64 min_bid = 8 [ (ethereum.eth.ext.cast_type) = "github.com/OffchainLabs/prysm/v7/consensus-types/validator.Uint64" ]; - optional uint64 builder_boost_factor = 8 [ + optional uint64 builder_boost_factor = 9 [ (ethereum.eth.ext.cast_type) = "github.com/OffchainLabs/prysm/v7/consensus-types/validator.Uint64" ]; diff --git a/validator/client/validator.go b/validator/client/validator.go index 7eaa476f5750..524bfa4aa9c3 100644 --- a/validator/client/validator.go +++ b/validator/client/validator.go @@ -1377,7 +1377,13 @@ type builderTarget struct { // builderTargetsForKey resolves the enabled builder list for pk; entries override // config-level fallbacks. TODO(gloas): minBid/boost/pubkey ride the #630 wire. func (v *validator) builderTargetsForKey(pk pubkey) []builderTarget { - bc := v.ProposerSettings().EffectiveBuilderConfig(pk) + ps := v.ProposerSettings() + // Builder entries imply v2: the API upgrades on write and v2 files declare it, + // so v1 settings have nothing legitimate to warm. + if ps == nil || ps.Version != proposer.SchemaV2 { + return nil + } + bc := ps.EffectiveBuilderConfig(pk) if !bc.IsEnabled() { return nil } @@ -1441,20 +1447,30 @@ func (v *validator) warmBuilderRequestAuthsForDuties(ctx context.Context, km key if len(targets) == 0 { continue } + // The v1 preference wire identifies a builder only by url, so same-url + // entries collapse to the safest (lowest) payment ceiling for now. + ceilings := make(map[string]uint64, len(targets)) + for _, t := range targets { + if cur, ok := ceilings[t.url]; !ok || t.maxPayment < cur { + ceilings[t.url] = t.maxPayment + } + } for _, proposalSlot := range duty.ProposerSlots { if proposalSlot <= slot { continue } - for _, t := range targets { - signed, err := v.signRequestAuthCached(ctx, km, pk, t.url, proposalSlot) + for url, maxPayment := range ceilings { + signed, err := v.signRequestAuthCached(ctx, km, pk, url, proposalSlot) if err != nil { log.WithError(err).Warn("Failed to sign builder request auth") continue } + // TODO(gloas): only maxPayment fits BuilderPreferencesV1; per-entry + // authData, minBid, boostFactor and pubkey ship with the #630 inline wire. reqs = append(reqs, ðpb.SubmitBuilderPreferencesRequest{ ValidatorPubkey: pk[:], Request: ðpb.BuilderPreferencesRequestV1{ - Preferences: ðpb.BuilderPreferencesV1{MaxExecutionPayment: primitives.Gwei(t.maxPayment)}, + Preferences: ðpb.BuilderPreferencesV1{MaxExecutionPayment: primitives.Gwei(maxPayment)}, Auth: signed, }, }) @@ -1518,7 +1534,10 @@ func (v *validator) buildSignedRegReqs( defer span.End() var signedValRegRequests []*ethpb.SignedValidatorRegistrationV1 - if v.ProposerSettings() == nil { + // One snapshot for the whole batch: concurrent keymanager writes swap the + // settings pointer, and mixing objects would tear version vs content. + ps := v.ProposerSettings() + if ps == nil { return signedValRegRequests } // if the timestamp is pre-genesis, don't create registrations @@ -1526,9 +1545,8 @@ func (v *validator) buildSignedRegReqs( return signedValRegRequests } - isV2 := v.ProposerSettings().Version == proposer.SchemaV2 - if v.ProposerSettings().DefaultConfig != nil && v.ProposerSettings().DefaultConfig.FeeRecipientConfig == nil && v.ProposerSettings().DefaultConfig.BuilderConfig != nil { - if isV2 { + if ps.DefaultConfig != nil && ps.DefaultConfig.FeeRecipientConfig == nil && ps.DefaultConfig.BuilderConfig != nil { + if ps.Version == proposer.SchemaV2 { log.Warn("Default builder config has no default fee recipient; only keys with their own fee recipient can register") } else { log.Warn("Builder is `enabled` in default config but will be ignored because no fee recipient was provided!") @@ -1542,58 +1560,14 @@ func (v *validator) buildSignedRegReqs( continue } - var enabled bool - feeRecipient := common.HexToAddress(params.BeaconConfig().EthBurnAddressHex) - gasLimit := params.BeaconConfig().DefaultBuilderGasLimit - - if isV2 { - hasFeeRecipient := false - if defaultConfig := v.ProposerSettings().DefaultConfig; defaultConfig != nil && defaultConfig.FeeRecipientConfig != nil { - feeRecipient = defaultConfig.FeeRecipientConfig.FeeRecipient - hasFeeRecipient = true - } - if config, ok := v.ProposerSettings().ProposeConfig[k]; ok && config != nil && config.FeeRecipientConfig != nil { - feeRecipient = config.FeeRecipientConfig.FeeRecipient - hasFeeRecipient = true - } - // Field-level resolution: per-key builder fields inherit from the default; - // registration still requires a fee recipient configured at some level. - bc := v.ProposerSettings().EffectiveBuilderConfig(k) - enabled = bc.IsEnabled() && hasFeeRecipient - if bc.GasLimit != 0 { - gasLimit = uint64(bc.GasLimit) - } - } else { - // V1 settings keep the pre-v2 object-level semantics so existing - // registrations are unchanged; inheritance starts at SchemaV2. - if defaultConfig := v.ProposerSettings().DefaultConfig; defaultConfig != nil && defaultConfig.FeeRecipientConfig != nil { - feeRecipient = defaultConfig.FeeRecipientConfig.FeeRecipient - if defaultConfig.BuilderConfig.IsEnabled() { - gasLimit = uint64(defaultConfig.BuilderConfig.GasLimit) - enabled = true - } - } - if v.ProposerSettings().ProposeConfig != nil { - if config, ok := v.ProposerSettings().ProposeConfig[k]; ok && config != nil && config.FeeRecipientConfig != nil { - feeRecipient = config.FeeRecipientConfig.FeeRecipient - if bc := config.BuilderConfig; bc != nil { - if bc.IsEnabled() { - gasLimit = uint64(bc.GasLimit) - enabled = true - } else { - enabled = false - } - } - } - } - } + feeRecipient, gasLimit, enabled := ps.RegistrationFor(k) if !enabled { continue } req := ðpb.ValidatorRegistrationV1{ FeeRecipient: feeRecipient[:], - GasLimit: gasLimit, + GasLimit: uint64(gasLimit), Timestamp: uint64(time.Now().UTC().Unix()), Pubkey: activePubkeys[i][:], } diff --git a/validator/client/validator_test.go b/validator/client/validator_test.go index 1250f5e52684..92b2720e2e37 100644 --- a/validator/client/validator_test.go +++ b/validator/client/validator_test.go @@ -3568,11 +3568,12 @@ func TestValidator_buildSignedRegReqs_V2Settings(t *testing.T) { assert.Equal(t, 2, len(actual)) assert.DeepEqual(t, feeRecipient1[:], actual[0].Message.FeeRecipient) - assert.Equal(t, uint64(7777), actual[0].Message.GasLimit, "per-key builder gas limit, not top-level") + // v2 gas limits live on the option, where UpgradeToV2 and the gas-limit API write them. + assert.Equal(t, uint64(1111), actual[0].Message.GasLimit, "per-key option gas limit wins") assert.DeepEqual(t, pubkey1[:], actual[0].Message.Pubkey) assert.DeepEqual(t, feeRecipient2[:], actual[1].Message.FeeRecipient) - assert.Equal(t, uint64(9999), actual[1].Message.GasLimit, "default builder gas limit, not top-level") + assert.Equal(t, uint64(2222), actual[1].Message.GasLimit, "per-key option gas limit wins over default builder") assert.DeepEqual(t, pubkey2[:], actual[1].Message.Pubkey) } @@ -3957,3 +3958,70 @@ func TestGetAttestationData_PostElectraConcurrentAccess(t *testing.T) { require.DeepEqual(t, expectedData, results[i]) } } + +func TestBuilderTargetsForKey(t *testing.T) { + pk := pubkeyFromString(t, "0x111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111") + u64 := func(v uint64) *validatorType.Uint64 { u := validatorType.Uint64(v); return &u } + proposeConfig := map[[fieldparams.BLSPubkeyLength]byte]*proposer.Option{ + pk: {BuilderConfig: &proposer.BuilderConfig{ + Enabled: true, + MaxExecutionPayment: u64(500), + Builders: []*proposer.BuilderEntry{ + {URL: "https://a.example", MaxExecutionPayment: u64(100)}, + {URL: "https://b.example"}, + }, + }}, + } + + t.Run("v1 settings produce no targets", func(t *testing.T) { + // Builder entries imply v2: the API upgrades on write and v2 files declare + // it, so entries on v1 settings are a malformed hybrid and stay inert. + v := &validator{proposerSettings: &proposer.Settings{Version: proposer.SchemaV1, ProposeConfig: proposeConfig}} + require.Equal(t, 0, len(v.builderTargetsForKey(pk))) + }) + + t.Run("disabled config produces no targets", func(t *testing.T) { + disabled := map[[fieldparams.BLSPubkeyLength]byte]*proposer.Option{ + pk: {BuilderConfig: &proposer.BuilderConfig{Enabled: false, Builders: proposeConfig[pk].BuilderConfig.Builders}}, + } + v := &validator{proposerSettings: &proposer.Settings{Version: proposer.SchemaV2, ProposeConfig: disabled}} + require.Equal(t, 0, len(v.builderTargetsForKey(pk))) + }) + + t.Run("entries resolve with config-level fallbacks", func(t *testing.T) { + v := &validator{proposerSettings: &proposer.Settings{Version: proposer.SchemaV2, ProposeConfig: proposeConfig}} + targets := v.builderTargetsForKey(pk) + require.Equal(t, 2, len(targets)) + require.Equal(t, uint64(100), targets[0].maxPayment) + require.Equal(t, uint64(500), targets[1].maxPayment) + }) +} + +func TestWarmBuilderRequestAuthsForDuties_CollapsesSameURL(t *testing.T) { + v, _, validatorKey, finish := setup(t, false) + defer finish() + var pk [fieldparams.BLSPubkeyLength]byte + copy(pk[:], validatorKey.PublicKey().Marshal()) + u64 := func(val uint64) *validatorType.Uint64 { u := validatorType.Uint64(val); return &u } + v.proposerSettings = &proposer.Settings{ + Version: proposer.SchemaV2, + ProposeConfig: map[[fieldparams.BLSPubkeyLength]byte]*proposer.Option{ + pk: {BuilderConfig: &proposer.BuilderConfig{ + Enabled: true, + Builders: []*proposer.BuilderEntry{ + {URL: "https://a.example", AuthData: []byte{1}, MaxExecutionPayment: u64(200)}, + {URL: "https://a.example", AuthData: []byte{2}, MaxExecutionPayment: u64(100)}, + }, + }}, + }, + } + km, err := v.Keymanager() + require.NoError(t, err) + duties := func(yield func(pubkey, *ethpb.ValidatorDuty) bool) { + yield(pk, ðpb.ValidatorDuty{PublicKey: pk[:], ProposerSlots: []primitives.Slot{10}}) + } + reqs := v.warmBuilderRequestAuthsForDuties(t.Context(), km, 5, duties) + // Same-url entries collapse to one request carrying the safest (lowest) ceiling. + require.Equal(t, 1, len(reqs)) + require.Equal(t, primitives.Gwei(100), reqs[0].Request.Preferences.MaxExecutionPayment) +} diff --git a/validator/db/filesystem/proposer_settings_test.go b/validator/db/filesystem/proposer_settings_test.go index ad68c57c722c..198c5e511427 100644 --- a/validator/db/filesystem/proposer_settings_test.go +++ b/validator/db/filesystem/proposer_settings_test.go @@ -263,4 +263,13 @@ func TestStore_ProposerSettings_V2BuildersRoundTrip(t *testing.T) { // The entry without auth_data must load back as nil, not empty-non-nil. require.IsNil(t, got.ProposeConfig[key].BuilderConfig.Builders[1].AuthData) + + // An explicit empty list (use no builders) must survive the round-trip + // rather than reloading as nil (inherit). + in.ProposeConfig[key].BuilderConfig.Builders = []*proposer.BuilderEntry{} + require.NoError(t, store.SaveProposerSettings(ctx, in)) + got, err = store.ProposerSettings(ctx) + require.NoError(t, err) + require.NotNil(t, got.ProposeConfig[key].BuilderConfig.Builders) + require.Equal(t, 0, len(got.ProposeConfig[key].BuilderConfig.Builders)) } diff --git a/validator/rpc/handlers_validator_config.go b/validator/rpc/handlers_validator_config.go index 86886536d1ad..746651eebfd8 100644 --- a/validator/rpc/handlers_validator_config.go +++ b/validator/rpc/handlers_validator_config.go @@ -78,8 +78,10 @@ func (s *Server) SetBuilders(w http.ResponseWriter, r *http.Request) { settings = &proposer.Settings{Version: proposer.SchemaV2} } else { settings = settings.Clone() + // Builder lists are v2 content: writing them migrates v1 settings in place. settings.UpgradeToV2() } + if settings.ProposeConfig == nil { settings.ProposeConfig = make(map[[fieldparams.BLSPubkeyLength]byte]*proposer.Option) } diff --git a/validator/rpc/handlers_validator_config_test.go b/validator/rpc/handlers_validator_config_test.go index bb9fa608b9d2..3aae119ec921 100644 --- a/validator/rpc/handlers_validator_config_test.go +++ b/validator/rpc/handlers_validator_config_test.go @@ -141,9 +141,10 @@ func TestServer_SetBuilders(t *testing.T) { require.Equal(t, http.StatusAccepted, postBuilders(t, srv, pk, `{"enabled":true,"builders":[{"url":"https://a.example"}]}`).Code) got := srv.validatorService.ProposerSettings() + // Builder lists are v2 content: POST migrates the schema in place. require.Equal(t, proposer.SchemaV2, got.Version) opt := got.ProposeConfig[keys[0]] - // The version bumps and the builder gas limit migrates to the option. + // The builder gas limit is hoisted to the option by the upgrade. require.Equal(t, validator.Uint64(999), opt.GasLimit) require.Equal(t, 1, len(opt.BuilderConfig.Builders)) }) @@ -181,6 +182,8 @@ func TestServer_SetBuilders(t *testing.T) { "invalid auth_data hex": {`{"enabled":true,"builders":[{"url":"https://a","auth_data":"0xzz"}]}`, "auth_data is not valid hex"}, "auth_data too long": {`{"enabled":true,"builders":[{"url":"https://a","auth_data":"0x` + strings.Repeat("ab", 4097) + `"}]}`, "auth_data exceeds 4096 bytes"}, "non-numeric min_bid": {`{"enabled":true,"min_bid":"abc","builders":[]}`, "min_bid is not a valid uint64"}, + "null entry": {`{"enabled":true,"builders":[null]}`, "builders[0] is null"}, + "non-numeric entry max": {`{"enabled":true,"builders":[{"url":"https://a"},{"url":"https://b","max_execution_payment":"x"}]}`, "builders[1].max_execution_payment is not a valid uint64"}, } for name, tc := range cases { t.Run(name, func(t *testing.T) { diff --git a/validator/rpc/structs.go b/validator/rpc/structs.go index 9ee6166c5f46..930df9d2a6bf 100644 --- a/validator/rpc/structs.go +++ b/validator/rpc/structs.go @@ -196,6 +196,9 @@ func (in *BuilderConfig) ToConsensus() (*proposer.BuilderConfig, error) { bc.Builders = make([]*proposer.BuilderEntry, 0, len(in.Builders)) seen := make(map[proposer.EntryIdentity]bool, len(in.Builders)) for i, entry := range in.Builders { + if entry == nil { + return nil, errors.Errorf("builders[%d] is null", i) + } be, err := entry.ToConsensus(i) if err != nil { return nil, err @@ -239,21 +242,21 @@ func (in *BuilderEntry) ToConsensus(i int) (*proposer.BuilderEntry, error) { be.AuthData = ad } if in.MaxExecutionPayment != nil { - v, err := parseUint(*in.MaxExecutionPayment, "builders[].max_execution_payment") + v, err := parseUint(*in.MaxExecutionPayment, fmt.Sprintf("builders[%d].max_execution_payment", i)) if err != nil { return nil, err } be.MaxExecutionPayment = &v } if in.MinBid != nil { - v, err := parseUint(*in.MinBid, "builders[].min_bid") + v, err := parseUint(*in.MinBid, fmt.Sprintf("builders[%d].min_bid", i)) if err != nil { return nil, err } be.MinBid = &v } if in.BuilderBoostFactor != nil { - v, err := parseUint(*in.BuilderBoostFactor, "builders[].builder_boost_factor") + v, err := parseUint(*in.BuilderBoostFactor, fmt.Sprintf("builders[%d].builder_boost_factor", i)) if err != nil { return nil, err } From 34298541fc9208e545d9c6ab98c91e88cca7cce9 Mon Sep 17 00:00:00 2001 From: james-prysm <90280386+james-prysm@users.noreply.github.com> Date: Mon, 10 Aug 2026 07:01:16 -0700 Subject: [PATCH 12/26] Update validator/rpc/structs.go Co-authored-by: Jun Song <87601811+syjn99@users.noreply.github.com> --- validator/rpc/structs.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/validator/rpc/structs.go b/validator/rpc/structs.go index 930df9d2a6bf..93a3c4536f85 100644 --- a/validator/rpc/structs.go +++ b/validator/rpc/structs.go @@ -123,7 +123,7 @@ type BuilderConfig struct { } type BuilderEntry struct { - Url string `json:"url,omitempty"` + Url string `json:"url"` AuthData *string `json:"auth_data,omitempty"` BuilderPubkey *string `json:"builder_pubkey,omitempty"` MaxExecutionPayment *string `json:"max_execution_payment,omitempty"` From 29ea6a0b524516c098aad8fe7251ab998e1c2c86 Mon Sep 17 00:00:00 2001 From: james-prysm <90280386+james-prysm@users.noreply.github.com> Date: Mon, 10 Aug 2026 07:01:39 -0700 Subject: [PATCH 13/26] Update validator/rpc/handlers_validator_config.go Co-authored-by: Jun Song <87601811+syjn99@users.noreply.github.com> --- validator/rpc/handlers_validator_config.go | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/validator/rpc/handlers_validator_config.go b/validator/rpc/handlers_validator_config.go index 746651eebfd8..b39c69d21af6 100644 --- a/validator/rpc/handlers_validator_config.go +++ b/validator/rpc/handlers_validator_config.go @@ -12,8 +12,7 @@ import ( "github.com/OffchainLabs/prysm/v7/network/httputil" ) -// GetBuilders implements GET /eth/v1/validator/{pubkey}/builders (keymanager-APIs -// #88): the key's config resolved against default_config, safe to re-submit. +// GetBuilders implements GET /eth/v1/validator/{pubkey}/builders: the key's config resolved against default_config, safe to re-submit. func (s *Server) GetBuilders(w http.ResponseWriter, r *http.Request) { _, span := trace.StartSpan(r.Context(), "validator.keymanagerAPI.GetBuilders") defer span.End() From 2a365bcfbc9529d0ef39077a3de483eea181df25 Mon Sep 17 00:00:00 2001 From: james-prysm Date: Mon, 10 Aug 2026 09:27:52 -0500 Subject: [PATCH 14/26] remove proxy that is no longer used --- .../proposer/effective_builder_config_test.go | 12 +- config/proposer/settings.go | 24 --- .../validator-client/keymanager.pb.go | 183 ++++++++---------- .../validator-client/keymanager.proto | 12 +- 4 files changed, 89 insertions(+), 142 deletions(-) diff --git a/config/proposer/effective_builder_config_test.go b/config/proposer/effective_builder_config_test.go index 9061c1613b53..b27dd891925b 100644 --- a/config/proposer/effective_builder_config_test.go +++ b/config/proposer/effective_builder_config_test.go @@ -5,7 +5,6 @@ import ( "github.com/OffchainLabs/prysm/v7/consensus-types/validator" "github.com/OffchainLabs/prysm/v7/testing/require" - "google.golang.org/protobuf/proto" ) // These cases mirror the test vectors proposed upstream on keymanager-APIs #87 @@ -76,11 +75,11 @@ func TestEffectiveBuilderConfig(t *testing.T) { perKey := &BuilderConfig{Enabled: true} require.Equal(t, validator.Uint64(30000000), effectiveBuilderConfig(perKey, def).GasLimit) }) - t.Run("proxy and boost inherit per field", func(t *testing.T) { - def := &BuilderConfig{Proxy: proto.String("http://side-car:9001"), BuilderBoostFactor: uint64ValPtr(90)} + t.Run("boost factor inherits per field", func(t *testing.T) { + def := &BuilderConfig{MinBid: uint64ValPtr(5000000), BuilderBoostFactor: uint64ValPtr(90)} perKey := &BuilderConfig{Enabled: true, BuilderBoostFactor: uint64ValPtr(120)} eff := effectiveBuilderConfig(perKey, def) - require.Equal(t, "http://side-car:9001", *eff.Proxy) + require.Equal(t, validator.Uint64(5000000), *eff.MinBid) require.Equal(t, validator.Uint64(120), *eff.BuilderBoostFactor) }) t.Run("both-set min_bid: per-key wins", func(t *testing.T) { @@ -88,11 +87,6 @@ func TestEffectiveBuilderConfig(t *testing.T) { perKey := &BuilderConfig{MinBid: uint64ValPtr(7000000)} require.Equal(t, validator.Uint64(7000000), *effectiveBuilderConfig(perKey, def).MinBid) }) - t.Run("both-set proxy: per-key wins", func(t *testing.T) { - def := &BuilderConfig{Proxy: proto.String("http://default:1")} - perKey := &BuilderConfig{Proxy: proto.String("http://mine:2")} - require.Equal(t, "http://mine:2", *effectiveBuilderConfig(perKey, def).Proxy) - }) t.Run("nonzero per-key gas limit wins over default", func(t *testing.T) { def := &BuilderConfig{GasLimit: validator.Uint64(30000000)} perKey := &BuilderConfig{GasLimit: validator.Uint64(45000000)} diff --git a/config/proposer/settings.go b/config/proposer/settings.go index 6757a6d5e152..5dfe4eac4c5e 100644 --- a/config/proposer/settings.go +++ b/config/proposer/settings.go @@ -114,7 +114,6 @@ type BuilderConfig struct { GasLimit validator.Uint64 `json:"gas_limit,omitempty" yaml:"gas_limit,omitempty"` MaxExecutionPayment *validator.Uint64 `json:"max_execution_payment,omitempty" yaml:"max_execution_payment,omitempty"` // explicit 0 = trustless-only; unset inherits Builders []*BuilderEntry `json:"builders,omitempty" yaml:"builders,omitempty"` - Proxy *string `json:"proxy,omitempty" yaml:"proxy,omitempty"` MinBid *validator.Uint64 `json:"min_bid,omitempty" yaml:"min_bid,omitempty"` BuilderBoostFactor *validator.Uint64 `json:"builder_boost_factor,omitempty" yaml:"builder_boost_factor,omitempty"` } @@ -125,7 +124,6 @@ type BuilderEntry struct { URL string `json:"url" yaml:"url"` Pubkey []byte `json:"pubkey,omitempty" yaml:"pubkey,omitempty"` AuthData []byte `json:"auth_data,omitempty" yaml:"auth_data,omitempty"` - Proxy *string `json:"proxy,omitempty" yaml:"proxy,omitempty"` MinBid *validator.Uint64 `json:"min_bid,omitempty" yaml:"min_bid,omitempty"` MaxExecutionPayment *validator.Uint64 `json:"max_execution_payment,omitempty" yaml:"max_execution_payment,omitempty"` BuilderBoostFactor *validator.Uint64 `json:"builder_boost_factor,omitempty" yaml:"builder_boost_factor,omitempty"` @@ -303,7 +301,6 @@ func effectiveBuilderConfig(perKey, def *BuilderConfig) *BuilderConfig { MaxExecutionPayment: coalesceUint64(perKey.MaxExecutionPayment, def.MaxExecutionPayment), MinBid: coalesceUint64(perKey.MinBid, def.MinBid), BuilderBoostFactor: coalesceUint64(perKey.BuilderBoostFactor, def.BuilderBoostFactor), - Proxy: coalesceString(perKey.Proxy, def.Proxy), Builders: perKey.Builders, } if eff.GasLimit == 0 { @@ -316,13 +313,6 @@ func effectiveBuilderConfig(perKey, def *BuilderConfig) *BuilderConfig { return eff } -func coalesceString(a, b *string) *string { - if a != nil { - return a - } - return b -} - func coalesceUint64(a, b *validator.Uint64) *validator.Uint64 { if a != nil { return a @@ -339,7 +329,6 @@ func BuilderConfigFromConsensus(from *validatorpb.BuilderConfig) *BuilderConfig Enabled: from.GetEnabled(), GasLimit: from.GasLimit, MaxExecutionPayment: cloneUint64(from.MaxExecutionPayment), - Proxy: cloneString(from.Proxy), MinBid: cloneUint64(from.MinBid), BuilderBoostFactor: cloneUint64(from.BuilderBoostFactor), } @@ -361,7 +350,6 @@ func builderEntryFromConsensus(from *validatorpb.BuilderEntry) *BuilderEntry { } e := &BuilderEntry{ URL: from.Url, - Proxy: from.Proxy, MinBid: from.MinBid, MaxExecutionPayment: from.MaxExecutionPayment, BuilderBoostFactor: from.BuilderBoostFactor, @@ -511,7 +499,6 @@ func (bc *BuilderConfig) Clone() *BuilderConfig { c.Enabled = bc.Enabled c.GasLimit = bc.GasLimit c.MaxExecutionPayment = cloneUint64(bc.MaxExecutionPayment) - c.Proxy = cloneString(bc.Proxy) c.MinBid = cloneUint64(bc.MinBid) c.BuilderBoostFactor = cloneUint64(bc.BuilderBoostFactor) // Preserve nil vs empty: an empty list is the "use no builders" marker. @@ -533,21 +520,12 @@ func (be *BuilderEntry) Clone() *BuilderEntry { URL: be.URL, Pubkey: bytesutil.SafeCopyBytes(be.Pubkey), AuthData: bytesutil.SafeCopyBytes(be.AuthData), - Proxy: cloneString(be.Proxy), MinBid: cloneUint64(be.MinBid), MaxExecutionPayment: cloneUint64(be.MaxExecutionPayment), BuilderBoostFactor: cloneUint64(be.BuilderBoostFactor), } } -func cloneString(v *string) *string { - if v == nil { - return nil - } - c := *v - return &c -} - func cloneUint64(v *validator.Uint64) *validator.Uint64 { if v == nil { return nil @@ -574,7 +552,6 @@ func (bc *BuilderConfig) ToConsensus() *validatorpb.BuilderConfig { c.Enabled = &enabled c.GasLimit = bc.GasLimit c.MaxExecutionPayment = cloneUint64(bc.MaxExecutionPayment) - c.Proxy = cloneString(bc.Proxy) c.MinBid = cloneUint64(bc.MinBid) c.BuilderBoostFactor = cloneUint64(bc.BuilderBoostFactor) // BuildersSet preserves nil-vs-empty across the wire: an explicit empty @@ -597,7 +574,6 @@ func (be *BuilderEntry) toConsensus() *validatorpb.BuilderEntry { Url: be.URL, Pubkey: bytesutil.SafeCopyBytes(be.Pubkey), AuthData: bytesutil.SafeCopyBytes(be.AuthData), - Proxy: cloneString(be.Proxy), MinBid: cloneUint64(be.MinBid), MaxExecutionPayment: cloneUint64(be.MaxExecutionPayment), BuilderBoostFactor: cloneUint64(be.BuilderBoostFactor), diff --git a/proto/prysm/v1alpha1/validator-client/keymanager.pb.go b/proto/prysm/v1alpha1/validator-client/keymanager.pb.go index 8919142bf51d..bad46e23035c 100755 --- a/proto/prysm/v1alpha1/validator-client/keymanager.pb.go +++ b/proto/prysm/v1alpha1/validator-client/keymanager.pb.go @@ -718,9 +718,8 @@ type BuilderConfig struct { MaxExecutionPayment *github_com_OffchainLabs_prysm_v7_consensus_types_validator.Uint64 `protobuf:"varint,4,opt,name=max_execution_payment,json=maxExecutionPayment,proto3,oneof" json:"max_execution_payment,omitempty" cast-type:"github.com/OffchainLabs/prysm/v7/consensus-types/validator.Uint64"` Builders []*BuilderEntry `protobuf:"bytes,5,rep,name=builders,proto3" json:"builders,omitempty"` BuildersSet bool `protobuf:"varint,6,opt,name=builders_set,json=buildersSet,proto3" json:"builders_set,omitempty"` - Proxy *string `protobuf:"bytes,7,opt,name=proxy,proto3,oneof" json:"proxy,omitempty"` - MinBid *github_com_OffchainLabs_prysm_v7_consensus_types_validator.Uint64 `protobuf:"varint,8,opt,name=min_bid,json=minBid,proto3,oneof" json:"min_bid,omitempty" cast-type:"github.com/OffchainLabs/prysm/v7/consensus-types/validator.Uint64"` - BuilderBoostFactor *github_com_OffchainLabs_prysm_v7_consensus_types_validator.Uint64 `protobuf:"varint,9,opt,name=builder_boost_factor,json=builderBoostFactor,proto3,oneof" json:"builder_boost_factor,omitempty" cast-type:"github.com/OffchainLabs/prysm/v7/consensus-types/validator.Uint64"` + MinBid *github_com_OffchainLabs_prysm_v7_consensus_types_validator.Uint64 `protobuf:"varint,7,opt,name=min_bid,json=minBid,proto3,oneof" json:"min_bid,omitempty" cast-type:"github.com/OffchainLabs/prysm/v7/consensus-types/validator.Uint64"` + BuilderBoostFactor *github_com_OffchainLabs_prysm_v7_consensus_types_validator.Uint64 `protobuf:"varint,8,opt,name=builder_boost_factor,json=builderBoostFactor,proto3,oneof" json:"builder_boost_factor,omitempty" cast-type:"github.com/OffchainLabs/prysm/v7/consensus-types/validator.Uint64"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } @@ -790,13 +789,6 @@ func (x *BuilderConfig) GetBuildersSet() bool { return false } -func (x *BuilderConfig) GetProxy() string { - if x != nil && x.Proxy != nil { - return *x.Proxy - } - return "" -} - func (x *BuilderConfig) GetMinBid() github_com_OffchainLabs_prysm_v7_consensus_types_validator.Uint64 { if x != nil && x.MinBid != nil { return *x.MinBid @@ -816,10 +808,9 @@ type BuilderEntry struct { Url string `protobuf:"bytes,1,opt,name=url,proto3" json:"url,omitempty"` Pubkey []byte `protobuf:"bytes,2,opt,name=pubkey,proto3,oneof" json:"pubkey,omitempty"` AuthData []byte `protobuf:"bytes,3,opt,name=auth_data,json=authData,proto3,oneof" json:"auth_data,omitempty"` - Proxy *string `protobuf:"bytes,4,opt,name=proxy,proto3,oneof" json:"proxy,omitempty"` - MinBid *github_com_OffchainLabs_prysm_v7_consensus_types_validator.Uint64 `protobuf:"varint,5,opt,name=min_bid,json=minBid,proto3,oneof" json:"min_bid,omitempty" cast-type:"github.com/OffchainLabs/prysm/v7/consensus-types/validator.Uint64"` - MaxExecutionPayment *github_com_OffchainLabs_prysm_v7_consensus_types_validator.Uint64 `protobuf:"varint,6,opt,name=max_execution_payment,json=maxExecutionPayment,proto3,oneof" json:"max_execution_payment,omitempty" cast-type:"github.com/OffchainLabs/prysm/v7/consensus-types/validator.Uint64"` - BuilderBoostFactor *github_com_OffchainLabs_prysm_v7_consensus_types_validator.Uint64 `protobuf:"varint,7,opt,name=builder_boost_factor,json=builderBoostFactor,proto3,oneof" json:"builder_boost_factor,omitempty" cast-type:"github.com/OffchainLabs/prysm/v7/consensus-types/validator.Uint64"` + MinBid *github_com_OffchainLabs_prysm_v7_consensus_types_validator.Uint64 `protobuf:"varint,4,opt,name=min_bid,json=minBid,proto3,oneof" json:"min_bid,omitempty" cast-type:"github.com/OffchainLabs/prysm/v7/consensus-types/validator.Uint64"` + MaxExecutionPayment *github_com_OffchainLabs_prysm_v7_consensus_types_validator.Uint64 `protobuf:"varint,5,opt,name=max_execution_payment,json=maxExecutionPayment,proto3,oneof" json:"max_execution_payment,omitempty" cast-type:"github.com/OffchainLabs/prysm/v7/consensus-types/validator.Uint64"` + BuilderBoostFactor *github_com_OffchainLabs_prysm_v7_consensus_types_validator.Uint64 `protobuf:"varint,6,opt,name=builder_boost_factor,json=builderBoostFactor,proto3,oneof" json:"builder_boost_factor,omitempty" cast-type:"github.com/OffchainLabs/prysm/v7/consensus-types/validator.Uint64"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } @@ -875,13 +866,6 @@ func (x *BuilderEntry) GetAuthData() []byte { return nil } -func (x *BuilderEntry) GetProxy() string { - if x != nil && x.Proxy != nil { - return *x.Proxy - } - return "" -} - func (x *BuilderEntry) GetMinBid() github_com_OffchainLabs_prysm_v7_consensus_types_validator.Uint64 { if x != nil && x.MinBid != nil { return *x.MinBid @@ -1192,7 +1176,7 @@ var file_proto_prysm_v1alpha1_validator_client_keymanager_proto_rawDesc = []byte 0x6f, 0x6e, 0x73, 0x65, 0x6e, 0x73, 0x75, 0x73, 0x2d, 0x74, 0x79, 0x70, 0x65, 0x73, 0x2f, 0x76, 0x61, 0x6c, 0x69, 0x64, 0x61, 0x74, 0x6f, 0x72, 0x2e, 0x55, 0x69, 0x6e, 0x74, 0x36, 0x34, 0x52, 0x08, 0x67, 0x61, 0x73, 0x4c, 0x69, 0x6d, 0x69, 0x74, 0x42, 0x0b, 0x0a, 0x09, 0x5f, 0x67, 0x72, - 0x61, 0x66, 0x66, 0x69, 0x74, 0x69, 0x22, 0xe0, 0x05, 0x0a, 0x0d, 0x42, 0x75, 0x69, 0x6c, 0x64, + 0x61, 0x66, 0x66, 0x69, 0x74, 0x69, 0x22, 0xbb, 0x05, 0x0a, 0x0d, 0x42, 0x75, 0x69, 0x6c, 0x64, 0x65, 0x72, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x1d, 0x0a, 0x07, 0x65, 0x6e, 0x61, 0x62, 0x6c, 0x65, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x08, 0x48, 0x00, 0x52, 0x07, 0x65, 0x6e, 0x61, 0x62, 0x6c, 0x65, 0x64, 0x88, 0x01, 0x01, 0x12, 0x62, 0x0a, 0x09, 0x67, 0x61, 0x73, 0x5f, 0x6c, @@ -1216,103 +1200,98 @@ var file_proto_prysm_v1alpha1_validator_client_keymanager_proto_rawDesc = []byte 0x75, 0x69, 0x6c, 0x64, 0x65, 0x72, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x52, 0x08, 0x62, 0x75, 0x69, 0x6c, 0x64, 0x65, 0x72, 0x73, 0x12, 0x21, 0x0a, 0x0c, 0x62, 0x75, 0x69, 0x6c, 0x64, 0x65, 0x72, 0x73, 0x5f, 0x73, 0x65, 0x74, 0x18, 0x06, 0x20, 0x01, 0x28, 0x08, 0x52, 0x0b, 0x62, 0x75, 0x69, - 0x6c, 0x64, 0x65, 0x72, 0x73, 0x53, 0x65, 0x74, 0x12, 0x19, 0x0a, 0x05, 0x70, 0x72, 0x6f, 0x78, - 0x79, 0x18, 0x07, 0x20, 0x01, 0x28, 0x09, 0x48, 0x02, 0x52, 0x05, 0x70, 0x72, 0x6f, 0x78, 0x79, - 0x88, 0x01, 0x01, 0x12, 0x63, 0x0a, 0x07, 0x6d, 0x69, 0x6e, 0x5f, 0x62, 0x69, 0x64, 0x18, 0x08, - 0x20, 0x01, 0x28, 0x04, 0x42, 0x45, 0x82, 0xb5, 0x18, 0x41, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, - 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x4f, 0x66, 0x66, 0x63, 0x68, 0x61, 0x69, 0x6e, 0x4c, 0x61, 0x62, - 0x73, 0x2f, 0x70, 0x72, 0x79, 0x73, 0x6d, 0x2f, 0x76, 0x37, 0x2f, 0x63, 0x6f, 0x6e, 0x73, 0x65, - 0x6e, 0x73, 0x75, 0x73, 0x2d, 0x74, 0x79, 0x70, 0x65, 0x73, 0x2f, 0x76, 0x61, 0x6c, 0x69, 0x64, - 0x61, 0x74, 0x6f, 0x72, 0x2e, 0x55, 0x69, 0x6e, 0x74, 0x36, 0x34, 0x48, 0x03, 0x52, 0x06, 0x6d, - 0x69, 0x6e, 0x42, 0x69, 0x64, 0x88, 0x01, 0x01, 0x12, 0x7c, 0x0a, 0x14, 0x62, 0x75, 0x69, 0x6c, - 0x64, 0x65, 0x72, 0x5f, 0x62, 0x6f, 0x6f, 0x73, 0x74, 0x5f, 0x66, 0x61, 0x63, 0x74, 0x6f, 0x72, - 0x18, 0x09, 0x20, 0x01, 0x28, 0x04, 0x42, 0x45, 0x82, 0xb5, 0x18, 0x41, 0x67, 0x69, 0x74, 0x68, - 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x4f, 0x66, 0x66, 0x63, 0x68, 0x61, 0x69, 0x6e, 0x4c, - 0x61, 0x62, 0x73, 0x2f, 0x70, 0x72, 0x79, 0x73, 0x6d, 0x2f, 0x76, 0x37, 0x2f, 0x63, 0x6f, 0x6e, - 0x73, 0x65, 0x6e, 0x73, 0x75, 0x73, 0x2d, 0x74, 0x79, 0x70, 0x65, 0x73, 0x2f, 0x76, 0x61, 0x6c, - 0x69, 0x64, 0x61, 0x74, 0x6f, 0x72, 0x2e, 0x55, 0x69, 0x6e, 0x74, 0x36, 0x34, 0x48, 0x04, 0x52, - 0x12, 0x62, 0x75, 0x69, 0x6c, 0x64, 0x65, 0x72, 0x42, 0x6f, 0x6f, 0x73, 0x74, 0x46, 0x61, 0x63, - 0x74, 0x6f, 0x72, 0x88, 0x01, 0x01, 0x42, 0x0a, 0x0a, 0x08, 0x5f, 0x65, 0x6e, 0x61, 0x62, 0x6c, - 0x65, 0x64, 0x42, 0x18, 0x0a, 0x16, 0x5f, 0x6d, 0x61, 0x78, 0x5f, 0x65, 0x78, 0x65, 0x63, 0x75, - 0x74, 0x69, 0x6f, 0x6e, 0x5f, 0x70, 0x61, 0x79, 0x6d, 0x65, 0x6e, 0x74, 0x42, 0x08, 0x0a, 0x06, - 0x5f, 0x70, 0x72, 0x6f, 0x78, 0x79, 0x42, 0x0a, 0x0a, 0x08, 0x5f, 0x6d, 0x69, 0x6e, 0x5f, 0x62, - 0x69, 0x64, 0x42, 0x17, 0x0a, 0x15, 0x5f, 0x62, 0x75, 0x69, 0x6c, 0x64, 0x65, 0x72, 0x5f, 0x62, - 0x6f, 0x6f, 0x73, 0x74, 0x5f, 0x66, 0x61, 0x63, 0x74, 0x6f, 0x72, 0x4a, 0x04, 0x08, 0x03, 0x10, - 0x04, 0x52, 0x06, 0x72, 0x65, 0x6c, 0x61, 0x79, 0x73, 0x22, 0xbf, 0x04, 0x0a, 0x0c, 0x42, 0x75, - 0x69, 0x6c, 0x64, 0x65, 0x72, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x12, 0x10, 0x0a, 0x03, 0x75, 0x72, - 0x6c, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x75, 0x72, 0x6c, 0x12, 0x1b, 0x0a, 0x06, - 0x70, 0x75, 0x62, 0x6b, 0x65, 0x79, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0c, 0x48, 0x00, 0x52, 0x06, - 0x70, 0x75, 0x62, 0x6b, 0x65, 0x79, 0x88, 0x01, 0x01, 0x12, 0x20, 0x0a, 0x09, 0x61, 0x75, 0x74, - 0x68, 0x5f, 0x64, 0x61, 0x74, 0x61, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0c, 0x48, 0x01, 0x52, 0x08, - 0x61, 0x75, 0x74, 0x68, 0x44, 0x61, 0x74, 0x61, 0x88, 0x01, 0x01, 0x12, 0x19, 0x0a, 0x05, 0x70, - 0x72, 0x6f, 0x78, 0x79, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x48, 0x02, 0x52, 0x05, 0x70, 0x72, - 0x6f, 0x78, 0x79, 0x88, 0x01, 0x01, 0x12, 0x63, 0x0a, 0x07, 0x6d, 0x69, 0x6e, 0x5f, 0x62, 0x69, - 0x64, 0x18, 0x05, 0x20, 0x01, 0x28, 0x04, 0x42, 0x45, 0x82, 0xb5, 0x18, 0x41, 0x67, 0x69, 0x74, + 0x6c, 0x64, 0x65, 0x72, 0x73, 0x53, 0x65, 0x74, 0x12, 0x63, 0x0a, 0x07, 0x6d, 0x69, 0x6e, 0x5f, + 0x62, 0x69, 0x64, 0x18, 0x07, 0x20, 0x01, 0x28, 0x04, 0x42, 0x45, 0x82, 0xb5, 0x18, 0x41, 0x67, + 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x4f, 0x66, 0x66, 0x63, 0x68, 0x61, + 0x69, 0x6e, 0x4c, 0x61, 0x62, 0x73, 0x2f, 0x70, 0x72, 0x79, 0x73, 0x6d, 0x2f, 0x76, 0x37, 0x2f, + 0x63, 0x6f, 0x6e, 0x73, 0x65, 0x6e, 0x73, 0x75, 0x73, 0x2d, 0x74, 0x79, 0x70, 0x65, 0x73, 0x2f, + 0x76, 0x61, 0x6c, 0x69, 0x64, 0x61, 0x74, 0x6f, 0x72, 0x2e, 0x55, 0x69, 0x6e, 0x74, 0x36, 0x34, + 0x48, 0x02, 0x52, 0x06, 0x6d, 0x69, 0x6e, 0x42, 0x69, 0x64, 0x88, 0x01, 0x01, 0x12, 0x7c, 0x0a, + 0x14, 0x62, 0x75, 0x69, 0x6c, 0x64, 0x65, 0x72, 0x5f, 0x62, 0x6f, 0x6f, 0x73, 0x74, 0x5f, 0x66, + 0x61, 0x63, 0x74, 0x6f, 0x72, 0x18, 0x08, 0x20, 0x01, 0x28, 0x04, 0x42, 0x45, 0x82, 0xb5, 0x18, + 0x41, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x4f, 0x66, 0x66, 0x63, + 0x68, 0x61, 0x69, 0x6e, 0x4c, 0x61, 0x62, 0x73, 0x2f, 0x70, 0x72, 0x79, 0x73, 0x6d, 0x2f, 0x76, + 0x37, 0x2f, 0x63, 0x6f, 0x6e, 0x73, 0x65, 0x6e, 0x73, 0x75, 0x73, 0x2d, 0x74, 0x79, 0x70, 0x65, + 0x73, 0x2f, 0x76, 0x61, 0x6c, 0x69, 0x64, 0x61, 0x74, 0x6f, 0x72, 0x2e, 0x55, 0x69, 0x6e, 0x74, + 0x36, 0x34, 0x48, 0x03, 0x52, 0x12, 0x62, 0x75, 0x69, 0x6c, 0x64, 0x65, 0x72, 0x42, 0x6f, 0x6f, + 0x73, 0x74, 0x46, 0x61, 0x63, 0x74, 0x6f, 0x72, 0x88, 0x01, 0x01, 0x42, 0x0a, 0x0a, 0x08, 0x5f, + 0x65, 0x6e, 0x61, 0x62, 0x6c, 0x65, 0x64, 0x42, 0x18, 0x0a, 0x16, 0x5f, 0x6d, 0x61, 0x78, 0x5f, + 0x65, 0x78, 0x65, 0x63, 0x75, 0x74, 0x69, 0x6f, 0x6e, 0x5f, 0x70, 0x61, 0x79, 0x6d, 0x65, 0x6e, + 0x74, 0x42, 0x0a, 0x0a, 0x08, 0x5f, 0x6d, 0x69, 0x6e, 0x5f, 0x62, 0x69, 0x64, 0x42, 0x17, 0x0a, + 0x15, 0x5f, 0x62, 0x75, 0x69, 0x6c, 0x64, 0x65, 0x72, 0x5f, 0x62, 0x6f, 0x6f, 0x73, 0x74, 0x5f, + 0x66, 0x61, 0x63, 0x74, 0x6f, 0x72, 0x4a, 0x04, 0x08, 0x03, 0x10, 0x04, 0x52, 0x06, 0x72, 0x65, + 0x6c, 0x61, 0x79, 0x73, 0x22, 0x9a, 0x04, 0x0a, 0x0c, 0x42, 0x75, 0x69, 0x6c, 0x64, 0x65, 0x72, + 0x45, 0x6e, 0x74, 0x72, 0x79, 0x12, 0x10, 0x0a, 0x03, 0x75, 0x72, 0x6c, 0x18, 0x01, 0x20, 0x01, + 0x28, 0x09, 0x52, 0x03, 0x75, 0x72, 0x6c, 0x12, 0x1b, 0x0a, 0x06, 0x70, 0x75, 0x62, 0x6b, 0x65, + 0x79, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0c, 0x48, 0x00, 0x52, 0x06, 0x70, 0x75, 0x62, 0x6b, 0x65, + 0x79, 0x88, 0x01, 0x01, 0x12, 0x20, 0x0a, 0x09, 0x61, 0x75, 0x74, 0x68, 0x5f, 0x64, 0x61, 0x74, + 0x61, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0c, 0x48, 0x01, 0x52, 0x08, 0x61, 0x75, 0x74, 0x68, 0x44, + 0x61, 0x74, 0x61, 0x88, 0x01, 0x01, 0x12, 0x63, 0x0a, 0x07, 0x6d, 0x69, 0x6e, 0x5f, 0x62, 0x69, + 0x64, 0x18, 0x04, 0x20, 0x01, 0x28, 0x04, 0x42, 0x45, 0x82, 0xb5, 0x18, 0x41, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x4f, 0x66, 0x66, 0x63, 0x68, 0x61, 0x69, 0x6e, 0x4c, 0x61, 0x62, 0x73, 0x2f, 0x70, 0x72, 0x79, 0x73, 0x6d, 0x2f, 0x76, 0x37, 0x2f, 0x63, 0x6f, 0x6e, 0x73, 0x65, 0x6e, 0x73, 0x75, 0x73, 0x2d, 0x74, 0x79, 0x70, 0x65, 0x73, 0x2f, 0x76, 0x61, - 0x6c, 0x69, 0x64, 0x61, 0x74, 0x6f, 0x72, 0x2e, 0x55, 0x69, 0x6e, 0x74, 0x36, 0x34, 0x48, 0x03, + 0x6c, 0x69, 0x64, 0x61, 0x74, 0x6f, 0x72, 0x2e, 0x55, 0x69, 0x6e, 0x74, 0x36, 0x34, 0x48, 0x02, 0x52, 0x06, 0x6d, 0x69, 0x6e, 0x42, 0x69, 0x64, 0x88, 0x01, 0x01, 0x12, 0x7e, 0x0a, 0x15, 0x6d, 0x61, 0x78, 0x5f, 0x65, 0x78, 0x65, 0x63, 0x75, 0x74, 0x69, 0x6f, 0x6e, 0x5f, 0x70, 0x61, 0x79, - 0x6d, 0x65, 0x6e, 0x74, 0x18, 0x06, 0x20, 0x01, 0x28, 0x04, 0x42, 0x45, 0x82, 0xb5, 0x18, 0x41, + 0x6d, 0x65, 0x6e, 0x74, 0x18, 0x05, 0x20, 0x01, 0x28, 0x04, 0x42, 0x45, 0x82, 0xb5, 0x18, 0x41, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x4f, 0x66, 0x66, 0x63, 0x68, 0x61, 0x69, 0x6e, 0x4c, 0x61, 0x62, 0x73, 0x2f, 0x70, 0x72, 0x79, 0x73, 0x6d, 0x2f, 0x76, 0x37, 0x2f, 0x63, 0x6f, 0x6e, 0x73, 0x65, 0x6e, 0x73, 0x75, 0x73, 0x2d, 0x74, 0x79, 0x70, 0x65, 0x73, 0x2f, 0x76, 0x61, 0x6c, 0x69, 0x64, 0x61, 0x74, 0x6f, 0x72, 0x2e, 0x55, 0x69, 0x6e, 0x74, 0x36, - 0x34, 0x48, 0x04, 0x52, 0x13, 0x6d, 0x61, 0x78, 0x45, 0x78, 0x65, 0x63, 0x75, 0x74, 0x69, 0x6f, + 0x34, 0x48, 0x03, 0x52, 0x13, 0x6d, 0x61, 0x78, 0x45, 0x78, 0x65, 0x63, 0x75, 0x74, 0x69, 0x6f, 0x6e, 0x50, 0x61, 0x79, 0x6d, 0x65, 0x6e, 0x74, 0x88, 0x01, 0x01, 0x12, 0x7c, 0x0a, 0x14, 0x62, 0x75, 0x69, 0x6c, 0x64, 0x65, 0x72, 0x5f, 0x62, 0x6f, 0x6f, 0x73, 0x74, 0x5f, 0x66, 0x61, 0x63, - 0x74, 0x6f, 0x72, 0x18, 0x07, 0x20, 0x01, 0x28, 0x04, 0x42, 0x45, 0x82, 0xb5, 0x18, 0x41, 0x67, + 0x74, 0x6f, 0x72, 0x18, 0x06, 0x20, 0x01, 0x28, 0x04, 0x42, 0x45, 0x82, 0xb5, 0x18, 0x41, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x4f, 0x66, 0x66, 0x63, 0x68, 0x61, 0x69, 0x6e, 0x4c, 0x61, 0x62, 0x73, 0x2f, 0x70, 0x72, 0x79, 0x73, 0x6d, 0x2f, 0x76, 0x37, 0x2f, 0x63, 0x6f, 0x6e, 0x73, 0x65, 0x6e, 0x73, 0x75, 0x73, 0x2d, 0x74, 0x79, 0x70, 0x65, 0x73, 0x2f, 0x76, 0x61, 0x6c, 0x69, 0x64, 0x61, 0x74, 0x6f, 0x72, 0x2e, 0x55, 0x69, 0x6e, 0x74, 0x36, 0x34, - 0x48, 0x05, 0x52, 0x12, 0x62, 0x75, 0x69, 0x6c, 0x64, 0x65, 0x72, 0x42, 0x6f, 0x6f, 0x73, 0x74, + 0x48, 0x04, 0x52, 0x12, 0x62, 0x75, 0x69, 0x6c, 0x64, 0x65, 0x72, 0x42, 0x6f, 0x6f, 0x73, 0x74, 0x46, 0x61, 0x63, 0x74, 0x6f, 0x72, 0x88, 0x01, 0x01, 0x42, 0x09, 0x0a, 0x07, 0x5f, 0x70, 0x75, 0x62, 0x6b, 0x65, 0x79, 0x42, 0x0c, 0x0a, 0x0a, 0x5f, 0x61, 0x75, 0x74, 0x68, 0x5f, 0x64, 0x61, - 0x74, 0x61, 0x42, 0x08, 0x0a, 0x06, 0x5f, 0x70, 0x72, 0x6f, 0x78, 0x79, 0x42, 0x0a, 0x0a, 0x08, - 0x5f, 0x6d, 0x69, 0x6e, 0x5f, 0x62, 0x69, 0x64, 0x42, 0x18, 0x0a, 0x16, 0x5f, 0x6d, 0x61, 0x78, - 0x5f, 0x65, 0x78, 0x65, 0x63, 0x75, 0x74, 0x69, 0x6f, 0x6e, 0x5f, 0x70, 0x61, 0x79, 0x6d, 0x65, - 0x6e, 0x74, 0x42, 0x17, 0x0a, 0x15, 0x5f, 0x62, 0x75, 0x69, 0x6c, 0x64, 0x65, 0x72, 0x5f, 0x62, - 0x6f, 0x6f, 0x73, 0x74, 0x5f, 0x66, 0x61, 0x63, 0x74, 0x6f, 0x72, 0x22, 0x81, 0x03, 0x0a, 0x17, - 0x50, 0x72, 0x6f, 0x70, 0x6f, 0x73, 0x65, 0x72, 0x53, 0x65, 0x74, 0x74, 0x69, 0x6e, 0x67, 0x73, - 0x50, 0x61, 0x79, 0x6c, 0x6f, 0x61, 0x64, 0x12, 0x74, 0x0a, 0x0f, 0x70, 0x72, 0x6f, 0x70, 0x6f, - 0x73, 0x65, 0x72, 0x5f, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, - 0x32, 0x4b, 0x2e, 0x65, 0x74, 0x68, 0x65, 0x72, 0x65, 0x75, 0x6d, 0x2e, 0x76, 0x61, 0x6c, 0x69, - 0x64, 0x61, 0x74, 0x6f, 0x72, 0x2e, 0x61, 0x63, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x73, 0x2e, 0x76, - 0x32, 0x2e, 0x50, 0x72, 0x6f, 0x70, 0x6f, 0x73, 0x65, 0x72, 0x53, 0x65, 0x74, 0x74, 0x69, 0x6e, - 0x67, 0x73, 0x50, 0x61, 0x79, 0x6c, 0x6f, 0x61, 0x64, 0x2e, 0x50, 0x72, 0x6f, 0x70, 0x6f, 0x73, - 0x65, 0x72, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x52, 0x0e, 0x70, - 0x72, 0x6f, 0x70, 0x6f, 0x73, 0x65, 0x72, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x5c, 0x0a, - 0x0e, 0x64, 0x65, 0x66, 0x61, 0x75, 0x6c, 0x74, 0x5f, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x18, - 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x35, 0x2e, 0x65, 0x74, 0x68, 0x65, 0x72, 0x65, 0x75, 0x6d, - 0x2e, 0x76, 0x61, 0x6c, 0x69, 0x64, 0x61, 0x74, 0x6f, 0x72, 0x2e, 0x61, 0x63, 0x63, 0x6f, 0x75, - 0x6e, 0x74, 0x73, 0x2e, 0x76, 0x32, 0x2e, 0x50, 0x72, 0x6f, 0x70, 0x6f, 0x73, 0x65, 0x72, 0x4f, - 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x50, 0x61, 0x79, 0x6c, 0x6f, 0x61, 0x64, 0x52, 0x0d, 0x64, 0x65, - 0x66, 0x61, 0x75, 0x6c, 0x74, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x18, 0x0a, 0x07, 0x76, - 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x07, 0x76, 0x65, - 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x1a, 0x78, 0x0a, 0x13, 0x50, 0x72, 0x6f, 0x70, 0x6f, 0x73, 0x65, - 0x72, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x12, 0x10, 0x0a, 0x03, - 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x4b, - 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x35, 0x2e, - 0x65, 0x74, 0x68, 0x65, 0x72, 0x65, 0x75, 0x6d, 0x2e, 0x76, 0x61, 0x6c, 0x69, 0x64, 0x61, 0x74, - 0x6f, 0x72, 0x2e, 0x61, 0x63, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x73, 0x2e, 0x76, 0x32, 0x2e, 0x50, - 0x72, 0x6f, 0x70, 0x6f, 0x73, 0x65, 0x72, 0x4f, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x50, 0x61, 0x79, - 0x6c, 0x6f, 0x61, 0x64, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x3a, 0x02, 0x38, 0x01, 0x42, - 0xcd, 0x01, 0x0a, 0x22, 0x6f, 0x72, 0x67, 0x2e, 0x65, 0x74, 0x68, 0x65, 0x72, 0x65, 0x75, 0x6d, - 0x2e, 0x76, 0x61, 0x6c, 0x69, 0x64, 0x61, 0x74, 0x6f, 0x72, 0x2e, 0x61, 0x63, 0x63, 0x6f, 0x75, - 0x6e, 0x74, 0x73, 0x2e, 0x76, 0x32, 0x42, 0x0f, 0x4b, 0x65, 0x79, 0x6d, 0x61, 0x6e, 0x61, 0x67, - 0x65, 0x72, 0x50, 0x72, 0x6f, 0x74, 0x6f, 0x50, 0x01, 0x5a, 0x52, 0x67, 0x69, 0x74, 0x68, 0x75, - 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x4f, 0x66, 0x66, 0x63, 0x68, 0x61, 0x69, 0x6e, 0x4c, 0x61, - 0x62, 0x73, 0x2f, 0x70, 0x72, 0x79, 0x73, 0x6d, 0x2f, 0x76, 0x37, 0x2f, 0x70, 0x72, 0x6f, 0x74, - 0x6f, 0x2f, 0x70, 0x72, 0x79, 0x73, 0x6d, 0x2f, 0x76, 0x31, 0x61, 0x6c, 0x70, 0x68, 0x61, 0x31, - 0x2f, 0x76, 0x61, 0x6c, 0x69, 0x64, 0x61, 0x74, 0x6f, 0x72, 0x2d, 0x63, 0x6c, 0x69, 0x65, 0x6e, - 0x74, 0x3b, 0x76, 0x61, 0x6c, 0x69, 0x64, 0x61, 0x74, 0x6f, 0x72, 0x70, 0x62, 0xaa, 0x02, 0x1e, - 0x45, 0x74, 0x68, 0x65, 0x72, 0x65, 0x75, 0x6d, 0x2e, 0x56, 0x61, 0x6c, 0x69, 0x64, 0x61, 0x74, - 0x6f, 0x72, 0x2e, 0x41, 0x63, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x73, 0x2e, 0x56, 0x32, 0xca, 0x02, - 0x1e, 0x45, 0x74, 0x68, 0x65, 0x72, 0x65, 0x75, 0x6d, 0x5c, 0x56, 0x61, 0x6c, 0x69, 0x64, 0x61, - 0x74, 0x6f, 0x72, 0x5c, 0x41, 0x63, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x73, 0x5c, 0x56, 0x32, 0x62, - 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, + 0x74, 0x61, 0x42, 0x0a, 0x0a, 0x08, 0x5f, 0x6d, 0x69, 0x6e, 0x5f, 0x62, 0x69, 0x64, 0x42, 0x18, + 0x0a, 0x16, 0x5f, 0x6d, 0x61, 0x78, 0x5f, 0x65, 0x78, 0x65, 0x63, 0x75, 0x74, 0x69, 0x6f, 0x6e, + 0x5f, 0x70, 0x61, 0x79, 0x6d, 0x65, 0x6e, 0x74, 0x42, 0x17, 0x0a, 0x15, 0x5f, 0x62, 0x75, 0x69, + 0x6c, 0x64, 0x65, 0x72, 0x5f, 0x62, 0x6f, 0x6f, 0x73, 0x74, 0x5f, 0x66, 0x61, 0x63, 0x74, 0x6f, + 0x72, 0x22, 0x81, 0x03, 0x0a, 0x17, 0x50, 0x72, 0x6f, 0x70, 0x6f, 0x73, 0x65, 0x72, 0x53, 0x65, + 0x74, 0x74, 0x69, 0x6e, 0x67, 0x73, 0x50, 0x61, 0x79, 0x6c, 0x6f, 0x61, 0x64, 0x12, 0x74, 0x0a, + 0x0f, 0x70, 0x72, 0x6f, 0x70, 0x6f, 0x73, 0x65, 0x72, 0x5f, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, + 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x4b, 0x2e, 0x65, 0x74, 0x68, 0x65, 0x72, 0x65, 0x75, + 0x6d, 0x2e, 0x76, 0x61, 0x6c, 0x69, 0x64, 0x61, 0x74, 0x6f, 0x72, 0x2e, 0x61, 0x63, 0x63, 0x6f, + 0x75, 0x6e, 0x74, 0x73, 0x2e, 0x76, 0x32, 0x2e, 0x50, 0x72, 0x6f, 0x70, 0x6f, 0x73, 0x65, 0x72, + 0x53, 0x65, 0x74, 0x74, 0x69, 0x6e, 0x67, 0x73, 0x50, 0x61, 0x79, 0x6c, 0x6f, 0x61, 0x64, 0x2e, + 0x50, 0x72, 0x6f, 0x70, 0x6f, 0x73, 0x65, 0x72, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x45, 0x6e, + 0x74, 0x72, 0x79, 0x52, 0x0e, 0x70, 0x72, 0x6f, 0x70, 0x6f, 0x73, 0x65, 0x72, 0x43, 0x6f, 0x6e, + 0x66, 0x69, 0x67, 0x12, 0x5c, 0x0a, 0x0e, 0x64, 0x65, 0x66, 0x61, 0x75, 0x6c, 0x74, 0x5f, 0x63, + 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x35, 0x2e, 0x65, 0x74, + 0x68, 0x65, 0x72, 0x65, 0x75, 0x6d, 0x2e, 0x76, 0x61, 0x6c, 0x69, 0x64, 0x61, 0x74, 0x6f, 0x72, + 0x2e, 0x61, 0x63, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x73, 0x2e, 0x76, 0x32, 0x2e, 0x50, 0x72, 0x6f, + 0x70, 0x6f, 0x73, 0x65, 0x72, 0x4f, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x50, 0x61, 0x79, 0x6c, 0x6f, + 0x61, 0x64, 0x52, 0x0d, 0x64, 0x65, 0x66, 0x61, 0x75, 0x6c, 0x74, 0x43, 0x6f, 0x6e, 0x66, 0x69, + 0x67, 0x12, 0x18, 0x0a, 0x07, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x18, 0x03, 0x20, 0x01, + 0x28, 0x0d, 0x52, 0x07, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x1a, 0x78, 0x0a, 0x13, 0x50, + 0x72, 0x6f, 0x70, 0x6f, 0x73, 0x65, 0x72, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x45, 0x6e, 0x74, + 0x72, 0x79, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, + 0x03, 0x6b, 0x65, 0x79, 0x12, 0x4b, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, + 0x01, 0x28, 0x0b, 0x32, 0x35, 0x2e, 0x65, 0x74, 0x68, 0x65, 0x72, 0x65, 0x75, 0x6d, 0x2e, 0x76, + 0x61, 0x6c, 0x69, 0x64, 0x61, 0x74, 0x6f, 0x72, 0x2e, 0x61, 0x63, 0x63, 0x6f, 0x75, 0x6e, 0x74, + 0x73, 0x2e, 0x76, 0x32, 0x2e, 0x50, 0x72, 0x6f, 0x70, 0x6f, 0x73, 0x65, 0x72, 0x4f, 0x70, 0x74, + 0x69, 0x6f, 0x6e, 0x50, 0x61, 0x79, 0x6c, 0x6f, 0x61, 0x64, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, + 0x65, 0x3a, 0x02, 0x38, 0x01, 0x42, 0xcd, 0x01, 0x0a, 0x22, 0x6f, 0x72, 0x67, 0x2e, 0x65, 0x74, + 0x68, 0x65, 0x72, 0x65, 0x75, 0x6d, 0x2e, 0x76, 0x61, 0x6c, 0x69, 0x64, 0x61, 0x74, 0x6f, 0x72, + 0x2e, 0x61, 0x63, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x73, 0x2e, 0x76, 0x32, 0x42, 0x0f, 0x4b, 0x65, + 0x79, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x72, 0x50, 0x72, 0x6f, 0x74, 0x6f, 0x50, 0x01, 0x5a, + 0x52, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x4f, 0x66, 0x66, 0x63, + 0x68, 0x61, 0x69, 0x6e, 0x4c, 0x61, 0x62, 0x73, 0x2f, 0x70, 0x72, 0x79, 0x73, 0x6d, 0x2f, 0x76, + 0x37, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2f, 0x70, 0x72, 0x79, 0x73, 0x6d, 0x2f, 0x76, 0x31, + 0x61, 0x6c, 0x70, 0x68, 0x61, 0x31, 0x2f, 0x76, 0x61, 0x6c, 0x69, 0x64, 0x61, 0x74, 0x6f, 0x72, + 0x2d, 0x63, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x3b, 0x76, 0x61, 0x6c, 0x69, 0x64, 0x61, 0x74, 0x6f, + 0x72, 0x70, 0x62, 0xaa, 0x02, 0x1e, 0x45, 0x74, 0x68, 0x65, 0x72, 0x65, 0x75, 0x6d, 0x2e, 0x56, + 0x61, 0x6c, 0x69, 0x64, 0x61, 0x74, 0x6f, 0x72, 0x2e, 0x41, 0x63, 0x63, 0x6f, 0x75, 0x6e, 0x74, + 0x73, 0x2e, 0x56, 0x32, 0xca, 0x02, 0x1e, 0x45, 0x74, 0x68, 0x65, 0x72, 0x65, 0x75, 0x6d, 0x5c, + 0x56, 0x61, 0x6c, 0x69, 0x64, 0x61, 0x74, 0x6f, 0x72, 0x5c, 0x41, 0x63, 0x63, 0x6f, 0x75, 0x6e, + 0x74, 0x73, 0x5c, 0x56, 0x32, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, } var ( diff --git a/proto/prysm/v1alpha1/validator-client/keymanager.proto b/proto/prysm/v1alpha1/validator-client/keymanager.proto index ebe7da3bab7d..4e97d27482de 100644 --- a/proto/prysm/v1alpha1/validator-client/keymanager.proto +++ b/proto/prysm/v1alpha1/validator-client/keymanager.proto @@ -149,12 +149,11 @@ message BuilderConfig { // builders_set preserves an explicitly configured empty builders list // ("use no builders"), which proto3 cannot distinguish from absent. bool builders_set = 6; - optional string proxy = 7; - optional uint64 min_bid = 8 [ + optional uint64 min_bid = 7 [ (ethereum.eth.ext.cast_type) = "github.com/OffchainLabs/prysm/v7/consensus-types/validator.Uint64" ]; - optional uint64 builder_boost_factor = 9 [ + optional uint64 builder_boost_factor = 8 [ (ethereum.eth.ext.cast_type) = "github.com/OffchainLabs/prysm/v7/consensus-types/validator.Uint64" ]; @@ -166,16 +165,15 @@ message BuilderEntry { string url = 1; optional bytes pubkey = 2; optional bytes auth_data = 3; - optional string proxy = 4; - optional uint64 min_bid = 5 [ + optional uint64 min_bid = 4 [ (ethereum.eth.ext.cast_type) = "github.com/OffchainLabs/prysm/v7/consensus-types/validator.Uint64" ]; - optional uint64 max_execution_payment = 6 [ + optional uint64 max_execution_payment = 5 [ (ethereum.eth.ext.cast_type) = "github.com/OffchainLabs/prysm/v7/consensus-types/validator.Uint64" ]; - optional uint64 builder_boost_factor = 7 [ + optional uint64 builder_boost_factor = 6 [ (ethereum.eth.ext.cast_type) = "github.com/OffchainLabs/prysm/v7/consensus-types/validator.Uint64" ]; From e68f2fe53b0e5cba4c56ed467dab2afd7c6f00f9 Mon Sep 17 00:00:00 2001 From: james-prysm Date: Tue, 11 Aug 2026 08:47:34 -0500 Subject: [PATCH 15/26] remove enabled tag and use builders tag instead --- ...mes-prysm_keymanager-builders-endpoints.md | 3 +- cmd/prysmctl/validator/proposer_settings.go | 3 +- config/proposer/loader/loader.go | 42 ++- config/proposer/loader/loader_test.go | 66 ++--- .../testdata/good-v2-proposer-config.json | 3 +- config/proposer/settings.go | 44 ++-- config/proposer/settings_test.go | 62 +++-- .../validator-client/keymanager.pb.go | 240 +++++++++--------- .../validator-client/keymanager.proto | 6 +- validator/client/validator.go | 18 +- validator/client/validator_test.go | 17 +- validator/rpc/handlers_validator_config.go | 8 +- .../rpc/handlers_validator_config_test.go | 78 +++--- validator/rpc/structs.go | 43 ++-- 14 files changed, 322 insertions(+), 311 deletions(-) diff --git a/changelog/james-prysm_keymanager-builders-endpoints.md b/changelog/james-prysm_keymanager-builders-endpoints.md index d3c69f6e99a9..a0d824667594 100644 --- a/changelog/james-prysm_keymanager-builders-endpoints.md +++ b/changelog/james-prysm_keymanager-builders-endpoints.md @@ -4,7 +4,8 @@ ### Changed -- Add v2 proposer settings (`"version": 2`): builder fields a key does not set inherit from `default_config`. `--enable-builder` still forces the default builder on; a key with explicit `enabled: false` stays opted out. +- Add v2 proposer settings (`"version": 2`): builder fields a key does not set inherit from `default_config`. v2 has no `enabled` field; a key participates in builder registration when its resolved `builders` list names at least one builder, and an explicit empty `builders` list opts the key out. +- v1 builder settings are not migrated to v2: at the gloas fork fee recipients, gas limits, and graffiti carry over and v1 builder content is replaced with defaults, with a warning. `--enable-builder` has no effect with v2 settings. - Setting a fee recipient, gas limit, or graffiti no longer snapshots `default_config`'s builder settings onto that key; the key keeps following the default as it changes. ### Fixed diff --git a/cmd/prysmctl/validator/proposer_settings.go b/cmd/prysmctl/validator/proposer_settings.go index 8498f9f8c847..0a97481eb9a5 100644 --- a/cmd/prysmctl/validator/proposer_settings.go +++ b/cmd/prysmctl/validator/proposer_settings.go @@ -69,9 +69,8 @@ func getProposerSettings(c *cli.Context, r io.Reader) error { log.Infof("The default fee recipient is set to %s", defaultFeeRecipient) var builderSettings *validatorpb.BuilderConfig if c.Bool(WithBuilderFlag.Name) { - enabled := true builderSettings = &validatorpb.BuilderConfig{ - Enabled: &enabled, + Enabled: true, GasLimit: validatorType.Uint64(params.BeaconConfig().DefaultBuilderGasLimit), } } else { diff --git a/config/proposer/loader/loader.go b/config/proposer/loader/loader.go index f52a6a80986f..e15fb9e8de5d 100644 --- a/config/proposer/loader/loader.go +++ b/config/proposer/loader/loader.go @@ -214,6 +214,7 @@ func (psl *SettingsLoader) loadFromFile(cliCtx *cli.Context, dbSettings *validat if settingFromFile == nil { return nil, errors.Errorf("proposer settings is empty after unmarshalling from file specified by %s flag", flags.ProposerSettingsFlag.Name) } + markExplicitEmptyBuilders(settingFromFile) log.WithField(flags.ProposerSettingsFlag.Name, cliCtx.String(flags.ProposerSettingsFlag.Name)).Info("Proposer settings loaded from file") return psl.processProposerSettings(settingFromFile, dbSettings), nil } @@ -226,6 +227,7 @@ func (psl *SettingsLoader) loadFromURL(cliCtx *cli.Context, dbSettings *validato if settingFromURL == nil { return nil, errors.Errorf("proposer settings is empty after unmarshalling from url specified by %s flag", flags.ProposerSettingsURLFlag.Name) } + markExplicitEmptyBuilders(settingFromURL) log.WithField(flags.ProposerSettingsURLFlag.Name, cliCtx.String(flags.ProposerSettingsURLFlag.Name)).Infof("Proposer settings loaded from URL") return psl.processProposerSettings(settingFromURL, dbSettings), nil } @@ -283,9 +285,27 @@ func mergeProposerSettings(loaded, db *validatorpb.ProposerSettingsPayload, opti return mergeProposerSettingsV1(merged, loaded, db, builderConfig, gasLimitOnly) } -// promotePayloadToV2 mirrors Settings.UpgradeToV2 plus legacy presence -// normalization, so v1 content merged into a v2 result reads correctly. +// markExplicitEmptyBuilders stamps the persistence marker for a user source's +// explicit "builders": [] (opt-out), which yaml keeps distinct from absent. +func markExplicitEmptyBuilders(p *validatorpb.ProposerSettingsPayload) { + mark := func(opt *validatorpb.ProposerOptionPayload) { + if opt == nil || opt.Builder == nil { + return + } + if opt.Builder.Builders != nil { + opt.Builder.BuildersSet = true + } + } + mark(p.DefaultConfig) + for _, opt := range p.ProposerConfig { + mark(opt) + } +} + +// promotePayloadToV2 mirrors Settings.UpgradeToV2: gas limits are promoted to +// the option level and v1 builder content, which does not apply to v2, is dropped. func promotePayloadToV2(p *validatorpb.ProposerSettingsPayload) { + dropped := false promote := func(opt *validatorpb.ProposerOptionPayload) { if opt == nil || opt.Builder == nil { return @@ -293,11 +313,16 @@ func promotePayloadToV2(p *validatorpb.ProposerSettingsPayload) { if opt.GasLimit == 0 { opt.GasLimit = opt.Builder.GasLimit } + opt.Builder = nil + dropped = true } promote(p.DefaultConfig) for _, opt := range p.ProposerConfig { promote(opt) } + if dropped { + log.Warn("v1 builder settings do not apply to the v2 schema and were replaced with defaults; provide v2 proposer settings to configure builders") + } } // selectProposerConfig keeps the pre-v2 source precedence: a loaded per-key @@ -363,17 +388,10 @@ func mergeProposerSettingsV2(merged, loaded, db *validatorpb.ProposerSettingsPay } merged.ProposerConfig = selectProposerConfig(db, loaded) - // --enable-builder forces the default toggle on, matching v1; per-key - // enabled is untouched so a single key can still opt out (keymanager #88). + // v2 has no enabled toggle: participation follows the configured builders + // list, so --enable-builder has nothing to force on. if builderConfig != nil { - if merged.DefaultConfig == nil { - merged.DefaultConfig = &validatorpb.ProposerOptionPayload{} - } - if merged.DefaultConfig.Builder == nil { - merged.DefaultConfig.Builder = &validatorpb.BuilderConfig{} - } - enabled := true - merged.DefaultConfig.Builder.Enabled = &enabled + log.Warnf("--%s has no effect with v2 proposer settings; configure builders via the settings source or keymanager API", flags.EnableBuilderFlag.Name) } if gasLimitOnly == nil { diff --git a/config/proposer/loader/loader_test.go b/config/proposer/loader/loader_test.go index 73a56a74b03f..69f17b5a8efc 100644 --- a/config/proposer/loader/loader_test.go +++ b/config/proposer/loader/loader_test.go @@ -12,7 +12,6 @@ import ( "github.com/ethereum/go-ethereum/common/hexutil" logtest "github.com/sirupsen/logrus/hooks/test" "github.com/urfave/cli/v2" - "google.golang.org/protobuf/proto" "github.com/OffchainLabs/prysm/v7/cmd/validator/flags" fieldparams "github.com/OffchainLabs/prysm/v7/config/fieldparams" @@ -426,8 +425,7 @@ func TestProposerSettingsLoader(t *testing.T) { }, GasLimit: 40000000, BuilderConfig: &proposer.BuilderConfig{ - Enabled: true, - MinBid: u64(500000000), + MinBid: u64(500000000), Builders: []*proposer.BuilderEntry{ {URL: "https://builder-a.example", MaxExecutionPayment: u64(1000000000)}, {URL: "https://builder-b.example"}, @@ -440,7 +438,7 @@ func TestProposerSettingsLoader(t *testing.T) { FeeRecipient: common.HexToAddress("0x6e35733c5af9B61374A128e6F85f553aF09ff89A"), }, GasLimit: 30000000, - BuilderConfig: &proposer.BuilderConfig{Enabled: false}, + BuilderConfig: &proposer.BuilderConfig{Builders: []*proposer.BuilderEntry{}}, }, } }, @@ -1225,15 +1223,15 @@ func Test_mergeProposerSettings_VersionPrecedence(t *testing.T) { merged := mergeProposerSettings( &validatorpb.ProposerSettingsPayload{ DefaultConfig: &validatorpb.ProposerOptionPayload{ - Builder: &validatorpb.BuilderConfig{Enabled: proto.Bool(true), GasLimit: 30000000}, + Builder: &validatorpb.BuilderConfig{Enabled: true, GasLimit: 30000000}, }, }, &validatorpb.ProposerSettingsPayload{Version: proposer.SchemaV2}, &flagOptions{}, ) require.Equal(t, uint32(proposer.SchemaV2), merged.Version) - require.NotNil(t, merged.DefaultConfig.Builder) - // The builder gas limit is promoted so v2 reads see it at the top level. + // v1 builder content does not carry into v2; the gas limit is promoted first. + require.IsNil(t, merged.DefaultConfig.Builder) require.Equal(t, validator.Uint64(30000000), merged.DefaultConfig.GasLimit) }) t.Run("file per-key section replaces the DB's entirely", func(t *testing.T) { @@ -1302,8 +1300,9 @@ func TestSettingsLoader_V1FileAfterMigratedDB(t *testing.T) { require.NotNil(t, got) require.Equal(t, proposer.SchemaV2, got.Version) - require.NotNil(t, got.DefaultConfig.BuilderConfig) - require.Equal(t, validator.Uint64(40000000), got.DefaultConfig.BuilderConfig.GasLimit) + // The v1 file's builder content is dropped; its gas limit is promoted first. + require.IsNil(t, got.DefaultConfig.BuilderConfig) + require.Equal(t, validator.Uint64(40000000), got.DefaultConfig.GasLimit) assert.LogsDoNotContain(t, hook, "deprecated v1 schema") // Already v2: the file's builder gas limits were promoted at merge time. @@ -1340,7 +1339,7 @@ func Test_mergeProposerSettings_V2GasLimitOnlyGoesToOption(t *testing.T) { func Test_mergeProposerSettings_VersionGatesBuilderReset(t *testing.T) { v1Builder := func() *validatorpb.BuilderConfig { - return &validatorpb.BuilderConfig{Enabled: proto.Bool(true), GasLimit: 40000000} + return &validatorpb.BuilderConfig{Enabled: true, GasLimit: 40000000} } t.Run("v1 db without enable-builder drops DB builder", func(t *testing.T) { db := &validatorpb.ProposerSettingsPayload{ @@ -1359,49 +1358,18 @@ func Test_mergeProposerSettings_VersionGatesBuilderReset(t *testing.T) { require.NotNil(t, merged.DefaultConfig.Builder) require.Equal(t, validator.Uint64(40000000), merged.DefaultConfig.Builder.GasLimit) }) - t.Run("v2 --enable-builder forces the default toggle on", func(t *testing.T) { + t.Run("v2 --enable-builder has no effect and warns", func(t *testing.T) { + hook := logtest.NewGlobal() opts := &flagOptions{builderConfig: &proposer.BuilderConfig{Enabled: true}} - - // No default builder: the flag creates it enabled. db := &validatorpb.ProposerSettingsPayload{ Version: proposer.SchemaV2, DefaultConfig: &validatorpb.ProposerOptionPayload{FeeRecipient: "0x"}, } merged := mergeProposerSettings(nil, db, opts) - require.NotNil(t, merged.DefaultConfig.Builder) - require.NotNil(t, merged.DefaultConfig.Builder.Enabled) - require.Equal(t, true, *merged.DefaultConfig.Builder.Enabled) - - // An explicit default enabled=false IS overridden by the flag, matching v1. - disabled := false - db2 := &validatorpb.ProposerSettingsPayload{ - Version: proposer.SchemaV2, - DefaultConfig: &validatorpb.ProposerOptionPayload{ - FeeRecipient: "0x", - Builder: &validatorpb.BuilderConfig{Enabled: &disabled}, - }, - } - merged2 := mergeProposerSettings(nil, db2, opts) - require.NotNil(t, merged2.DefaultConfig.Builder.Enabled) - require.Equal(t, true, *merged2.DefaultConfig.Builder.Enabled) - - // Per-key entries are untouched: explicit enabled=false still opts the key out. - keyDisabled := false - db3 := &validatorpb.ProposerSettingsPayload{ - Version: proposer.SchemaV2, - DefaultConfig: &validatorpb.ProposerOptionPayload{FeeRecipient: "0x"}, - ProposerConfig: map[string]*validatorpb.ProposerOptionPayload{ - "0xkey": {FeeRecipient: "0xk"}, - "0xkey2": {FeeRecipient: "0xk2", Builder: &validatorpb.BuilderConfig{Enabled: &keyDisabled}}, - }, - } - merged3 := mergeProposerSettings(nil, db3, opts) - require.IsNil(t, merged3.ProposerConfig["0xkey"].Builder) - require.Equal(t, false, *merged3.ProposerConfig["0xkey2"].Builder.Enabled) + require.IsNil(t, merged.DefaultConfig.Builder) + assert.LogsContain(t, hook, "has no effect with v2 proposer settings") }) - t.Run("v1-promoted default under --enable-builder stays enabled, matching v1", func(t *testing.T) { - opts := &flagOptions{builderConfig: &proposer.BuilderConfig{Enabled: true}} - // v1 file with a builder block and no enabled key: the v1 flag turned it on. + t.Run("v1 builder content merged into v2 is dropped with gas limit promoted", func(t *testing.T) { file := &validatorpb.ProposerSettingsPayload{ DefaultConfig: &validatorpb.ProposerOptionPayload{ FeeRecipient: "0x", @@ -1409,9 +1377,9 @@ func Test_mergeProposerSettings_VersionGatesBuilderReset(t *testing.T) { }, } db := &validatorpb.ProposerSettingsPayload{Version: proposer.SchemaV2} - merged := mergeProposerSettings(file, db, opts) - require.NotNil(t, merged.DefaultConfig.Builder.Enabled) - require.Equal(t, true, *merged.DefaultConfig.Builder.Enabled) + merged := mergeProposerSettings(file, db, &flagOptions{}) + require.IsNil(t, merged.DefaultConfig.Builder) + require.Equal(t, validator.Uint64(30000000), merged.DefaultConfig.GasLimit) }) } diff --git a/config/proposer/loader/testdata/good-v2-proposer-config.json b/config/proposer/loader/testdata/good-v2-proposer-config.json index 06f9c133f5d9..c75ef417ec30 100644 --- a/config/proposer/loader/testdata/good-v2-proposer-config.json +++ b/config/proposer/loader/testdata/good-v2-proposer-config.json @@ -4,7 +4,6 @@ "fee_recipient": "0x50155530FCE8a85ec7055A5F8b2bE214B3DaeFd3", "gas_limit": "40000000", "builder": { - "enabled": true, "min_bid": "500000000", "builders": [ { "url": "https://builder-a.example", "max_execution_payment": "1000000000" }, @@ -18,7 +17,7 @@ "fee_recipient": "0x6e35733c5af9B61374A128e6F85f553aF09ff89A", "gas_limit": "30000000", "builder": { - "enabled": false + "builders": [] } }, "version": 2 diff --git a/config/proposer/settings.go b/config/proposer/settings.go index 5dfe4eac4c5e..04b7730b07d9 100644 --- a/config/proposer/settings.go +++ b/config/proposer/settings.go @@ -122,7 +122,7 @@ type BuilderConfig struct { // fall back to the enclosing BuilderConfig, then default_config. type BuilderEntry struct { URL string `json:"url" yaml:"url"` - Pubkey []byte `json:"pubkey,omitempty" yaml:"pubkey,omitempty"` + Pubkeys [][]byte `json:"builder_pubkeys,omitempty" yaml:"builder_pubkeys,omitempty"` AuthData []byte `json:"auth_data,omitempty" yaml:"auth_data,omitempty"` MinBid *validator.Uint64 `json:"min_bid,omitempty" yaml:"min_bid,omitempty"` MaxExecutionPayment *validator.Uint64 `json:"max_execution_payment,omitempty" yaml:"max_execution_payment,omitempty"` @@ -185,7 +185,9 @@ func (ps *Settings) RegistrationFor(pubkey [fieldparams.BLSPubkeyLength]byte) (c case bc != nil && bc.GasLimit != 0: gasLimit = bc.GasLimit } - return feeRecipient, gasLimit, bc.IsEnabled() && hasFeeRecipient + // v2 has no enabled flag: a key participates in builder registration + // exactly when its resolved builders list names at least one builder. + return feeRecipient, gasLimit, bc != nil && len(bc.Builders) > 0 && hasFeeRecipient } // v1 keeps object-level semantics: a per-key builder config wins wholesale, // so a disabled one opts the key out even under an enabled default. @@ -280,8 +282,8 @@ func (be *BuilderEntry) EffectiveMaxExecutionPayment(bc *BuilderConfig) validato return bc.EffectiveMaxExecutionPayment() } -// IsEnabled reports whether the builder path is explicitly enabled. A nil config -// or unset enabled field is treated as disabled. +// IsEnabled reports whether the legacy v1 builder path is explicitly enabled. +// v2 settings ignore the enabled field entirely. func (bc *BuilderConfig) IsEnabled() bool { return bc != nil && bc.Enabled } @@ -296,7 +298,6 @@ func effectiveBuilderConfig(perKey, def *BuilderConfig) *BuilderConfig { return perKey } eff := &BuilderConfig{ - Enabled: perKey.Enabled, GasLimit: perKey.GasLimit, MaxExecutionPayment: coalesceUint64(perKey.MaxExecutionPayment, def.MaxExecutionPayment), MinBid: coalesceUint64(perKey.MinBid, def.MinBid), @@ -326,7 +327,7 @@ func BuilderConfigFromConsensus(from *validatorpb.BuilderConfig) *BuilderConfig return nil } c := &BuilderConfig{ - Enabled: from.GetEnabled(), + Enabled: from.Enabled, GasLimit: from.GasLimit, MaxExecutionPayment: cloneUint64(from.MaxExecutionPayment), MinBid: cloneUint64(from.MinBid), @@ -355,8 +356,8 @@ func builderEntryFromConsensus(from *validatorpb.BuilderEntry) *BuilderEntry { BuilderBoostFactor: from.BuilderBoostFactor, } // Treat empty as absent so bolt (nil) and filesystem (empty) round-trips agree. - if len(from.Pubkey) != 0 { - e.Pubkey = bytesutil.SafeCopyBytes(from.Pubkey) + if len(from.Pubkeys) != 0 { + e.Pubkeys = bytesutil.SafeCopy2dBytes(from.Pubkeys) } if len(from.AuthData) != 0 { e.AuthData = bytesutil.SafeCopyBytes(from.AuthData) @@ -518,7 +519,7 @@ func (be *BuilderEntry) Clone() *BuilderEntry { } return &BuilderEntry{ URL: be.URL, - Pubkey: bytesutil.SafeCopyBytes(be.Pubkey), + Pubkeys: bytesutil.SafeCopy2dBytes(be.Pubkeys), AuthData: bytesutil.SafeCopyBytes(be.AuthData), MinBid: cloneUint64(be.MinBid), MaxExecutionPayment: cloneUint64(be.MaxExecutionPayment), @@ -548,8 +549,7 @@ func (bc *BuilderConfig) ToConsensus() *validatorpb.BuilderConfig { return nil } c := &validatorpb.BuilderConfig{} - enabled := bc.Enabled - c.Enabled = &enabled + c.Enabled = bc.Enabled c.GasLimit = bc.GasLimit c.MaxExecutionPayment = cloneUint64(bc.MaxExecutionPayment) c.MinBid = cloneUint64(bc.MinBid) @@ -572,7 +572,7 @@ func (be *BuilderEntry) toConsensus() *validatorpb.BuilderEntry { } return &validatorpb.BuilderEntry{ Url: be.URL, - Pubkey: bytesutil.SafeCopyBytes(be.Pubkey), + Pubkeys: bytesutil.SafeCopy2dBytes(be.Pubkeys), AuthData: bytesutil.SafeCopyBytes(be.AuthData), MinBid: cloneUint64(be.MinBid), MaxExecutionPayment: cloneUint64(be.MaxExecutionPayment), @@ -591,14 +591,16 @@ func (ps *Settings) WarnDeprecatedSchema() { return } // Fee recipients and graffiti behave identically across schemas; only - // builder content gives the migration something to change. - if !ps.hasBuilderContent() { + // builder content gives the cutover something to drop. + if !ps.HasBuilderContent() { return } - log.Warn("Proposer settings use the deprecated v1 schema; they are upgraded automatically at the gloas fork. Please migrate your settings source to v2.") + log.Warn("Proposer settings use the deprecated v1 schema; v1 builder settings do not apply to gloas and are replaced with defaults at the fork (fee recipients, gas limits and graffiti carry over). Please migrate your settings source to v2.") } -func (ps *Settings) hasBuilderContent() bool { +// HasBuilderContent reports whether any level carries a builder config, i.e. +// whether the v1 cutover has anything to drop. +func (ps *Settings) HasBuilderContent() bool { if ps.DefaultConfig != nil && ps.DefaultConfig.BuilderConfig != nil { return true } @@ -610,12 +612,13 @@ func (ps *Settings) hasBuilderContent() bool { return false } -// UpgradeToV2 migrates v1 settings in place: builder gas limits are promoted to -// the option level unless one is set. Returns true if anything changed. +// UpgradeToV2 is the v1 cutover: gas limits are promoted to the option level and +// v1 builder configs, which do not apply to gloas, are dropped. Returns true if changed. func (ps *Settings) UpgradeToV2() bool { if ps == nil || ps.isV2() { return false } + dropped := false migrate := func(opt *Option) { if opt == nil || opt.BuilderConfig == nil { return @@ -623,12 +626,17 @@ func (ps *Settings) UpgradeToV2() bool { if opt.GasLimit == 0 { opt.GasLimit = opt.BuilderConfig.GasLimit } + opt.BuilderConfig = nil + dropped = true } migrate(ps.DefaultConfig) for _, opt := range ps.ProposeConfig { migrate(opt) } ps.Version = SchemaV2 + if dropped { + log.Warn("v1 builder settings do not apply to gloas and were replaced with defaults; provide v2 proposer settings to configure builders") + } return true } diff --git a/config/proposer/settings_test.go b/config/proposer/settings_test.go index 0e77b07e6e62..377a8498d44a 100644 --- a/config/proposer/settings_test.go +++ b/config/proposer/settings_test.go @@ -6,7 +6,6 @@ import ( "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/common/hexutil" logtest "github.com/sirupsen/logrus/hooks/test" - "google.golang.org/protobuf/proto" fieldparams "github.com/OffchainLabs/prysm/v7/config/fieldparams" "github.com/OffchainLabs/prysm/v7/config/params" @@ -516,7 +515,7 @@ func TestSettings_UpgradeToV2(t *testing.T) { require.Equal(t, false, ps.UpgradeToV2()) }) - t.Run("v1 default lifts BuilderConfig.GasLimit to top-level and retains builder relays", func(t *testing.T) { + t.Run("v1 default lifts BuilderConfig.GasLimit to top-level and drops the builder config", func(t *testing.T) { ps := &Settings{ DefaultConfig: &Option{ BuilderConfig: &BuilderConfig{Enabled: true, GasLimit: validator.Uint64(42_000_000)}, @@ -525,8 +524,8 @@ func TestSettings_UpgradeToV2(t *testing.T) { require.Equal(t, true, ps.UpgradeToV2()) require.Equal(t, SchemaV2, ps.Version) require.Equal(t, validator.Uint64(42_000_000), ps.DefaultConfig.GasLimit) - // BuilderConfig is retained so the gloas builder-API relays/enabled survive the upgrade. - require.NotNil(t, ps.DefaultConfig.BuilderConfig) + // v1 builder opinions are about mev-boost and do not carry into v2. + require.IsNil(t, ps.DefaultConfig.BuilderConfig) }) t.Run("v1 top-level GasLimit already set is preserved", func(t *testing.T) { @@ -540,7 +539,7 @@ func TestSettings_UpgradeToV2(t *testing.T) { require.Equal(t, validator.Uint64(70_000_000), ps.DefaultConfig.GasLimit) }) - t.Run("per-validator builder gas limits promoted and builders retained", func(t *testing.T) { + t.Run("per-validator builder gas limits promoted and builder configs dropped", func(t *testing.T) { pubkey2, err := hexutil.Decode("0xbedefeaa94e03438ea819bd4033c6c1bf6b04320ee2075b77273c08d02f8a61bcc303c2cdddddddddddddddddddddddd") require.NoError(t, err) pk2 := bytesutil.ToBytes48(pubkey2) @@ -554,10 +553,10 @@ func TestSettings_UpgradeToV2(t *testing.T) { require.Equal(t, SchemaV2, ps.Version) require.Equal(t, true, ps.DefaultConfig == nil) require.Equal(t, validator.Uint64(35_000_000), ps.ProposeConfig[pk].GasLimit) - require.NotNil(t, ps.ProposeConfig[pk].BuilderConfig) + require.IsNil(t, ps.ProposeConfig[pk].BuilderConfig) // An explicit top-level gas limit wins over the builder value. require.Equal(t, validator.Uint64(50_000_000), ps.ProposeConfig[pk2].GasLimit) - require.NotNil(t, ps.ProposeConfig[pk2].BuilderConfig) + require.IsNil(t, ps.ProposeConfig[pk2].BuilderConfig) }) t.Run("already v2 is left untouched", func(t *testing.T) { @@ -634,7 +633,6 @@ func TestSettingFromConsensus(t *testing.T) { Version: SchemaV2, DefaultConfig: &validatorpb.ProposerOptionPayload{ Builder: &validatorpb.BuilderConfig{ - Enabled: proto.Bool(true), Builders: []*validatorpb.BuilderEntry{ {Url: "https://b.example", AuthData: []byte("first")}, {Url: "https://b.example", AuthData: []byte("second")}, @@ -679,11 +677,6 @@ func TestSettingFromConsensus(t *testing.T) { perKey := got.ProposeConfig[key].BuilderConfig require.Equal(t, false, perKey.Enabled) require.Equal(t, (*validator.Uint64)(nil), perKey.MaxExecutionPayment) - - // A disabled per-key section can never silently activate under an enabled default: - // EffectiveBuilderConfig takes the per-key enabled, not the default's. - eff := effectiveBuilderConfig(perKey, &BuilderConfig{Enabled: true}) - require.Equal(t, false, eff.IsEnabled()) }) t.Run("v2 presence preserved", func(t *testing.T) { @@ -716,12 +709,12 @@ func TestRegistrationFor(t *testing.T) { require.Equal(t, recipient, fr) }) - t.Run("v2 inherits the default builder and fee recipient", func(t *testing.T) { + t.Run("v2 inherits the default builders and fee recipient", func(t *testing.T) { ps := &Settings{ Version: SchemaV2, DefaultConfig: &Option{ FeeRecipientConfig: &FeeRecipientConfig{FeeRecipient: recipient}, - BuilderConfig: &BuilderConfig{Enabled: true, GasLimit: 123}, + BuilderConfig: &BuilderConfig{GasLimit: 123, Builders: []*BuilderEntry{{URL: "https://b.example"}}}, }, } fr, gl, enabled := ps.RegistrationFor(key) @@ -733,8 +726,21 @@ func TestRegistrationFor(t *testing.T) { t.Run("v2 without a fee recipient at any level does not register", func(t *testing.T) { ps := &Settings{ Version: SchemaV2, - DefaultConfig: &Option{BuilderConfig: &BuilderConfig{Enabled: true}}, + DefaultConfig: &Option{BuilderConfig: &BuilderConfig{Builders: []*BuilderEntry{{URL: "https://b.example"}}}}, + } + _, _, enabled := ps.RegistrationFor(key) + require.Equal(t, false, enabled) + }) + + t.Run("v2 config without builders does not register", func(t *testing.T) { + ps := &Settings{ + Version: SchemaV2, + DefaultConfig: &Option{ + FeeRecipientConfig: &FeeRecipientConfig{FeeRecipient: recipient}, + BuilderConfig: &BuilderConfig{Enabled: true, MinBid: uint64ValPtr(1)}, + }, } + // enabled is legacy v1 content and is ignored by v2 resolution. _, _, enabled := ps.RegistrationFor(key) require.Equal(t, false, enabled) }) @@ -759,17 +765,17 @@ func TestRegistrationFor(t *testing.T) { perKeyRecipient := common.HexToAddress("0x6e35733c5af9B61374A128e6F85f553aF09ff89A") - t.Run("v2 per-key fee recipient and enabled:false both win over the default", func(t *testing.T) { + t.Run("v2 per-key fee recipient and explicit empty builders both win over the default", func(t *testing.T) { ps := &Settings{ Version: SchemaV2, DefaultConfig: &Option{ FeeRecipientConfig: &FeeRecipientConfig{FeeRecipient: recipient}, - BuilderConfig: &BuilderConfig{Enabled: true}, + BuilderConfig: &BuilderConfig{Builders: []*BuilderEntry{{URL: "https://b.example"}}}, }, ProposeConfig: map[[fieldparams.BLSPubkeyLength]byte]*Option{ key: { FeeRecipientConfig: &FeeRecipientConfig{FeeRecipient: perKeyRecipient}, - BuilderConfig: &BuilderConfig{Enabled: false}, + BuilderConfig: &BuilderConfig{Builders: []*BuilderEntry{}}, }, }, } @@ -783,7 +789,7 @@ func TestRegistrationFor(t *testing.T) { Version: SchemaV2, DefaultConfig: &Option{ FeeRecipientConfig: &FeeRecipientConfig{FeeRecipient: recipient}, - BuilderConfig: &BuilderConfig{Enabled: true, GasLimit: 123}, + BuilderConfig: &BuilderConfig{GasLimit: 123, Builders: []*BuilderEntry{{URL: "https://b.example"}}}, }, ProposeConfig: map[[fieldparams.BLSPubkeyLength]byte]*Option{ key: {GasLimit: 999}, @@ -800,7 +806,7 @@ func TestRegistrationFor(t *testing.T) { Version: SchemaV2, DefaultConfig: &Option{ FeeRecipientConfig: &FeeRecipientConfig{FeeRecipient: recipient}, - BuilderConfig: &BuilderConfig{Enabled: true}, + BuilderConfig: &BuilderConfig{Builders: []*BuilderEntry{{URL: "https://b.example"}}}, }, } _, gl, enabled := ps.RegistrationFor(key) @@ -884,14 +890,14 @@ func TestHasBuilderContent(t *testing.T) { key: {GraffitiConfig: &GraffitiConfig{Graffiti: "hi"}}, }, } - require.Equal(t, false, neutral.hasBuilderContent()) + require.Equal(t, false, neutral.HasBuilderContent()) neutral.ProposeConfig[key].BuilderConfig = &BuilderConfig{} - require.Equal(t, true, neutral.hasBuilderContent()) + require.Equal(t, true, neutral.HasBuilderContent()) } -// An explicit trustless-only ceiling survives the v2 upgrade untouched; an -// absent one stays absent and follows v2 inheritance. -func TestUpgradeToV2_PreservesExplicitMaxPayment(t *testing.T) { +// The cutover drops all v1 builder content, including gloas knobs like +// max_execution_payment; v2 settings are the only source of builder opinions. +func TestUpgradeToV2_DropsBuilderContent(t *testing.T) { key := [fieldparams.BLSPubkeyLength]byte{9} ps := &Settings{ Version: SchemaV1, @@ -902,7 +908,7 @@ func TestUpgradeToV2_PreservesExplicitMaxPayment(t *testing.T) { } require.Equal(t, true, ps.UpgradeToV2()) require.Equal(t, SchemaV2, ps.Version) - require.Equal(t, validator.Uint64(0), *ps.DefaultConfig.BuilderConfig.MaxExecutionPayment) - require.IsNil(t, ps.ProposeConfig[key].BuilderConfig.MaxExecutionPayment) + require.IsNil(t, ps.DefaultConfig.BuilderConfig) + require.IsNil(t, ps.ProposeConfig[key].BuilderConfig) require.Equal(t, false, ps.UpgradeToV2()) } diff --git a/proto/prysm/v1alpha1/validator-client/keymanager.pb.go b/proto/prysm/v1alpha1/validator-client/keymanager.pb.go index bad46e23035c..ce46c9b6ba0c 100755 --- a/proto/prysm/v1alpha1/validator-client/keymanager.pb.go +++ b/proto/prysm/v1alpha1/validator-client/keymanager.pb.go @@ -713,7 +713,7 @@ func (x *ProposerOptionPayload) GetGasLimit() github_com_OffchainLabs_prysm_v7_c type BuilderConfig struct { state protoimpl.MessageState `protogen:"open.v1"` - Enabled *bool `protobuf:"varint,1,opt,name=enabled,proto3,oneof" json:"enabled,omitempty"` + Enabled bool `protobuf:"varint,1,opt,name=enabled,proto3" json:"enabled,omitempty"` GasLimit github_com_OffchainLabs_prysm_v7_consensus_types_validator.Uint64 `protobuf:"varint,2,opt,name=gas_limit,json=gasLimit,proto3" json:"gas_limit,omitempty" cast-type:"github.com/OffchainLabs/prysm/v7/consensus-types/validator.Uint64"` MaxExecutionPayment *github_com_OffchainLabs_prysm_v7_consensus_types_validator.Uint64 `protobuf:"varint,4,opt,name=max_execution_payment,json=maxExecutionPayment,proto3,oneof" json:"max_execution_payment,omitempty" cast-type:"github.com/OffchainLabs/prysm/v7/consensus-types/validator.Uint64"` Builders []*BuilderEntry `protobuf:"bytes,5,rep,name=builders,proto3" json:"builders,omitempty"` @@ -755,8 +755,8 @@ func (*BuilderConfig) Descriptor() ([]byte, []int) { } func (x *BuilderConfig) GetEnabled() bool { - if x != nil && x.Enabled != nil { - return *x.Enabled + if x != nil { + return x.Enabled } return false } @@ -806,7 +806,7 @@ func (x *BuilderConfig) GetBuilderBoostFactor() github_com_OffchainLabs_prysm_v7 type BuilderEntry struct { state protoimpl.MessageState `protogen:"open.v1"` Url string `protobuf:"bytes,1,opt,name=url,proto3" json:"url,omitempty"` - Pubkey []byte `protobuf:"bytes,2,opt,name=pubkey,proto3,oneof" json:"pubkey,omitempty"` + Pubkeys [][]byte `protobuf:"bytes,2,rep,name=pubkeys,proto3" json:"pubkeys,omitempty"` AuthData []byte `protobuf:"bytes,3,opt,name=auth_data,json=authData,proto3,oneof" json:"auth_data,omitempty"` MinBid *github_com_OffchainLabs_prysm_v7_consensus_types_validator.Uint64 `protobuf:"varint,4,opt,name=min_bid,json=minBid,proto3,oneof" json:"min_bid,omitempty" cast-type:"github.com/OffchainLabs/prysm/v7/consensus-types/validator.Uint64"` MaxExecutionPayment *github_com_OffchainLabs_prysm_v7_consensus_types_validator.Uint64 `protobuf:"varint,5,opt,name=max_execution_payment,json=maxExecutionPayment,proto3,oneof" json:"max_execution_payment,omitempty" cast-type:"github.com/OffchainLabs/prysm/v7/consensus-types/validator.Uint64"` @@ -852,9 +852,9 @@ func (x *BuilderEntry) GetUrl() string { return "" } -func (x *BuilderEntry) GetPubkey() []byte { +func (x *BuilderEntry) GetPubkeys() [][]byte { if x != nil { - return x.Pubkey + return x.Pubkeys } return nil } @@ -1176,122 +1176,120 @@ var file_proto_prysm_v1alpha1_validator_client_keymanager_proto_rawDesc = []byte 0x6f, 0x6e, 0x73, 0x65, 0x6e, 0x73, 0x75, 0x73, 0x2d, 0x74, 0x79, 0x70, 0x65, 0x73, 0x2f, 0x76, 0x61, 0x6c, 0x69, 0x64, 0x61, 0x74, 0x6f, 0x72, 0x2e, 0x55, 0x69, 0x6e, 0x74, 0x36, 0x34, 0x52, 0x08, 0x67, 0x61, 0x73, 0x4c, 0x69, 0x6d, 0x69, 0x74, 0x42, 0x0b, 0x0a, 0x09, 0x5f, 0x67, 0x72, - 0x61, 0x66, 0x66, 0x69, 0x74, 0x69, 0x22, 0xbb, 0x05, 0x0a, 0x0d, 0x42, 0x75, 0x69, 0x6c, 0x64, - 0x65, 0x72, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x1d, 0x0a, 0x07, 0x65, 0x6e, 0x61, 0x62, - 0x6c, 0x65, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x08, 0x48, 0x00, 0x52, 0x07, 0x65, 0x6e, 0x61, - 0x62, 0x6c, 0x65, 0x64, 0x88, 0x01, 0x01, 0x12, 0x62, 0x0a, 0x09, 0x67, 0x61, 0x73, 0x5f, 0x6c, - 0x69, 0x6d, 0x69, 0x74, 0x18, 0x02, 0x20, 0x01, 0x28, 0x04, 0x42, 0x45, 0x82, 0xb5, 0x18, 0x41, - 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x4f, 0x66, 0x66, 0x63, 0x68, - 0x61, 0x69, 0x6e, 0x4c, 0x61, 0x62, 0x73, 0x2f, 0x70, 0x72, 0x79, 0x73, 0x6d, 0x2f, 0x76, 0x37, - 0x2f, 0x63, 0x6f, 0x6e, 0x73, 0x65, 0x6e, 0x73, 0x75, 0x73, 0x2d, 0x74, 0x79, 0x70, 0x65, 0x73, - 0x2f, 0x76, 0x61, 0x6c, 0x69, 0x64, 0x61, 0x74, 0x6f, 0x72, 0x2e, 0x55, 0x69, 0x6e, 0x74, 0x36, - 0x34, 0x52, 0x08, 0x67, 0x61, 0x73, 0x4c, 0x69, 0x6d, 0x69, 0x74, 0x12, 0x7e, 0x0a, 0x15, 0x6d, - 0x61, 0x78, 0x5f, 0x65, 0x78, 0x65, 0x63, 0x75, 0x74, 0x69, 0x6f, 0x6e, 0x5f, 0x70, 0x61, 0x79, - 0x6d, 0x65, 0x6e, 0x74, 0x18, 0x04, 0x20, 0x01, 0x28, 0x04, 0x42, 0x45, 0x82, 0xb5, 0x18, 0x41, - 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x4f, 0x66, 0x66, 0x63, 0x68, - 0x61, 0x69, 0x6e, 0x4c, 0x61, 0x62, 0x73, 0x2f, 0x70, 0x72, 0x79, 0x73, 0x6d, 0x2f, 0x76, 0x37, - 0x2f, 0x63, 0x6f, 0x6e, 0x73, 0x65, 0x6e, 0x73, 0x75, 0x73, 0x2d, 0x74, 0x79, 0x70, 0x65, 0x73, - 0x2f, 0x76, 0x61, 0x6c, 0x69, 0x64, 0x61, 0x74, 0x6f, 0x72, 0x2e, 0x55, 0x69, 0x6e, 0x74, 0x36, - 0x34, 0x48, 0x01, 0x52, 0x13, 0x6d, 0x61, 0x78, 0x45, 0x78, 0x65, 0x63, 0x75, 0x74, 0x69, 0x6f, - 0x6e, 0x50, 0x61, 0x79, 0x6d, 0x65, 0x6e, 0x74, 0x88, 0x01, 0x01, 0x12, 0x48, 0x0a, 0x08, 0x62, - 0x75, 0x69, 0x6c, 0x64, 0x65, 0x72, 0x73, 0x18, 0x05, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x2c, 0x2e, - 0x65, 0x74, 0x68, 0x65, 0x72, 0x65, 0x75, 0x6d, 0x2e, 0x76, 0x61, 0x6c, 0x69, 0x64, 0x61, 0x74, - 0x6f, 0x72, 0x2e, 0x61, 0x63, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x73, 0x2e, 0x76, 0x32, 0x2e, 0x42, - 0x75, 0x69, 0x6c, 0x64, 0x65, 0x72, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x52, 0x08, 0x62, 0x75, 0x69, - 0x6c, 0x64, 0x65, 0x72, 0x73, 0x12, 0x21, 0x0a, 0x0c, 0x62, 0x75, 0x69, 0x6c, 0x64, 0x65, 0x72, - 0x73, 0x5f, 0x73, 0x65, 0x74, 0x18, 0x06, 0x20, 0x01, 0x28, 0x08, 0x52, 0x0b, 0x62, 0x75, 0x69, - 0x6c, 0x64, 0x65, 0x72, 0x73, 0x53, 0x65, 0x74, 0x12, 0x63, 0x0a, 0x07, 0x6d, 0x69, 0x6e, 0x5f, - 0x62, 0x69, 0x64, 0x18, 0x07, 0x20, 0x01, 0x28, 0x04, 0x42, 0x45, 0x82, 0xb5, 0x18, 0x41, 0x67, - 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x4f, 0x66, 0x66, 0x63, 0x68, 0x61, - 0x69, 0x6e, 0x4c, 0x61, 0x62, 0x73, 0x2f, 0x70, 0x72, 0x79, 0x73, 0x6d, 0x2f, 0x76, 0x37, 0x2f, - 0x63, 0x6f, 0x6e, 0x73, 0x65, 0x6e, 0x73, 0x75, 0x73, 0x2d, 0x74, 0x79, 0x70, 0x65, 0x73, 0x2f, - 0x76, 0x61, 0x6c, 0x69, 0x64, 0x61, 0x74, 0x6f, 0x72, 0x2e, 0x55, 0x69, 0x6e, 0x74, 0x36, 0x34, - 0x48, 0x02, 0x52, 0x06, 0x6d, 0x69, 0x6e, 0x42, 0x69, 0x64, 0x88, 0x01, 0x01, 0x12, 0x7c, 0x0a, - 0x14, 0x62, 0x75, 0x69, 0x6c, 0x64, 0x65, 0x72, 0x5f, 0x62, 0x6f, 0x6f, 0x73, 0x74, 0x5f, 0x66, - 0x61, 0x63, 0x74, 0x6f, 0x72, 0x18, 0x08, 0x20, 0x01, 0x28, 0x04, 0x42, 0x45, 0x82, 0xb5, 0x18, - 0x41, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x4f, 0x66, 0x66, 0x63, - 0x68, 0x61, 0x69, 0x6e, 0x4c, 0x61, 0x62, 0x73, 0x2f, 0x70, 0x72, 0x79, 0x73, 0x6d, 0x2f, 0x76, - 0x37, 0x2f, 0x63, 0x6f, 0x6e, 0x73, 0x65, 0x6e, 0x73, 0x75, 0x73, 0x2d, 0x74, 0x79, 0x70, 0x65, - 0x73, 0x2f, 0x76, 0x61, 0x6c, 0x69, 0x64, 0x61, 0x74, 0x6f, 0x72, 0x2e, 0x55, 0x69, 0x6e, 0x74, - 0x36, 0x34, 0x48, 0x03, 0x52, 0x12, 0x62, 0x75, 0x69, 0x6c, 0x64, 0x65, 0x72, 0x42, 0x6f, 0x6f, - 0x73, 0x74, 0x46, 0x61, 0x63, 0x74, 0x6f, 0x72, 0x88, 0x01, 0x01, 0x42, 0x0a, 0x0a, 0x08, 0x5f, - 0x65, 0x6e, 0x61, 0x62, 0x6c, 0x65, 0x64, 0x42, 0x18, 0x0a, 0x16, 0x5f, 0x6d, 0x61, 0x78, 0x5f, - 0x65, 0x78, 0x65, 0x63, 0x75, 0x74, 0x69, 0x6f, 0x6e, 0x5f, 0x70, 0x61, 0x79, 0x6d, 0x65, 0x6e, - 0x74, 0x42, 0x0a, 0x0a, 0x08, 0x5f, 0x6d, 0x69, 0x6e, 0x5f, 0x62, 0x69, 0x64, 0x42, 0x17, 0x0a, - 0x15, 0x5f, 0x62, 0x75, 0x69, 0x6c, 0x64, 0x65, 0x72, 0x5f, 0x62, 0x6f, 0x6f, 0x73, 0x74, 0x5f, - 0x66, 0x61, 0x63, 0x74, 0x6f, 0x72, 0x4a, 0x04, 0x08, 0x03, 0x10, 0x04, 0x52, 0x06, 0x72, 0x65, - 0x6c, 0x61, 0x79, 0x73, 0x22, 0x9a, 0x04, 0x0a, 0x0c, 0x42, 0x75, 0x69, 0x6c, 0x64, 0x65, 0x72, - 0x45, 0x6e, 0x74, 0x72, 0x79, 0x12, 0x10, 0x0a, 0x03, 0x75, 0x72, 0x6c, 0x18, 0x01, 0x20, 0x01, - 0x28, 0x09, 0x52, 0x03, 0x75, 0x72, 0x6c, 0x12, 0x1b, 0x0a, 0x06, 0x70, 0x75, 0x62, 0x6b, 0x65, - 0x79, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0c, 0x48, 0x00, 0x52, 0x06, 0x70, 0x75, 0x62, 0x6b, 0x65, - 0x79, 0x88, 0x01, 0x01, 0x12, 0x20, 0x0a, 0x09, 0x61, 0x75, 0x74, 0x68, 0x5f, 0x64, 0x61, 0x74, - 0x61, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0c, 0x48, 0x01, 0x52, 0x08, 0x61, 0x75, 0x74, 0x68, 0x44, - 0x61, 0x74, 0x61, 0x88, 0x01, 0x01, 0x12, 0x63, 0x0a, 0x07, 0x6d, 0x69, 0x6e, 0x5f, 0x62, 0x69, - 0x64, 0x18, 0x04, 0x20, 0x01, 0x28, 0x04, 0x42, 0x45, 0x82, 0xb5, 0x18, 0x41, 0x67, 0x69, 0x74, - 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x4f, 0x66, 0x66, 0x63, 0x68, 0x61, 0x69, 0x6e, - 0x4c, 0x61, 0x62, 0x73, 0x2f, 0x70, 0x72, 0x79, 0x73, 0x6d, 0x2f, 0x76, 0x37, 0x2f, 0x63, 0x6f, - 0x6e, 0x73, 0x65, 0x6e, 0x73, 0x75, 0x73, 0x2d, 0x74, 0x79, 0x70, 0x65, 0x73, 0x2f, 0x76, 0x61, - 0x6c, 0x69, 0x64, 0x61, 0x74, 0x6f, 0x72, 0x2e, 0x55, 0x69, 0x6e, 0x74, 0x36, 0x34, 0x48, 0x02, - 0x52, 0x06, 0x6d, 0x69, 0x6e, 0x42, 0x69, 0x64, 0x88, 0x01, 0x01, 0x12, 0x7e, 0x0a, 0x15, 0x6d, - 0x61, 0x78, 0x5f, 0x65, 0x78, 0x65, 0x63, 0x75, 0x74, 0x69, 0x6f, 0x6e, 0x5f, 0x70, 0x61, 0x79, - 0x6d, 0x65, 0x6e, 0x74, 0x18, 0x05, 0x20, 0x01, 0x28, 0x04, 0x42, 0x45, 0x82, 0xb5, 0x18, 0x41, - 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x4f, 0x66, 0x66, 0x63, 0x68, - 0x61, 0x69, 0x6e, 0x4c, 0x61, 0x62, 0x73, 0x2f, 0x70, 0x72, 0x79, 0x73, 0x6d, 0x2f, 0x76, 0x37, - 0x2f, 0x63, 0x6f, 0x6e, 0x73, 0x65, 0x6e, 0x73, 0x75, 0x73, 0x2d, 0x74, 0x79, 0x70, 0x65, 0x73, - 0x2f, 0x76, 0x61, 0x6c, 0x69, 0x64, 0x61, 0x74, 0x6f, 0x72, 0x2e, 0x55, 0x69, 0x6e, 0x74, 0x36, - 0x34, 0x48, 0x03, 0x52, 0x13, 0x6d, 0x61, 0x78, 0x45, 0x78, 0x65, 0x63, 0x75, 0x74, 0x69, 0x6f, - 0x6e, 0x50, 0x61, 0x79, 0x6d, 0x65, 0x6e, 0x74, 0x88, 0x01, 0x01, 0x12, 0x7c, 0x0a, 0x14, 0x62, - 0x75, 0x69, 0x6c, 0x64, 0x65, 0x72, 0x5f, 0x62, 0x6f, 0x6f, 0x73, 0x74, 0x5f, 0x66, 0x61, 0x63, - 0x74, 0x6f, 0x72, 0x18, 0x06, 0x20, 0x01, 0x28, 0x04, 0x42, 0x45, 0x82, 0xb5, 0x18, 0x41, 0x67, - 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x4f, 0x66, 0x66, 0x63, 0x68, 0x61, - 0x69, 0x6e, 0x4c, 0x61, 0x62, 0x73, 0x2f, 0x70, 0x72, 0x79, 0x73, 0x6d, 0x2f, 0x76, 0x37, 0x2f, - 0x63, 0x6f, 0x6e, 0x73, 0x65, 0x6e, 0x73, 0x75, 0x73, 0x2d, 0x74, 0x79, 0x70, 0x65, 0x73, 0x2f, - 0x76, 0x61, 0x6c, 0x69, 0x64, 0x61, 0x74, 0x6f, 0x72, 0x2e, 0x55, 0x69, 0x6e, 0x74, 0x36, 0x34, - 0x48, 0x04, 0x52, 0x12, 0x62, 0x75, 0x69, 0x6c, 0x64, 0x65, 0x72, 0x42, 0x6f, 0x6f, 0x73, 0x74, - 0x46, 0x61, 0x63, 0x74, 0x6f, 0x72, 0x88, 0x01, 0x01, 0x42, 0x09, 0x0a, 0x07, 0x5f, 0x70, 0x75, - 0x62, 0x6b, 0x65, 0x79, 0x42, 0x0c, 0x0a, 0x0a, 0x5f, 0x61, 0x75, 0x74, 0x68, 0x5f, 0x64, 0x61, - 0x74, 0x61, 0x42, 0x0a, 0x0a, 0x08, 0x5f, 0x6d, 0x69, 0x6e, 0x5f, 0x62, 0x69, 0x64, 0x42, 0x18, - 0x0a, 0x16, 0x5f, 0x6d, 0x61, 0x78, 0x5f, 0x65, 0x78, 0x65, 0x63, 0x75, 0x74, 0x69, 0x6f, 0x6e, - 0x5f, 0x70, 0x61, 0x79, 0x6d, 0x65, 0x6e, 0x74, 0x42, 0x17, 0x0a, 0x15, 0x5f, 0x62, 0x75, 0x69, - 0x6c, 0x64, 0x65, 0x72, 0x5f, 0x62, 0x6f, 0x6f, 0x73, 0x74, 0x5f, 0x66, 0x61, 0x63, 0x74, 0x6f, - 0x72, 0x22, 0x81, 0x03, 0x0a, 0x17, 0x50, 0x72, 0x6f, 0x70, 0x6f, 0x73, 0x65, 0x72, 0x53, 0x65, - 0x74, 0x74, 0x69, 0x6e, 0x67, 0x73, 0x50, 0x61, 0x79, 0x6c, 0x6f, 0x61, 0x64, 0x12, 0x74, 0x0a, - 0x0f, 0x70, 0x72, 0x6f, 0x70, 0x6f, 0x73, 0x65, 0x72, 0x5f, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, - 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x4b, 0x2e, 0x65, 0x74, 0x68, 0x65, 0x72, 0x65, 0x75, - 0x6d, 0x2e, 0x76, 0x61, 0x6c, 0x69, 0x64, 0x61, 0x74, 0x6f, 0x72, 0x2e, 0x61, 0x63, 0x63, 0x6f, - 0x75, 0x6e, 0x74, 0x73, 0x2e, 0x76, 0x32, 0x2e, 0x50, 0x72, 0x6f, 0x70, 0x6f, 0x73, 0x65, 0x72, - 0x53, 0x65, 0x74, 0x74, 0x69, 0x6e, 0x67, 0x73, 0x50, 0x61, 0x79, 0x6c, 0x6f, 0x61, 0x64, 0x2e, + 0x61, 0x66, 0x66, 0x69, 0x74, 0x69, 0x22, 0xaa, 0x05, 0x0a, 0x0d, 0x42, 0x75, 0x69, 0x6c, 0x64, + 0x65, 0x72, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x18, 0x0a, 0x07, 0x65, 0x6e, 0x61, 0x62, + 0x6c, 0x65, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x08, 0x52, 0x07, 0x65, 0x6e, 0x61, 0x62, 0x6c, + 0x65, 0x64, 0x12, 0x62, 0x0a, 0x09, 0x67, 0x61, 0x73, 0x5f, 0x6c, 0x69, 0x6d, 0x69, 0x74, 0x18, + 0x02, 0x20, 0x01, 0x28, 0x04, 0x42, 0x45, 0x82, 0xb5, 0x18, 0x41, 0x67, 0x69, 0x74, 0x68, 0x75, + 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x4f, 0x66, 0x66, 0x63, 0x68, 0x61, 0x69, 0x6e, 0x4c, 0x61, + 0x62, 0x73, 0x2f, 0x70, 0x72, 0x79, 0x73, 0x6d, 0x2f, 0x76, 0x37, 0x2f, 0x63, 0x6f, 0x6e, 0x73, + 0x65, 0x6e, 0x73, 0x75, 0x73, 0x2d, 0x74, 0x79, 0x70, 0x65, 0x73, 0x2f, 0x76, 0x61, 0x6c, 0x69, + 0x64, 0x61, 0x74, 0x6f, 0x72, 0x2e, 0x55, 0x69, 0x6e, 0x74, 0x36, 0x34, 0x52, 0x08, 0x67, 0x61, + 0x73, 0x4c, 0x69, 0x6d, 0x69, 0x74, 0x12, 0x7e, 0x0a, 0x15, 0x6d, 0x61, 0x78, 0x5f, 0x65, 0x78, + 0x65, 0x63, 0x75, 0x74, 0x69, 0x6f, 0x6e, 0x5f, 0x70, 0x61, 0x79, 0x6d, 0x65, 0x6e, 0x74, 0x18, + 0x04, 0x20, 0x01, 0x28, 0x04, 0x42, 0x45, 0x82, 0xb5, 0x18, 0x41, 0x67, 0x69, 0x74, 0x68, 0x75, + 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x4f, 0x66, 0x66, 0x63, 0x68, 0x61, 0x69, 0x6e, 0x4c, 0x61, + 0x62, 0x73, 0x2f, 0x70, 0x72, 0x79, 0x73, 0x6d, 0x2f, 0x76, 0x37, 0x2f, 0x63, 0x6f, 0x6e, 0x73, + 0x65, 0x6e, 0x73, 0x75, 0x73, 0x2d, 0x74, 0x79, 0x70, 0x65, 0x73, 0x2f, 0x76, 0x61, 0x6c, 0x69, + 0x64, 0x61, 0x74, 0x6f, 0x72, 0x2e, 0x55, 0x69, 0x6e, 0x74, 0x36, 0x34, 0x48, 0x00, 0x52, 0x13, + 0x6d, 0x61, 0x78, 0x45, 0x78, 0x65, 0x63, 0x75, 0x74, 0x69, 0x6f, 0x6e, 0x50, 0x61, 0x79, 0x6d, + 0x65, 0x6e, 0x74, 0x88, 0x01, 0x01, 0x12, 0x48, 0x0a, 0x08, 0x62, 0x75, 0x69, 0x6c, 0x64, 0x65, + 0x72, 0x73, 0x18, 0x05, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x2c, 0x2e, 0x65, 0x74, 0x68, 0x65, 0x72, + 0x65, 0x75, 0x6d, 0x2e, 0x76, 0x61, 0x6c, 0x69, 0x64, 0x61, 0x74, 0x6f, 0x72, 0x2e, 0x61, 0x63, + 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x73, 0x2e, 0x76, 0x32, 0x2e, 0x42, 0x75, 0x69, 0x6c, 0x64, 0x65, + 0x72, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x52, 0x08, 0x62, 0x75, 0x69, 0x6c, 0x64, 0x65, 0x72, 0x73, + 0x12, 0x21, 0x0a, 0x0c, 0x62, 0x75, 0x69, 0x6c, 0x64, 0x65, 0x72, 0x73, 0x5f, 0x73, 0x65, 0x74, + 0x18, 0x06, 0x20, 0x01, 0x28, 0x08, 0x52, 0x0b, 0x62, 0x75, 0x69, 0x6c, 0x64, 0x65, 0x72, 0x73, + 0x53, 0x65, 0x74, 0x12, 0x63, 0x0a, 0x07, 0x6d, 0x69, 0x6e, 0x5f, 0x62, 0x69, 0x64, 0x18, 0x07, + 0x20, 0x01, 0x28, 0x04, 0x42, 0x45, 0x82, 0xb5, 0x18, 0x41, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, + 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x4f, 0x66, 0x66, 0x63, 0x68, 0x61, 0x69, 0x6e, 0x4c, 0x61, 0x62, + 0x73, 0x2f, 0x70, 0x72, 0x79, 0x73, 0x6d, 0x2f, 0x76, 0x37, 0x2f, 0x63, 0x6f, 0x6e, 0x73, 0x65, + 0x6e, 0x73, 0x75, 0x73, 0x2d, 0x74, 0x79, 0x70, 0x65, 0x73, 0x2f, 0x76, 0x61, 0x6c, 0x69, 0x64, + 0x61, 0x74, 0x6f, 0x72, 0x2e, 0x55, 0x69, 0x6e, 0x74, 0x36, 0x34, 0x48, 0x01, 0x52, 0x06, 0x6d, + 0x69, 0x6e, 0x42, 0x69, 0x64, 0x88, 0x01, 0x01, 0x12, 0x7c, 0x0a, 0x14, 0x62, 0x75, 0x69, 0x6c, + 0x64, 0x65, 0x72, 0x5f, 0x62, 0x6f, 0x6f, 0x73, 0x74, 0x5f, 0x66, 0x61, 0x63, 0x74, 0x6f, 0x72, + 0x18, 0x08, 0x20, 0x01, 0x28, 0x04, 0x42, 0x45, 0x82, 0xb5, 0x18, 0x41, 0x67, 0x69, 0x74, 0x68, + 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x4f, 0x66, 0x66, 0x63, 0x68, 0x61, 0x69, 0x6e, 0x4c, + 0x61, 0x62, 0x73, 0x2f, 0x70, 0x72, 0x79, 0x73, 0x6d, 0x2f, 0x76, 0x37, 0x2f, 0x63, 0x6f, 0x6e, + 0x73, 0x65, 0x6e, 0x73, 0x75, 0x73, 0x2d, 0x74, 0x79, 0x70, 0x65, 0x73, 0x2f, 0x76, 0x61, 0x6c, + 0x69, 0x64, 0x61, 0x74, 0x6f, 0x72, 0x2e, 0x55, 0x69, 0x6e, 0x74, 0x36, 0x34, 0x48, 0x02, 0x52, + 0x12, 0x62, 0x75, 0x69, 0x6c, 0x64, 0x65, 0x72, 0x42, 0x6f, 0x6f, 0x73, 0x74, 0x46, 0x61, 0x63, + 0x74, 0x6f, 0x72, 0x88, 0x01, 0x01, 0x42, 0x18, 0x0a, 0x16, 0x5f, 0x6d, 0x61, 0x78, 0x5f, 0x65, + 0x78, 0x65, 0x63, 0x75, 0x74, 0x69, 0x6f, 0x6e, 0x5f, 0x70, 0x61, 0x79, 0x6d, 0x65, 0x6e, 0x74, + 0x42, 0x0a, 0x0a, 0x08, 0x5f, 0x6d, 0x69, 0x6e, 0x5f, 0x62, 0x69, 0x64, 0x42, 0x17, 0x0a, 0x15, + 0x5f, 0x62, 0x75, 0x69, 0x6c, 0x64, 0x65, 0x72, 0x5f, 0x62, 0x6f, 0x6f, 0x73, 0x74, 0x5f, 0x66, + 0x61, 0x63, 0x74, 0x6f, 0x72, 0x4a, 0x04, 0x08, 0x03, 0x10, 0x04, 0x52, 0x06, 0x72, 0x65, 0x6c, + 0x61, 0x79, 0x73, 0x22, 0x8c, 0x04, 0x0a, 0x0c, 0x42, 0x75, 0x69, 0x6c, 0x64, 0x65, 0x72, 0x45, + 0x6e, 0x74, 0x72, 0x79, 0x12, 0x10, 0x0a, 0x03, 0x75, 0x72, 0x6c, 0x18, 0x01, 0x20, 0x01, 0x28, + 0x09, 0x52, 0x03, 0x75, 0x72, 0x6c, 0x12, 0x18, 0x0a, 0x07, 0x70, 0x75, 0x62, 0x6b, 0x65, 0x79, + 0x73, 0x18, 0x02, 0x20, 0x03, 0x28, 0x0c, 0x52, 0x07, 0x70, 0x75, 0x62, 0x6b, 0x65, 0x79, 0x73, + 0x12, 0x20, 0x0a, 0x09, 0x61, 0x75, 0x74, 0x68, 0x5f, 0x64, 0x61, 0x74, 0x61, 0x18, 0x03, 0x20, + 0x01, 0x28, 0x0c, 0x48, 0x00, 0x52, 0x08, 0x61, 0x75, 0x74, 0x68, 0x44, 0x61, 0x74, 0x61, 0x88, + 0x01, 0x01, 0x12, 0x63, 0x0a, 0x07, 0x6d, 0x69, 0x6e, 0x5f, 0x62, 0x69, 0x64, 0x18, 0x04, 0x20, + 0x01, 0x28, 0x04, 0x42, 0x45, 0x82, 0xb5, 0x18, 0x41, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, + 0x63, 0x6f, 0x6d, 0x2f, 0x4f, 0x66, 0x66, 0x63, 0x68, 0x61, 0x69, 0x6e, 0x4c, 0x61, 0x62, 0x73, + 0x2f, 0x70, 0x72, 0x79, 0x73, 0x6d, 0x2f, 0x76, 0x37, 0x2f, 0x63, 0x6f, 0x6e, 0x73, 0x65, 0x6e, + 0x73, 0x75, 0x73, 0x2d, 0x74, 0x79, 0x70, 0x65, 0x73, 0x2f, 0x76, 0x61, 0x6c, 0x69, 0x64, 0x61, + 0x74, 0x6f, 0x72, 0x2e, 0x55, 0x69, 0x6e, 0x74, 0x36, 0x34, 0x48, 0x01, 0x52, 0x06, 0x6d, 0x69, + 0x6e, 0x42, 0x69, 0x64, 0x88, 0x01, 0x01, 0x12, 0x7e, 0x0a, 0x15, 0x6d, 0x61, 0x78, 0x5f, 0x65, + 0x78, 0x65, 0x63, 0x75, 0x74, 0x69, 0x6f, 0x6e, 0x5f, 0x70, 0x61, 0x79, 0x6d, 0x65, 0x6e, 0x74, + 0x18, 0x05, 0x20, 0x01, 0x28, 0x04, 0x42, 0x45, 0x82, 0xb5, 0x18, 0x41, 0x67, 0x69, 0x74, 0x68, + 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x4f, 0x66, 0x66, 0x63, 0x68, 0x61, 0x69, 0x6e, 0x4c, + 0x61, 0x62, 0x73, 0x2f, 0x70, 0x72, 0x79, 0x73, 0x6d, 0x2f, 0x76, 0x37, 0x2f, 0x63, 0x6f, 0x6e, + 0x73, 0x65, 0x6e, 0x73, 0x75, 0x73, 0x2d, 0x74, 0x79, 0x70, 0x65, 0x73, 0x2f, 0x76, 0x61, 0x6c, + 0x69, 0x64, 0x61, 0x74, 0x6f, 0x72, 0x2e, 0x55, 0x69, 0x6e, 0x74, 0x36, 0x34, 0x48, 0x02, 0x52, + 0x13, 0x6d, 0x61, 0x78, 0x45, 0x78, 0x65, 0x63, 0x75, 0x74, 0x69, 0x6f, 0x6e, 0x50, 0x61, 0x79, + 0x6d, 0x65, 0x6e, 0x74, 0x88, 0x01, 0x01, 0x12, 0x7c, 0x0a, 0x14, 0x62, 0x75, 0x69, 0x6c, 0x64, + 0x65, 0x72, 0x5f, 0x62, 0x6f, 0x6f, 0x73, 0x74, 0x5f, 0x66, 0x61, 0x63, 0x74, 0x6f, 0x72, 0x18, + 0x06, 0x20, 0x01, 0x28, 0x04, 0x42, 0x45, 0x82, 0xb5, 0x18, 0x41, 0x67, 0x69, 0x74, 0x68, 0x75, + 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x4f, 0x66, 0x66, 0x63, 0x68, 0x61, 0x69, 0x6e, 0x4c, 0x61, + 0x62, 0x73, 0x2f, 0x70, 0x72, 0x79, 0x73, 0x6d, 0x2f, 0x76, 0x37, 0x2f, 0x63, 0x6f, 0x6e, 0x73, + 0x65, 0x6e, 0x73, 0x75, 0x73, 0x2d, 0x74, 0x79, 0x70, 0x65, 0x73, 0x2f, 0x76, 0x61, 0x6c, 0x69, + 0x64, 0x61, 0x74, 0x6f, 0x72, 0x2e, 0x55, 0x69, 0x6e, 0x74, 0x36, 0x34, 0x48, 0x03, 0x52, 0x12, + 0x62, 0x75, 0x69, 0x6c, 0x64, 0x65, 0x72, 0x42, 0x6f, 0x6f, 0x73, 0x74, 0x46, 0x61, 0x63, 0x74, + 0x6f, 0x72, 0x88, 0x01, 0x01, 0x42, 0x0c, 0x0a, 0x0a, 0x5f, 0x61, 0x75, 0x74, 0x68, 0x5f, 0x64, + 0x61, 0x74, 0x61, 0x42, 0x0a, 0x0a, 0x08, 0x5f, 0x6d, 0x69, 0x6e, 0x5f, 0x62, 0x69, 0x64, 0x42, + 0x18, 0x0a, 0x16, 0x5f, 0x6d, 0x61, 0x78, 0x5f, 0x65, 0x78, 0x65, 0x63, 0x75, 0x74, 0x69, 0x6f, + 0x6e, 0x5f, 0x70, 0x61, 0x79, 0x6d, 0x65, 0x6e, 0x74, 0x42, 0x17, 0x0a, 0x15, 0x5f, 0x62, 0x75, + 0x69, 0x6c, 0x64, 0x65, 0x72, 0x5f, 0x62, 0x6f, 0x6f, 0x73, 0x74, 0x5f, 0x66, 0x61, 0x63, 0x74, + 0x6f, 0x72, 0x22, 0x81, 0x03, 0x0a, 0x17, 0x50, 0x72, 0x6f, 0x70, 0x6f, 0x73, 0x65, 0x72, 0x53, + 0x65, 0x74, 0x74, 0x69, 0x6e, 0x67, 0x73, 0x50, 0x61, 0x79, 0x6c, 0x6f, 0x61, 0x64, 0x12, 0x74, + 0x0a, 0x0f, 0x70, 0x72, 0x6f, 0x70, 0x6f, 0x73, 0x65, 0x72, 0x5f, 0x63, 0x6f, 0x6e, 0x66, 0x69, + 0x67, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x4b, 0x2e, 0x65, 0x74, 0x68, 0x65, 0x72, 0x65, + 0x75, 0x6d, 0x2e, 0x76, 0x61, 0x6c, 0x69, 0x64, 0x61, 0x74, 0x6f, 0x72, 0x2e, 0x61, 0x63, 0x63, + 0x6f, 0x75, 0x6e, 0x74, 0x73, 0x2e, 0x76, 0x32, 0x2e, 0x50, 0x72, 0x6f, 0x70, 0x6f, 0x73, 0x65, + 0x72, 0x53, 0x65, 0x74, 0x74, 0x69, 0x6e, 0x67, 0x73, 0x50, 0x61, 0x79, 0x6c, 0x6f, 0x61, 0x64, + 0x2e, 0x50, 0x72, 0x6f, 0x70, 0x6f, 0x73, 0x65, 0x72, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x45, + 0x6e, 0x74, 0x72, 0x79, 0x52, 0x0e, 0x70, 0x72, 0x6f, 0x70, 0x6f, 0x73, 0x65, 0x72, 0x43, 0x6f, + 0x6e, 0x66, 0x69, 0x67, 0x12, 0x5c, 0x0a, 0x0e, 0x64, 0x65, 0x66, 0x61, 0x75, 0x6c, 0x74, 0x5f, + 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x35, 0x2e, 0x65, + 0x74, 0x68, 0x65, 0x72, 0x65, 0x75, 0x6d, 0x2e, 0x76, 0x61, 0x6c, 0x69, 0x64, 0x61, 0x74, 0x6f, + 0x72, 0x2e, 0x61, 0x63, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x73, 0x2e, 0x76, 0x32, 0x2e, 0x50, 0x72, + 0x6f, 0x70, 0x6f, 0x73, 0x65, 0x72, 0x4f, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x50, 0x61, 0x79, 0x6c, + 0x6f, 0x61, 0x64, 0x52, 0x0d, 0x64, 0x65, 0x66, 0x61, 0x75, 0x6c, 0x74, 0x43, 0x6f, 0x6e, 0x66, + 0x69, 0x67, 0x12, 0x18, 0x0a, 0x07, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x18, 0x03, 0x20, + 0x01, 0x28, 0x0d, 0x52, 0x07, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x1a, 0x78, 0x0a, 0x13, 0x50, 0x72, 0x6f, 0x70, 0x6f, 0x73, 0x65, 0x72, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x45, 0x6e, - 0x74, 0x72, 0x79, 0x52, 0x0e, 0x70, 0x72, 0x6f, 0x70, 0x6f, 0x73, 0x65, 0x72, 0x43, 0x6f, 0x6e, - 0x66, 0x69, 0x67, 0x12, 0x5c, 0x0a, 0x0e, 0x64, 0x65, 0x66, 0x61, 0x75, 0x6c, 0x74, 0x5f, 0x63, - 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x35, 0x2e, 0x65, 0x74, - 0x68, 0x65, 0x72, 0x65, 0x75, 0x6d, 0x2e, 0x76, 0x61, 0x6c, 0x69, 0x64, 0x61, 0x74, 0x6f, 0x72, - 0x2e, 0x61, 0x63, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x73, 0x2e, 0x76, 0x32, 0x2e, 0x50, 0x72, 0x6f, - 0x70, 0x6f, 0x73, 0x65, 0x72, 0x4f, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x50, 0x61, 0x79, 0x6c, 0x6f, - 0x61, 0x64, 0x52, 0x0d, 0x64, 0x65, 0x66, 0x61, 0x75, 0x6c, 0x74, 0x43, 0x6f, 0x6e, 0x66, 0x69, - 0x67, 0x12, 0x18, 0x0a, 0x07, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x18, 0x03, 0x20, 0x01, - 0x28, 0x0d, 0x52, 0x07, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x1a, 0x78, 0x0a, 0x13, 0x50, - 0x72, 0x6f, 0x70, 0x6f, 0x73, 0x65, 0x72, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x45, 0x6e, 0x74, - 0x72, 0x79, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, - 0x03, 0x6b, 0x65, 0x79, 0x12, 0x4b, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, - 0x01, 0x28, 0x0b, 0x32, 0x35, 0x2e, 0x65, 0x74, 0x68, 0x65, 0x72, 0x65, 0x75, 0x6d, 0x2e, 0x76, - 0x61, 0x6c, 0x69, 0x64, 0x61, 0x74, 0x6f, 0x72, 0x2e, 0x61, 0x63, 0x63, 0x6f, 0x75, 0x6e, 0x74, - 0x73, 0x2e, 0x76, 0x32, 0x2e, 0x50, 0x72, 0x6f, 0x70, 0x6f, 0x73, 0x65, 0x72, 0x4f, 0x70, 0x74, - 0x69, 0x6f, 0x6e, 0x50, 0x61, 0x79, 0x6c, 0x6f, 0x61, 0x64, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, - 0x65, 0x3a, 0x02, 0x38, 0x01, 0x42, 0xcd, 0x01, 0x0a, 0x22, 0x6f, 0x72, 0x67, 0x2e, 0x65, 0x74, - 0x68, 0x65, 0x72, 0x65, 0x75, 0x6d, 0x2e, 0x76, 0x61, 0x6c, 0x69, 0x64, 0x61, 0x74, 0x6f, 0x72, - 0x2e, 0x61, 0x63, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x73, 0x2e, 0x76, 0x32, 0x42, 0x0f, 0x4b, 0x65, - 0x79, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x72, 0x50, 0x72, 0x6f, 0x74, 0x6f, 0x50, 0x01, 0x5a, - 0x52, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x4f, 0x66, 0x66, 0x63, - 0x68, 0x61, 0x69, 0x6e, 0x4c, 0x61, 0x62, 0x73, 0x2f, 0x70, 0x72, 0x79, 0x73, 0x6d, 0x2f, 0x76, - 0x37, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2f, 0x70, 0x72, 0x79, 0x73, 0x6d, 0x2f, 0x76, 0x31, - 0x61, 0x6c, 0x70, 0x68, 0x61, 0x31, 0x2f, 0x76, 0x61, 0x6c, 0x69, 0x64, 0x61, 0x74, 0x6f, 0x72, - 0x2d, 0x63, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x3b, 0x76, 0x61, 0x6c, 0x69, 0x64, 0x61, 0x74, 0x6f, - 0x72, 0x70, 0x62, 0xaa, 0x02, 0x1e, 0x45, 0x74, 0x68, 0x65, 0x72, 0x65, 0x75, 0x6d, 0x2e, 0x56, - 0x61, 0x6c, 0x69, 0x64, 0x61, 0x74, 0x6f, 0x72, 0x2e, 0x41, 0x63, 0x63, 0x6f, 0x75, 0x6e, 0x74, - 0x73, 0x2e, 0x56, 0x32, 0xca, 0x02, 0x1e, 0x45, 0x74, 0x68, 0x65, 0x72, 0x65, 0x75, 0x6d, 0x5c, - 0x56, 0x61, 0x6c, 0x69, 0x64, 0x61, 0x74, 0x6f, 0x72, 0x5c, 0x41, 0x63, 0x63, 0x6f, 0x75, 0x6e, - 0x74, 0x73, 0x5c, 0x56, 0x32, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, + 0x74, 0x72, 0x79, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, + 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x4b, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, + 0x20, 0x01, 0x28, 0x0b, 0x32, 0x35, 0x2e, 0x65, 0x74, 0x68, 0x65, 0x72, 0x65, 0x75, 0x6d, 0x2e, + 0x76, 0x61, 0x6c, 0x69, 0x64, 0x61, 0x74, 0x6f, 0x72, 0x2e, 0x61, 0x63, 0x63, 0x6f, 0x75, 0x6e, + 0x74, 0x73, 0x2e, 0x76, 0x32, 0x2e, 0x50, 0x72, 0x6f, 0x70, 0x6f, 0x73, 0x65, 0x72, 0x4f, 0x70, + 0x74, 0x69, 0x6f, 0x6e, 0x50, 0x61, 0x79, 0x6c, 0x6f, 0x61, 0x64, 0x52, 0x05, 0x76, 0x61, 0x6c, + 0x75, 0x65, 0x3a, 0x02, 0x38, 0x01, 0x42, 0xcd, 0x01, 0x0a, 0x22, 0x6f, 0x72, 0x67, 0x2e, 0x65, + 0x74, 0x68, 0x65, 0x72, 0x65, 0x75, 0x6d, 0x2e, 0x76, 0x61, 0x6c, 0x69, 0x64, 0x61, 0x74, 0x6f, + 0x72, 0x2e, 0x61, 0x63, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x73, 0x2e, 0x76, 0x32, 0x42, 0x0f, 0x4b, + 0x65, 0x79, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x72, 0x50, 0x72, 0x6f, 0x74, 0x6f, 0x50, 0x01, + 0x5a, 0x52, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x4f, 0x66, 0x66, + 0x63, 0x68, 0x61, 0x69, 0x6e, 0x4c, 0x61, 0x62, 0x73, 0x2f, 0x70, 0x72, 0x79, 0x73, 0x6d, 0x2f, + 0x76, 0x37, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2f, 0x70, 0x72, 0x79, 0x73, 0x6d, 0x2f, 0x76, + 0x31, 0x61, 0x6c, 0x70, 0x68, 0x61, 0x31, 0x2f, 0x76, 0x61, 0x6c, 0x69, 0x64, 0x61, 0x74, 0x6f, + 0x72, 0x2d, 0x63, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x3b, 0x76, 0x61, 0x6c, 0x69, 0x64, 0x61, 0x74, + 0x6f, 0x72, 0x70, 0x62, 0xaa, 0x02, 0x1e, 0x45, 0x74, 0x68, 0x65, 0x72, 0x65, 0x75, 0x6d, 0x2e, + 0x56, 0x61, 0x6c, 0x69, 0x64, 0x61, 0x74, 0x6f, 0x72, 0x2e, 0x41, 0x63, 0x63, 0x6f, 0x75, 0x6e, + 0x74, 0x73, 0x2e, 0x56, 0x32, 0xca, 0x02, 0x1e, 0x45, 0x74, 0x68, 0x65, 0x72, 0x65, 0x75, 0x6d, + 0x5c, 0x56, 0x61, 0x6c, 0x69, 0x64, 0x61, 0x74, 0x6f, 0x72, 0x5c, 0x41, 0x63, 0x63, 0x6f, 0x75, + 0x6e, 0x74, 0x73, 0x5c, 0x56, 0x32, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, } var ( diff --git a/proto/prysm/v1alpha1/validator-client/keymanager.proto b/proto/prysm/v1alpha1/validator-client/keymanager.proto index 4e97d27482de..cb2497d737dc 100644 --- a/proto/prysm/v1alpha1/validator-client/keymanager.proto +++ b/proto/prysm/v1alpha1/validator-client/keymanager.proto @@ -129,7 +129,8 @@ message ProposerOptionPayload { // BuilderConfig is a property of ProposerOptionPayload message BuilderConfig { - optional bool enabled = 1; + // enabled is legacy v1 (mev-boost) content; v2 settings ignore it. + bool enabled = 1; uint64 gas_limit = 2 [ (ethereum.eth.ext.cast_type) = "github.com/OffchainLabs/prysm/v7/consensus-types/validator.Uint64" @@ -163,7 +164,8 @@ message BuilderConfig { // left unset inherit from the enclosing BuilderConfig, then default_config. message BuilderEntry { string url = 1; - optional bytes pubkey = 2; + // pubkeys filters accepted bids to these builder keys; empty accepts any. + repeated bytes pubkeys = 2; optional bytes auth_data = 3; optional uint64 min_bid = 4 [ (ethereum.eth.ext.cast_type) = diff --git a/validator/client/validator.go b/validator/client/validator.go index 554ec00a8397..0d7e76c23ecd 100644 --- a/validator/client/validator.go +++ b/validator/client/validator.go @@ -875,6 +875,12 @@ func (v *validator) PushProposerSettings(ctx context.Context, slot primitives.Sl }); err != nil { return err } + // Gloas preferences start being prepared one epoch ahead; v1 builder + // settings do not apply to them, so warn while there is time to act. + if ps := v.ProposerSettings(); currentEpoch+1 >= params.BeaconConfig().GloasForkEpoch && + ps != nil && ps.Version != proposer.SchemaV2 && ps.HasBuilderContent() && slots.IsEpochStart(slot) { + log.Warn("Proposer settings still use the v1 schema; gloas proposer preferences are prepared with defaults. Provide v2 proposer settings.") + } } else { v.upgradeProposerSettingsToV2(ctx) } @@ -1443,14 +1449,14 @@ func (v *validator) submittedPrefSlotsCount() int { type builderTarget struct { url string authData []byte - pubkey []byte + pubkeys [][]byte maxPayment uint64 minBid *uint64 boostFactor *uint64 } -// builderTargetsForKey resolves the enabled builder list for pk; entries override -// config-level fallbacks. TODO(gloas): minBid/boost/pubkey ride the #630 wire. +// builderTargetsForKey resolves the configured builder list for pk; entries override +// config-level fallbacks. TODO(gloas): minBid/boost/pubkeys ride the #630 wire. func (v *validator) builderTargetsForKey(pk pubkey) []builderTarget { ps := v.ProposerSettings() // Builder entries imply v2: the API upgrades on write and v2 files declare it, @@ -1459,7 +1465,7 @@ func (v *validator) builderTargetsForKey(pk pubkey) []builderTarget { return nil } bc := ps.EffectiveBuilderConfig(pk) - if !bc.IsEnabled() { + if bc == nil { return nil } fbMax := uint64(bc.EffectiveMaxExecutionPayment()) @@ -1477,7 +1483,7 @@ func (v *validator) builderTargetsForKey(pk pubkey) []builderTarget { continue } seen[e.Identity()] = true - t := builderTarget{url: e.URL, authData: e.EffectiveAuthData(), pubkey: e.Pubkey, maxPayment: fbMax, minBid: fbMin, boostFactor: fbBoost} + t := builderTarget{url: e.URL, authData: e.EffectiveAuthData(), pubkeys: e.Pubkeys, maxPayment: fbMax, minBid: fbMin, boostFactor: fbBoost} if e.MaxExecutionPayment != nil { t.maxPayment = uint64(*e.MaxExecutionPayment) } @@ -1541,7 +1547,7 @@ func (v *validator) warmBuilderRequestAuthsForDuties(ctx context.Context, km key continue } // TODO(gloas): only maxPayment fits BuilderPreferencesV1; per-entry - // authData, minBid, boostFactor and pubkey ship with the #630 inline wire. + // authData, minBid, boostFactor and pubkeys ship with the #630 inline wire. reqs = append(reqs, ðpb.SubmitBuilderPreferencesRequest{ ValidatorPubkey: pk[:], Request: ðpb.BuilderPreferencesRequestV1{ diff --git a/validator/client/validator_test.go b/validator/client/validator_test.go index 98f8ed290b86..9d9b7be821df 100644 --- a/validator/client/validator_test.go +++ b/validator/client/validator_test.go @@ -3067,7 +3067,8 @@ func TestValidator_buildProposerPreferences_GasLimitSources(t *testing.T) { } require.Equal(t, proposer.SchemaV2, ps.Version) require.Equal(t, tt.upgradedGasLimit, ps.DefaultConfig.GasLimit) - require.NotNil(t, ps.DefaultConfig.BuilderConfig) + // The cutover drops v1 builder content after promoting its gas limit. + require.IsNil(t, ps.DefaultConfig.BuilderConfig) dbps, err := v.db.ProposerSettings(t.Context()) require.NoError(t, err) @@ -3528,7 +3529,7 @@ func TestValidator_buildSignedRegReqs_V2Settings(t *testing.T) { FeeRecipient: defaultFeeRecipient, }, GasLimit: 8888, - BuilderConfig: &proposer.BuilderConfig{Enabled: true, GasLimit: 9999}, + BuilderConfig: &proposer.BuilderConfig{GasLimit: 9999, Builders: []*proposer.BuilderEntry{{URL: "https://default.example"}}}, }, ProposeConfig: map[[48]byte]*proposer.Option{ pubkey1: { @@ -3536,7 +3537,7 @@ func TestValidator_buildSignedRegReqs_V2Settings(t *testing.T) { FeeRecipient: feeRecipient1, }, GasLimit: 1111, - BuilderConfig: &proposer.BuilderConfig{Enabled: true, GasLimit: 7777}, + BuilderConfig: &proposer.BuilderConfig{GasLimit: 7777, Builders: []*proposer.BuilderEntry{{URL: "https://b.example"}}}, }, pubkey2: { FeeRecipientConfig: &proposer.FeeRecipientConfig{ @@ -3548,7 +3549,7 @@ func TestValidator_buildSignedRegReqs_V2Settings(t *testing.T) { FeeRecipientConfig: &proposer.FeeRecipientConfig{ FeeRecipient: feeRecipient2, }, - BuilderConfig: &proposer.BuilderConfig{Enabled: false}, + BuilderConfig: &proposer.BuilderConfig{Builders: []*proposer.BuilderEntry{}}, }, }, }, @@ -3984,11 +3985,11 @@ func TestBuilderTargetsForKey(t *testing.T) { require.Equal(t, 0, len(v.builderTargetsForKey(pk))) }) - t.Run("disabled config produces no targets", func(t *testing.T) { - disabled := map[[fieldparams.BLSPubkeyLength]byte]*proposer.Option{ - pk: {BuilderConfig: &proposer.BuilderConfig{Enabled: false, Builders: proposeConfig[pk].BuilderConfig.Builders}}, + t.Run("explicit empty builders list produces no targets", func(t *testing.T) { + optedOut := map[[fieldparams.BLSPubkeyLength]byte]*proposer.Option{ + pk: {BuilderConfig: &proposer.BuilderConfig{Builders: []*proposer.BuilderEntry{}}}, } - v := &validator{proposerSettings: &proposer.Settings{Version: proposer.SchemaV2, ProposeConfig: disabled}} + v := &validator{proposerSettings: &proposer.Settings{Version: proposer.SchemaV2, ProposeConfig: optedOut}} require.Equal(t, 0, len(v.builderTargetsForKey(pk))) }) diff --git a/validator/rpc/handlers_validator_config.go b/validator/rpc/handlers_validator_config.go index b39c69d21af6..4e86f76ce574 100644 --- a/validator/rpc/handlers_validator_config.go +++ b/validator/rpc/handlers_validator_config.go @@ -36,7 +36,7 @@ func (s *Server) GetBuilders(w http.ResponseWriter, r *http.Request) { if out.Builders == nil { out.Builders = []*BuilderEntry{} } - httputil.WriteJson(w, out) + httputil.WriteJson(w, &GetBuildersResponse{Data: out}) } // SetBuilders implements POST /eth/v1/validator/{pubkey}/builders, replacing the @@ -59,10 +59,6 @@ func (s *Server) SetBuilders(w http.ResponseWriter, r *http.Request) { httputil.HandleError(w, "Could not decode builder config: "+err.Error(), http.StatusBadRequest) return } - if body.Enabled == nil { - httputil.HandleError(w, "enabled is required", http.StatusBadRequest) - return - } bc, err := body.ToConsensus() if err != nil { httputil.HandleError(w, err.Error(), http.StatusBadRequest) @@ -100,7 +96,7 @@ func (s *Server) SetBuilders(w http.ResponseWriter, r *http.Request) { } // DeleteBuilders implements DELETE /eth/v1/validator/{pubkey}/builders: the key -// follows the validator client defaults again; not the same as enabled:false. +// follows the validator client defaults again; not the same as builders: []. func (s *Server) DeleteBuilders(w http.ResponseWriter, r *http.Request) { ctx, span := trace.StartSpan(r.Context(), "validator.keymanagerAPI.DeleteBuilders") defer span.End() diff --git a/validator/rpc/handlers_validator_config_test.go b/validator/rpc/handlers_validator_config_test.go index 3aae119ec921..528b6631ff98 100644 --- a/validator/rpc/handlers_validator_config_test.go +++ b/validator/rpc/handlers_validator_config_test.go @@ -51,7 +51,10 @@ func getBuilders(t *testing.T, s *Server, pubkey string) (*httptest.ResponseReco s.GetBuilders(w, req) cfg := &BuilderConfig{} if w.Code == http.StatusOK { - require.NoError(t, json.Unmarshal(w.Body.Bytes(), cfg)) + resp := &GetBuildersResponse{} + require.NoError(t, json.Unmarshal(w.Body.Bytes(), resp)) + require.NotNil(t, resp.Data) + cfg = resp.Data } return w, cfg } @@ -69,15 +72,13 @@ func TestServer_SetBuilders(t *testing.T) { t.Run("round trip via GET", func(t *testing.T) { srv, keys := setupConfigServer(t, 1) pk := hexutil.Encode(keys[0][:]) - body := `{"enabled":true,"min_bid":"5","builder_boost_factor":"120",` + + body := `{"min_bid":"5","builder_boost_factor":"120",` + `"builders":[{"url":"https://b.example","auth_data":"0x0102","max_execution_payment":"1000"}]}` w := postBuilders(t, srv, pk, body) require.Equal(t, http.StatusAccepted, w.Code) _, cfg := getBuilders(t, srv, pk) - require.NotNil(t, cfg.Enabled) - require.Equal(t, true, *cfg.Enabled) require.Equal(t, "5", *cfg.MinBid) require.Equal(t, "120", *cfg.BuilderBoostFactor) require.Equal(t, 1, len(cfg.Builders)) @@ -93,9 +94,9 @@ func TestServer_SetBuilders(t *testing.T) { pk := hexutil.Encode(keys[0][:]) require.Equal(t, http.StatusAccepted, postBuilders(t, srv, pk, - `{"enabled":true,"builders":[{"url":"https://a.example"},{"url":"https://b.example"}]}`).Code) + `{"builders":[{"url":"https://a.example"},{"url":"https://b.example"}]}`).Code) require.Equal(t, http.StatusAccepted, postBuilders(t, srv, pk, - `{"enabled":true,"builders":[{"url":"https://c.example"}]}`).Code) + `{"builders":[{"url":"https://c.example"}]}`).Code) _, cfg := getBuilders(t, srv, pk) require.Equal(t, 1, len(cfg.Builders)) @@ -105,7 +106,7 @@ func TestServer_SetBuilders(t *testing.T) { t.Run("empty array is use-none", func(t *testing.T) { srv, keys := setupConfigServer(t, 1) pk := hexutil.Encode(keys[0][:]) - require.Equal(t, http.StatusAccepted, postBuilders(t, srv, pk, `{"enabled":true,"builders":[]}`).Code) + require.Equal(t, http.StatusAccepted, postBuilders(t, srv, pk, `{"builders":[]}`).Code) _, cfg := getBuilders(t, srv, pk) require.NotNil(t, cfg.Builders) @@ -119,11 +120,11 @@ func TestServer_SetBuilders(t *testing.T) { require.NoError(t, srv.validatorService.SetProposerSettings(t.Context(), &proposer.Settings{ Version: proposer.SchemaV2, DefaultConfig: &proposer.Option{ - BuilderConfig: &proposer.BuilderConfig{Enabled: true, Builders: []*proposer.BuilderEntry{{URL: "https://default.example"}}}, + BuilderConfig: &proposer.BuilderConfig{Builders: []*proposer.BuilderEntry{{URL: "https://default.example"}}}, }, })) - require.Equal(t, http.StatusAccepted, postBuilders(t, srv, pkA, `{"enabled":true,"builders":[]}`).Code) - require.Equal(t, http.StatusAccepted, postBuilders(t, srv, pkB, `{"enabled":true,"builders":[{"url":"https://b.example"}]}`).Code) + require.Equal(t, http.StatusAccepted, postBuilders(t, srv, pkA, `{"builders":[]}`).Code) + require.Equal(t, http.StatusAccepted, postBuilders(t, srv, pkB, `{"builders":[{"url":"https://b.example"}]}`).Code) _, cfg := getBuilders(t, srv, pkA) require.Equal(t, 0, len(cfg.Builders)) @@ -138,7 +139,7 @@ func TestServer_SetBuilders(t *testing.T) { keys[0]: {BuilderConfig: &proposer.BuilderConfig{Enabled: true, GasLimit: 999}}, }, })) - require.Equal(t, http.StatusAccepted, postBuilders(t, srv, pk, `{"enabled":true,"builders":[{"url":"https://a.example"}]}`).Code) + require.Equal(t, http.StatusAccepted, postBuilders(t, srv, pk, `{"builders":[{"url":"https://a.example"}]}`).Code) got := srv.validatorService.ProposerSettings() // Builder lists are v2 content: POST migrates the schema in place. @@ -149,17 +150,21 @@ func TestServer_SetBuilders(t *testing.T) { require.Equal(t, 1, len(opt.BuilderConfig.Builders)) }) - t.Run("builder_pubkey rides along as the entry's response filter", func(t *testing.T) { + t.Run("builder_pubkeys ride along as the entry's response filter", func(t *testing.T) { srv, keys := setupConfigServer(t, 1) pk := hexutil.Encode(keys[0][:]) bpk := "0x" + strings.Repeat("ab", 48) - body := `{"enabled":true,"builders":[{"url":"https://a.example","builder_pubkey":"` + bpk + `"}]}` + bpk2 := "0x" + strings.Repeat("cd", 48) + body := `{"builders":[{"url":"https://a.example","builder_pubkeys":["` + bpk + `","` + bpk2 + `"]},{"url":"https://b.example"}]}` require.Equal(t, http.StatusAccepted, postBuilders(t, srv, pk, body).Code) _, cfg := getBuilders(t, srv, pk) - require.Equal(t, 1, len(cfg.Builders)) + require.Equal(t, 2, len(cfg.Builders)) require.Equal(t, "https://a.example", cfg.Builders[0].Url) - require.Equal(t, bpk, *cfg.Builders[0].BuilderPubkey) + require.DeepEqual(t, []string{bpk, bpk2}, cfg.Builders[0].BuilderPubkeys) + // Omitted builder_pubkeys resolves to the empty list. + require.NotNil(t, cfg.Builders[1].BuilderPubkeys) + require.Equal(t, 0, len(cfg.Builders[1].BuilderPubkeys)) }) t.Run("rejects invalid input", func(t *testing.T) { @@ -171,19 +176,20 @@ func TestServer_SetBuilders(t *testing.T) { cases := map[string]struct { body, contains string }{ - "missing enabled": {`{"builders":[{"url":"https://a"}]}`, "enabled is required"}, - "entry without url": {`{"enabled":true,"builders":[{"min_bid":"1"}]}`, "url is required"}, - "pubkey-only entry": {`{"enabled":true,"builders":[{"builder_pubkey":"` + bpk + `"}]}`, "url is required"}, - "same url and auth_data": {`{"enabled":true,"builders":[{"url":"https://a"},{"url":"https://a"}]}`, "share the same url and auth_data"}, - "omitted auth_data collides with its derived value": {`{"enabled":true,"builders":[{"url":"https://a"},{"url":"https://a","auth_data":"` + hexutil.Encode([]byte("https://a")) + `"}]}`, "share the same url and auth_data"}, - "invalid url": {`{"enabled":true,"builders":[{"url":"not a url"}]}`, "url is not a valid URL"}, - "url too long": {`{"enabled":true,"builders":[{"url":"` + longURL + `"}]}`, "url exceeds 2048 bytes"}, - "invalid builder_pubkey": {`{"enabled":true,"builders":[{"url":"https://a","builder_pubkey":"0x1234"}]}`, "builder_pubkey is not a valid BLS public key"}, - "invalid auth_data hex": {`{"enabled":true,"builders":[{"url":"https://a","auth_data":"0xzz"}]}`, "auth_data is not valid hex"}, - "auth_data too long": {`{"enabled":true,"builders":[{"url":"https://a","auth_data":"0x` + strings.Repeat("ab", 4097) + `"}]}`, "auth_data exceeds 4096 bytes"}, - "non-numeric min_bid": {`{"enabled":true,"min_bid":"abc","builders":[]}`, "min_bid is not a valid uint64"}, - "null entry": {`{"enabled":true,"builders":[null]}`, "builders[0] is null"}, - "non-numeric entry max": {`{"enabled":true,"builders":[{"url":"https://a"},{"url":"https://b","max_execution_payment":"x"}]}`, "builders[1].max_execution_payment is not a valid uint64"}, + "entry without url": {`{"builders":[{"min_bid":"1"}]}`, "url is required"}, + "pubkey-only entry": {`{"builders":[{"builder_pubkeys":["` + bpk + `"]}]}`, "url is required"}, + "same url and auth_data": {`{"builders":[{"url":"https://a"},{"url":"https://a"}]}`, "share the same url and auth_data"}, + "omitted auth_data collides with its derived value": {`{"builders":[{"url":"https://a"},{"url":"https://a","auth_data":"` + hexutil.Encode([]byte("https://a")) + `"}]}`, "share the same url and auth_data"}, + "invalid url": {`{"builders":[{"url":"not a url"}]}`, "url is not a valid URL"}, + "url too long": {`{"builders":[{"url":"` + longURL + `"}]}`, "url exceeds 2048 bytes"}, + "invalid builder_pubkeys entry": {`{"builders":[{"url":"https://a","builder_pubkeys":["0x1234"]}]}`, "builder_pubkeys contains an invalid BLS public key"}, + "invalid auth_data hex": {`{"builders":[{"url":"https://a","auth_data":"0xzz"}]}`, "auth_data is not valid hex"}, + "auth_data too long": {`{"builders":[{"url":"https://a","auth_data":"0x` + strings.Repeat("ab", 4097) + `"}]}`, "auth_data must be 1 to 4096 bytes"}, + "auth_data empty": {`{"builders":[{"url":"https://a","auth_data":"0x"}]}`, "auth_data must be 1 to 4096 bytes"}, + "too many builder_pubkeys": {`{"builders":[{"url":"https://a","builder_pubkeys":[` + strings.TrimSuffix(strings.Repeat(`"`+bpk+`",`, 65), ",") + `]}]}`, "builder_pubkeys exceeds 64 keys"}, + "non-numeric min_bid": {`{"min_bid":"abc","builders":[]}`, "min_bid is not a valid uint64"}, + "null entry": {`{"builders":[null]}`, "builders[0] is null"}, + "non-numeric entry max": {`{"builders":[{"url":"https://a"},{"url":"https://b","max_execution_payment":"x"}]}`, "builders[1].max_execution_payment is not a valid uint64"}, } for name, tc := range cases { t.Run(name, func(t *testing.T) { @@ -197,7 +203,7 @@ func TestServer_SetBuilders(t *testing.T) { t.Run("same url with distinct auth_data is a legal pair", func(t *testing.T) { srv, keys := setupConfigServer(t, 1) pk := hexutil.Encode(keys[0][:]) - body := `{"enabled":true,"builders":[{"url":"https://a","auth_data":"0x01"},{"url":"https://a","auth_data":"0x02"}]}` + body := `{"builders":[{"url":"https://a","auth_data":"0x01"},{"url":"https://a","auth_data":"0x02"}]}` require.Equal(t, http.StatusAccepted, postBuilders(t, srv, pk, body).Code) _, cfg := getBuilders(t, srv, pk) require.Equal(t, 2, len(cfg.Builders)) @@ -210,7 +216,7 @@ func TestServer_SetBuilders(t *testing.T) { for i := 0; i <= maxBuilderEntries; i++ { entries = append(entries, `{"url":"https://b`+strconv.Itoa(i)+`.example"}`) } - body := `{"enabled":true,"builders":[` + strings.Join(entries, ",") + `]}` + body := `{"builders":[` + strings.Join(entries, ",") + `]}` w := postBuilders(t, srv, pk, body) require.Equal(t, http.StatusBadRequest, w.Code) require.Equal(t, true, strings.Contains(w.Body.String(), "exceeds 64 entries")) @@ -219,7 +225,7 @@ func TestServer_SetBuilders(t *testing.T) { t.Run("validator service nil", func(t *testing.T) { srv, keys := setupConfigServer(t, 1) srv.validatorService = nil - w := postBuilders(t, srv, hexutil.Encode(keys[0][:]), `{"enabled":true}`) + w := postBuilders(t, srv, hexutil.Encode(keys[0][:]), `{"builders":[]}`) require.Equal(t, http.StatusServiceUnavailable, w.Code) }) } @@ -231,7 +237,7 @@ func TestServer_GetBuilders(t *testing.T) { srv, keys := setupConfigServer(t, 1) pk := hexutil.Encode(keys[0][:]) require.Equal(t, http.StatusAccepted, postBuilders(t, srv, pk, - `{"enabled":true,"builders":[{"url":"https://a.example"}]}`).Code) + `{"builders":[{"url":"https://a.example"}]}`).Code) _, cfg := getBuilders(t, srv, pk) require.Equal(t, "0", *cfg.MinBid) require.Equal(t, "100", *cfg.BuilderBoostFactor) @@ -248,13 +254,11 @@ func TestServer_GetBuilders(t *testing.T) { require.NoError(t, srv.validatorService.SetProposerSettings(t.Context(), &proposer.Settings{ Version: proposer.SchemaV2, DefaultConfig: &proposer.Option{ - BuilderConfig: &proposer.BuilderConfig{Enabled: true, Builders: []*proposer.BuilderEntry{{URL: "https://default.example"}}}, + BuilderConfig: &proposer.BuilderConfig{Builders: []*proposer.BuilderEntry{{URL: "https://default.example"}}}, }, })) _, cfg := getBuilders(t, srv, pk) - require.NotNil(t, cfg.Enabled) - require.Equal(t, true, *cfg.Enabled) require.Equal(t, 1, len(cfg.Builders)) require.Equal(t, "https://default.example", cfg.Builders[0].Url) }) @@ -267,12 +271,10 @@ func TestServer_DeleteBuilders(t *testing.T) { // Removing an absent configuration succeeds. require.Equal(t, http.StatusNoContent, deleteBuilders(t, srv, pk).Code) - require.Equal(t, http.StatusAccepted, postBuilders(t, srv, pk, `{"enabled":true,"builders":[{"url":"https://a"}]}`).Code) + require.Equal(t, http.StatusAccepted, postBuilders(t, srv, pk, `{"builders":[{"url":"https://a"}]}`).Code) require.Equal(t, http.StatusNoContent, deleteBuilders(t, srv, pk).Code) // After delete the key follows defaults (no per-key builders). _, cfg := getBuilders(t, srv, pk) - require.NotNil(t, cfg.Enabled) - require.Equal(t, false, *cfg.Enabled) require.Equal(t, 0, len(cfg.Builders)) } diff --git a/validator/rpc/structs.go b/validator/rpc/structs.go index 93a3c4536f85..30460f95765d 100644 --- a/validator/rpc/structs.go +++ b/validator/rpc/structs.go @@ -113,34 +113,36 @@ type GraffitiData struct { Graffiti string `json:"graffiti"` } +type GetBuildersResponse struct { + Data *BuilderConfig `json:"data"` +} + // BuilderConfig is the keymanager-APIs #88 wire form: integers are decimal // strings, bytes 0x-hex. Builders nil = inherit, [] = use none (kept by no omitempty). type BuilderConfig struct { - Enabled *bool `json:"enabled"` MinBid *string `json:"min_bid,omitempty"` BuilderBoostFactor *string `json:"builder_boost_factor,omitempty"` Builders []*BuilderEntry `json:"builders"` } type BuilderEntry struct { - Url string `json:"url"` - AuthData *string `json:"auth_data,omitempty"` - BuilderPubkey *string `json:"builder_pubkey,omitempty"` - MaxExecutionPayment *string `json:"max_execution_payment,omitempty"` - MinBid *string `json:"min_bid,omitempty"` - BuilderBoostFactor *string `json:"builder_boost_factor,omitempty"` + Url string `json:"url"` + AuthData *string `json:"auth_data,omitempty"` + BuilderPubkeys []string `json:"builder_pubkeys"` + MaxExecutionPayment *string `json:"max_execution_payment,omitempty"` + MinBid *string `json:"min_bid,omitempty"` + BuilderBoostFactor *string `json:"builder_boost_factor,omitempty"` } const ( maxBuilderEntries = 64 // MAX_BUILDER_ENTRIES maxBuilderURLSize = 2048 // MAX_BUILDER_URL_SIZE maxAuthDataSize = 4096 // MAX_DATA_SIZE + maxBuilderPubkeys = 64 // MAX_BUILDER_PUBKEYS ) func builderConfigFromConsensus(bc *proposer.BuilderConfig) *BuilderConfig { - enabled := bc.Enabled out := &BuilderConfig{ - Enabled: &enabled, MinBid: new(strconv.FormatUint(uint64(bc.EffectiveMinBid()), 10)), BuilderBoostFactor: new(strconv.FormatUint(uint64(bc.EffectiveBuilderBoostFactor()), 10)), } @@ -155,6 +157,8 @@ func builderConfigFromConsensus(bc *proposer.BuilderConfig) *BuilderConfig { func builderEntryFromConsensus(be *proposer.BuilderEntry, bc *proposer.BuilderConfig) *BuilderEntry { out := &BuilderEntry{ + // Omitted builder_pubkeys resolves to the empty list (accept any builder). + BuilderPubkeys: make([]string, 0, len(be.Pubkeys)), MaxExecutionPayment: new(strconv.FormatUint(uint64(be.EffectiveMaxExecutionPayment(bc)), 10)), MinBid: new(strconv.FormatUint(uint64(be.EffectiveMinBid(bc)), 10)), BuilderBoostFactor: new(strconv.FormatUint(uint64(be.EffectiveBuilderBoostFactor(bc)), 10)), @@ -163,14 +167,14 @@ func builderEntryFromConsensus(be *proposer.BuilderEntry, bc *proposer.BuilderCo out.Url = be.URL out.AuthData = new(hexutil.Encode(be.EffectiveAuthData())) } - if len(be.Pubkey) != 0 { - out.BuilderPubkey = new(hexutil.Encode(be.Pubkey)) + for _, pk := range be.Pubkeys { + out.BuilderPubkeys = append(out.BuilderPubkeys, hexutil.Encode(pk)) } return out } func (in *BuilderConfig) ToConsensus() (*proposer.BuilderConfig, error) { - bc := &proposer.BuilderConfig{Enabled: in.Enabled != nil && *in.Enabled} + bc := &proposer.BuilderConfig{} if in.MinBid != nil { v, err := parseUint(*in.MinBid, "min_bid") if err != nil { @@ -224,20 +228,23 @@ func (in *BuilderEntry) ToConsensus(i int) (*proposer.BuilderEntry, error) { return nil, errors.Errorf("builders[%d].url is not a valid URL", i) } be.URL = in.Url - if in.BuilderPubkey != nil { - pk, err := hexutil.Decode(*in.BuilderPubkey) + if len(in.BuilderPubkeys) > maxBuilderPubkeys { + return nil, errors.Errorf("builders[%d].builder_pubkeys exceeds %d keys", i, maxBuilderPubkeys) + } + for _, raw := range in.BuilderPubkeys { + pk, err := hexutil.Decode(raw) if err != nil || len(pk) != fieldparams.BLSPubkeyLength { - return nil, errors.Errorf("builders[%d].builder_pubkey is not a valid BLS public key", i) + return nil, errors.Errorf("builders[%d].builder_pubkeys contains an invalid BLS public key", i) } - be.Pubkey = pk + be.Pubkeys = append(be.Pubkeys, pk) } if in.AuthData != nil { ad, err := hexutil.Decode(*in.AuthData) if err != nil { return nil, errors.Errorf("builders[%d].auth_data is not valid hex", i) } - if len(ad) > maxAuthDataSize { - return nil, errors.Errorf("builders[%d].auth_data exceeds %d bytes", i, maxAuthDataSize) + if len(ad) == 0 || len(ad) > maxAuthDataSize { + return nil, errors.Errorf("builders[%d].auth_data must be 1 to %d bytes", i, maxAuthDataSize) } be.AuthData = ad } From 4ea3e1aa2ce1ba309d847a2b3fc9f26f540a4a04 Mon Sep 17 00:00:00 2001 From: james-prysm Date: Tue, 11 Aug 2026 09:53:43 -0500 Subject: [PATCH 16/26] gaz --- config/proposer/BUILD.bazel | 1 - config/proposer/loader/BUILD.bazel | 1 - 2 files changed, 2 deletions(-) diff --git a/config/proposer/BUILD.bazel b/config/proposer/BUILD.bazel index 7fe6d79d0076..2e6c757fd354 100644 --- a/config/proposer/BUILD.bazel +++ b/config/proposer/BUILD.bazel @@ -40,6 +40,5 @@ go_test( "@com_github_ethereum_go_ethereum//common:go_default_library", "@com_github_ethereum_go_ethereum//common/hexutil:go_default_library", "@com_github_sirupsen_logrus//hooks/test:go_default_library", - "@org_golang_google_protobuf//proto:go_default_library", ], ) diff --git a/config/proposer/loader/BUILD.bazel b/config/proposer/loader/BUILD.bazel index 53f04c5029e9..401a0fbcb33c 100644 --- a/config/proposer/loader/BUILD.bazel +++ b/config/proposer/loader/BUILD.bazel @@ -22,7 +22,6 @@ go_test( "@com_github_ethereum_go_ethereum//common/hexutil:go_default_library", "@com_github_sirupsen_logrus//hooks/test:go_default_library", "@com_github_urfave_cli_v2//:go_default_library", - "@org_golang_google_protobuf//proto:go_default_library", ], ) From 9698cf1da3598dd68f414880be814b3f8265c407 Mon Sep 17 00:00:00 2001 From: james-prysm Date: Tue, 11 Aug 2026 11:27:14 -0500 Subject: [PATCH 17/26] self review points, don't migrate gas limit in v1 and take in in v2 if explicit --- ...mes-prysm_keymanager-builders-endpoints.md | 6 ++- cmd/prysmctl/validator/cmd.go | 3 +- cmd/prysmctl/validator/proposer_settings.go | 1 + config/proposer/loader/loader.go | 11 ++---- config/proposer/loader/loader_test.go | 16 ++++---- config/proposer/settings.go | 38 ++++++++----------- config/proposer/settings_test.go | 16 ++++---- validator/client/validator_test.go | 20 +++++----- .../rpc/handlers_validator_config_test.go | 4 +- 9 files changed, 56 insertions(+), 59 deletions(-) diff --git a/changelog/james-prysm_keymanager-builders-endpoints.md b/changelog/james-prysm_keymanager-builders-endpoints.md index a0d824667594..8239afcc8eaf 100644 --- a/changelog/james-prysm_keymanager-builders-endpoints.md +++ b/changelog/james-prysm_keymanager-builders-endpoints.md @@ -5,9 +5,13 @@ ### Changed - Add v2 proposer settings (`"version": 2`): builder fields a key does not set inherit from `default_config`. v2 has no `enabled` field; a key participates in builder registration when its resolved `builders` list names at least one builder, and an explicit empty `builders` list opts the key out. -- v1 builder settings are not migrated to v2: at the gloas fork fee recipients, gas limits, and graffiti carry over and v1 builder content is replaced with defaults, with a warning. `--enable-builder` has no effect with v2 settings. +- v1 builder settings are not migrated to v2: at the gloas fork fee recipients and graffiti carry over, while v1 builder content — including its gas limits — is replaced with defaults, with a warning. Gas limits apply only when explicitly set on v2 settings, so validators follow future chain-default gas limit increases unless they opt out. `--enable-builder` has no effect with v2 settings. - Setting a fee recipient, gas limit, or graffiti no longer snapshots `default_config`'s builder settings onto that key; the key keeps following the default as it changes. +### Deprecated + +- `--with-builder` generates legacy (pre-gloas) mev-boost builder settings, which are discontinued at the gloas fork; the command now warns when the flag is used. + ### Fixed - Fix the minimal slashing protection database decoding unset builder settings fields as explicit zero values; both validator DB backends now read the same stored settings identically. diff --git a/cmd/prysmctl/validator/cmd.go b/cmd/prysmctl/validator/cmd.go index 635a71cc4a33..3a24809ff534 100644 --- a/cmd/prysmctl/validator/cmd.go +++ b/cmd/prysmctl/validator/cmd.go @@ -55,7 +55,8 @@ var ( WithBuilderFlag = &cli.BoolFlag{ Name: "with-builder", Aliases: []string{"wb"}, - Usage: "adds default builder options to proposer settings output, used for enabling mev-boost and relays", + Usage: "adds legacy (pre-gloas) mev-boost builder options to the proposer settings output; " + + "these are discontinued at the gloas fork, where builder participation requires v2 settings with a builders list", } DefaultFeeRecipientFlag = &cli.StringFlag{ diff --git a/cmd/prysmctl/validator/proposer_settings.go b/cmd/prysmctl/validator/proposer_settings.go index 0a97481eb9a5..487adcef3c8e 100644 --- a/cmd/prysmctl/validator/proposer_settings.go +++ b/cmd/prysmctl/validator/proposer_settings.go @@ -69,6 +69,7 @@ func getProposerSettings(c *cli.Context, r io.Reader) error { log.Infof("The default fee recipient is set to %s", defaultFeeRecipient) var builderSettings *validatorpb.BuilderConfig if c.Bool(WithBuilderFlag.Name) { + log.Warnf("--%s generates legacy (pre-gloas) mev-boost settings; they are discontinued at the gloas fork. Gloas builder participation requires v2 proposer settings with a builders list.", WithBuilderFlag.Name) builderSettings = &validatorpb.BuilderConfig{ Enabled: true, GasLimit: validatorType.Uint64(params.BeaconConfig().DefaultBuilderGasLimit), diff --git a/config/proposer/loader/loader.go b/config/proposer/loader/loader.go index e15fb9e8de5d..a8e4614aa4d0 100644 --- a/config/proposer/loader/loader.go +++ b/config/proposer/loader/loader.go @@ -253,8 +253,6 @@ func (psl *SettingsLoader) processProposerSettings(loadedSettings, dbSettings *v // through Builder; v2 lives on Option directly. func mergeProposerSettings(loaded, db *validatorpb.ProposerSettingsPayload, options *flagOptions) *validatorpb.ProposerSettingsPayload { merged := &validatorpb.ProposerSettingsPayload{} - // The schema version never regresses: the highest version wins and the - // lower-version side's content is promoted before merging. if db != nil { merged.Version = db.Version } @@ -302,17 +300,14 @@ func markExplicitEmptyBuilders(p *validatorpb.ProposerSettingsPayload) { } } -// promotePayloadToV2 mirrors Settings.UpgradeToV2: gas limits are promoted to -// the option level and v1 builder content, which does not apply to v2, is dropped. +// promotePayloadToV2 mirrors Settings.UpgradeToV2: v1 builder content, including +// its gas limits, does not apply to v2 and is dropped. func promotePayloadToV2(p *validatorpb.ProposerSettingsPayload) { dropped := false promote := func(opt *validatorpb.ProposerOptionPayload) { if opt == nil || opt.Builder == nil { return } - if opt.GasLimit == 0 { - opt.GasLimit = opt.Builder.GasLimit - } opt.Builder = nil dropped = true } @@ -321,7 +316,7 @@ func promotePayloadToV2(p *validatorpb.ProposerSettingsPayload) { promote(opt) } if dropped { - log.Warn("v1 builder settings do not apply to the v2 schema and were replaced with defaults; provide v2 proposer settings to configure builders") + log.Warn("v1 builder settings, including gas limits, do not apply to the v2 schema and were replaced with defaults; provide v2 proposer settings to configure builders") } } diff --git a/config/proposer/loader/loader_test.go b/config/proposer/loader/loader_test.go index 69f17b5a8efc..b4a531027739 100644 --- a/config/proposer/loader/loader_test.go +++ b/config/proposer/loader/loader_test.go @@ -1230,9 +1230,9 @@ func Test_mergeProposerSettings_VersionPrecedence(t *testing.T) { &flagOptions{}, ) require.Equal(t, uint32(proposer.SchemaV2), merged.Version) - // v1 builder content does not carry into v2; the gas limit is promoted first. + // v1 builder content, including its gas limit, does not carry into v2. require.IsNil(t, merged.DefaultConfig.Builder) - require.Equal(t, validator.Uint64(30000000), merged.DefaultConfig.GasLimit) + require.Equal(t, validator.Uint64(0), merged.DefaultConfig.GasLimit) }) t.Run("file per-key section replaces the DB's entirely", func(t *testing.T) { dbPayload := &validatorpb.ProposerSettingsPayload{ @@ -1300,16 +1300,16 @@ func TestSettingsLoader_V1FileAfterMigratedDB(t *testing.T) { require.NotNil(t, got) require.Equal(t, proposer.SchemaV2, got.Version) - // The v1 file's builder content is dropped; its gas limit is promoted first. + // The v1 file's builder content, including gas limits, is dropped at merge time. require.IsNil(t, got.DefaultConfig.BuilderConfig) - require.Equal(t, validator.Uint64(40000000), got.DefaultConfig.GasLimit) + require.Equal(t, validator.Uint64(0), got.DefaultConfig.GasLimit) assert.LogsDoNotContain(t, hook, "deprecated v1 schema") - // Already v2: the file's builder gas limits were promoted at merge time. require.Equal(t, false, got.UpgradeToV2()) key1, err := hexutil.Decode("0xa057816155ad77931185101128655c0191bd0214c201ca48ed887f6c4c6adf334070efcd75140eada5ac83a92506dd7a") require.NoError(t, err) - require.Equal(t, validator.Uint64(60000000), got.GasLimit(bytesutil.ToBytes48(key1))) + // The key follows the chain default rather than its stale v1 builder gas limit. + require.Equal(t, validator.Uint64(params.BeaconConfig().DefaultBuilderGasLimit), got.GasLimit(bytesutil.ToBytes48(key1))) } func Test_mergeProposerSettings_CreatesDefaultFromGasLimitFlag(t *testing.T) { @@ -1369,7 +1369,7 @@ func Test_mergeProposerSettings_VersionGatesBuilderReset(t *testing.T) { require.IsNil(t, merged.DefaultConfig.Builder) assert.LogsContain(t, hook, "has no effect with v2 proposer settings") }) - t.Run("v1 builder content merged into v2 is dropped with gas limit promoted", func(t *testing.T) { + t.Run("v1 builder content merged into v2 is dropped including its gas limit", func(t *testing.T) { file := &validatorpb.ProposerSettingsPayload{ DefaultConfig: &validatorpb.ProposerOptionPayload{ FeeRecipient: "0x", @@ -1379,7 +1379,7 @@ func Test_mergeProposerSettings_VersionGatesBuilderReset(t *testing.T) { db := &validatorpb.ProposerSettingsPayload{Version: proposer.SchemaV2} merged := mergeProposerSettings(file, db, &flagOptions{}) require.IsNil(t, merged.DefaultConfig.Builder) - require.Equal(t, validator.Uint64(30000000), merged.DefaultConfig.GasLimit) + require.Equal(t, validator.Uint64(0), merged.DefaultConfig.GasLimit) }) } diff --git a/config/proposer/settings.go b/config/proposer/settings.go index 04b7730b07d9..725b24e37861 100644 --- a/config/proposer/settings.go +++ b/config/proposer/settings.go @@ -70,7 +70,7 @@ func SettingFromConsensus(ps *validatorpb.ProposerSettingsPayload) (*Settings, e // Persisted configs may predate url-required and (url, auth_data) uniqueness; // url-less entries are dropped and the first entry wins, matching POST validation. func (ps *Settings) dedupBuilders() { - fix := func(opt *Option) { + dedup := func(opt *Option) { if opt == nil || opt.BuilderConfig == nil || len(opt.BuilderConfig.Builders) == 0 { return } @@ -88,9 +88,9 @@ func (ps *Settings) dedupBuilders() { opt.BuilderConfig.Builders = kept } } - fix(ps.DefaultConfig) + dedup(ps.DefaultConfig) for _, opt := range ps.ProposeConfig { - fix(opt) + dedup(opt) } } @@ -107,13 +107,12 @@ func verifyOption(key string, option *validatorpb.ProposerOptionPayload) error { return nil } -// BuilderConfig is the struct representation of the JSON config file set in the validator through the CLI. -// GasLimit is a number set to help the network decide on the maximum gas in each block. +// BuilderConfig is the in-memory builder settings. type BuilderConfig struct { - Enabled bool `json:"enabled,omitempty" yaml:"enabled,omitempty"` - GasLimit validator.Uint64 `json:"gas_limit,omitempty" yaml:"gas_limit,omitempty"` + Enabled bool `json:"enabled" yaml:"enabled"` // legacy v1 (mev-boost); ignored by v2, dropped at the gloas cutover + GasLimit validator.Uint64 `json:"gas_limit,omitempty" yaml:"gas_limit,omitempty"` // legacy v1; v2 gas limits live on the option MaxExecutionPayment *validator.Uint64 `json:"max_execution_payment,omitempty" yaml:"max_execution_payment,omitempty"` // explicit 0 = trustless-only; unset inherits - Builders []*BuilderEntry `json:"builders,omitempty" yaml:"builders,omitempty"` + Builders []*BuilderEntry `json:"builders" yaml:"builders"` // nil = inherit, [] = use none; no omitempty so the marker survives marshal MinBid *validator.Uint64 `json:"min_bid,omitempty" yaml:"min_bid,omitempty"` BuilderBoostFactor *validator.Uint64 `json:"builder_boost_factor,omitempty" yaml:"builder_boost_factor,omitempty"` } @@ -175,15 +174,13 @@ func (ps *Settings) RegistrationFor(pubkey [fieldparams.BLSPubkeyLength]byte) (c // Per-key builder fields inherit from the default, but registration still // requires a fee recipient configured at some level. bc := ps.EffectiveBuilderConfig(pubkey) - // v2 gas limits live on the option (where UpgradeToV2 and the gas-limit - // API write them); the builder-level value is a legacy fallback. + // v2 reads only explicitly set option-level gas limits (the gas-limit + // API writes there); legacy builder-level values are never consulted. switch { case ps.ProposeConfig[pubkey] != nil && ps.ProposeConfig[pubkey].GasLimit != 0: gasLimit = ps.ProposeConfig[pubkey].GasLimit case ps.DefaultConfig != nil && ps.DefaultConfig.GasLimit != 0: gasLimit = ps.DefaultConfig.GasLimit - case bc != nil && bc.GasLimit != 0: - gasLimit = bc.GasLimit } // v2 has no enabled flag: a key participates in builder registration // exactly when its resolved builders list names at least one builder. @@ -595,7 +592,7 @@ func (ps *Settings) WarnDeprecatedSchema() { if !ps.HasBuilderContent() { return } - log.Warn("Proposer settings use the deprecated v1 schema; v1 builder settings do not apply to gloas and are replaced with defaults at the fork (fee recipients, gas limits and graffiti carry over). Please migrate your settings source to v2.") + log.Warn("Proposer settings use the deprecated v1 schema; v1 builder settings, including gas limits, are replaced with defaults at the gloas fork (fee recipients and graffiti carry over). Please migrate your settings source to v2.") } // HasBuilderContent reports whether any level carries a builder config, i.e. @@ -612,30 +609,27 @@ func (ps *Settings) HasBuilderContent() bool { return false } -// UpgradeToV2 is the v1 cutover: gas limits are promoted to the option level and -// v1 builder configs, which do not apply to gloas, are dropped. Returns true if changed. +// UpgradeToV2 is the v1 cutover: v1 builder configs, including their gas limits, +// do not apply to gloas and are dropped. Returns true if anything changed. func (ps *Settings) UpgradeToV2() bool { if ps == nil || ps.isV2() { return false } dropped := false - migrate := func(opt *Option) { + drop := func(opt *Option) { if opt == nil || opt.BuilderConfig == nil { return } - if opt.GasLimit == 0 { - opt.GasLimit = opt.BuilderConfig.GasLimit - } opt.BuilderConfig = nil dropped = true } - migrate(ps.DefaultConfig) + drop(ps.DefaultConfig) for _, opt := range ps.ProposeConfig { - migrate(opt) + drop(opt) } ps.Version = SchemaV2 if dropped { - log.Warn("v1 builder settings do not apply to gloas and were replaced with defaults; provide v2 proposer settings to configure builders") + log.Warn("v1 builder settings, including gas limits, do not apply to gloas and were replaced with defaults; provide v2 proposer settings to configure builders") } return true } diff --git a/config/proposer/settings_test.go b/config/proposer/settings_test.go index 377a8498d44a..06838334215d 100644 --- a/config/proposer/settings_test.go +++ b/config/proposer/settings_test.go @@ -515,7 +515,7 @@ func TestSettings_UpgradeToV2(t *testing.T) { require.Equal(t, false, ps.UpgradeToV2()) }) - t.Run("v1 default lifts BuilderConfig.GasLimit to top-level and drops the builder config", func(t *testing.T) { + t.Run("v1 builder gas limit is not promoted; the builder config is dropped", func(t *testing.T) { ps := &Settings{ DefaultConfig: &Option{ BuilderConfig: &BuilderConfig{Enabled: true, GasLimit: validator.Uint64(42_000_000)}, @@ -523,8 +523,8 @@ func TestSettings_UpgradeToV2(t *testing.T) { } require.Equal(t, true, ps.UpgradeToV2()) require.Equal(t, SchemaV2, ps.Version) - require.Equal(t, validator.Uint64(42_000_000), ps.DefaultConfig.GasLimit) - // v1 builder opinions are about mev-boost and do not carry into v2. + // v1 gas limits are not carried over: the key follows the chain default. + require.Equal(t, validator.Uint64(0), ps.DefaultConfig.GasLimit) require.IsNil(t, ps.DefaultConfig.BuilderConfig) }) @@ -539,7 +539,7 @@ func TestSettings_UpgradeToV2(t *testing.T) { require.Equal(t, validator.Uint64(70_000_000), ps.DefaultConfig.GasLimit) }) - t.Run("per-validator builder gas limits promoted and builder configs dropped", func(t *testing.T) { + t.Run("per-validator builder gas limits are dropped, explicit top-level ones kept", func(t *testing.T) { pubkey2, err := hexutil.Decode("0xbedefeaa94e03438ea819bd4033c6c1bf6b04320ee2075b77273c08d02f8a61bcc303c2cdddddddddddddddddddddddd") require.NoError(t, err) pk2 := bytesutil.ToBytes48(pubkey2) @@ -552,9 +552,9 @@ func TestSettings_UpgradeToV2(t *testing.T) { require.Equal(t, true, ps.UpgradeToV2()) require.Equal(t, SchemaV2, ps.Version) require.Equal(t, true, ps.DefaultConfig == nil) - require.Equal(t, validator.Uint64(35_000_000), ps.ProposeConfig[pk].GasLimit) + require.Equal(t, validator.Uint64(0), ps.ProposeConfig[pk].GasLimit) require.IsNil(t, ps.ProposeConfig[pk].BuilderConfig) - // An explicit top-level gas limit wins over the builder value. + // An explicitly set top-level gas limit is not builder content and survives. require.Equal(t, validator.Uint64(50_000_000), ps.ProposeConfig[pk2].GasLimit) require.IsNil(t, ps.ProposeConfig[pk2].BuilderConfig) }) @@ -714,12 +714,14 @@ func TestRegistrationFor(t *testing.T) { Version: SchemaV2, DefaultConfig: &Option{ FeeRecipientConfig: &FeeRecipientConfig{FeeRecipient: recipient}, - BuilderConfig: &BuilderConfig{GasLimit: 123, Builders: []*BuilderEntry{{URL: "https://b.example"}}}, + GasLimit: 123, + BuilderConfig: &BuilderConfig{GasLimit: 456, Builders: []*BuilderEntry{{URL: "https://b.example"}}}, }, } fr, gl, enabled := ps.RegistrationFor(key) require.Equal(t, true, enabled) require.Equal(t, recipient, fr) + // Only the explicitly set option-level gas limit is read; never the builder's. require.Equal(t, validator.Uint64(123), gl) }) diff --git a/validator/client/validator_test.go b/validator/client/validator_test.go index 9d9b7be821df..664a0302c867 100644 --- a/validator/client/validator_test.go +++ b/validator/client/validator_test.go @@ -2946,8 +2946,8 @@ func TestValidator_buildProposerPreferences_GasLimitSources(t *testing.T) { settings *proposer.Settings needsDB bool wantGasLimit uint64 - // >0 asserts migration ran and persisted; 0 asserts shape preserved. - upgradedGasLimit validatorType.Uint64 + // asserts the cutover ran and persisted v2 with builder content dropped. + wantMigrated bool }{ { name: "v2 top-level GasLimit wins over legacy BuilderConfig.GasLimit", @@ -2963,7 +2963,7 @@ func TestValidator_buildProposerPreferences_GasLimitSources(t *testing.T) { wantGasLimit: 55555555, }, { - name: "gloas active: v1 settings upgraded; BuilderConfig.GasLimit promoted to Option.GasLimit", + name: "gloas active: v1 settings cut over; builder gas limit dropped, chain default used", gloasForkEpoch: 0, settings: &proposer.Settings{ DefaultConfig: &proposer.Option{ @@ -2971,9 +2971,9 @@ func TestValidator_buildProposerPreferences_GasLimitSources(t *testing.T) { BuilderConfig: &proposer.BuilderConfig{Enabled: true, GasLimit: 42000000}, }, }, - needsDB: true, - wantGasLimit: 42000000, - upgradedGasLimit: validatorType.Uint64(42000000), + needsDB: true, + wantGasLimit: chainDefault, + wantMigrated: true, }, { // Migration must not fire while pre-gloas registration path still @@ -3058,7 +3058,7 @@ func TestValidator_buildProposerPreferences_GasLimitSources(t *testing.T) { require.Equal(t, tt.wantGasLimit, prefs[0].Message.TargetGasLimit) ps := v.ProposerSettings() - if tt.upgradedGasLimit == 0 { + if !tt.wantMigrated { require.Equal(t, tt.settings.Version, ps.Version) if tt.settings.DefaultConfig != nil && tt.settings.DefaultConfig.BuilderConfig != nil { require.NotNil(t, ps.DefaultConfig.BuilderConfig) @@ -3066,14 +3066,14 @@ func TestValidator_buildProposerPreferences_GasLimitSources(t *testing.T) { return } require.Equal(t, proposer.SchemaV2, ps.Version) - require.Equal(t, tt.upgradedGasLimit, ps.DefaultConfig.GasLimit) - // The cutover drops v1 builder content after promoting its gas limit. + // The cutover drops v1 builder content including its gas limit. + require.Equal(t, validatorType.Uint64(0), ps.DefaultConfig.GasLimit) require.IsNil(t, ps.DefaultConfig.BuilderConfig) dbps, err := v.db.ProposerSettings(t.Context()) require.NoError(t, err) require.Equal(t, proposer.SchemaV2, dbps.Version) - require.Equal(t, tt.upgradedGasLimit, dbps.DefaultConfig.GasLimit) + require.Equal(t, validatorType.Uint64(0), dbps.DefaultConfig.GasLimit) }) } } diff --git a/validator/rpc/handlers_validator_config_test.go b/validator/rpc/handlers_validator_config_test.go index 528b6631ff98..b44bdc5f0e82 100644 --- a/validator/rpc/handlers_validator_config_test.go +++ b/validator/rpc/handlers_validator_config_test.go @@ -145,8 +145,8 @@ func TestServer_SetBuilders(t *testing.T) { // Builder lists are v2 content: POST migrates the schema in place. require.Equal(t, proposer.SchemaV2, got.Version) opt := got.ProposeConfig[keys[0]] - // The builder gas limit is hoisted to the option by the upgrade. - require.Equal(t, validator.Uint64(999), opt.GasLimit) + // The v1 builder gas limit is dropped with the rest of the v1 builder content. + require.Equal(t, validator.Uint64(0), opt.GasLimit) require.Equal(t, 1, len(opt.BuilderConfig.Builders)) }) From bf4fd8c34daf22fb5544281a8bc0d18422680161 Mon Sep 17 00:00:00 2001 From: james-prysm Date: Tue, 11 Aug 2026 13:07:23 -0500 Subject: [PATCH 18/26] fixing test order --- validator/client/validator_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/validator/client/validator_test.go b/validator/client/validator_test.go index 664a0302c867..c802dc307aee 100644 --- a/validator/client/validator_test.go +++ b/validator/client/validator_test.go @@ -2944,8 +2944,8 @@ func TestValidator_buildProposerPreferences_GasLimitSources(t *testing.T) { name string gloasForkEpoch primitives.Epoch settings *proposer.Settings - needsDB bool wantGasLimit uint64 + needsDB bool // asserts the cutover ran and persisted v2 with builder content dropped. wantMigrated bool }{ From 3cd047b9d57b44908076adca876a154c158b376f Mon Sep 17 00:00:00 2001 From: james-prysm Date: Tue, 11 Aug 2026 16:57:35 -0500 Subject: [PATCH 19/26] adding gloas gates for endpoints, fixing bugs after self review, make sure we only use gas limits on explicitly set upgraded settings --- ...mes-prysm_keymanager-builders-endpoints.md | 7 +- config/proposer/loader/loader.go | 55 +++--- config/proposer/loader/loader_test.go | 67 +++++-- .../good-v2-proposer-config-unversioned.json | 16 ++ config/proposer/settings.go | 176 +++++++----------- config/proposer/settings_test.go | 76 ++++---- validator/client/propose.go | 3 +- validator/client/propose_test.go | 8 +- validator/client/validator.go | 16 +- validator/client/validator_test.go | 23 ++- validator/rpc/handlers_keymanager.go | 17 +- validator/rpc/handlers_keymanager_test.go | 81 ++++---- validator/rpc/handlers_validator_config.go | 27 ++- .../rpc/handlers_validator_config_test.go | 29 +++ 14 files changed, 341 insertions(+), 260 deletions(-) create mode 100644 config/proposer/loader/testdata/good-v2-proposer-config-unversioned.json diff --git a/changelog/james-prysm_keymanager-builders-endpoints.md b/changelog/james-prysm_keymanager-builders-endpoints.md index 8239afcc8eaf..d603b2710c48 100644 --- a/changelog/james-prysm_keymanager-builders-endpoints.md +++ b/changelog/james-prysm_keymanager-builders-endpoints.md @@ -1,11 +1,12 @@ ### Added -- Add `GET`/`POST`/`DELETE /eth/v1/validator/{pubkey}/builders` keymanager endpoints for per-key builder configuration (keymanager-APIs #88). +- Add `GET`/`POST`/`DELETE /eth/v1/validator/{pubkey}/builders` keymanager endpoints for per-key builder configuration (keymanager-APIs #88). The endpoints respond 501 on networks without a scheduled gloas fork, where builder configuration cannot take effect. ### Changed -- Add v2 proposer settings (`"version": 2`): builder fields a key does not set inherit from `default_config`. v2 has no `enabled` field; a key participates in builder registration when its resolved `builders` list names at least one builder, and an explicit empty `builders` list opts the key out. -- v1 builder settings are not migrated to v2: at the gloas fork fee recipients and graffiti carry over, while v1 builder content — including its gas limits — is replaced with defaults, with a warning. Gas limits apply only when explicitly set on v2 settings, so validators follow future chain-default gas limit increases unless they opt out. `--enable-builder` has no effect with v2 settings. +- Add v2 proposer settings (`"version": 2`): builder fields a key does not set inherit from `default_config`; v2 has no `enabled` field. A settings source without a `version` that contains v2 builder fields (`builders`, `min_bid`, `builder_boost_factor`, `max_execution_payment`) is treated as version 2 automatically. Proposer-settings semantics are keyed on the gloas fork, not the schema version: before the fork the legacy mev-boost rules apply (with a resolved nonempty `builders` list also opting a key in, and an explicit empty list opting it out), and from the fork on only v2 content is read. Writing builders via the keymanager API never changes other keys' behavior or drops v1 content. +- v1 builder settings are not migrated to v2: at the gloas fork fee recipients and graffiti carry over, while v1 builder content — including its gas limits — is dropped and replaced with defaults, with a warning. Gas limits apply post-fork only when explicitly set at the option level, so validators follow future chain-default gas limit increases unless they opt out. +- The gas-limit keymanager API writes the option-level gas limit and no longer requires an enabled builder; deleting a gas limit unsets it (following the chain default) instead of pinning the current default value. Builder registration resolves fee recipients and participation independently, so a key with an enabled builder and only a default fee recipient now registers. - Setting a fee recipient, gas limit, or graffiti no longer snapshots `default_config`'s builder settings onto that key; the key keeps following the default as it changes. ### Deprecated diff --git a/config/proposer/loader/loader.go b/config/proposer/loader/loader.go index a8e4614aa4d0..5ae4500fc5d9 100644 --- a/config/proposer/loader/loader.go +++ b/config/proposer/loader/loader.go @@ -215,6 +215,7 @@ func (psl *SettingsLoader) loadFromFile(cliCtx *cli.Context, dbSettings *validat return nil, errors.Errorf("proposer settings is empty after unmarshalling from file specified by %s flag", flags.ProposerSettingsFlag.Name) } markExplicitEmptyBuilders(settingFromFile) + inferSchemaVersion(settingFromFile) log.WithField(flags.ProposerSettingsFlag.Name, cliCtx.String(flags.ProposerSettingsFlag.Name)).Info("Proposer settings loaded from file") return psl.processProposerSettings(settingFromFile, dbSettings), nil } @@ -228,6 +229,7 @@ func (psl *SettingsLoader) loadFromURL(cliCtx *cli.Context, dbSettings *validato return nil, errors.Errorf("proposer settings is empty after unmarshalling from url specified by %s flag", flags.ProposerSettingsURLFlag.Name) } markExplicitEmptyBuilders(settingFromURL) + inferSchemaVersion(settingFromURL) log.WithField(flags.ProposerSettingsURLFlag.Name, cliCtx.String(flags.ProposerSettingsURLFlag.Name)).Infof("Proposer settings loaded from URL") return psl.processProposerSettings(settingFromURL, dbSettings), nil } @@ -259,14 +261,6 @@ func mergeProposerSettings(loaded, db *validatorpb.ProposerSettingsPayload, opti if loaded != nil && loaded.Version > merged.Version { merged.Version = loaded.Version } - if merged.Version == proposer.SchemaV2 { - if db != nil && db.Version < proposer.SchemaV2 { - promotePayloadToV2(db) - } - if loaded != nil && loaded.Version < proposer.SchemaV2 { - promotePayloadToV2(loaded) - } - } var builderConfig *validatorpb.BuilderConfig var gasLimitOnly *validator.Uint64 @@ -300,24 +294,32 @@ func markExplicitEmptyBuilders(p *validatorpb.ProposerSettingsPayload) { } } -// promotePayloadToV2 mirrors Settings.UpgradeToV2: v1 builder content, including -// its gas limits, does not apply to v2 and is dropped. -func promotePayloadToV2(p *validatorpb.ProposerSettingsPayload) { - dropped := false - promote := func(opt *validatorpb.ProposerOptionPayload) { +// inferSchemaVersion stamps version 2 on an unversioned source carrying v2-only +// builder fields, so a forgotten "version" cannot get gloas content dropped as v1. +func inferSchemaVersion(p *validatorpb.ProposerSettingsPayload) { + if p.Version != proposer.SchemaV1Unset { + return + } + hasV2 := func(opt *validatorpb.ProposerOptionPayload) bool { if opt == nil || opt.Builder == nil { - return + return false } - opt.Builder = nil - dropped = true + b := opt.Builder + return len(b.Builders) > 0 || b.BuildersSet || b.MinBid != nil || + b.BuilderBoostFactor != nil || b.MaxExecutionPayment != nil } - promote(p.DefaultConfig) + found := hasV2(p.DefaultConfig) for _, opt := range p.ProposerConfig { - promote(opt) + if found { + break + } + found = hasV2(opt) } - if dropped { - log.Warn("v1 builder settings, including gas limits, do not apply to the v2 schema and were replaced with defaults; provide v2 proposer settings to configure builders") + if !found { + return } + p.Version = proposer.SchemaV2 + log.Info("Proposer settings contain v2 builder fields but no version; treating the source as version 2") } // selectProposerConfig keeps the pre-v2 source precedence: a loaded per-key @@ -383,10 +385,17 @@ func mergeProposerSettingsV2(merged, loaded, db *validatorpb.ProposerSettingsPay } merged.ProposerConfig = selectProposerConfig(db, loaded) - // v2 has no enabled toggle: participation follows the configured builders - // list, so --enable-builder has nothing to force on. + // --enable-builder is legacy content: it still forces the default mev-boost + // toggle on for pre-gloas registrations, and is inert from the fork onward. if builderConfig != nil { - log.Warnf("--%s has no effect with v2 proposer settings; configure builders via the settings source or keymanager API", flags.EnableBuilderFlag.Name) + if merged.DefaultConfig == nil { + merged.DefaultConfig = &validatorpb.ProposerOptionPayload{} + } + if merged.DefaultConfig.Builder == nil { + merged.DefaultConfig.Builder = &validatorpb.BuilderConfig{} + } + merged.DefaultConfig.Builder.Enabled = true + log.Warnf("--%s is legacy (pre-gloas) mev-boost content and has no effect after the gloas fork; configure builders via the settings source or keymanager API", flags.EnableBuilderFlag.Name) } if gasLimitOnly == nil { diff --git a/config/proposer/loader/loader_test.go b/config/proposer/loader/loader_test.go index b4a531027739..3b00c36773e7 100644 --- a/config/proposer/loader/loader_test.go +++ b/config/proposer/loader/loader_test.go @@ -405,6 +405,41 @@ func TestProposerSettingsLoader(t *testing.T) { }, wantErr: "", }, + { + name: "unversioned file with v2 builder fields is inferred as v2", + args: args{ + proposerSettingsFlagValues: &proposerSettingsFlag{ + dir: "./testdata/good-v2-proposer-config-unversioned.json", + }, + }, + want: func() *proposer.Settings { + key1, err := hexutil.Decode("0xa057816155ad77931185101128655c0191bd0214c201ca48ed887f6c4c6adf334070efcd75140eada5ac83a92506dd7a") + require.NoError(t, err) + u64 := func(v uint64) *validator.Uint64 { u := validator.Uint64(v); return &u } + return &proposer.Settings{ + Version: proposer.SchemaV2, + ProposeConfig: map[[fieldparams.BLSPubkeyLength]byte]*proposer.Option{ + bytesutil.ToBytes48(key1): { + FeeRecipientConfig: &proposer.FeeRecipientConfig{ + FeeRecipient: common.HexToAddress("0x50155530FCE8a85ec7055A5F8b2bE214B3DaeFd3"), + }, + BuilderConfig: &proposer.BuilderConfig{ + MinBid: u64(500000000), + Builders: []*proposer.BuilderEntry{ + {URL: "https://builder-a.example"}, + }, + }, + }, + }, + DefaultConfig: &proposer.Option{ + FeeRecipientConfig: &proposer.FeeRecipientConfig{ + FeeRecipient: common.HexToAddress("0x6e35733c5af9B61374A128e6F85f553aF09ff89A"), + }, + }, + } + }, + wantErr: "", + }, { name: "v2 file with builders list loads at v2 and dedups duplicate builder urls", args: args{ @@ -1219,7 +1254,7 @@ func Test_mergeProposerSettings_VersionPrecedence(t *testing.T) { ) require.Equal(t, uint32(proposer.SchemaV2), merged.Version) }) - t.Run("v1 content merged into a v2 db is promoted, version never regresses", func(t *testing.T) { + t.Run("v1 content merged into a v2 db coexists; version never regresses", func(t *testing.T) { merged := mergeProposerSettings( &validatorpb.ProposerSettingsPayload{ DefaultConfig: &validatorpb.ProposerOptionPayload{ @@ -1230,9 +1265,10 @@ func Test_mergeProposerSettings_VersionPrecedence(t *testing.T) { &flagOptions{}, ) require.Equal(t, uint32(proposer.SchemaV2), merged.Version) - // v1 builder content, including its gas limit, does not carry into v2. - require.IsNil(t, merged.DefaultConfig.Builder) - require.Equal(t, validator.Uint64(0), merged.DefaultConfig.GasLimit) + // Semantics are fork-keyed: legacy content stays for pre-gloas reads and + // is stripped by the post-fork cleanup, not by the merge. + require.NotNil(t, merged.DefaultConfig.Builder) + require.Equal(t, true, merged.DefaultConfig.Builder.Enabled) }) t.Run("file per-key section replaces the DB's entirely", func(t *testing.T) { dbPayload := &validatorpb.ProposerSettingsPayload{ @@ -1300,16 +1336,16 @@ func TestSettingsLoader_V1FileAfterMigratedDB(t *testing.T) { require.NotNil(t, got) require.Equal(t, proposer.SchemaV2, got.Version) - // The v1 file's builder content, including gas limits, is dropped at merge time. - require.IsNil(t, got.DefaultConfig.BuilderConfig) - require.Equal(t, validator.Uint64(0), got.DefaultConfig.GasLimit) + // The v1 file's builder content survives the merge for pre-gloas reads; + // the post-fork cleanup is what strips it. + require.NotNil(t, got.DefaultConfig.BuilderConfig) assert.LogsDoNotContain(t, hook, "deprecated v1 schema") require.Equal(t, false, got.UpgradeToV2()) key1, err := hexutil.Decode("0xa057816155ad77931185101128655c0191bd0214c201ca48ed887f6c4c6adf334070efcd75140eada5ac83a92506dd7a") require.NoError(t, err) - // The key follows the chain default rather than its stale v1 builder gas limit. - require.Equal(t, validator.Uint64(params.BeaconConfig().DefaultBuilderGasLimit), got.GasLimit(bytesutil.ToBytes48(key1))) + // Pre-gloas reads still resolve the v1 builder gas limit as a fallback. + require.Equal(t, validator.Uint64(60000000), got.GasLimit(bytesutil.ToBytes48(key1))) } func Test_mergeProposerSettings_CreatesDefaultFromGasLimitFlag(t *testing.T) { @@ -1358,7 +1394,7 @@ func Test_mergeProposerSettings_VersionGatesBuilderReset(t *testing.T) { require.NotNil(t, merged.DefaultConfig.Builder) require.Equal(t, validator.Uint64(40000000), merged.DefaultConfig.Builder.GasLimit) }) - t.Run("v2 --enable-builder has no effect and warns", func(t *testing.T) { + t.Run("v2 --enable-builder still forces the legacy toggle and warns", func(t *testing.T) { hook := logtest.NewGlobal() opts := &flagOptions{builderConfig: &proposer.BuilderConfig{Enabled: true}} db := &validatorpb.ProposerSettingsPayload{ @@ -1366,10 +1402,11 @@ func Test_mergeProposerSettings_VersionGatesBuilderReset(t *testing.T) { DefaultConfig: &validatorpb.ProposerOptionPayload{FeeRecipient: "0x"}, } merged := mergeProposerSettings(nil, db, opts) - require.IsNil(t, merged.DefaultConfig.Builder) - assert.LogsContain(t, hook, "has no effect with v2 proposer settings") + require.NotNil(t, merged.DefaultConfig.Builder) + require.Equal(t, true, merged.DefaultConfig.Builder.Enabled) + assert.LogsContain(t, hook, "no effect after the gloas fork") }) - t.Run("v1 builder content merged into v2 is dropped including its gas limit", func(t *testing.T) { + t.Run("v1 builder content merged into v2 coexists until the post-fork cleanup", func(t *testing.T) { file := &validatorpb.ProposerSettingsPayload{ DefaultConfig: &validatorpb.ProposerOptionPayload{ FeeRecipient: "0x", @@ -1378,8 +1415,8 @@ func Test_mergeProposerSettings_VersionGatesBuilderReset(t *testing.T) { } db := &validatorpb.ProposerSettingsPayload{Version: proposer.SchemaV2} merged := mergeProposerSettings(file, db, &flagOptions{}) - require.IsNil(t, merged.DefaultConfig.Builder) - require.Equal(t, validator.Uint64(0), merged.DefaultConfig.GasLimit) + require.NotNil(t, merged.DefaultConfig.Builder) + require.Equal(t, validator.Uint64(30000000), merged.DefaultConfig.Builder.GasLimit) }) } diff --git a/config/proposer/loader/testdata/good-v2-proposer-config-unversioned.json b/config/proposer/loader/testdata/good-v2-proposer-config-unversioned.json new file mode 100644 index 000000000000..bced45e8c438 --- /dev/null +++ b/config/proposer/loader/testdata/good-v2-proposer-config-unversioned.json @@ -0,0 +1,16 @@ +{ + "proposer_config": { + "0xa057816155ad77931185101128655c0191bd0214c201ca48ed887f6c4c6adf334070efcd75140eada5ac83a92506dd7a": { + "fee_recipient": "0x50155530FCE8a85ec7055A5F8b2bE214B3DaeFd3", + "builder": { + "min_bid": "500000000", + "builders": [ + { "url": "https://builder-a.example" } + ] + } + } + }, + "default_config": { + "fee_recipient": "0x6e35733c5af9B61374A128e6F85f553aF09ff89A" + } +} diff --git a/config/proposer/settings.go b/config/proposer/settings.go index 725b24e37861..eda4a55a0769 100644 --- a/config/proposer/settings.go +++ b/config/proposer/settings.go @@ -153,67 +153,48 @@ func (ps *Settings) EffectiveBuilderConfig(key [fieldparams.BLSPubkeyLength]byte return effectiveBuilderConfig(perKey, def) } -// RegistrationFor resolves pubkey's validator registration: fee recipient, gas -// limit, and whether to register with the builder at all. +// RegistrationFor resolves pubkey's mev-boost registration: fee recipient, gas +// limit, and participation. Registrations are pushed pre-gloas only. func (ps *Settings) RegistrationFor(pubkey [fieldparams.BLSPubkeyLength]byte) (common.Address, validator.Uint64, bool) { feeRecipient := common.HexToAddress(params.BeaconConfig().EthBurnAddressHex) gasLimit := validator.Uint64(params.BeaconConfig().DefaultBuilderGasLimit) if ps == nil { return feeRecipient, gasLimit, false } - if ps.isV2() { - hasFeeRecipient := false - if ps.DefaultConfig != nil && ps.DefaultConfig.FeeRecipientConfig != nil { - feeRecipient = ps.DefaultConfig.FeeRecipientConfig.FeeRecipient - hasFeeRecipient = true - } - if opt, ok := ps.ProposeConfig[pubkey]; ok && opt != nil && opt.FeeRecipientConfig != nil { - feeRecipient = opt.FeeRecipientConfig.FeeRecipient - hasFeeRecipient = true - } - // Per-key builder fields inherit from the default, but registration still - // requires a fee recipient configured at some level. - bc := ps.EffectiveBuilderConfig(pubkey) - // v2 reads only explicitly set option-level gas limits (the gas-limit - // API writes there); legacy builder-level values are never consulted. - switch { - case ps.ProposeConfig[pubkey] != nil && ps.ProposeConfig[pubkey].GasLimit != 0: - gasLimit = ps.ProposeConfig[pubkey].GasLimit - case ps.DefaultConfig != nil && ps.DefaultConfig.GasLimit != 0: - gasLimit = ps.DefaultConfig.GasLimit - } - // v2 has no enabled flag: a key participates in builder registration - // exactly when its resolved builders list names at least one builder. - return feeRecipient, gasLimit, bc != nil && len(bc.Builders) > 0 && hasFeeRecipient - } - // v1 keeps object-level semantics: a per-key builder config wins wholesale, - // so a disabled one opts the key out even under an enabled default. - enabled := false + hasFeeRecipient := false if ps.DefaultConfig != nil && ps.DefaultConfig.FeeRecipientConfig != nil { feeRecipient = ps.DefaultConfig.FeeRecipientConfig.FeeRecipient - if ps.DefaultConfig.BuilderConfig.IsEnabled() { - // Zero means unset (API-created configs): keep the chain default, - // matching the loader's reviewGasLimit normalization. - if ps.DefaultConfig.BuilderConfig.GasLimit != 0 { - gasLimit = ps.DefaultConfig.BuilderConfig.GasLimit - } - enabled = true - } + hasFeeRecipient = true } - if opt, ok := ps.ProposeConfig[pubkey]; ok && opt != nil && opt.FeeRecipientConfig != nil { + opt := ps.ProposeConfig[pubkey] + if opt != nil && opt.FeeRecipientConfig != nil { feeRecipient = opt.FeeRecipientConfig.FeeRecipient - if bc := opt.BuilderConfig; bc != nil { - if bc.IsEnabled() { - if bc.GasLimit != 0 { - gasLimit = bc.GasLimit - } - enabled = true - } else { - enabled = false - } - } + hasFeeRecipient = true } - return feeRecipient, gasLimit, enabled + // Legacy enabled is object-level: a per-key builder config wins wholesale, + // so a disabled one opts the key out even under an enabled default. + enabled := ps.DefaultConfig != nil && ps.DefaultConfig.BuilderConfig.IsEnabled() + if opt != nil && opt.BuilderConfig != nil { + enabled = opt.BuilderConfig.IsEnabled() + } + // v2 content opts in independently: a resolved nonempty builders list + // participates; an explicit empty list stays opted out. + if bc := ps.EffectiveBuilderConfig(pubkey); bc != nil && len(bc.Builders) > 0 { + enabled = true + } + // Explicitly set option-level gas limits win; legacy builder-level values + // are the pre-gloas fallback. + switch { + case opt != nil && opt.GasLimit != 0: + gasLimit = opt.GasLimit + case ps.DefaultConfig != nil && ps.DefaultConfig.GasLimit != 0: + gasLimit = ps.DefaultConfig.GasLimit + case opt != nil && opt.BuilderConfig != nil && opt.BuilderConfig.GasLimit != 0: + gasLimit = opt.BuilderConfig.GasLimit + case ps.DefaultConfig != nil && ps.DefaultConfig.BuilderConfig != nil && ps.DefaultConfig.BuilderConfig.GasLimit != 0: + gasLimit = ps.DefaultConfig.BuilderConfig.GasLimit + } + return feeRecipient, gasLimit, enabled && hasFeeRecipient } // EntryIdentity is what makes a builder entry unique: its url compared as the @@ -370,6 +351,15 @@ const ( SchemaV2 uint32 = 2 ) +// FreshSettingsVersion is the schema stamped on settings the keymanager APIs +// create from nothing: v2 once the network schedules gloas, legacy before. +func FreshSettingsVersion() uint32 { + if params.GloasEnabled() { + return SchemaV2 + } + return SchemaV1Unset +} + // Settings is a Prysm internal representation of the fee recipient config on the validator client. // validatorpb.ProposerSettingsPayload maps to Settings on import through the CLI. type Settings struct { @@ -650,20 +640,22 @@ func (ps *Settings) TargetGasLimit(pubkey [fieldparams.BLSPubkeyLength]byte) val return chainDefault } -// GasLimit resolves pubkey's gas limit: per-key, else default config, else chain -// default. v1 reads builder gas limits; v2 reads the top-level fields. +// GasLimit resolves pubkey's gas limit: explicitly set option-level values win, +// legacy builder-level values are the fallback, else the chain default. func (ps *Settings) GasLimit(pubkey [fieldparams.BLSPubkeyLength]byte) validator.Uint64 { chainDefault := validator.Uint64(params.BeaconConfig().DefaultBuilderGasLimit) if ps == nil { return chainDefault } - if ps.isV2() { - return ps.TargetGasLimit(pubkey) - } - if opt, ok := ps.ProposeConfig[pubkey]; ok && opt != nil && opt.BuilderConfig != nil && opt.BuilderConfig.GasLimit != 0 { + opt := ps.ProposeConfig[pubkey] + switch { + case opt != nil && opt.GasLimit != 0: + return opt.GasLimit + case ps.DefaultConfig != nil && ps.DefaultConfig.GasLimit != 0: + return ps.DefaultConfig.GasLimit + case opt != nil && opt.BuilderConfig != nil && opt.BuilderConfig.GasLimit != 0: return opt.BuilderConfig.GasLimit - } - if ps.DefaultConfig != nil && ps.DefaultConfig.BuilderConfig != nil && ps.DefaultConfig.BuilderConfig.GasLimit != 0 { + case ps.DefaultConfig != nil && ps.DefaultConfig.BuilderConfig != nil && ps.DefaultConfig.BuilderConfig.GasLimit != 0: return ps.DefaultConfig.BuilderConfig.GasLimit } return chainDefault @@ -683,42 +675,13 @@ func (ps *Settings) UpsertProposeOption(pubkey [fieldparams.BLSPubkeyLength]byte return opt } -// SetGasLimit writes the per-pubkey gas limit. v1 requires existing settings -// with builder enabled. +// SetGasLimit writes the per-pubkey gas limit at the option level, where both +// pre-gloas registrations and post-gloas preferences read it first. func (ps *Settings) SetGasLimit(pubkey [fieldparams.BLSPubkeyLength]byte, gasLimit validator.Uint64) error { if ps == nil { return errors.New("No proposer settings were found to update") } - if ps.isV2() { - ps.UpsertProposeOption(pubkey).GasLimit = gasLimit - return nil - } - builderEnabled := func(o *Option) bool { - return o != nil && o.BuilderConfig.IsEnabled() - } - if ps.ProposeConfig == nil { - if !builderEnabled(ps.DefaultConfig) { - return errors.New("Gas limit changes only apply when builder is enabled") - } - ps.ProposeConfig = make(map[[fieldparams.BLSPubkeyLength]byte]*Option) - opt := ps.DefaultConfig.Clone() - opt.BuilderConfig.GasLimit = gasLimit - ps.ProposeConfig[pubkey] = opt - return nil - } - if opt, found := ps.ProposeConfig[pubkey]; found { - if !builderEnabled(opt) { - return errors.New("Gas limit changes only apply when builder is enabled") - } - opt.BuilderConfig.GasLimit = gasLimit - return nil - } - if !builderEnabled(ps.DefaultConfig) { - return errors.New("Gas limit changes only apply when builder is enabled") - } - opt := ps.DefaultConfig.Clone() - opt.BuilderConfig.GasLimit = gasLimit - ps.ProposeConfig[pubkey] = opt + ps.UpsertProposeOption(pubkey).GasLimit = gasLimit return nil } @@ -729,26 +692,27 @@ func (ps *Settings) ResetGasLimit(pubkey [fieldparams.BLSPubkeyLength]byte) bool return false } chainDefault := validator.Uint64(params.BeaconConfig().DefaultBuilderGasLimit) - if ps.isV2() { - opt, found := ps.ProposeConfig[pubkey] - if !found || opt == nil || opt.GasLimit == 0 { - return false - } + opt, found := ps.ProposeConfig[pubkey] + if !found || opt == nil { + return false + } + reset := false + if opt.GasLimit != 0 { if ps.DefaultConfig != nil && ps.DefaultConfig.GasLimit != 0 { opt.GasLimit = ps.DefaultConfig.GasLimit } else { - opt.GasLimit = chainDefault + opt.GasLimit = 0 } - return true + reset = true } - opt, found := ps.ProposeConfig[pubkey] - if !found || opt == nil || opt.BuilderConfig == nil { - return false - } - if ps.DefaultConfig != nil && ps.DefaultConfig.BuilderConfig != nil { - opt.BuilderConfig.GasLimit = ps.DefaultConfig.BuilderConfig.GasLimit - } else { - opt.BuilderConfig.GasLimit = chainDefault + // Legacy per-key builder gas limits reset to the default's builder value. + if opt.BuilderConfig != nil && opt.BuilderConfig.GasLimit != 0 { + if ps.DefaultConfig != nil && ps.DefaultConfig.BuilderConfig != nil { + opt.BuilderConfig.GasLimit = ps.DefaultConfig.BuilderConfig.GasLimit + } else { + opt.BuilderConfig.GasLimit = chainDefault + } + reset = true } - return true + return reset } diff --git a/config/proposer/settings_test.go b/config/proposer/settings_test.go index 06838334215d..aa8875737d49 100644 --- a/config/proposer/settings_test.go +++ b/config/proposer/settings_test.go @@ -316,19 +316,19 @@ func TestSettings_SetGasLimit(t *testing.T) { require.NoError(t, err) pk := bytesutil.ToBytes48(pubkey) - t.Run("nil settings rejects with v1 error message", func(t *testing.T) { + t.Run("nil settings rejects", func(t *testing.T) { var ps *Settings err := ps.SetGasLimit(pk, validator.Uint64(70_000_000)) require.ErrorContains(t, "No proposer settings were found to update", err) }) - t.Run("v2 writes per-validator GasLimit", func(t *testing.T) { + t.Run("writes per-validator option-level GasLimit", func(t *testing.T) { ps := &Settings{Version: SchemaV2} require.NoError(t, ps.SetGasLimit(pk, validator.Uint64(70_000_000))) require.Equal(t, validator.Uint64(70_000_000), ps.ProposeConfig[pk].GasLimit) }) - t.Run("v2 updates existing per-validator entry", func(t *testing.T) { + t.Run("updates existing per-validator entry", func(t *testing.T) { ps := &Settings{ Version: SchemaV2, ProposeConfig: map[[fieldparams.BLSPubkeyLength]byte]*Option{ @@ -339,52 +339,28 @@ func TestSettings_SetGasLimit(t *testing.T) { require.Equal(t, validator.Uint64(20_000_000), ps.ProposeConfig[pk].GasLimit) }) - t.Run("v1 with no builder rejects", func(t *testing.T) { - ps := &Settings{} - err := ps.SetGasLimit(pk, validator.Uint64(80_000_000)) - require.ErrorContains(t, "Gas limit changes only apply when builder is enabled", err) - }) - - t.Run("v1 with disabled builder rejects", func(t *testing.T) { + t.Run("v1 settings accept option-level writes without builder gating", func(t *testing.T) { + // The option-level value feeds pre-gloas registrations first, so the + // write no longer requires an enabled builder. ps := &Settings{ DefaultConfig: &Option{BuilderConfig: &BuilderConfig{Enabled: false}}, } - err := ps.SetGasLimit(pk, validator.Uint64(80_000_000)) - require.ErrorContains(t, "Gas limit changes only apply when builder is enabled", err) - }) - - t.Run("v1 clones enabled-builder default into new per-validator entry", func(t *testing.T) { - feeRecipient := common.HexToAddress("0x50155530FCE8a85ec7055A5F8b2bE214B3DaeFd3") - ps := &Settings{ - DefaultConfig: &Option{ - FeeRecipientConfig: &FeeRecipientConfig{FeeRecipient: feeRecipient}, - BuilderConfig: &BuilderConfig{Enabled: true, GasLimit: validator.Uint64(30_000_000)}, - }, - } - require.NoError(t, ps.SetGasLimit(pk, validator.Uint64(90_000_000))) - opt := ps.ProposeConfig[pk] - require.Equal(t, feeRecipient, opt.FeeRecipientConfig.FeeRecipient) - require.Equal(t, validator.Uint64(90_000_000), opt.BuilderConfig.GasLimit) + require.NoError(t, ps.SetGasLimit(pk, validator.Uint64(80_000_000))) + require.Equal(t, validator.Uint64(80_000_000), ps.ProposeConfig[pk].GasLimit) + require.IsNil(t, ps.ProposeConfig[pk].BuilderConfig) }) - t.Run("v1 updates existing enabled-builder per-validator entry", func(t *testing.T) { + t.Run("v1 per-key builder entry keeps its builder config untouched", func(t *testing.T) { ps := &Settings{ ProposeConfig: map[[fieldparams.BLSPubkeyLength]byte]*Option{ pk: {BuilderConfig: &BuilderConfig{Enabled: true, GasLimit: validator.Uint64(10_000_000)}}, }, } require.NoError(t, ps.SetGasLimit(pk, validator.Uint64(20_000_000))) - require.Equal(t, validator.Uint64(20_000_000), ps.ProposeConfig[pk].BuilderConfig.GasLimit) - }) - - t.Run("v1 per-validator entry with disabled builder rejects", func(t *testing.T) { - ps := &Settings{ - ProposeConfig: map[[fieldparams.BLSPubkeyLength]byte]*Option{ - pk: {BuilderConfig: &BuilderConfig{Enabled: false}}, - }, - } - err := ps.SetGasLimit(pk, validator.Uint64(20_000_000)) - require.ErrorContains(t, "Gas limit changes only apply when builder is enabled", err) + require.Equal(t, validator.Uint64(20_000_000), ps.ProposeConfig[pk].GasLimit) + // The legacy builder-level value stays; option-level wins on every read. + require.Equal(t, validator.Uint64(10_000_000), ps.ProposeConfig[pk].BuilderConfig.GasLimit) + require.Equal(t, validator.Uint64(20_000_000), ps.GasLimit(pk)) }) } @@ -416,7 +392,7 @@ func TestSettings_ResetGasLimit(t *testing.T) { require.Equal(t, validator.Uint64(40_000_000), ps.ProposeConfig[pk].GasLimit) }) - t.Run("v2 resets per-validator to chain default when no default", func(t *testing.T) { + t.Run("v2 resets per-validator to unset when no default", func(t *testing.T) { ps := &Settings{ Version: SchemaV2, ProposeConfig: map[[fieldparams.BLSPubkeyLength]byte]*Option{ @@ -424,7 +400,10 @@ func TestSettings_ResetGasLimit(t *testing.T) { }, } require.Equal(t, true, ps.ResetGasLimit(pk)) - require.Equal(t, chainDefault, ps.ProposeConfig[pk].GasLimit) + // Unset rather than pinned to today's chain default, so the key follows + // future default gas limit increases. + require.Equal(t, validator.Uint64(0), ps.ProposeConfig[pk].GasLimit) + require.Equal(t, chainDefault, ps.GasLimit(pk)) }) t.Run("v1 returns false for missing per-validator entry", func(t *testing.T) { @@ -734,7 +713,9 @@ func TestRegistrationFor(t *testing.T) { require.Equal(t, false, enabled) }) - t.Run("v2 config without builders does not register", func(t *testing.T) { + t.Run("legacy enabled content registers regardless of version stamp", func(t *testing.T) { + // Semantics are fork-keyed, not version-keyed: registrations exist only + // pre-gloas, where legacy enabled content stays authoritative. ps := &Settings{ Version: SchemaV2, DefaultConfig: &Option{ @@ -742,7 +723,18 @@ func TestRegistrationFor(t *testing.T) { BuilderConfig: &BuilderConfig{Enabled: true, MinBid: uint64ValPtr(1)}, }, } - // enabled is legacy v1 content and is ignored by v2 resolution. + _, _, enabled := ps.RegistrationFor(key) + require.Equal(t, true, enabled) + }) + + t.Run("no builder content anywhere does not register", func(t *testing.T) { + ps := &Settings{ + Version: SchemaV2, + DefaultConfig: &Option{ + FeeRecipientConfig: &FeeRecipientConfig{FeeRecipient: recipient}, + BuilderConfig: &BuilderConfig{MinBid: uint64ValPtr(1)}, + }, + } _, _, enabled := ps.RegistrationFor(key) require.Equal(t, false, enabled) }) diff --git a/validator/client/propose.go b/validator/client/propose.go index 56ddb352170f..9e242507ebec 100644 --- a/validator/client/propose.go +++ b/validator/client/propose.go @@ -577,7 +577,8 @@ func (v *validator) SetGraffiti(ctx context.Context, pubkey [fieldparams.BLSPubk if graffiti == nil { return nil } - settings := &proposer.Settings{} + // API-created settings carry no v1 content: v2 once gloas is scheduled. + settings := &proposer.Settings{Version: proposer.FreshSettingsVersion()} if v.proposerSettings != nil { settings = v.proposerSettings.Clone() } diff --git a/validator/client/propose_test.go b/validator/client/propose_test.go index 2d69cbc33ab6..e6db9a489df7 100644 --- a/validator/client/propose_test.go +++ b/validator/client/propose_test.go @@ -1206,6 +1206,11 @@ func Test_validator_DeleteGraffiti(t *testing.T) { } func Test_validator_SetGraffiti(t *testing.T) { + // Fresh settings are stamped v2 only once the network schedules gloas. + params.SetupTestConfigCleanup(t) + cfg := params.BeaconConfig().Copy() + cfg.GloasForkEpoch = 100 + params.OverrideBeaconConfig(cfg) pubKey := [fieldparams.BLSPubkeyLength]byte{'a'} tests := []struct { name string @@ -1264,7 +1269,8 @@ func Test_validator_SetGraffiti(t *testing.T) { Graffiti: "specific graffiti", }, } - return &proposer.Settings{ProposeConfig: config} + // API-created settings carry no v1 content and start at v2. + return &proposer.Settings{Version: proposer.SchemaV2, ProposeConfig: config} }(), }, } diff --git a/validator/client/validator.go b/validator/client/validator.go index 0d7e76c23ecd..e77290b909d4 100644 --- a/validator/client/validator.go +++ b/validator/client/validator.go @@ -1220,13 +1220,19 @@ func (v *validator) buildProposerSettingsRequests( return prepareProposerReqs } -// upgradeProposerSettingsToV2 migrates v1 settings to v2 and persists them; callers -// gate on gloas-active. Upgrades a clone then swaps so readers never see a half state. +// upgradeProposerSettingsToV2 is idempotent post-fork cleanup: it strips dead v1 +// builder content from a clone and swaps; callers gate on gloas-active. func (v *validator) upgradeProposerSettingsToV2(ctx context.Context) { - ps := v.ProposerSettings().Clone() + snapshot := v.ProposerSettings() + ps := snapshot.Clone() if !ps.UpgradeToV2() { return } + // Pointer changed = a keymanager write landed after our snapshot; swapping + // our stale clone would erase it. This cleanup simply reruns next cycle. + if v.ProposerSettings() != snapshot { + return + } if err := v.SetProposerSettings(ctx, ps); err != nil { log.WithError(err).Warn("Failed to persist v1->v2 proposer settings upgrade") } @@ -1459,9 +1465,7 @@ type builderTarget struct { // config-level fallbacks. TODO(gloas): minBid/boost/pubkeys ride the #630 wire. func (v *validator) builderTargetsForKey(pk pubkey) []builderTarget { ps := v.ProposerSettings() - // Builder entries imply v2: the API upgrades on write and v2 files declare it, - // so v1 settings have nothing legitimate to warm. - if ps == nil || ps.Version != proposer.SchemaV2 { + if ps == nil { return nil } bc := ps.EffectiveBuilderConfig(pk) diff --git a/validator/client/validator_test.go b/validator/client/validator_test.go index c802dc307aee..35e9553b7e7f 100644 --- a/validator/client/validator_test.go +++ b/validator/client/validator_test.go @@ -3325,14 +3325,19 @@ func TestValidator_buildSignedRegReqs_DefaultConfigDisabled(t *testing.T) { } actual := v.buildSignedRegReqs(ctx, pubkeys, signer, 0, false) - assert.Equal(t, 1, len(actual)) + assert.Equal(t, 2, len(actual)) assert.DeepEqual(t, feeRecipient1[:], actual[0].Message.FeeRecipient) assert.Equal(t, uint64(1111), actual[0].Message.GasLimit) assert.DeepEqual(t, pubkey1[:], actual[0].Message.Pubkey) + // Fee recipient and participation resolve independently: an explicitly + // enabled key without its own fee recipient registers with the default's. + assert.DeepEqual(t, defaultFeeRecipient[:], actual[1].Message.FeeRecipient) + assert.Equal(t, uint64(3333), actual[1].Message.GasLimit) + assert.DeepEqual(t, pubkey3[:], actual[1].Message.Pubkey) } -// The same settings shape registers differently by schema version: v1 keeps the -// legacy object-level semantics, v2 applies field-level inheritance. +// Semantics are fork-keyed, not version-keyed: the same settings shape registers +// identically whatever the stored schema version says. func TestValidator_buildSignedRegReqs_VersionGatesInheritance(t *testing.T) { pubkey1 := pubkeyFromString(t, "0x111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111") defaultFeeRecipient := feeRecipientFromString(t, "0xdddddddddddddddddddddddddddddddddddddddd") @@ -3368,8 +3373,9 @@ func TestValidator_buildSignedRegReqs_VersionGatesInheritance(t *testing.T) { } pubkeys := [][fieldparams.BLSPubkeyLength]byte{pubkey1} + // The per-key disable is honored regardless of the stored version stamp. v.proposerSettings = newSettings(0) - assert.Equal(t, 1, len(v.buildSignedRegReqs(ctx, pubkeys, signer, 0, false))) + assert.Equal(t, 0, len(v.buildSignedRegReqs(ctx, pubkeys, signer, 0, false))) v.proposerSettings = newSettings(proposer.SchemaV2) v.signedValidatorRegistrations = map[[48]byte]*ethpb.SignedValidatorRegistrationV1{} @@ -3475,7 +3481,8 @@ func TestValidator_buildSignedRegReqs_DefaultConfigEnabled(t *testing.T) { assert.DeepEqual(t, pubkey1[:], actual[0].Message.Pubkey) assert.DeepEqual(t, defaultFeeRecipient[:], actual[1].Message.FeeRecipient) - assert.Equal(t, uint64(9999), actual[1].Message.GasLimit) + // The per-key builder gas limit outranks the default's builder value. + assert.Equal(t, uint64(3333), actual[1].Message.GasLimit) assert.DeepEqual(t, pubkey3[:], actual[1].Message.Pubkey) t.Run("mid epoch only pushes newly added key", func(t *testing.T) { @@ -3978,11 +3985,9 @@ func TestBuilderTargetsForKey(t *testing.T) { }}, } - t.Run("v1 settings produce no targets", func(t *testing.T) { - // Builder entries imply v2: the API upgrades on write and v2 files declare - // it, so entries on v1 settings are a malformed hybrid and stay inert. + t.Run("targets resolve regardless of the stored version stamp", func(t *testing.T) { v := &validator{proposerSettings: &proposer.Settings{Version: proposer.SchemaV1, ProposeConfig: proposeConfig}} - require.Equal(t, 0, len(v.builderTargetsForKey(pk))) + require.Equal(t, 2, len(v.builderTargetsForKey(pk))) }) t.Run("explicit empty builders list produces no targets", func(t *testing.T) { diff --git a/validator/rpc/handlers_keymanager.go b/validator/rpc/handlers_keymanager.go index c60b957ae862..ba55e1a03d14 100644 --- a/validator/rpc/handlers_keymanager.go +++ b/validator/rpc/handlers_keymanager.go @@ -630,7 +630,11 @@ func (s *Server) SetFeeRecipientByPubkey(w http.ResponseWriter, r *http.Request) defer s.proposerSettingsLock.Unlock() settings := s.validatorService.ProposerSettings() if settings == nil { - settings = &proposer.Settings{} + // API-created settings carry no v1 content: v2 once gloas is scheduled. + settings = &proposer.Settings{Version: proposer.FreshSettingsVersion()} + } else { + // Clone-then-swap: lock-free readers must never see in-place mutation. + settings = settings.Clone() } // A newly created option leaves BuilderConfig nil so the key inherits default_config. settings.UpsertProposeOption(bytesutil.ToBytes48(pubkey)).FeeRecipientConfig = &proposer.FeeRecipientConfig{FeeRecipient: feeRecipient} @@ -660,6 +664,7 @@ func (s *Server) DeleteFeeRecipientByPubkey(w http.ResponseWriter, r *http.Reque defer s.proposerSettingsLock.Unlock() settings := s.validatorService.ProposerSettings() if settings != nil && settings.ProposeConfig != nil { + settings = settings.Clone() proposerOption, found := settings.ProposeConfig[bytesutil.ToBytes48(pubkey)] if found { proposerOption.FeeRecipientConfig = nil @@ -731,7 +736,12 @@ func (s *Server) SetGasLimit(w http.ResponseWriter, r *http.Request) { s.proposerSettingsLock.Lock() defer s.proposerSettingsLock.Unlock() - settings := s.validatorService.ProposerSettings() + // Clone-then-swap: lock-free readers must never see in-place mutation. + settings := s.validatorService.ProposerSettings().Clone() + if settings == nil { + // API-created settings carry no v1 content: v2 once gloas is scheduled. + settings = &proposer.Settings{Version: proposer.FreshSettingsVersion()} + } if err := settings.SetGasLimit(bytesutil.ToBytes48(pubkey), validator.Uint64(gasLimit)); err != nil { httputil.HandleError(w, err.Error(), http.StatusInternalServerError) return @@ -760,7 +770,8 @@ func (s *Server) DeleteGasLimit(w http.ResponseWriter, r *http.Request) { s.proposerSettingsLock.Lock() defer s.proposerSettingsLock.Unlock() - settings := s.validatorService.ProposerSettings() + // Clone-then-swap: lock-free readers must never see in-place mutation. + settings := s.validatorService.ProposerSettings().Clone() if !settings.ResetGasLimit(bytesutil.ToBytes48(pubkey)) { httputil.HandleError(w, fmt.Sprintf("No gas limit found for pubkey %q", rawPubkey), http.StatusNotFound) return diff --git a/validator/rpc/handlers_keymanager_test.go b/validator/rpc/handlers_keymanager_test.go index 23bc7d369357..b4f0ffb76464 100644 --- a/validator/rpc/handlers_keymanager_test.go +++ b/validator/rpc/handlers_keymanager_test.go @@ -986,50 +986,34 @@ func TestServer_SetGasLimit(t *testing.T) { wantErr string }{ { - name: "ProposerSettings is nil", + name: "nil settings create fresh v2 settings with the option-level value", pubkey: pubkey1, newGasLimit: 9999, proposerSettings: nil, - wantErr: "No proposer settings were found to update", + w: []*want{{pubkey1, 9999}}, }, { - name: "ProposerSettings.ProposeConfig is nil AND ProposerSettings.DefaultConfig is nil", + name: "empty settings accept the write at the option level", pubkey: pubkey1, newGasLimit: 9999, proposerSettings: &proposer.Settings{ ProposeConfig: nil, DefaultConfig: nil, }, - wantErr: "Gas limit changes only apply when builder is enabled", + w: []*want{{pubkey1, 9999}}, }, { - name: "ProposerSettings.ProposeConfig is nil AND ProposerSettings.DefaultConfig.BuilderConfig is nil", + name: "no builder config anywhere still accepts the write", pubkey: pubkey1, newGasLimit: 9999, proposerSettings: &proposer.Settings{ ProposeConfig: nil, - DefaultConfig: &proposer.Option{ - BuilderConfig: nil, - }, + DefaultConfig: &proposer.Option{BuilderConfig: nil}, }, - wantErr: "Gas limit changes only apply when builder is enabled", + w: []*want{{pubkey1, 9999}}, }, { - name: "ProposerSettings.ProposeConfig is defined for pubkey, BuilderConfig is nil AND ProposerSettings.DefaultConfig is nil", - pubkey: pubkey1, - newGasLimit: 9999, - proposerSettings: &proposer.Settings{ - ProposeConfig: map[[48]byte]*proposer.Option{ - bytesutil.ToBytes48(pubkey1): { - BuilderConfig: nil, - }, - }, - DefaultConfig: nil, - }, - wantErr: "Gas limit changes only apply when builder is enabled", - }, - { - name: "ProposerSettings.ProposeConfig is defined for pubkey, BuilderConfig is defined AND ProposerSettings.DefaultConfig is nil", + name: "disabled builder no longer gates the write", pubkey: pubkey1, newGasLimit: 9999, proposerSettings: &proposer.Settings{ @@ -1040,10 +1024,10 @@ func TestServer_SetGasLimit(t *testing.T) { }, DefaultConfig: nil, }, - wantErr: "Gas limit changes only apply when builder is enabled", + w: []*want{{pubkey1, 9999}}, }, { - name: "ProposerSettings.ProposeConfig is NOT defined for pubkey, BuilderConfig is defined AND ProposerSettings.DefaultConfig is nil", + name: "option-level write wins over an existing builder-level value", pubkey: pubkey2, newGasLimit: 9999, proposerSettings: &proposer.Settings{ @@ -1057,33 +1041,21 @@ func TestServer_SetGasLimit(t *testing.T) { }, DefaultConfig: nil, }, - w: []*want{{ - pubkey2, - 9999, - }, - }, + w: []*want{{pubkey2, 9999}}, }, { - name: "ProposerSettings.ProposeConfig is defined for pubkey, BuilderConfig is nil AND ProposerSettings.DefaultConfig.BuilderConfig is defined", + name: "write for a key with no option creates one", pubkey: pubkey1, newGasLimit: 9999, proposerSettings: &proposer.Settings{ ProposeConfig: map[[48]byte]*proposer.Option{ - bytesutil.ToBytes48(pubkey2): { - BuilderConfig: nil, - }, + bytesutil.ToBytes48(pubkey2): {BuilderConfig: nil}, }, DefaultConfig: &proposer.Option{ - BuilderConfig: &proposer.BuilderConfig{ - Enabled: true, - }, + BuilderConfig: &proposer.BuilderConfig{Enabled: true}, }, }, - w: []*want{{ - pubkey1, - 9999, - }, - }, + w: []*want{{pubkey1, 9999}}, }, } for _, isSlashingProtectionMinimal := range [...]bool{false, true} { @@ -1127,7 +1099,7 @@ func TestServer_SetGasLimit(t *testing.T) { } else { assert.Equal(t, http.StatusAccepted, w.Code) for _, wantObj := range tt.w { - assert.Equal(t, wantObj.gaslimit, uint64(s.validatorService.ProposerSettings().ProposeConfig[bytesutil.ToBytes48(wantObj.pubkey)].BuilderConfig.GasLimit)) + assert.Equal(t, wantObj.gaslimit, uint64(s.validatorService.ProposerSettings().GasLimit(bytesutil.ToBytes48(wantObj.pubkey)))) } } }) @@ -1161,6 +1133,11 @@ func TestServer_SetGasLimit_InvalidPubKey(t *testing.T) { } func TestServer_SetGasLimit_NilSettings(t *testing.T) { + // Fresh settings are stamped v2 only once the network schedules gloas. + params.SetupTestConfigCleanup(t) + cfg := params.BeaconConfig().Copy() + cfg.GloasForkEpoch = 100 + params.OverrideBeaconConfig(cfg) ctx := t.Context() pubkey, err := hexutil.Decode("0xaf2e7ba294e03438ea819bd4033c6c1bf6b04320ee2075b77273c08d02f8a61bcc303c2c06bd3713cb442072ae591493") require.NoError(t, err) @@ -1184,8 +1161,12 @@ func TestServer_SetGasLimit_NilSettings(t *testing.T) { w.Body = &bytes.Buffer{} s.SetGasLimit(w, req) - assert.NotEqual(t, http.StatusAccepted, w.Code) - require.StringContains(t, "No proposer settings were found to update", w.Body.String()) + // Nil settings are created fresh at v2 with the option-level value. + assert.Equal(t, http.StatusAccepted, w.Code) + ps := s.validatorService.ProposerSettings() + require.NotNil(t, ps) + assert.Equal(t, proposer.SchemaV2, ps.Version) + assert.Equal(t, validator.Uint64(9999), ps.ProposeConfig[bytesutil.ToBytes48(pubkey)].GasLimit) } func TestServer_DeleteGasLimit(t *testing.T) { @@ -1400,7 +1381,7 @@ func TestServer_GasLimit_V2Schema(t *testing.T) { assert.Equal(t, "42424242", resp.Data.GasLimit) }) - t.Run("DeleteGasLimit resets per-validator GasLimit to chain default on v2", func(t *testing.T) { + t.Run("DeleteGasLimit unsets the per-validator GasLimit on v2", func(t *testing.T) { params.BeaconConfig().DefaultBuilderGasLimit = uint64(0xbbdd) s := setupServer(t, &proposer.Settings{ Version: 2, @@ -1415,7 +1396,11 @@ func TestServer_GasLimit_V2Schema(t *testing.T) { s.DeleteGasLimit(w, req) assert.Equal(t, http.StatusNoContent, w.Code) - assert.Equal(t, validator.Uint64(0xbbdd), s.validatorService.ProposerSettings().ProposeConfig[bytesutil.ToBytes48(pubkey1)].GasLimit) + // Unset rather than pinned to today's chain default, so the key follows + // future default gas limit increases; reads resolve the chain default. + ps := s.validatorService.ProposerSettings() + assert.Equal(t, validator.Uint64(0), ps.ProposeConfig[bytesutil.ToBytes48(pubkey1)].GasLimit) + assert.Equal(t, validator.Uint64(0xbbdd), ps.GasLimit(bytesutil.ToBytes48(pubkey1))) }) t.Run("DeleteGasLimit returns 404 on v2 when no per-validator entry exists", func(t *testing.T) { diff --git a/validator/rpc/handlers_validator_config.go b/validator/rpc/handlers_validator_config.go index 4e86f76ce574..dfa041424683 100644 --- a/validator/rpc/handlers_validator_config.go +++ b/validator/rpc/handlers_validator_config.go @@ -6,17 +6,31 @@ import ( "github.com/OffchainLabs/prysm/v7/beacon-chain/rpc/eth/shared" fieldparams "github.com/OffchainLabs/prysm/v7/config/fieldparams" + "github.com/OffchainLabs/prysm/v7/config/params" "github.com/OffchainLabs/prysm/v7/config/proposer" "github.com/OffchainLabs/prysm/v7/encoding/bytesutil" "github.com/OffchainLabs/prysm/v7/monitoring/tracing/trace" "github.com/OffchainLabs/prysm/v7/network/httputil" ) +// requireGloasScheduled rejects builders configuration on networks that never +// activate it; the endpoints are meaningless without a gloas fork epoch. +func requireGloasScheduled(w http.ResponseWriter) bool { + if params.GloasEnabled() { + return true + } + httputil.HandleError(w, "Builders configuration requires a network with the gloas fork scheduled", http.StatusNotImplemented) + return false +} + // GetBuilders implements GET /eth/v1/validator/{pubkey}/builders: the key's config resolved against default_config, safe to re-submit. func (s *Server) GetBuilders(w http.ResponseWriter, r *http.Request) { _, span := trace.StartSpan(r.Context(), "validator.keymanagerAPI.GetBuilders") defer span.End() + if !requireGloasScheduled(w) { + return + } if s.validatorService == nil { httputil.HandleError(w, "Validator service not ready.", http.StatusServiceUnavailable) return @@ -45,6 +59,9 @@ func (s *Server) SetBuilders(w http.ResponseWriter, r *http.Request) { ctx, span := trace.StartSpan(r.Context(), "validator.keymanagerAPI.SetBuilders") defer span.End() + if !requireGloasScheduled(w) { + return + } if s.validatorService == nil { httputil.HandleError(w, "Validator service not ready.", http.StatusServiceUnavailable) return @@ -70,11 +87,12 @@ func (s *Server) SetBuilders(w http.ResponseWriter, r *http.Request) { settings := s.validatorService.ProposerSettings() if settings == nil { - settings = &proposer.Settings{Version: proposer.SchemaV2} + settings = &proposer.Settings{Version: proposer.FreshSettingsVersion()} } else { settings = settings.Clone() - // Builder lists are v2 content: writing them migrates v1 settings in place. - settings.UpgradeToV2() + // Stamp the storage format only: semantics are fork-keyed, so writing + // builders never drops v1 content or changes other keys' behavior. + settings.Version = proposer.SchemaV2 } if settings.ProposeConfig == nil { @@ -101,6 +119,9 @@ func (s *Server) DeleteBuilders(w http.ResponseWriter, r *http.Request) { ctx, span := trace.StartSpan(r.Context(), "validator.keymanagerAPI.DeleteBuilders") defer span.End() + if !requireGloasScheduled(w) { + return + } if s.validatorService == nil { httputil.HandleError(w, "Validator service not ready.", http.StatusServiceUnavailable) return diff --git a/validator/rpc/handlers_validator_config_test.go b/validator/rpc/handlers_validator_config_test.go index b44bdc5f0e82..848538d25d48 100644 --- a/validator/rpc/handlers_validator_config_test.go +++ b/validator/rpc/handlers_validator_config_test.go @@ -10,6 +10,7 @@ import ( "testing" fieldparams "github.com/OffchainLabs/prysm/v7/config/fieldparams" + "github.com/OffchainLabs/prysm/v7/config/params" "github.com/OffchainLabs/prysm/v7/config/proposer" "github.com/OffchainLabs/prysm/v7/consensus-types/validator" "github.com/OffchainLabs/prysm/v7/testing/require" @@ -20,7 +21,12 @@ import ( // setupConfigServer builds a Server with a derived keymanager holding numKeys // recovered accounts, and returns the server plus the known validating pubkeys. +// The builders endpoints require a gloas-scheduled network, so one is configured. func setupConfigServer(t *testing.T, numKeys int) (*Server, [][48]byte) { + params.SetupTestConfigCleanup(t) + cfg := params.BeaconConfig().Copy() + cfg.GloasForkEpoch = 100 + params.OverrideBeaconConfig(cfg) ctx := t.Context() srv := setupServerWithWallet(t) km, err := srv.validatorService.Keymanager() @@ -264,6 +270,29 @@ func TestServer_GetBuilders(t *testing.T) { }) } +func TestServer_Builders_RequireGloasScheduled(t *testing.T) { + // The default test config has no gloas fork epoch: all three endpoints refuse. + srv := &Server{} + pk := "0x" + strings.Repeat("ab", 48) + for name, w := range map[string]*httptest.ResponseRecorder{ + "get": getBuildersRecorder(srv, pk), + "post": postBuilders(t, srv, pk, `{"builders":[]}`), + "delete": deleteBuilders(t, srv, pk), + } { + require.Equal(t, http.StatusNotImplemented, w.Code, "endpoint: %s", name) + require.Equal(t, true, strings.Contains(w.Body.String(), "gloas fork scheduled"), "endpoint: %s", name) + } +} + +func getBuildersRecorder(s *Server, pubkey string) *httptest.ResponseRecorder { + req := httptest.NewRequest(http.MethodGet, "/eth/v1/validator/"+pubkey+"/builders", nil) + req.SetPathValue("pubkey", pubkey) + w := httptest.NewRecorder() + w.Body = &bytes.Buffer{} + s.GetBuilders(w, req) + return w +} + func TestServer_DeleteBuilders(t *testing.T) { srv, keys := setupConfigServer(t, 1) pk := hexutil.Encode(keys[0][:]) From da52e2e500a58f034b22c231bb65630e96daf604 Mon Sep 17 00:00:00 2001 From: james-prysm Date: Tue, 11 Aug 2026 17:02:06 -0500 Subject: [PATCH 20/26] gaz --- validator/rpc/BUILD.bazel | 1 + 1 file changed, 1 insertion(+) diff --git a/validator/rpc/BUILD.bazel b/validator/rpc/BUILD.bazel index 964555e13b92..e54fb475c879 100644 --- a/validator/rpc/BUILD.bazel +++ b/validator/rpc/BUILD.bazel @@ -37,6 +37,7 @@ go_library( "//cmd/validator/flags:go_default_library", "//config/features:go_default_library", "//config/fieldparams:go_default_library", + "//config/params:go_default_library", "//config/proposer:go_default_library", "//consensus-types/primitives:go_default_library", "//consensus-types/validator:go_default_library", From ae648dc62ebfaf37ebf2a6ca24c52ad3ca3723da Mon Sep 17 00:00:00 2001 From: james-prysm Date: Wed, 12 Aug 2026 13:41:01 -0500 Subject: [PATCH 21/26] jun's comments --- config/proposer/loader/loader.go | 2 +- config/proposer/loader/loader_test.go | 85 +++++++++++++++++++++++ config/proposer/settings.go | 6 +- validator/client/validator.go | 20 +++--- validator/client/validator_test.go | 21 ++++++ validator/db/convert_test.go | 3 +- validator/db/kv/proposer_settings_test.go | 5 +- 7 files changed, 124 insertions(+), 18 deletions(-) diff --git a/config/proposer/loader/loader.go b/config/proposer/loader/loader.go index 1d5244bc6bab..cf25fb98282f 100644 --- a/config/proposer/loader/loader.go +++ b/config/proposer/loader/loader.go @@ -135,7 +135,7 @@ func (psl *SettingsLoader) Load(cliCtx *cli.Context) (*proposer.Settings, error) } dbSettings = dbps.ToConsensus() log.WithField("version", dbSettings.Version). - WithField("proposerKeys", len(dbSettings.ProposerConfig)). + WithField("proposerConfigCount", len(dbSettings.ProposerConfig)). Debug("Loaded proposer settings from DB") } diff --git a/config/proposer/loader/loader_test.go b/config/proposer/loader/loader_test.go index 3b00c36773e7..cf8479605d48 100644 --- a/config/proposer/loader/loader_test.go +++ b/config/proposer/loader/loader_test.go @@ -1484,3 +1484,88 @@ func Test_mergeProposerSettings_V2GasLimitOverwritesPerValidator(t *testing.T) { require.Equal(t, gl, merged.DefaultConfig.GasLimit) require.Equal(t, gl, merged.ProposerConfig["0xkey"].GasLimit) } + +func Test_markExplicitEmptyBuilders(t *testing.T) { + entry := &validatorpb.BuilderEntry{Url: "https://a.example"} + t.Run("explicit empty list gains the marker", func(t *testing.T) { + p := &validatorpb.ProposerSettingsPayload{ + DefaultConfig: &validatorpb.ProposerOptionPayload{ + Builder: &validatorpb.BuilderConfig{Builders: []*validatorpb.BuilderEntry{}}, + }, + } + markExplicitEmptyBuilders(p) + require.Equal(t, true, p.DefaultConfig.Builder.BuildersSet) + }) + t.Run("nonempty list gains the marker too", func(t *testing.T) { + p := &validatorpb.ProposerSettingsPayload{ + ProposerConfig: map[string]*validatorpb.ProposerOptionPayload{ + "0xaa": {Builder: &validatorpb.BuilderConfig{Builders: []*validatorpb.BuilderEntry{entry}}}, + }, + } + markExplicitEmptyBuilders(p) + require.Equal(t, true, p.ProposerConfig["0xaa"].Builder.BuildersSet) + }) + t.Run("absent list stays unmarked", func(t *testing.T) { + p := &validatorpb.ProposerSettingsPayload{ + DefaultConfig: &validatorpb.ProposerOptionPayload{Builder: &validatorpb.BuilderConfig{Enabled: true}}, + ProposerConfig: map[string]*validatorpb.ProposerOptionPayload{"0xaa": {}, "0xbb": nil}, + } + markExplicitEmptyBuilders(p) + require.Equal(t, false, p.DefaultConfig.Builder.BuildersSet) + }) +} + +func Test_inferSchemaVersion(t *testing.T) { + u64 := func(v uint64) *validator.Uint64 { u := validator.Uint64(v); return &u } + v2Cases := map[string]*validatorpb.BuilderConfig{ + "builders list": {Builders: []*validatorpb.BuilderEntry{{Url: "https://a.example"}}}, + "builders set marker": {BuildersSet: true}, + "min_bid": {MinBid: u64(1)}, + "builder_boost_factor": {BuilderBoostFactor: u64(100)}, + "max_execution_payment": {MaxExecutionPayment: u64(0)}, + } + for name, bc := range v2Cases { + t.Run("unversioned with "+name+" infers v2", func(t *testing.T) { + p := &validatorpb.ProposerSettingsPayload{ + DefaultConfig: &validatorpb.ProposerOptionPayload{Builder: bc}, + } + inferSchemaVersion(p) + require.Equal(t, uint32(proposer.SchemaV2), p.Version) + }) + } + t.Run("per-key v2 content infers v2", func(t *testing.T) { + p := &validatorpb.ProposerSettingsPayload{ + ProposerConfig: map[string]*validatorpb.ProposerOptionPayload{ + "0xaa": {Builder: &validatorpb.BuilderConfig{MinBid: u64(1)}}, + }, + } + inferSchemaVersion(p) + require.Equal(t, uint32(proposer.SchemaV2), p.Version) + }) + t.Run("pure v1 content stays unversioned", func(t *testing.T) { + p := &validatorpb.ProposerSettingsPayload{ + DefaultConfig: &validatorpb.ProposerOptionPayload{ + Builder: &validatorpb.BuilderConfig{Enabled: true, GasLimit: 30000000}, + }, + } + inferSchemaVersion(p) + require.Equal(t, uint32(proposer.SchemaV1Unset), p.Version) + }) + t.Run("explicit version is never overridden", func(t *testing.T) { + p := &validatorpb.ProposerSettingsPayload{ + Version: proposer.SchemaV1, + DefaultConfig: &validatorpb.ProposerOptionPayload{ + Builder: &validatorpb.BuilderConfig{MinBid: u64(1)}, + }, + } + inferSchemaVersion(p) + require.Equal(t, uint32(proposer.SchemaV1), p.Version) + }) + t.Run("no builder content stays unversioned", func(t *testing.T) { + p := &validatorpb.ProposerSettingsPayload{ + DefaultConfig: &validatorpb.ProposerOptionPayload{FeeRecipient: "0x"}, + } + inferSchemaVersion(p) + require.Equal(t, uint32(proposer.SchemaV1Unset), p.Version) + }) +} diff --git a/config/proposer/settings.go b/config/proposer/settings.go index 2c5ceb3db603..c1a14be9854e 100644 --- a/config/proposer/settings.go +++ b/config/proposer/settings.go @@ -139,9 +139,9 @@ func (be *BuilderEntry) EffectiveAuthData() []byte { return []byte(be.URL) } -// EffectiveBuilderConfig resolves key's builder config against default_config; +// EffectiveBuilderConfig resolves pubkey's builder config against default_config; // nil when neither level configures a builder. -func (ps *Settings) EffectiveBuilderConfig(key [fieldparams.BLSPubkeyLength]byte) *BuilderConfig { +func (ps *Settings) EffectiveBuilderConfig(pubkey [fieldparams.BLSPubkeyLength]byte) *BuilderConfig { if ps == nil { return nil } @@ -149,7 +149,7 @@ func (ps *Settings) EffectiveBuilderConfig(key [fieldparams.BLSPubkeyLength]byte if ps.DefaultConfig != nil { def = ps.DefaultConfig.BuilderConfig } - if opt, ok := ps.ProposeConfig[key]; ok && opt != nil { + if opt, ok := ps.ProposeConfig[pubkey]; ok && opt != nil { perKey = opt.BuilderConfig } return effectiveBuilderConfig(perKey, def) diff --git a/validator/client/validator.go b/validator/client/validator.go index ee9069abff85..268bfe22b7a4 100644 --- a/validator/client/validator.go +++ b/validator/client/validator.go @@ -1529,30 +1529,32 @@ func (v *validator) warmBuilderRequestAuthsForDuties(ctx context.Context, km key if len(targets) == 0 { continue } - // The v1 preference wire identifies a builder only by url, so same-url - // entries collapse to the safest (lowest) payment ceiling for now. - ceilings := make(map[string]uint64, len(targets)) + // The v1 wire holds one max_execution_payment per validator (no builder + // identity), so the lowest configured value is submitted for every builder. + minPayment := targets[0].maxPayment + urls := make(map[string]bool, len(targets)) for _, t := range targets { - if cur, ok := ceilings[t.url]; !ok || t.maxPayment < cur { - ceilings[t.url] = t.maxPayment + if t.maxPayment < minPayment { + minPayment = t.maxPayment } + urls[t.url] = true } for _, proposalSlot := range duty.ProposerSlots { if proposalSlot <= slot { continue } - for url, maxPayment := range ceilings { + for url := range urls { signed, err := v.signRequestAuthCached(ctx, km, pk, url, proposalSlot) if err != nil { log.WithError(err).Warn("Failed to sign builder request auth") continue } - // TODO(gloas): only maxPayment fits BuilderPreferencesV1; per-entry - // authData, minBid, boostFactor and pubkeys ship with the #630 inline wire. + // TODO(gloas): per-entry max_execution_payment, authData, minBid, boost + // and pubkeys need the beacon-APIs #630 inline wire's builder identity. reqs = append(reqs, ðpb.SubmitBuilderPreferencesRequest{ ValidatorPubkey: pk[:], Request: ðpb.BuilderPreferencesRequestV1{ - Preferences: ðpb.BuilderPreferencesV1{MaxExecutionPayment: primitives.Gwei(maxPayment)}, + Preferences: ðpb.BuilderPreferencesV1{MaxExecutionPayment: primitives.Gwei(minPayment)}, Auth: signed, }, }) diff --git a/validator/client/validator_test.go b/validator/client/validator_test.go index e16130c40b2d..4156f2bbb701 100644 --- a/validator/client/validator_test.go +++ b/validator/client/validator_test.go @@ -4104,6 +4104,27 @@ func TestWarmBuilderRequestAuthsForDuties_CollapsesSameURL(t *testing.T) { // Same-url entries collapse to one request carrying the safest (lowest) ceiling. require.Equal(t, 1, len(reqs)) require.Equal(t, primitives.Gwei(100), reqs[0].Request.Preferences.MaxExecutionPayment) + + t.Run("distinct urls all submit the lowest max_execution_payment", func(t *testing.T) { + // The beacon node holds one max_execution_payment per validator, so every + // url's submission carries the lowest entry until #630 adds builder identity. + v.proposerSettings = &proposer.Settings{ + Version: proposer.SchemaV2, + ProposeConfig: map[[fieldparams.BLSPubkeyLength]byte]*proposer.Option{ + pk: {BuilderConfig: &proposer.BuilderConfig{ + Builders: []*proposer.BuilderEntry{ + {URL: "https://a.example", MaxExecutionPayment: u64(0)}, + {URL: "https://b.example", MaxExecutionPayment: u64(250)}, + }, + }}, + }, + } + reqs := v.warmBuilderRequestAuthsForDuties(t.Context(), km, 5, duties) + require.Equal(t, 2, len(reqs)) + for _, r := range reqs { + require.Equal(t, primitives.Gwei(0), r.Request.Preferences.MaxExecutionPayment) + } + }) } func headValidator() *validator { diff --git a/validator/db/convert_test.go b/validator/db/convert_test.go index 2e4fbb933ba1..bc888d9b4f07 100644 --- a/validator/db/convert_test.go +++ b/validator/db/convert_test.go @@ -5,8 +5,6 @@ import ( "path/filepath" "testing" - "github.com/ethereum/go-ethereum/common/hexutil" - fieldparams "github.com/OffchainLabs/prysm/v7/config/fieldparams" "github.com/OffchainLabs/prysm/v7/config/proposer" "github.com/OffchainLabs/prysm/v7/consensus-types/primitives" @@ -17,6 +15,7 @@ import ( "github.com/OffchainLabs/prysm/v7/validator/db/filesystem" "github.com/OffchainLabs/prysm/v7/validator/db/iface" "github.com/OffchainLabs/prysm/v7/validator/db/kv" + "github.com/ethereum/go-ethereum/common/hexutil" ) func getPubkeyFromString(t *testing.T, pubkeyString string) [fieldparams.BLSPubkeyLength]byte { diff --git a/validator/db/kv/proposer_settings_test.go b/validator/db/kv/proposer_settings_test.go index 7e31529a972e..b32a3d87267c 100644 --- a/validator/db/kv/proposer_settings_test.go +++ b/validator/db/kv/proposer_settings_test.go @@ -3,15 +3,14 @@ package kv import ( "testing" - "github.com/ethereum/go-ethereum/common" - "github.com/ethereum/go-ethereum/common/hexutil" - fieldparams "github.com/OffchainLabs/prysm/v7/config/fieldparams" "github.com/OffchainLabs/prysm/v7/config/params" "github.com/OffchainLabs/prysm/v7/config/proposer" "github.com/OffchainLabs/prysm/v7/consensus-types/validator" "github.com/OffchainLabs/prysm/v7/encoding/bytesutil" "github.com/OffchainLabs/prysm/v7/testing/require" + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/common/hexutil" ) func TestStore_ProposerSettings_ReadAndWrite(t *testing.T) { From 3234435d301d4c540ea0a777318168985c61c8fc Mon Sep 17 00:00:00 2001 From: james-prysm Date: Wed, 12 Aug 2026 15:10:32 -0500 Subject: [PATCH 22/26] adding test --- validator/rpc/handlers_validator_config_test.go | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/validator/rpc/handlers_validator_config_test.go b/validator/rpc/handlers_validator_config_test.go index 848538d25d48..99f7a746a5a2 100644 --- a/validator/rpc/handlers_validator_config_test.go +++ b/validator/rpc/handlers_validator_config_test.go @@ -239,6 +239,19 @@ func TestServer_SetBuilders(t *testing.T) { func TestServer_GetBuilders(t *testing.T) { // GET is fully resolved: omitted auth_data becomes the url's UTF-8 bytes, and // unset values become the runtime fallbacks (no floor, neutral boost, trustless-only). + t.Run("nil proposer settings resolve to runtime defaults", func(t *testing.T) { + srv, keys := setupConfigServer(t, 1) + pk := hexutil.Encode(keys[0][:]) + // No settings were ever created: the nil-receiver chain must still + // produce a fully resolved response rather than panic or error. + w, cfg := getBuilders(t, srv, pk) + require.Equal(t, http.StatusOK, w.Code) + require.Equal(t, "0", *cfg.MinBid) + require.Equal(t, "100", *cfg.BuilderBoostFactor) + require.NotNil(t, cfg.Builders) + require.Equal(t, 0, len(cfg.Builders)) + }) + t.Run("resolves omitted values", func(t *testing.T) { srv, keys := setupConfigServer(t, 1) pk := hexutil.Encode(keys[0][:]) From 57a91748575431fbf206287a543099483fa641a3 Mon Sep 17 00:00:00 2001 From: james-prysm Date: Wed, 12 Aug 2026 22:12:45 -0500 Subject: [PATCH 23/26] terence's comments --- ...mes-prysm_keymanager-builders-endpoints.md | 1 + config/proposer/loader/loader.go | 21 +++-- config/proposer/loader/loader_test.go | 38 ++++----- config/proposer/settings.go | 62 ++++++++++---- config/proposer/settings_test.go | 68 +++++++++++++-- testing/validator-mock/validator_mock.go | 14 +++ validator/client/iface/validator.go | 1 + validator/client/propose.go | 36 ++++---- validator/client/service.go | 6 ++ validator/client/testutil/mock_validator.go | 13 +++ validator/client/validator.go | 46 +++++++--- validator/rpc/handlers_keymanager.go | 85 ++++++++----------- validator/rpc/handlers_validator_config.go | 78 ++++++++--------- validator/rpc/server.go | 2 - 14 files changed, 291 insertions(+), 180 deletions(-) diff --git a/changelog/james-prysm_keymanager-builders-endpoints.md b/changelog/james-prysm_keymanager-builders-endpoints.md index d603b2710c48..ebaacd2cdff9 100644 --- a/changelog/james-prysm_keymanager-builders-endpoints.md +++ b/changelog/james-prysm_keymanager-builders-endpoints.md @@ -12,6 +12,7 @@ ### Deprecated - `--with-builder` generates legacy (pre-gloas) mev-boost builder settings, which are discontinued at the gloas fork; the command now warns when the flag is used. +- `--enable-builder` and `--suggested-gas-limit` produce only legacy (pre-gloas) content: they still drive mev-boost registrations before the fork, but never override v2 proposer settings or the gas limit schedule, and warn that they have no effect after gloas. An explicitly configured gas limit below the scheduled network gas limit is honored with a once-per-epoch warning. ### Fixed diff --git a/config/proposer/loader/loader.go b/config/proposer/loader/loader.go index cf25fb98282f..05577b96b212 100644 --- a/config/proposer/loader/loader.go +++ b/config/proposer/loader/loader.go @@ -401,18 +401,17 @@ func mergeProposerSettingsV2(merged, loaded, db *validatorpb.ProposerSettingsPay log.Warnf("--%s is legacy (pre-gloas) mev-boost content and has no effect after the gloas fork; configure builders via the settings source or keymanager API", flags.EnableBuilderFlag.Name) } - if gasLimitOnly == nil { - return merged - } - if merged.DefaultConfig == nil { - merged.DefaultConfig = &validatorpb.ProposerOptionPayload{GasLimit: *gasLimitOnly} - } else { - merged.DefaultConfig.GasLimit = *gasLimitOnly - } - for _, option := range merged.ProposerConfig { - if option != nil { - option.GasLimit = *gasLimitOnly + // --suggested-gas-limit is likewise legacy content: it applies to the + // pre-gloas builder gas limit and never overrides v2 or schedule values. + if gasLimitOnly != nil { + if merged.DefaultConfig == nil { + merged.DefaultConfig = &validatorpb.ProposerOptionPayload{} + } + if merged.DefaultConfig.Builder == nil { + merged.DefaultConfig.Builder = &validatorpb.BuilderConfig{} } + merged.DefaultConfig.Builder.GasLimit = *gasLimitOnly + log.Warnf("--%s is legacy (pre-gloas) content and has no effect after the gloas fork; set gas limits in v2 proposer settings or via the keymanager API", flags.BuilderGasLimitFlag.Name) } return merged } diff --git a/config/proposer/loader/loader_test.go b/config/proposer/loader/loader_test.go index cf8479605d48..7f1a55a4a069 100644 --- a/config/proposer/loader/loader_test.go +++ b/config/proposer/loader/loader_test.go @@ -1341,11 +1341,16 @@ func TestSettingsLoader_V1FileAfterMigratedDB(t *testing.T) { require.NotNil(t, got.DefaultConfig.BuilderConfig) assert.LogsDoNotContain(t, hook, "deprecated v1 schema") - require.Equal(t, false, got.UpgradeToV2()) key1, err := hexutil.Decode("0xa057816155ad77931185101128655c0191bd0214c201ca48ed887f6c4c6adf334070efcd75140eada5ac83a92506dd7a") require.NoError(t, err) // Pre-gloas reads still resolve the v1 builder gas limit as a fallback. require.Equal(t, validator.Uint64(60000000), got.GasLimit(bytesutil.ToBytes48(key1))) + + // The cutover scrubs the v1 content even under the v2 stamp, then no-ops. + require.Equal(t, true, got.UpgradeToV2()) + require.IsNil(t, got.DefaultConfig.BuilderConfig) + require.Equal(t, validator.Uint64(params.BeaconConfig().DefaultBuilderGasLimit), got.GasLimit(bytesutil.ToBytes48(key1))) + require.Equal(t, false, got.UpgradeToV2()) } func Test_mergeProposerSettings_CreatesDefaultFromGasLimitFlag(t *testing.T) { @@ -1361,16 +1366,19 @@ func Test_mergeProposerSettings_CreatesDefaultFromGasLimitFlag(t *testing.T) { require.Equal(t, gl, merged.DefaultConfig.Builder.GasLimit) } -func Test_mergeProposerSettings_V2GasLimitOnlyGoesToOption(t *testing.T) { +func Test_mergeProposerSettings_V2GasLimitIsLegacyContent(t *testing.T) { gl := validator.Uint64(12345678) merged := mergeProposerSettings( nil, &validatorpb.ProposerSettingsPayload{Version: proposer.SchemaV2}, &flagOptions{gasLimit: &gl}, ) + // The flag writes only legacy builder-level content, so post-fork + // resolution and the gas limit schedule are never overridden by it. require.NotNil(t, merged.DefaultConfig) - require.IsNil(t, merged.DefaultConfig.Builder) - require.Equal(t, gl, merged.DefaultConfig.GasLimit) + require.Equal(t, validator.Uint64(0), merged.DefaultConfig.GasLimit) + require.NotNil(t, merged.DefaultConfig.Builder) + require.Equal(t, gl, merged.DefaultConfig.Builder.GasLimit) } func Test_mergeProposerSettings_VersionGatesBuilderReset(t *testing.T) { @@ -1458,20 +1466,7 @@ func Test_mergeProposerSettings_V2LoadedOverridesDB(t *testing.T) { }) } -func Test_mergeProposerSettings_V2GasLimitOverwritesExistingDefault(t *testing.T) { - gl := validator.Uint64(12345678) - db := &validatorpb.ProposerSettingsPayload{ - Version: proposer.SchemaV2, - DefaultConfig: &validatorpb.ProposerOptionPayload{FeeRecipient: "0xdb", GasLimit: 1}, - } - merged := mergeProposerSettings(nil, db, &flagOptions{gasLimit: &gl}) - require.NotNil(t, merged.DefaultConfig) - require.IsNil(t, merged.DefaultConfig.Builder) - require.Equal(t, "0xdb", merged.DefaultConfig.FeeRecipient) - require.Equal(t, gl, merged.DefaultConfig.GasLimit) -} - -func Test_mergeProposerSettings_V2GasLimitOverwritesPerValidator(t *testing.T) { +func Test_mergeProposerSettings_V2GasLimitNeverOverridesOptions(t *testing.T) { gl := validator.Uint64(12345678) db := &validatorpb.ProposerSettingsPayload{ Version: proposer.SchemaV2, @@ -1481,8 +1476,11 @@ func Test_mergeProposerSettings_V2GasLimitOverwritesPerValidator(t *testing.T) { }, } merged := mergeProposerSettings(nil, db, &flagOptions{gasLimit: &gl}) - require.Equal(t, gl, merged.DefaultConfig.GasLimit) - require.Equal(t, gl, merged.ProposerConfig["0xkey"].GasLimit) + // Explicit v2 option-level values are the operator's; the legacy flag + // no longer stomps them at any level. + require.Equal(t, validator.Uint64(1), merged.DefaultConfig.GasLimit) + require.Equal(t, validator.Uint64(2), merged.ProposerConfig["0xkey"].GasLimit) + require.Equal(t, gl, merged.DefaultConfig.Builder.GasLimit) } func Test_markExplicitEmptyBuilders(t *testing.T) { diff --git a/config/proposer/settings.go b/config/proposer/settings.go index c1a14be9854e..c20138d0e73a 100644 --- a/config/proposer/settings.go +++ b/config/proposer/settings.go @@ -601,29 +601,40 @@ func (ps *Settings) HasBuilderContent() bool { return false } -// UpgradeToV2 is the v1 cutover: v1 builder configs, including their gas limits, -// do not apply to gloas and are dropped. Returns true if anything changed. +// UpgradeToV2 is the gloas cutover: legacy v1 builder fields are scrubbed +// wherever they appear — even under a v2 stamp — and the version is stamped. func (ps *Settings) UpgradeToV2() bool { - if ps == nil || ps.isV2() { + if ps == nil { return false } - dropped := false - drop := func(opt *Option) { + scrubbed := false + scrub := func(opt *Option) { if opt == nil || opt.BuilderConfig == nil { return } - opt.BuilderConfig = nil - dropped = true + bc := opt.BuilderConfig + if bc.Enabled || bc.GasLimit != 0 { + bc.Enabled = false + bc.GasLimit = 0 + scrubbed = true + } + // A config left with no v2 content disappears entirely; an explicit + // empty builders list is v2 content and survives. + if bc.Builders == nil && bc.MinBid == nil && bc.BuilderBoostFactor == nil && bc.MaxExecutionPayment == nil { + opt.BuilderConfig = nil + scrubbed = true + } } - drop(ps.DefaultConfig) + scrub(ps.DefaultConfig) for _, opt := range ps.ProposeConfig { - drop(opt) + scrub(opt) } + changed := scrubbed || ps.Version != SchemaV2 ps.Version = SchemaV2 - if dropped { + if scrubbed { log.Warn("v1 builder settings, including gas limits, do not apply to gloas and were replaced with defaults; provide v2 proposer settings to configure builders") } - return true + return changed } // TargetGasLimit resolves pubkey's proposer-preference gas limit at epoch: the @@ -640,6 +651,9 @@ func (ps *Settings) TargetGasLimit(pubkey [fieldparams.BLSPubkeyLength]byte, epo if active && uint64(operator) > scheduled { warnGasLimitExceedsSchedule(uint64(operator), scheduled, epoch) } + if active && uint64(operator) < scheduled { + warnGasLimitBelowSchedule(uint64(operator), scheduled, epoch) + } return operator } @@ -659,17 +673,33 @@ func (ps *Settings) operatorGasLimit(pubkey [fieldparams.BLSPubkeyLength]byte) ( var warnedGasLimitScheduleEpoch atomic.Uint64 func warnGasLimitExceedsSchedule(operator, scheduled uint64, epoch primitives.Epoch) { + if !warnOncePerEpoch(&warnedGasLimitScheduleEpoch, epoch) { + return + } + log.Warnf("Configured gas limit %d exceeds the recommended maximum of %d at epoch %d", operator, scheduled, epoch) +} + +var warnedGasLimitBelowScheduleEpoch atomic.Uint64 + +func warnGasLimitBelowSchedule(operator, scheduled uint64, epoch primitives.Epoch) { + if !warnOncePerEpoch(&warnedGasLimitBelowScheduleEpoch, epoch) { + return + } + log.Warnf("Configured gas limit %d is below the scheduled network gas limit of %d at epoch %d; remove the explicit gas limit to follow the schedule", operator, scheduled, epoch) +} + +// warnOncePerEpoch reports whether the caller won this epoch's single warning slot. +func warnOncePerEpoch(guard *atomic.Uint64, epoch primitives.Epoch) bool { e := uint64(epoch) + 1 for { - prev := warnedGasLimitScheduleEpoch.Load() + prev := guard.Load() if e <= prev { - return + return false } - if warnedGasLimitScheduleEpoch.CompareAndSwap(prev, e) { - break + if guard.CompareAndSwap(prev, e) { + return true } } - log.Warnf("Configured gas limit %d exceeds the recommended maximum of %d at epoch %d", operator, scheduled, epoch) } // GasLimit resolves pubkey's gas limit: explicitly set option-level values win, diff --git a/config/proposer/settings_test.go b/config/proposer/settings_test.go index de5e9727de7b..1c4197680385 100644 --- a/config/proposer/settings_test.go +++ b/config/proposer/settings_test.go @@ -538,16 +538,50 @@ func TestSettings_UpgradeToV2(t *testing.T) { require.IsNil(t, ps.ProposeConfig[pk2].BuilderConfig) }) - t.Run("already v2 is left untouched", func(t *testing.T) { + t.Run("v1 content under a v2 stamp is still scrubbed", func(t *testing.T) { ps := &Settings{ Version: SchemaV2, DefaultConfig: &Option{ BuilderConfig: &BuilderConfig{Enabled: true, GasLimit: validator.Uint64(42_000_000)}, }, } - require.Equal(t, false, ps.UpgradeToV2()) + require.Equal(t, true, ps.UpgradeToV2()) require.Equal(t, validator.Uint64(0), ps.DefaultConfig.GasLimit) + require.IsNil(t, ps.DefaultConfig.BuilderConfig) + require.Equal(t, false, ps.UpgradeToV2()) + }) + + t.Run("mixed config keeps its v2 fields and loses the v1 ones", func(t *testing.T) { + ps := &Settings{ + Version: SchemaV2, + DefaultConfig: &Option{ + BuilderConfig: &BuilderConfig{ + Enabled: true, + GasLimit: validator.Uint64(42_000_000), + Builders: []*BuilderEntry{{URL: "https://b.example"}}, + }, + }, + } + require.Equal(t, true, ps.UpgradeToV2()) + bc := ps.DefaultConfig.BuilderConfig + require.NotNil(t, bc) + require.Equal(t, false, bc.Enabled) + require.Equal(t, validator.Uint64(0), bc.GasLimit) + require.Equal(t, 1, len(bc.Builders)) + require.Equal(t, false, ps.UpgradeToV2()) + }) + + t.Run("explicit empty builders list survives the scrub", func(t *testing.T) { + ps := &Settings{ + Version: SchemaV2, + DefaultConfig: &Option{ + BuilderConfig: &BuilderConfig{Enabled: true, Builders: []*BuilderEntry{}}, + }, + } + require.Equal(t, true, ps.UpgradeToV2()) require.NotNil(t, ps.DefaultConfig.BuilderConfig) + require.NotNil(t, ps.DefaultConfig.BuilderConfig.Builders) + require.Equal(t, 0, len(ps.DefaultConfig.BuilderConfig.Builders)) }) t.Run("default with no builder and zero GasLimit still bumps to v2", func(t *testing.T) { @@ -640,11 +674,28 @@ func TestSettings_TargetGasLimit_Schedule(t *testing.T) { require.LogsDoNotContain(t, hook, "exceeds the recommended maximum") }) - t.Run("operator value below the schedule is honored silently", func(t *testing.T) { + t.Run("operator value below the schedule is honored with a loud warning", func(t *testing.T) { hook := logtest.NewGlobal() - warnedGasLimitScheduleEpoch.Store(0) + warnedGasLimitBelowScheduleEpoch.Store(0) ps := &Settings{DefaultConfig: &Option{GasLimit: validator.Uint64(50_000_000)}} require.Equal(t, validator.Uint64(50_000_000), ps.TargetGasLimit(pk, 200)) + require.LogsContain(t, hook, "below the scheduled network gas limit") + require.LogsDoNotContain(t, hook, "exceeds the recommended maximum") + // Deduplicated within the epoch, warned again in the next one. + hook.Reset() + require.Equal(t, validator.Uint64(50_000_000), ps.TargetGasLimit(pk, 200)) + require.LogsDoNotContain(t, hook, "below the scheduled network gas limit") + require.Equal(t, validator.Uint64(50_000_000), ps.TargetGasLimit(pk, 201)) + require.LogsContain(t, hook, "below the scheduled network gas limit") + }) + + t.Run("operator value matching the schedule warns nothing", func(t *testing.T) { + hook := logtest.NewGlobal() + warnedGasLimitScheduleEpoch.Store(0) + warnedGasLimitBelowScheduleEpoch.Store(0) + ps := &Settings{DefaultConfig: &Option{GasLimit: validator.Uint64(90_000_000)}} + require.Equal(t, validator.Uint64(90_000_000), ps.TargetGasLimit(pk, 200)) + require.LogsDoNotContain(t, hook, "below the scheduled network gas limit") require.LogsDoNotContain(t, hook, "exceeds the recommended maximum") }) } @@ -935,8 +986,8 @@ func TestHasBuilderContent(t *testing.T) { require.Equal(t, true, neutral.HasBuilderContent()) } -// The cutover drops all v1 builder content, including gloas knobs like -// max_execution_payment; v2 settings are the only source of builder opinions. +// The cutover scrubs v1 builder fields; v2 content — including an explicit +// max_execution_payment — survives it. func TestUpgradeToV2_DropsBuilderContent(t *testing.T) { key := [fieldparams.BLSPubkeyLength]byte{9} ps := &Settings{ @@ -948,7 +999,10 @@ func TestUpgradeToV2_DropsBuilderContent(t *testing.T) { } require.Equal(t, true, ps.UpgradeToV2()) require.Equal(t, SchemaV2, ps.Version) - require.IsNil(t, ps.DefaultConfig.BuilderConfig) + // The explicit trustless-only ceiling is v2 content and survives. + require.NotNil(t, ps.DefaultConfig.BuilderConfig) + require.Equal(t, validator.Uint64(0), *ps.DefaultConfig.BuilderConfig.MaxExecutionPayment) + // The pure-v1 per-key config is gone entirely. require.IsNil(t, ps.ProposeConfig[key].BuilderConfig) require.Equal(t, false, ps.UpgradeToV2()) } diff --git a/testing/validator-mock/validator_mock.go b/testing/validator-mock/validator_mock.go index 900ad0b5ee4a..780af232b307 100644 --- a/testing/validator-mock/validator_mock.go +++ b/testing/validator-mock/validator_mock.go @@ -503,6 +503,20 @@ func (mr *MockValidatorMockRecorder) UpdateDuties(ctx any) *gomock.Call { return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateDuties", reflect.TypeOf((*MockValidator)(nil).UpdateDuties), ctx) } +// UpdateProposerSettings mocks base method. +func (m *MockValidator) UpdateProposerSettings(arg0 context.Context, arg1 func(*proposer.Settings) (*proposer.Settings, error)) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "UpdateProposerSettings", arg0, arg1) + ret0, _ := ret[0].(error) + return ret0 +} + +// UpdateProposerSettings indicates an expected call of UpdateProposerSettings. +func (mr *MockValidatorMockRecorder) UpdateProposerSettings(arg0, arg1 any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateProposerSettings", reflect.TypeOf((*MockValidator)(nil).UpdateProposerSettings), arg0, arg1) +} + // WaitForActivation mocks base method. func (m *MockValidator) WaitForActivation(ctx context.Context) error { m.ctrl.T.Helper() diff --git a/validator/client/iface/validator.go b/validator/client/iface/validator.go index 2814426ffbc3..baaf937b2808 100644 --- a/validator/client/iface/validator.go +++ b/validator/client/iface/validator.go @@ -68,6 +68,7 @@ type Validator interface { ProcessEvent(ctx context.Context, event *event.Event) ProposerSettings() *proposer.Settings SetProposerSettings(context.Context, *proposer.Settings) error + UpdateProposerSettings(context.Context, func(*proposer.Settings) (*proposer.Settings, error)) error Graffiti(ctx context.Context, pubKey [fieldparams.BLSPubkeyLength]byte) ([]byte, error) SetGraffiti(ctx context.Context, pubKey [fieldparams.BLSPubkeyLength]byte, graffiti []byte) error DeleteGraffiti(ctx context.Context, pubKey [fieldparams.BLSPubkeyLength]byte) error diff --git a/validator/client/propose.go b/validator/client/propose.go index 9e242507ebec..7e0a4abd8fd8 100644 --- a/validator/client/propose.go +++ b/validator/client/propose.go @@ -577,29 +577,31 @@ func (v *validator) SetGraffiti(ctx context.Context, pubkey [fieldparams.BLSPubk if graffiti == nil { return nil } - // API-created settings carry no v1 content: v2 once gloas is scheduled. - settings := &proposer.Settings{Version: proposer.FreshSettingsVersion()} - if v.proposerSettings != nil { - settings = v.proposerSettings.Clone() - } - settings.UpsertProposeOption(pubkey).GraffitiConfig = &proposer.GraffitiConfig{Graffiti: string(graffiti)} - return v.SetProposerSettings(ctx, settings) + return v.UpdateProposerSettings(ctx, func(settings *proposer.Settings) (*proposer.Settings, error) { + if settings == nil { + // API-created settings carry no v1 content: v2 once gloas is scheduled. + settings = &proposer.Settings{Version: proposer.FreshSettingsVersion()} + } + settings.UpsertProposeOption(pubkey).GraffitiConfig = &proposer.GraffitiConfig{Graffiti: string(graffiti)} + return settings, nil + }) } func (v *validator) DeleteGraffiti(ctx context.Context, pubKey [fieldparams.BLSPubkeyLength]byte) error { ctx, span := trace.StartSpan(ctx, "validator.DeleteGraffiti") defer span.End() - if v.proposerSettings == nil || v.proposerSettings.ProposeConfig == nil { - return errors.New("attempted to delete graffiti without proposer settings, graffiti will default to flag options") - } - ps := v.proposerSettings.Clone() - option, ok := ps.ProposeConfig[pubKey] - if !ok || option == nil { - return fmt.Errorf("graffiti not found in proposer settings for pubkey:%s", hexutil.Encode(pubKey[:])) - } - option.GraffitiConfig = nil - return v.SetProposerSettings(ctx, ps) // save the proposer settings + return v.UpdateProposerSettings(ctx, func(ps *proposer.Settings) (*proposer.Settings, error) { + if ps == nil || ps.ProposeConfig == nil { + return nil, errors.New("attempted to delete graffiti without proposer settings, graffiti will default to flag options") + } + option, ok := ps.ProposeConfig[pubKey] + if !ok || option == nil { + return nil, fmt.Errorf("graffiti not found in proposer settings for pubkey:%s", hexutil.Encode(pubKey[:])) + } + option.GraffitiConfig = nil + return ps, nil + }) } func blockLogFields(pubKey [fieldparams.BLSPubkeyLength]byte, blk interfaces.ReadOnlyBeaconBlock, sig []byte) logrus.Fields { diff --git a/validator/client/service.go b/validator/client/service.go index ecea097b572f..4ebe6476bc99 100644 --- a/validator/client/service.go +++ b/validator/client/service.go @@ -311,6 +311,12 @@ func (v *ValidatorService) ProposerSettings() *proposer.Settings { return nil } +// UpdateProposerSettings atomically mutates the proposer settings on the +// underlying validator; see iface.Validator.UpdateProposerSettings. +func (v *ValidatorService) UpdateProposerSettings(ctx context.Context, mutate func(*proposer.Settings) (*proposer.Settings, error)) error { + return v.validator.UpdateProposerSettings(ctx, mutate) +} + // SetProposerSettings sets the proposer settings on the validator service as well as the underlying validator func (v *ValidatorService) SetProposerSettings(ctx context.Context, settings *proposer.Settings) error { // validator service proposer settings is only used for pass through from node -> validator service -> validator. diff --git a/validator/client/testutil/mock_validator.go b/validator/client/testutil/mock_validator.go index 89d415a34697..9108c0059818 100644 --- a/validator/client/testutil/mock_validator.go +++ b/validator/client/testutil/mock_validator.go @@ -302,6 +302,19 @@ func (fv *FakeValidator) SetProposerSettings(_ context.Context, settings *propos return nil } +// UpdateProposerSettings for mocking +func (fv *FakeValidator) UpdateProposerSettings(_ context.Context, mutate func(*proposer.Settings) (*proposer.Settings, error)) error { + next, err := mutate(fv.proposerSettings.Clone()) + if err != nil { + return err + } + if next == nil { + return nil + } + fv.proposerSettings = next + return nil +} + // Graffiti for mocking func (fv *FakeValidator) Graffiti(_ context.Context, _ [fieldparams.BLSPubkeyLength]byte) ([]byte, error) { return []byte(fv.graffiti), nil diff --git a/validator/client/validator.go b/validator/client/validator.go index 268bfe22b7a4..546dc1f003af 100644 --- a/validator/client/validator.go +++ b/validator/client/validator.go @@ -92,7 +92,8 @@ type validator struct { startBalances map[[fieldparams.BLSPubkeyLength]byte]uint64 attestedSlotsByKeyByEpoch map[primitives.Epoch]map[[fieldparams.BLSPubkeyLength]byte]primitives.Slot web3SignerConfig *remoteweb3signer.SetupConfig - proposerSettings *proposer.Settings + proposerSettings *proposer.Settings // clone-then-swap under proposerSettingsMu; reads are lock-free + proposerSettingsMu sync.Mutex submittedPrefSlots map[primitives.Slot]bool connTracker connTracker // per push kind, the conn generation last confirmed pushed submittedAtts map[submittedAttKey]*submittedAtt @@ -805,6 +806,12 @@ func (v *validator) ProposerSettings() *proposer.Settings { // SetProposerSettings sets and saves the passed in proposer settings overriding the in memory one func (v *validator) SetProposerSettings(ctx context.Context, settings *proposer.Settings) error { + v.proposerSettingsMu.Lock() + defer v.proposerSettingsMu.Unlock() + return v.setProposerSettingsLocked(ctx, settings) +} + +func (v *validator) setProposerSettingsLocked(ctx context.Context, settings *proposer.Settings) error { ctx, span := trace.StartSpan(ctx, "validator.SetProposerSettings") defer span.End() @@ -818,6 +825,21 @@ func (v *validator) SetProposerSettings(ctx context.Context, settings *proposer. return nil } +// UpdateProposerSettings atomically mutates the proposer settings: mutate gets a +// deep copy (nil when unset) and returns what to persist, or nil for a no-op. +func (v *validator) UpdateProposerSettings(ctx context.Context, mutate func(*proposer.Settings) (*proposer.Settings, error)) error { + v.proposerSettingsMu.Lock() + defer v.proposerSettingsMu.Unlock() + next, err := mutate(v.proposerSettings.Clone()) + if err != nil { + return err + } + if next == nil { + return nil + } + return v.setProposerSettingsLocked(ctx, next) +} + // PushProposerSettings calls the prepareBeaconProposer RPC to set the fee recipient and also the register validator API if using a custom builder. func (v *validator) PushProposerSettings(ctx context.Context, slot primitives.Slot, forceFullPush bool) error { ctx, span := trace.StartSpan(ctx, "validator.PushProposerSettings") @@ -1219,20 +1241,16 @@ func (v *validator) buildProposerSettingsRequests( return prepareProposerReqs } -// upgradeProposerSettingsToV2 is idempotent post-fork cleanup: it strips dead v1 -// builder content from a clone and swaps; callers gate on gloas-active. +// upgradeProposerSettingsToV2 is idempotent post-fork cleanup: it scrubs dead v1 +// builder content transactionally; callers gate on gloas-active. func (v *validator) upgradeProposerSettingsToV2(ctx context.Context) { - snapshot := v.ProposerSettings() - ps := snapshot.Clone() - if !ps.UpgradeToV2() { - return - } - // Pointer changed = a keymanager write landed after our snapshot; swapping - // our stale clone would erase it. This cleanup simply reruns next cycle. - if v.ProposerSettings() != snapshot { - return - } - if err := v.SetProposerSettings(ctx, ps); err != nil { + err := v.UpdateProposerSettings(ctx, func(ps *proposer.Settings) (*proposer.Settings, error) { + if !ps.UpgradeToV2() { + return nil, nil + } + return ps, nil + }) + if err != nil { log.WithError(err).Warn("Failed to persist v1->v2 proposer settings upgrade") } } diff --git a/validator/rpc/handlers_keymanager.go b/validator/rpc/handlers_keymanager.go index ba55e1a03d14..562f8ed8f07d 100644 --- a/validator/rpc/handlers_keymanager.go +++ b/validator/rpc/handlers_keymanager.go @@ -626,19 +626,15 @@ func (s *Server) SetFeeRecipientByPubkey(w http.ResponseWriter, r *http.Request) return } feeRecipient := common.BytesToAddress(ethAddress) - s.proposerSettingsLock.Lock() - defer s.proposerSettingsLock.Unlock() - settings := s.validatorService.ProposerSettings() - if settings == nil { - // API-created settings carry no v1 content: v2 once gloas is scheduled. - settings = &proposer.Settings{Version: proposer.FreshSettingsVersion()} - } else { - // Clone-then-swap: lock-free readers must never see in-place mutation. - settings = settings.Clone() - } - // A newly created option leaves BuilderConfig nil so the key inherits default_config. - settings.UpsertProposeOption(bytesutil.ToBytes48(pubkey)).FeeRecipientConfig = &proposer.FeeRecipientConfig{FeeRecipient: feeRecipient} - if err := s.validatorService.SetProposerSettings(ctx, settings); err != nil { + if err := s.validatorService.UpdateProposerSettings(ctx, func(settings *proposer.Settings) (*proposer.Settings, error) { + if settings == nil { + // API-created settings carry no v1 content: v2 once gloas is scheduled. + settings = &proposer.Settings{Version: proposer.FreshSettingsVersion()} + } + // A newly created option leaves BuilderConfig nil so the key inherits default_config. + settings.UpsertProposeOption(bytesutil.ToBytes48(pubkey)).FeeRecipientConfig = &proposer.FeeRecipientConfig{FeeRecipient: feeRecipient} + return settings, nil + }); err != nil { httputil.HandleError(w, "Could not set proposer settings: "+err.Error(), http.StatusInternalServerError) return } @@ -660,19 +656,16 @@ func (s *Server) DeleteFeeRecipientByPubkey(w http.ResponseWriter, r *http.Reque return } - s.proposerSettingsLock.Lock() - defer s.proposerSettingsLock.Unlock() - settings := s.validatorService.ProposerSettings() - if settings != nil && settings.ProposeConfig != nil { - settings = settings.Clone() + if err := s.validatorService.UpdateProposerSettings(ctx, func(settings *proposer.Settings) (*proposer.Settings, error) { + if settings == nil || settings.ProposeConfig == nil { + return nil, nil + } proposerOption, found := settings.ProposeConfig[bytesutil.ToBytes48(pubkey)] if found { proposerOption.FeeRecipientConfig = nil } - } - - // save the settings - if err := s.validatorService.SetProposerSettings(ctx, settings); err != nil { + return settings, nil + }); err != nil { httputil.HandleError(w, "Could not set proposer settings: "+err.Error(), http.StatusInternalServerError) return } @@ -734,20 +727,16 @@ func (s *Server) SetGasLimit(w http.ResponseWriter, r *http.Request) { return } - s.proposerSettingsLock.Lock() - defer s.proposerSettingsLock.Unlock() - // Clone-then-swap: lock-free readers must never see in-place mutation. - settings := s.validatorService.ProposerSettings().Clone() - if settings == nil { - // API-created settings carry no v1 content: v2 once gloas is scheduled. - settings = &proposer.Settings{Version: proposer.FreshSettingsVersion()} - } - if err := settings.SetGasLimit(bytesutil.ToBytes48(pubkey), validator.Uint64(gasLimit)); err != nil { - httputil.HandleError(w, err.Error(), http.StatusInternalServerError) - return - } - - if err := s.validatorService.SetProposerSettings(ctx, settings); err != nil { + if err := s.validatorService.UpdateProposerSettings(ctx, func(settings *proposer.Settings) (*proposer.Settings, error) { + if settings == nil { + // API-created settings carry no v1 content: v2 once gloas is scheduled. + settings = &proposer.Settings{Version: proposer.FreshSettingsVersion()} + } + if err := settings.SetGasLimit(bytesutil.ToBytes48(pubkey), validator.Uint64(gasLimit)); err != nil { + return nil, err + } + return settings, nil + }); err != nil { httputil.HandleError(w, "Could not set proposer settings: "+err.Error(), http.StatusInternalServerError) return } @@ -768,16 +757,19 @@ func (s *Server) DeleteGasLimit(w http.ResponseWriter, r *http.Request) { return } - s.proposerSettingsLock.Lock() - defer s.proposerSettingsLock.Unlock() - // Clone-then-swap: lock-free readers must never see in-place mutation. - settings := s.validatorService.ProposerSettings().Clone() - if !settings.ResetGasLimit(bytesutil.ToBytes48(pubkey)) { - httputil.HandleError(w, fmt.Sprintf("No gas limit found for pubkey %q", rawPubkey), http.StatusNotFound) + reset := false + if err := s.validatorService.UpdateProposerSettings(ctx, func(settings *proposer.Settings) (*proposer.Settings, error) { + if !settings.ResetGasLimit(bytesutil.ToBytes48(pubkey)) { + return nil, nil + } + reset = true + return settings, nil + }); err != nil { + httputil.HandleError(w, "Could not set proposer settings: "+err.Error(), http.StatusBadRequest) return } - if err := s.validatorService.SetProposerSettings(ctx, settings); err != nil { - httputil.HandleError(w, "Could not set proposer settings: "+err.Error(), http.StatusBadRequest) + if !reset { + httputil.HandleError(w, fmt.Sprintf("No gas limit found for pubkey %q", rawPubkey), http.StatusNotFound) return } w.WriteHeader(http.StatusNoContent) @@ -840,9 +832,6 @@ func (s *Server) SetGraffiti(w http.ResponseWriter, r *http.Request) { return } - // Graffiti writes proposer settings, so it serializes with the other writers. - s.proposerSettingsLock.Lock() - defer s.proposerSettingsLock.Unlock() if err := s.validatorService.SetGraffiti(ctx, bytesutil.ToBytes48(pubkey), []byte(req.Graffiti)); err != nil { httputil.HandleError(w, err.Error(), http.StatusInternalServerError) return @@ -863,8 +852,6 @@ func (s *Server) DeleteGraffiti(w http.ResponseWriter, r *http.Request) { return } - s.proposerSettingsLock.Lock() - defer s.proposerSettingsLock.Unlock() if err := s.validatorService.DeleteGraffiti(ctx, bytesutil.ToBytes48(pubkey)); err != nil { httputil.HandleError(w, err.Error(), http.StatusNotFound) return diff --git a/validator/rpc/handlers_validator_config.go b/validator/rpc/handlers_validator_config.go index dfa041424683..81236ec70bee 100644 --- a/validator/rpc/handlers_validator_config.go +++ b/validator/rpc/handlers_validator_config.go @@ -82,31 +82,27 @@ func (s *Server) SetBuilders(w http.ResponseWriter, r *http.Request) { return } - s.proposerSettingsLock.Lock() - defer s.proposerSettingsLock.Unlock() - - settings := s.validatorService.ProposerSettings() - if settings == nil { - settings = &proposer.Settings{Version: proposer.FreshSettingsVersion()} - } else { - settings = settings.Clone() - // Stamp the storage format only: semantics are fork-keyed, so writing - // builders never drops v1 content or changes other keys' behavior. - settings.Version = proposer.SchemaV2 - } - - if settings.ProposeConfig == nil { - settings.ProposeConfig = make(map[[fieldparams.BLSPubkeyLength]byte]*proposer.Option) - } - key := bytesutil.ToBytes48(pubkey) - opt := settings.ProposeConfig[key] - if opt == nil { - opt = &proposer.Option{} - settings.ProposeConfig[key] = opt - } - opt.BuilderConfig = bc - - if err := s.validatorService.SetProposerSettings(ctx, settings); err != nil { + err = s.validatorService.UpdateProposerSettings(ctx, func(settings *proposer.Settings) (*proposer.Settings, error) { + if settings == nil { + settings = &proposer.Settings{Version: proposer.FreshSettingsVersion()} + } else { + // Stamp the storage format only: semantics are fork-keyed, so writing + // builders never drops v1 content or changes other keys' behavior. + settings.Version = proposer.SchemaV2 + } + if settings.ProposeConfig == nil { + settings.ProposeConfig = make(map[[fieldparams.BLSPubkeyLength]byte]*proposer.Option) + } + key := bytesutil.ToBytes48(pubkey) + opt := settings.ProposeConfig[key] + if opt == nil { + opt = &proposer.Option{} + settings.ProposeConfig[key] = opt + } + opt.BuilderConfig = bc + return settings, nil + }) + if err != nil { httputil.HandleError(w, "Could not set proposer settings: "+err.Error(), http.StatusInternalServerError) return } @@ -131,25 +127,19 @@ func (s *Server) DeleteBuilders(w http.ResponseWriter, r *http.Request) { return } - s.proposerSettingsLock.Lock() - defer s.proposerSettingsLock.Unlock() - - settings := s.validatorService.ProposerSettings() - key := bytesutil.ToBytes48(pubkey) - // Removing an absent configuration succeeds; the key already follows the defaults. - if settings == nil || settings.ProposeConfig == nil { - w.WriteHeader(http.StatusNoContent) - return - } - opt, found := settings.ProposeConfig[key] - if !found || opt == nil || opt.BuilderConfig == nil { - w.WriteHeader(http.StatusNoContent) - return - } - - settings = settings.Clone() - settings.ProposeConfig[key].BuilderConfig = nil - if err := s.validatorService.SetProposerSettings(ctx, settings); err != nil { + err := s.validatorService.UpdateProposerSettings(ctx, func(settings *proposer.Settings) (*proposer.Settings, error) { + // Removing an absent configuration succeeds; the key already follows the defaults. + if settings == nil || settings.ProposeConfig == nil { + return nil, nil + } + opt, found := settings.ProposeConfig[bytesutil.ToBytes48(pubkey)] + if !found || opt == nil || opt.BuilderConfig == nil { + return nil, nil + } + opt.BuilderConfig = nil + return settings, nil + }) + if err != nil { httputil.HandleError(w, "Could not set proposer settings: "+err.Error(), http.StatusInternalServerError) return } diff --git a/validator/rpc/server.go b/validator/rpc/server.go index d82eabe18fda..1b14dfbc7704 100644 --- a/validator/rpc/server.go +++ b/validator/rpc/server.go @@ -7,7 +7,6 @@ import ( "net/http" "path/filepath" "strings" - "sync" "time" "github.com/OffchainLabs/prysm/v7/api" @@ -82,7 +81,6 @@ type Server struct { walletDir string jwtSecret []byte grpcHeaders []string - proposerSettingsLock sync.Mutex } // NewServer instantiates a new HTTP server. From 25d6063e741ffbc8ad0e2b54c3134e3d0529cc91 Mon Sep 17 00:00:00 2001 From: james-prysm Date: Thu, 13 Aug 2026 13:43:04 -0500 Subject: [PATCH 24/26] adding test --- validator/client/validator_test.go | 51 ++++++++++++++++++++++++++++++ 1 file changed, 51 insertions(+) diff --git a/validator/client/validator_test.go b/validator/client/validator_test.go index 0b3e6b1fa2a2..b89457c627d4 100644 --- a/validator/client/validator_test.go +++ b/validator/client/validator_test.go @@ -4222,3 +4222,54 @@ func TestProcessEvent_HeadV2_PayloadStatus(t *testing.T) { }) } } + +// The post-fork settings cleanup runs concurrently with keymanager writes; the +// transactional update must serialize them so no write is ever lost. +func TestValidator_UpdateProposerSettings_Concurrency(t *testing.T) { + ctx := t.Context() + db := dbTest.SetupDB(t, t.TempDir(), [][fieldparams.BLSPubkeyLength]byte{}, false) + v := &validator{ + db: db, + proposerSettings: &proposer.Settings{ + Version: proposer.SchemaV1, + DefaultConfig: &proposer.Option{ + BuilderConfig: &proposer.BuilderConfig{Enabled: true, GasLimit: 30_000_000}, + }, + }, + } + + const writers = 16 + errs := make(chan error, writers) + var wg sync.WaitGroup + for i := range writers { + key := [fieldparams.BLSPubkeyLength]byte{byte(i + 1)} + wg.Add(2) + go func() { + defer wg.Done() + errs <- v.UpdateProposerSettings(ctx, func(ps *proposer.Settings) (*proposer.Settings, error) { + if ps == nil { + ps = &proposer.Settings{Version: proposer.SchemaV2} + } + ps.UpsertProposeOption(key).GasLimit = 1 + return ps, nil + }) + }() + go func() { + defer wg.Done() + v.upgradeProposerSettingsToV2(ctx) + }() + } + wg.Wait() + close(errs) + for err := range errs { + require.NoError(t, err) + } + + ps := v.ProposerSettings() + require.NotNil(t, ps) + // Every keymanager write survived the concurrent cleanup passes. + require.Equal(t, writers, len(ps.ProposeConfig)) + require.Equal(t, proposer.SchemaV2, ps.Version) + // A final cleanup pass leaves nothing to scrub. + require.Equal(t, false, v.ProposerSettings().Clone().UpgradeToV2()) +} From 3d761b1eb6102d786b49c1504b8256c28555c1bb Mon Sep 17 00:00:00 2001 From: james-prysm Date: Fri, 14 Aug 2026 15:36:50 -0500 Subject: [PATCH 25/26] self review, making sure fork transitions are smoother --- .../rpc/prysm/v1alpha1/validator/server.go | 2 +- ...mes-prysm_keymanager-builders-endpoints.md | 4 +- config/proposer/settings.go | 78 ++++++++---- config/proposer/settings_test.go | 111 ++++++++++++++++-- validator/client/validator.go | 7 +- .../rpc/handlers_validator_config_test.go | 24 ++++ 6 files changed, 186 insertions(+), 40 deletions(-) diff --git a/beacon-chain/rpc/prysm/v1alpha1/validator/server.go b/beacon-chain/rpc/prysm/v1alpha1/validator/server.go index 64c9c54c4b9b..f0073ac274c8 100644 --- a/beacon-chain/rpc/prysm/v1alpha1/validator/server.go +++ b/beacon-chain/rpc/prysm/v1alpha1/validator/server.go @@ -97,7 +97,7 @@ type Server struct { lastBidSlot primitives.Slot lastBidSource bidSource // Guarded by lastBidLock, set during Gloas block build, read when proposing. lastBidBuilderURL string // Guarded by lastBidLock, winning Builder-API URL for lastBidSlot. - maxExecutionPayments sync.Map // validator pubkey [48]byte -> max execution payment (Gwei uint64). + maxExecutionPayments sync.Map // pubkey [48]byte -> Gwei uint64, min-collapsed per-entry caps; TODO(gloas): re-key per builder identity with beacon-APIs #630. } // Deprecated: The gRPC API will remain the default and fully supported through v8 (expected in 2026) but will be eventually removed in favor of REST API. diff --git a/changelog/james-prysm_keymanager-builders-endpoints.md b/changelog/james-prysm_keymanager-builders-endpoints.md index ebaacd2cdff9..e67ad257bce9 100644 --- a/changelog/james-prysm_keymanager-builders-endpoints.md +++ b/changelog/james-prysm_keymanager-builders-endpoints.md @@ -4,7 +4,9 @@ ### Changed -- Add v2 proposer settings (`"version": 2`): builder fields a key does not set inherit from `default_config`; v2 has no `enabled` field. A settings source without a `version` that contains v2 builder fields (`builders`, `min_bid`, `builder_boost_factor`, `max_execution_payment`) is treated as version 2 automatically. Proposer-settings semantics are keyed on the gloas fork, not the schema version: before the fork the legacy mev-boost rules apply (with a resolved nonempty `builders` list also opting a key in, and an explicit empty list opting it out), and from the fork on only v2 content is read. Writing builders via the keymanager API never changes other keys' behavior or drops v1 content. +- Add v2 proposer settings (`"version": 2`) for configuring gloas builders; see the keymanager-APIs specification for the schema. +- Proposer settings sources without a `version` are treated as version 2 when they contain v2 builder fields. +- In v2 proposer settings, a nonempty `builders` list opts a key into mev-boost registration before the gloas fork, and an explicit empty list opts it out. - v1 builder settings are not migrated to v2: at the gloas fork fee recipients and graffiti carry over, while v1 builder content — including its gas limits — is dropped and replaced with defaults, with a warning. Gas limits apply post-fork only when explicitly set at the option level, so validators follow future chain-default gas limit increases unless they opt out. - The gas-limit keymanager API writes the option-level gas limit and no longer requires an enabled builder; deleting a gas limit unsets it (following the chain default) instead of pinning the current default value. Builder registration resolves fee recipients and participation independently, so a key with an enabled builder and only a default fee recipient now registers. - Setting a fee recipient, gas limit, or graffiti no longer snapshots `default_config`'s builder settings onto that key; the key keeps following the default as it changes. diff --git a/config/proposer/settings.go b/config/proposer/settings.go index c20138d0e73a..9f73dbd8dee7 100644 --- a/config/proposer/settings.go +++ b/config/proposer/settings.go @@ -173,16 +173,17 @@ func (ps *Settings) RegistrationFor(pubkey [fieldparams.BLSPubkeyLength]byte) (c feeRecipient = opt.FeeRecipientConfig.FeeRecipient hasFeeRecipient = true } - // Legacy enabled is object-level: a per-key builder config wins wholesale, - // so a disabled one opts the key out even under an enabled default. - enabled := ps.DefaultConfig != nil && ps.DefaultConfig.BuilderConfig.IsEnabled() - if opt != nil && opt.BuilderConfig != nil { - enabled = opt.BuilderConfig.IsEnabled() + enabled := false + if ps.DefaultConfig != nil { + if in, ok := ps.DefaultConfig.BuilderConfig.registrationEnabled(); ok { + enabled = in + } } - // v2 content opts in independently: a resolved nonempty builders list - // participates; an explicit empty list stays opted out. - if bc := ps.EffectiveBuilderConfig(pubkey); bc != nil && len(bc.Builders) > 0 { - enabled = true + // A per-key choice wins over the default's. + if opt != nil { + if in, ok := opt.BuilderConfig.registrationEnabled(); ok { + enabled = in + } } // Explicitly set option-level gas limits win; legacy builder-level values // are the pre-gloas fallback. @@ -268,6 +269,33 @@ func (bc *BuilderConfig) IsEnabled() bool { return bc != nil && bc.Enabled } +// hasV2Content reports whether any v2 builder field is set; an explicit empty +// builders list counts. +func (bc *BuilderConfig) hasV2Content() bool { + return bc != nil && (bc.Builders != nil || bc.MinBid != nil || bc.BuilderBoostFactor != nil || bc.MaxExecutionPayment != nil) +} + +// registrationEnabled reports whether this config opts the key in or out of +// mev-boost registration. ok is false when the config says neither. +func (bc *BuilderConfig) registrationEnabled() (enabled, ok bool) { + switch { + case bc == nil: + return false, false + case len(bc.Builders) > 0 || bc.Enabled: + // A nonempty v2 builders list opts in pre-gloas just like v1 enabled. + return true, true + case bc.Builders != nil: + // An explicit empty list means self-build everywhere: no mev-boost either. + return false, true + case !bc.hasV2Content(): + // A pure-v1 config without enabled is the legacy wins-wholesale disable. + return false, true + default: + // Gloas knobs (min_bid etc.) without a builders list: no registration choice. + return false, false + } +} + // effectiveBuilderConfig merges two config levels with field-level inheritance; // the builders list replaces rather than merges. func effectiveBuilderConfig(perKey, def *BuilderConfig) *BuilderConfig { @@ -573,28 +601,34 @@ func (ps *Settings) isV2() bool { return ps != nil && ps.Version == SchemaV2 } -// WarnDeprecatedSchema logs a warning when v1 settings are used on a network -// with gloas scheduled. +// WarnDeprecatedSchema logs a warning when legacy v1 builder content is loaded +// on a network with gloas scheduled, regardless of the schema stamp. func (ps *Settings) WarnDeprecatedSchema() { - if ps == nil || ps.Version == SchemaV2 || !params.GloasEnabled() { + if ps == nil || !params.GloasEnabled() { return } - // Fee recipients and graffiti behave identically across schemas; only - // builder content gives the cutover something to drop. - if !ps.HasBuilderContent() { + // Fee recipients and graffiti behave identically across schemas; only v1 + // builder fields give the cutover something to drop. + if !ps.HasLegacyBuilderContent() { return } - log.Warn("Proposer settings use the deprecated v1 schema; v1 builder settings, including gas limits, are replaced with defaults at the gloas fork (fee recipients and graffiti carry over). Please migrate your settings source to v2.") + log.Warn("Proposer settings contain deprecated v1 builder fields (enabled, builder-level gas limits); they stop applying at the gloas fork and are replaced with defaults (fee recipients and graffiti carry over). Configure gloas builders via v2 settings or the keymanager API.") } -// HasBuilderContent reports whether any level carries a builder config, i.e. -// whether the v1 cutover has anything to drop. -func (ps *Settings) HasBuilderContent() bool { - if ps.DefaultConfig != nil && ps.DefaultConfig.BuilderConfig != nil { +// HasLegacyBuilderContent reports whether any level carries v1 builder fields, +// i.e. whether the gloas cutover has anything to drop. +func (ps *Settings) HasLegacyBuilderContent() bool { + if ps == nil { + return false + } + legacy := func(opt *Option) bool { + return opt != nil && opt.BuilderConfig != nil && (opt.BuilderConfig.Enabled || opt.BuilderConfig.GasLimit != 0) + } + if legacy(ps.DefaultConfig) { return true } for _, opt := range ps.ProposeConfig { - if opt != nil && opt.BuilderConfig != nil { + if legacy(opt) { return true } } @@ -620,7 +654,7 @@ func (ps *Settings) UpgradeToV2() bool { } // A config left with no v2 content disappears entirely; an explicit // empty builders list is v2 content and survives. - if bc.Builders == nil && bc.MinBid == nil && bc.BuilderBoostFactor == nil && bc.MaxExecutionPayment == nil { + if !bc.hasV2Content() { opt.BuilderConfig = nil scrubbed = true } diff --git a/config/proposer/settings_test.go b/config/proposer/settings_test.go index 1c4197680385..9d63c345d087 100644 --- a/config/proposer/settings_test.go +++ b/config/proposer/settings_test.go @@ -457,25 +457,43 @@ func TestSettings_WarnDeprecatedSchema(t *testing.T) { params.OverrideBeaconConfig(cfg) hook := logtest.NewGlobal() v1Settings.WarnDeprecatedSchema() - assert.LogsContain(t, hook, "deprecated v1 schema") + assert.LogsContain(t, hook, "deprecated v1 builder fields") }) t.Run("v1 without gloas scheduled silent", func(t *testing.T) { hook := logtest.NewGlobal() v1Settings.WarnDeprecatedSchema() - assert.LogsDoNotContain(t, hook, "deprecated v1 schema") + assert.LogsDoNotContain(t, hook, "deprecated v1 builder fields") }) - t.Run("v2 silent", func(t *testing.T) { + t.Run("v2 stamp with v1 content still warns", func(t *testing.T) { + params.SetupTestConfigCleanup(t) + cfg := params.BeaconConfig().Copy() + cfg.GloasForkEpoch = 100 + params.OverrideBeaconConfig(cfg) + hook := logtest.NewGlobal() + mixed := &Settings{ + Version: SchemaV2, + DefaultConfig: &Option{ + BuilderConfig: &BuilderConfig{Enabled: true, Builders: []*BuilderEntry{{URL: "https://b.example"}}}, + }, + } + mixed.WarnDeprecatedSchema() + assert.LogsContain(t, hook, "deprecated v1 builder fields") + }) + t.Run("pure v2 content silent", func(t *testing.T) { params.SetupTestConfigCleanup(t) cfg := params.BeaconConfig().Copy() cfg.GloasForkEpoch = 100 params.OverrideBeaconConfig(cfg) hook := logtest.NewGlobal() v2 := &Settings{ - Version: SchemaV2, - DefaultConfig: &Option{GasLimit: 30000000}, + Version: SchemaV2, + DefaultConfig: &Option{ + GasLimit: 30000000, + BuilderConfig: &BuilderConfig{Builders: []*BuilderEntry{{URL: "https://b.example"}}}, + }, } v2.WarnDeprecatedSchema() - assert.LogsDoNotContain(t, hook, "deprecated v1 schema") + assert.LogsDoNotContain(t, hook, "deprecated v1 builder fields") }) } @@ -836,6 +854,66 @@ func TestRegistrationFor(t *testing.T) { require.Equal(t, false, enabled) }) + t.Run("v2-only extras are neutral: key keeps the enabled default's toggle", func(t *testing.T) { + // A gloas-only preference (min_bid, no builders list) must not silently + // opt the key out of an enabled v1 default. + ps := &Settings{ + Version: SchemaV2, + DefaultConfig: &Option{ + FeeRecipientConfig: &FeeRecipientConfig{FeeRecipient: recipient}, + BuilderConfig: &BuilderConfig{Enabled: true}, + }, + ProposeConfig: map[[fieldparams.BLSPubkeyLength]byte]*Option{ + key: {BuilderConfig: &BuilderConfig{MinBid: uint64ValPtr(5)}}, + }, + } + _, _, enabled := ps.RegistrationFor(key) + require.Equal(t, true, enabled) + }) + + t.Run("per-key explicit empty builders opts out of an enabled v1 default", func(t *testing.T) { + ps := &Settings{ + Version: SchemaV2, + DefaultConfig: &Option{ + FeeRecipientConfig: &FeeRecipientConfig{FeeRecipient: recipient}, + BuilderConfig: &BuilderConfig{Enabled: true}, + }, + ProposeConfig: map[[fieldparams.BLSPubkeyLength]byte]*Option{ + key: {BuilderConfig: &BuilderConfig{Builders: []*BuilderEntry{}}}, + }, + } + _, _, enabled := ps.RegistrationFor(key) + require.Equal(t, false, enabled) + }) + + t.Run("transition config registers: enabled with explicit empty builders", func(t *testing.T) { + // enabled:true + builders:[] means mev-boost until the fork, self-build after. + ps := &Settings{ + Version: SchemaV2, + DefaultConfig: &Option{ + FeeRecipientConfig: &FeeRecipientConfig{FeeRecipient: recipient}, + BuilderConfig: &BuilderConfig{Enabled: true, Builders: []*BuilderEntry{}}, + }, + } + _, _, enabled := ps.RegistrationFor(key) + require.Equal(t, true, enabled) + }) + + t.Run("per-key v1 disable wins over a default with builders", func(t *testing.T) { + ps := &Settings{ + Version: SchemaV2, + DefaultConfig: &Option{ + FeeRecipientConfig: &FeeRecipientConfig{FeeRecipient: recipient}, + BuilderConfig: &BuilderConfig{Builders: []*BuilderEntry{{URL: "https://b.example"}}}, + }, + ProposeConfig: map[[fieldparams.BLSPubkeyLength]byte]*Option{ + key: {BuilderConfig: &BuilderConfig{Enabled: false}}, + }, + } + _, _, enabled := ps.RegistrationFor(key) + require.Equal(t, false, enabled) + }) + t.Run("v1 per-key disabled builder opts the key out", func(t *testing.T) { ps := &Settings{ Version: SchemaV1, @@ -970,20 +1048,27 @@ func TestRegistrationFor(t *testing.T) { }) } -// Version-neutral settings (fee recipient, graffiti) give the deprecation -// warning nothing to say; only builder content triggers it. -func TestHasBuilderContent(t *testing.T) { +// Version-neutral settings (fee recipient, graffiti) and pure v2 builder content +// give the deprecation warning nothing to say; only v1 fields trigger it. +func TestHasLegacyBuilderContent(t *testing.T) { key := [fieldparams.BLSPubkeyLength]byte{9} - neutral := &Settings{ + ps := &Settings{ Version: SchemaV1, DefaultConfig: &Option{FeeRecipientConfig: &FeeRecipientConfig{}}, ProposeConfig: map[[fieldparams.BLSPubkeyLength]byte]*Option{ key: {GraffitiConfig: &GraffitiConfig{Graffiti: "hi"}}, }, } - require.Equal(t, false, neutral.HasBuilderContent()) - neutral.ProposeConfig[key].BuilderConfig = &BuilderConfig{} - require.Equal(t, true, neutral.HasBuilderContent()) + require.Equal(t, false, ps.HasLegacyBuilderContent()) + ps.ProposeConfig[key].BuilderConfig = &BuilderConfig{MinBid: uint64ValPtr(1), Builders: []*BuilderEntry{}} + require.Equal(t, false, ps.HasLegacyBuilderContent()) + ps.ProposeConfig[key].BuilderConfig.GasLimit = 30000000 + require.Equal(t, true, ps.HasLegacyBuilderContent()) + ps.ProposeConfig[key].BuilderConfig.GasLimit = 0 + ps.DefaultConfig.BuilderConfig = &BuilderConfig{Enabled: true} + require.Equal(t, true, ps.HasLegacyBuilderContent()) + var nilSettings *Settings + require.Equal(t, false, nilSettings.HasLegacyBuilderContent()) } // The cutover scrubs v1 builder fields; v2 content — including an explicit diff --git a/validator/client/validator.go b/validator/client/validator.go index dd2ba341d4e8..25e29f3d76c6 100644 --- a/validator/client/validator.go +++ b/validator/client/validator.go @@ -904,8 +904,8 @@ func (v *validator) PushProposerSettings(ctx context.Context, slot primitives.Sl // Gloas preferences start being prepared one epoch ahead; v1 builder // settings do not apply to them, so warn while there is time to act. if ps := v.ProposerSettings(); currentEpoch+1 >= params.BeaconConfig().GloasForkEpoch && - ps != nil && ps.Version != proposer.SchemaV2 && ps.HasBuilderContent() && slots.IsEpochStart(slot) { - log.Warn("Proposer settings still use the v1 schema; gloas proposer preferences are prepared with defaults. Provide v2 proposer settings.") + ps.HasLegacyBuilderContent() && slots.IsEpochStart(slot) { + log.Warn("Proposer settings contain v1 builder fields, which do not apply to gloas; proposer preferences are prepared with defaults. Configure builders via v2 proposer settings.") } } else { v.upgradeProposerSettingsToV2(ctx) @@ -1482,7 +1482,8 @@ type builderTarget struct { } // builderTargetsForKey resolves the configured builder list for pk; entries override -// config-level fallbacks. TODO(gloas): minBid/boost/pubkeys ride the #630 wire. +// config-level fallbacks. TODO(gloas): per-entry max_execution_payment, minBid, +// boost and pubkeys ride the beacon-APIs #630 wire. func (v *validator) builderTargetsForKey(pk pubkey) []builderTarget { ps := v.ProposerSettings() if ps == nil { diff --git a/validator/rpc/handlers_validator_config_test.go b/validator/rpc/handlers_validator_config_test.go index 10aa008d819b..58b643a87e66 100644 --- a/validator/rpc/handlers_validator_config_test.go +++ b/validator/rpc/handlers_validator_config_test.go @@ -18,6 +18,7 @@ import ( validatormock "github.com/OffchainLabs/prysm/v7/testing/validator-mock" "github.com/OffchainLabs/prysm/v7/validator/keymanager/derived" mocks "github.com/OffchainLabs/prysm/v7/validator/testing" + "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/common/hexutil" "go.uber.org/mock/gomock" ) @@ -161,6 +162,29 @@ func TestServer_SetBuilders(t *testing.T) { require.Equal(t, 0, len(cfg.Builders)) }) + t.Run("gloas-only preferences keep the key's pre-fork registration", func(t *testing.T) { + srv, keys := setupConfigServer(t, 1) + pk := hexutil.Encode(keys[0][:]) + recipient := common.HexToAddress("0x50155530FCE8a85ec7055A5F8b2bE214B3DaeFd3") + require.NoError(t, srv.validatorService.SetProposerSettings(t.Context(), &proposer.Settings{ + Version: proposer.SchemaV1, + DefaultConfig: &proposer.Option{ + FeeRecipientConfig: &proposer.FeeRecipientConfig{FeeRecipient: recipient}, + BuilderConfig: &proposer.BuilderConfig{Enabled: true}, + }, + })) + require.Equal(t, http.StatusAccepted, postBuilders(t, srv, pk, `{"min_bid":"5"}`).Code) + + // The POST expressed no registration choice, so the enabled default still applies. + _, _, enabled := srv.validatorService.ProposerSettings().RegistrationFor(keys[0]) + require.Equal(t, true, enabled) + + // An explicit empty list is a registration choice: it opts the key out. + require.Equal(t, http.StatusAccepted, postBuilders(t, srv, pk, `{"builders":[]}`).Code) + _, _, enabled = srv.validatorService.ProposerSettings().RegistrationFor(keys[0]) + require.Equal(t, false, enabled) + }) + t.Run("upgrades v1 settings in place", func(t *testing.T) { srv, keys := setupConfigServer(t, 1) pk := hexutil.Encode(keys[0][:]) From 6255ad6de52a4e713cb5b1903e14bab1d22b0d67 Mon Sep 17 00:00:00 2001 From: james-prysm Date: Mon, 17 Aug 2026 09:25:46 -0500 Subject: [PATCH 26/26] handling edge case defined in keymanager 88 pr --- validator/rpc/handlers_validator_config.go | 5 +++++ .../rpc/handlers_validator_config_test.go | 19 +++++++++++++++++++ 2 files changed, 24 insertions(+) diff --git a/validator/rpc/handlers_validator_config.go b/validator/rpc/handlers_validator_config.go index 81236ec70bee..006e9e10c135 100644 --- a/validator/rpc/handlers_validator_config.go +++ b/validator/rpc/handlers_validator_config.go @@ -81,6 +81,11 @@ func (s *Server) SetBuilders(w http.ResponseWriter, r *http.Request) { httputil.HandleError(w, err.Error(), http.StatusBadRequest) return } + // Per spec an override-free body ({}) configures nothing: store no per-key + // builder config, so the key keeps following the client defaults. + if bc.Builders == nil && bc.MinBid == nil && bc.BuilderBoostFactor == nil { + bc = nil + } err = s.validatorService.UpdateProposerSettings(ctx, func(settings *proposer.Settings) (*proposer.Settings, error) { if settings == nil { diff --git a/validator/rpc/handlers_validator_config_test.go b/validator/rpc/handlers_validator_config_test.go index 58b643a87e66..7e46dad637c4 100644 --- a/validator/rpc/handlers_validator_config_test.go +++ b/validator/rpc/handlers_validator_config_test.go @@ -162,6 +162,25 @@ func TestServer_SetBuilders(t *testing.T) { require.Equal(t, 0, len(cfg.Builders)) }) + t.Run("empty object is override-free", func(t *testing.T) { + srv, keys := setupConfigServer(t, 1) + pk := hexutil.Encode(keys[0][:]) + recipient := common.HexToAddress("0x50155530FCE8a85ec7055A5F8b2bE214B3DaeFd3") + require.NoError(t, srv.validatorService.SetProposerSettings(t.Context(), &proposer.Settings{ + Version: proposer.SchemaV1, + DefaultConfig: &proposer.Option{ + FeeRecipientConfig: &proposer.FeeRecipientConfig{FeeRecipient: recipient}, + BuilderConfig: &proposer.BuilderConfig{Enabled: true}, + }, + })) + require.Equal(t, http.StatusAccepted, postBuilders(t, srv, pk, `{}`).Code) + + // Nothing is stored for the key and the enabled default still applies. + require.IsNil(t, srv.validatorService.ProposerSettings().ProposeConfig[keys[0]].BuilderConfig) + _, _, enabled := srv.validatorService.ProposerSettings().RegistrationFor(keys[0]) + require.Equal(t, true, enabled) + }) + t.Run("gloas-only preferences keep the key's pre-fork registration", func(t *testing.T) { srv, keys := setupConfigServer(t, 1) pk := hexutil.Encode(keys[0][:])