diff --git a/main.go b/main.go index be63f77..144fa24 100644 --- a/main.go +++ b/main.go @@ -31,6 +31,7 @@ var labels playground.MapStringFlag var disableLogs bool var platform string var contenderEnabled bool +var contenderArgs []string var rootCmd = &cobra.Command{ Use: "playground", @@ -181,6 +182,7 @@ func main() { recipeCmd.Flags().BoolVar(&disableLogs, "disable-logs", false, "disable logs") recipeCmd.Flags().StringVar(&platform, "platform", "", "docker platform to use") recipeCmd.Flags().BoolVar(&contenderEnabled, "contender", false, "spam nodes with contender") + recipeCmd.Flags().StringArrayVar(&contenderArgs, "contender.arg", []string{}, "add/override contender CLI flags") cookCmd.AddCommand(recipeCmd) } @@ -226,7 +228,14 @@ func runIt(recipe playground.Recipe) error { return err } - svcManager := recipe.Apply(&playground.ExContext{LogLevel: logLevel, ContenderEnabled: contenderEnabled}, artifacts) + // if contender.tps is set, assume contender is enabled + svcManager := recipe.Apply(&playground.ExContext{ + LogLevel: logLevel, + Contender: &playground.ContenderContext{ + Enabled: contenderEnabled, + ExtraArgs: contenderArgs, + }, + }, artifacts) if err := svcManager.Validate(); err != nil { return fmt.Errorf("failed to validate manifest: %w", err) } diff --git a/playground/components.go b/playground/components.go index 38a0c61..753ad4f 100644 --- a/playground/components.go +++ b/playground/components.go @@ -101,8 +101,8 @@ func (o *OpRbuilder) Name() string { type FlashblocksRPC struct { FlashblocksWSService string - BaseOverlay bool - UseWebsocketProxy bool // Whether to add /ws path for websocket proxy + BaseOverlay bool + UseWebsocketProxy bool // Whether to add /ws path for websocket proxy } func (f *FlashblocksRPC) Run(service *Service, ctx *ExContext) { @@ -130,19 +130,19 @@ func (f *FlashblocksRPC) Run(service *Service, ctx *ExContext) { ) } service.WithArgs( - "--authrpc.port", `{{Port "authrpc" 8551}}`, - "--authrpc.addr", "0.0.0.0", - "--authrpc.jwtsecret", "/data/jwtsecret", - "--http", - "--http.addr", "0.0.0.0", - "--http.port", `{{Port "http" 8545}}`, - "--chain", "/data/l2-genesis.json", - "--datadir", "/data_op_reth", - "--disable-discovery", - "--color", "never", - "--metrics", `0.0.0.0:{{Port "metrics" 9090}}`, - "--port", `{{Port "rpc" 30303}}`, - ). + "--authrpc.port", `{{Port "authrpc" 8551}}`, + "--authrpc.addr", "0.0.0.0", + "--authrpc.jwtsecret", "/data/jwtsecret", + "--http", + "--http.addr", "0.0.0.0", + "--http.port", `{{Port "http" 8545}}`, + "--chain", "/data/l2-genesis.json", + "--datadir", "/data_op_reth", + "--disable-discovery", + "--color", "never", + "--metrics", `0.0.0.0:{{Port "metrics" 9090}}`, + "--port", `{{Port "rpc" 30303}}`, + ). WithArtifact("/data/jwtsecret", "jwtsecret"). WithArtifact("/data/l2-genesis.json", "l2-genesis.json"). WithVolume("data", "/data_flashblocks_rpc") @@ -159,13 +159,13 @@ func (f *FlashblocksRPC) Name() string { } type BProxy struct { - TargetAuthrpc string - Peers []string - Flashblocks bool + TargetAuthrpc string + Peers []string + Flashblocks bool FlashblocksBuilderURL string } -func (f* BProxy) Run(service *Service, ctx *ExContext) { +func (f *BProxy) Run(service *Service, ctx *ExContext) { peers := []string{} for _, peer := range f.Peers { peers = append(peers, Connect(peer, "authrpc")) @@ -813,20 +813,110 @@ func (n *nullService) Name() string { } type Contender struct { + ExtraArgs []string } func (c *Contender) Name() string { return "contender" } +// parse "key=value" OR "key value"; remainder after first space is the value (may contain spaces) +func parseKV(s string) (name, val string, hasVal, usedEq bool) { + s = strings.TrimSpace(s) + if s == "" { + return "", "", false, false + } + eq := strings.IndexByte(s, '=') + ws := indexWS(s) + + // prefer '=' if it appears before any whitespace + if eq > 0 && (ws == -1 || eq < ws) { + return strings.TrimSpace(s[:eq]), strings.TrimSpace(s[eq+1:]), true, true + } + if ws == -1 { + return s, "", false, false + } + return strings.TrimSpace(s[:ws]), strings.TrimSpace(s[ws+1:]), true, false +} + +func indexWS(s string) int { + for i, r := range s { + if r == ' ' || r == '\t' { + return i + } + } + return -1 +} + func (c *Contender) Run(service *Service, ctx *ExContext) { - args := []string{ - "spam", - "-l", // loop indefinitely - "--min-balance", "10 ether", // give each spammer 10 ether (sender must have 100 ether because default number of spammers is 10) - "-r", Connect("el", "http"), // connect to whatever EL node is available - "--tps", "20", // send 20 txs per second + type opt struct { + name string + val string + hasVal bool + } + defaults := []opt{ + {name: "-l"}, + {name: "--min-balance", val: "10 ether", hasVal: true}, + {name: "-r", val: Connect("el", "http"), hasVal: true}, + {name: "--tps", val: "20", hasVal: true}, + } + + // Parse extras and track seen flags + type extra struct { + name string + val string + hasVal bool + usedEq bool + } + var extras []extra + seen := map[string]bool{} + + for _, s := range c.ExtraArgs { + name, val, hasVal, usedEq := parseKV(s) + if name == "" { + continue + } + extras = append(extras, extra{name, val, hasVal, usedEq}) + seen[name] = true + } + + // Minimal conflict example: --loops overrides default "-l" + conflict := func(flag string) bool { + if seen[flag] { + return true + } + if flag == "-l" && seen["--loops"] { + return true + } + return false } + + args := []string{"spam"} + + // Add defaults unless overridden + for _, d := range defaults { + if conflict(d.name) { + continue + } + args = append(args, d.name) + if d.hasVal { + args = append(args, d.val) + } + } + + // Append extras verbatim, preserving "=" vs space + for _, e := range extras { + if !e.hasVal { + args = append(args, e.name) + continue + } + if e.usedEq { + args = append(args, e.name+"="+e.val) + } else { + args = append(args, e.name, e.val) + } + } + service.WithImage("flashbots/contender"). WithTag("latest"). WithArgs(args...). diff --git a/playground/manifest.go b/playground/manifest.go index 030002d..ee49f9a 100644 --- a/playground/manifest.go +++ b/playground/manifest.go @@ -70,6 +70,14 @@ func (l *LogLevel) Unmarshal(s string) error { return nil } +type ContenderContext struct { + // Run `contender spam` automatically once all playground services are running. + Enabled bool + + // Provide additional args to contender's CLI + ExtraArgs []string +} + // Execution context type ExContext struct { LogLevel LogLevel @@ -83,7 +91,7 @@ type ExContext struct { // TODO: Extend for CL nodes too Bootnode *BootnodeRef - ContenderEnabled bool + Contender *ContenderContext } type BootnodeRef struct { diff --git a/playground/recipe_buildernet.go b/playground/recipe_buildernet.go index ce7af20..e5216fb 100644 --- a/playground/recipe_buildernet.go +++ b/playground/recipe_buildernet.go @@ -59,8 +59,10 @@ func (b *BuilderNetRecipe) Apply(ctx *ExContext, artifacts *Artifacts) *Manifest }) } - if ctx.ContenderEnabled { - svcManager.AddService("contender", &Contender{}) + if ctx.Contender.Enabled { + svcManager.AddService("contender", &Contender{ + ExtraArgs: ctx.Contender.ExtraArgs, + }) } return svcManager diff --git a/playground/recipe_l1.go b/playground/recipe_l1.go index d8b00f1..9d4cfb5 100644 --- a/playground/recipe_l1.go +++ b/playground/recipe_l1.go @@ -107,8 +107,10 @@ func (l *L1Recipe) Apply(ctx *ExContext, artifacts *Artifacts) *Manifest { }) } - if ctx.ContenderEnabled { - svcManager.AddService("contender", &Contender{}) + if ctx.Contender.Enabled { + svcManager.AddService("contender", &Contender{ + ExtraArgs: ctx.Contender.ExtraArgs, + }) } return svcManager diff --git a/playground/recipe_opstack.go b/playground/recipe_opstack.go index 9da5652..98cf50f 100644 --- a/playground/recipe_opstack.go +++ b/playground/recipe_opstack.go @@ -32,7 +32,7 @@ type OpRecipe struct { flashblocksBuilderURL string // Indicates that flashblocks-rpc should use base image - baseOverlay bool + baseOverlay bool // whether to enable websocket proxy enableWebsocketProxy bool @@ -108,13 +108,13 @@ func (o *OpRecipe) Apply(ctx *ExContext, artifacts *Artifacts) *Manifest { if o.flashblocks { peers = append(peers, "flashblocks-rpc") } - + // Only enable bproxy if flashblocks is enabled (since flashblocks-rpc is the only service that needs it) if o.flashblocks { svcManager.AddService("bproxy", &BProxy{ - TargetAuthrpc: externalBuilderRef, - Peers: peers, - Flashblocks: o.flashblocks, + TargetAuthrpc: externalBuilderRef, + Peers: peers, + Flashblocks: o.flashblocks, FlashblocksBuilderURL: flashblocksBuilderURLRef, }) } @@ -146,7 +146,6 @@ func (o *OpRecipe) Apply(ctx *ExContext, artifacts *Artifacts) *Manifest { }) } - if o.flashblocks { // Determine which service to use for flashblocks websocket connection flashblocksWSService := "rollup-boost" @@ -158,8 +157,8 @@ func (o *OpRecipe) Apply(ctx *ExContext, artifacts *Artifacts) *Manifest { svcManager.AddService("flashblocks-rpc", &FlashblocksRPC{ FlashblocksWSService: flashblocksWSService, - BaseOverlay: o.baseOverlay, - UseWebsocketProxy: useWebsocketProxy, + BaseOverlay: o.baseOverlay, + UseWebsocketProxy: useWebsocketProxy, }) } @@ -176,8 +175,10 @@ func (o *OpRecipe) Apply(ctx *ExContext, artifacts *Artifacts) *Manifest { MaxChannelDuration: o.batcherMaxChannelDuration, }) - if ctx.ContenderEnabled { - svcManager.AddService("contender", &Contender{}) + if ctx.Contender.Enabled { + svcManager.AddService("contender", &Contender{ + ExtraArgs: ctx.Contender.ExtraArgs, + }) } return svcManager