diff --git a/docs/recipes/buildernet.md b/docs/recipes/buildernet.md index 60fba1a..d27592a 100644 --- a/docs/recipes/buildernet.md +++ b/docs/recipes/buildernet.md @@ -5,6 +5,8 @@ Deploy a full L1 stack with mev-boost and builder-hub. ## Flags - `block-time` (duration): Block time to use for the L1. Default to '12s'. +- `builder-config` (string): Builder config in YAML format. Default to ''. +- `builder-ip` (string): IP address of the external builder to register in BuilderHub. Default to '127.0.0.1'. - `latest-fork` (bool): use the latest fork. Default to 'false'. - `secondary-el` (string): Address or port to use for the secondary EL (execution layer); Can be a port number (e.g., '8551') in which case the full URL is derived as `http://localhost:` or a complete URL (e.g., `http://docker-container-name:8551`), use `http://host.docker.internal:` to reach a secondary execution client that runs on your host and not within Docker.. Default to ''. - `use-native-reth` (bool): use the native reth binary. Default to 'false'. diff --git a/go.mod b/go.mod index 341e153..5232251 100644 --- a/go.mod +++ b/go.mod @@ -15,6 +15,7 @@ require ( github.com/flashbots/go-template v1.0.0 github.com/flashbots/mev-boost-relay v0.32.0-rc2 github.com/go-chi/httplog/v2 v2.1.1 + github.com/goccy/go-yaml v1.17.1 github.com/hashicorp/go-uuid v1.0.3 github.com/holiman/uint256 v1.3.2 github.com/otiai10/copy v1.14.1 @@ -75,7 +76,6 @@ require ( github.com/go-logr/stdr v1.2.2 // indirect github.com/go-ole/go-ole v1.3.0 // indirect github.com/goccy/go-json v0.10.4 // indirect - github.com/goccy/go-yaml v1.17.1 // indirect github.com/gofrs/flock v0.12.1 // indirect github.com/gogo/protobuf v1.3.2 // indirect github.com/golang/snappy v0.0.5-0.20231225225746-43d5d4cd4e0e // indirect diff --git a/main.go b/main.go index 2862c8d..f6c1490 100644 --- a/main.go +++ b/main.go @@ -477,6 +477,12 @@ func runIt(recipe playground.Recipe) error { return fmt.Errorf("failed to wait for service readiness: %w", err) } + // run post hook operations + if err := svcManager.ExecutePostHookActions(); err != nil { + dockerRunner.Stop(keepFlag) + return fmt.Errorf("failed to execute post-hook operations: %w", err) + } + slog.Info("All services are healthy! Ready to accept transactions. 🚀", "session-id", svcManager.ID) // get the output from the recipe diff --git a/playground/components.go b/playground/components.go index a0ca688..8d9ec99 100644 --- a/playground/components.go +++ b/playground/components.go @@ -2,6 +2,7 @@ package playground import ( "fmt" + "os" "strconv" "strings" "time" @@ -827,7 +828,10 @@ func (c *Contender) Apply(manifest *Manifest) { } } -type BuilderHub struct{} +type BuilderHub struct { + BuilderIP string + BuilderConfig string +} func (b *BuilderHub) Apply(manifest *Manifest) { // Database service @@ -848,7 +852,7 @@ func (b *BuilderHub) Apply(manifest *Manifest) { }) // API service - manifest.NewService("builder-hub-api"). + apiSrv := manifest.NewService("builder-hub-api"). WithImage("docker.io/flashbots/builder-hub"). WithTag("0.3.1-alpha1"). DependsOnHealthy("builder-hub-db"). @@ -872,6 +876,13 @@ func (b *BuilderHub) Apply(manifest *Manifest) { StartPeriod: 1 * time.Second, }) + apiSrv.WithPostHook(&postHook{ + Name: "register-builder", + Action: func(s *Service) error { + return registerBuilderHook(manifest, s, b) + }, + }) + // Proxy service manifest.NewService("builder-hub-proxy"). WithImage("docker.io/flashbots/builder-hub-mock-proxy"). @@ -888,6 +899,44 @@ func (b *BuilderHub) Apply(manifest *Manifest) { }) } +func registerBuilderHook(manifest *Manifest, s *Service, b *BuilderHub) error { + genesis, err := manifest.out.Read("genesis.json") + if err != nil { + return err + } + + configYaml, err := os.ReadFile(b.BuilderConfig) + if err != nil { + return err + } + + // we need to convert the config to JSON because it is what the API server accepts + configJSON, err := yamlToJson([]byte(configYaml)) + if err != nil { + return err + } + + overrideConfig := map[string]interface{}{ + "genesis": genesis, + } + if configJSON, err = overrideJSON(configJSON, overrideConfig); err != nil { + return err + } + + endpoint := fmt.Sprintf("http://localhost:%d", s.MustGetPort("admin").HostPort) + input := &builderHubRegisterBuilderInput{ + BuilderID: "builder", + BuilderIP: b.BuilderIP, + MeasurementID: "test", + Network: "playground", + Config: string(configJSON), + } + if err := registerBuilder(endpoint, input); err != nil { + return err + } + return nil +} + const ( healthmonBeacon = "beacon" healthmonExecution = "execution" diff --git a/playground/manifest.go b/playground/manifest.go index 1e19060..bf155a8 100644 --- a/playground/manifest.go +++ b/playground/manifest.go @@ -3,6 +3,7 @@ package playground import ( "encoding/json" "fmt" + "log/slog" "os" "path/filepath" "strings" @@ -336,7 +337,8 @@ type Service struct { UngracefulShutdown bool `json:"ungraceful_shutdown,omitempty"` - release *release + postHook *postHook + release *release } type DependsOnCondition string @@ -488,6 +490,29 @@ func (s *Service) WithReady(check ReadyCheck) *Service { return s } +type postHook struct { + Name string + Action func(s *Service) error +} + +func (s *Service) WithPostHook(hook *postHook) *Service { + s.postHook = hook + return s +} + +func (m *Manifest) ExecutePostHookActions() error { + for _, svc := range m.Services { + if svc.postHook != nil { + slog.Info("Executing post-hook operation", "name", svc.postHook.Name) + if err := svc.postHook.Action(svc); err != nil { + return err + } + } + } + + return nil +} + type ReadyCheck struct { QueryURL string `json:"query_url"` Test []string `json:"test"` diff --git a/playground/recipe_buildernet.go b/playground/recipe_buildernet.go index 5da278f..f99c5f6 100644 --- a/playground/recipe_buildernet.go +++ b/playground/recipe_buildernet.go @@ -7,6 +7,7 @@ import ( "io" "net/http" + "github.com/goccy/go-yaml" flag "github.com/spf13/pflag" ) @@ -16,6 +17,9 @@ var _ Recipe = &BuilderNetRecipe{} type BuilderNetRecipe struct { // Embed the L1Recipe to reuse its functionality l1Recipe L1Recipe + + builderIP string + builderConfig string } func (b *BuilderNetRecipe) Name() string { @@ -29,6 +33,8 @@ func (b *BuilderNetRecipe) Description() string { func (b *BuilderNetRecipe) Flags() *flag.FlagSet { // Reuse the L1Recipe flags flags := b.l1Recipe.Flags() + flags.StringVar(&b.builderIP, "builder-ip", "127.0.0.1", "IP address of the external builder to register in BuilderHub") + flags.StringVar(&b.builderConfig, "builder-config", "", "Builder config in YAML format") return flags } @@ -41,7 +47,10 @@ func (b *BuilderNetRecipe) Apply(svcManager *Manifest) { // Start with the L1Recipe manifest b.l1Recipe.Apply(svcManager) - svcManager.AddComponent(&BuilderHub{}) + svcManager.AddComponent(&BuilderHub{ + BuilderIP: b.builderIP, + BuilderConfig: b.builderConfig, + }) svcManager.RunContenderIfEnabled() } @@ -160,3 +169,25 @@ func registerBuilder(httpEndpoint string, input *builderHubRegisterBuilderInput) return nil } + +// YAMLToJSON converts a YAML string to a JSON string +func yamlToJson(yamlStr []byte) ([]byte, error) { + // Unmarshal YAML into a map + var data interface{} + err := yaml.Unmarshal(yamlStr, &data) + if err != nil { + return nil, fmt.Errorf("failed to unmarshal YAML: %w", err) + } + + if data == nil { + return []byte("{}"), nil + } + + // Convert to JSON + jsonBytes, err := json.MarshalIndent(data, "", " ") + if err != nil { + return nil, fmt.Errorf("failed to marshal JSON: %w", err) + } + + return jsonBytes, nil +}