diff --git a/internal/components.go b/internal/components.go index 470e69d..75fdeaf 100644 --- a/internal/components.go +++ b/internal/components.go @@ -165,6 +165,7 @@ func (o *OpGeth) Run(service *service, ctx *ExContext) { "--gcmode archive "+ "--state.scheme hash "+ "--port "+`{{Port "rpc" 30303}} `+ + "--bootnodes enode://"+defaultDiscoveryEnodeID+"@bootnode:30301 "+ nodeKeyFlag+ "--metrics "+ "--metrics.addr 0.0.0.0 "+ @@ -253,6 +254,7 @@ func (r *RethEL) Run(svc *service, ctx *ExContext) { "--ipcpath", "{{.Dir}}/reth.ipc", "--addr", "127.0.0.1", "--port", `{{Port "rpc" 30303}}`, + "--bootnodes", "enode://"+defaultDiscoveryEnodeID+"@bootnode:30301", // "--disable-discovery", // http config "--http", @@ -318,6 +320,7 @@ func (l *LighthouseBeaconNode) Run(svc *service, ctx *ExContext) { "--always-prepare-payload", "--prepare-payload-lookahead", "8000", "--suggested-fee-recipient", "0x690B9A9E9aa1C9dB991C7721a92d351Db4FaC990", + "--boot-nodes", "enode://"+defaultDiscoveryEnodeID+"@bootnode:30301", ). WithReady(ReadyCheck{ QueryURL: "http://localhost:3500/eth/v1/node/syncing", @@ -544,3 +547,44 @@ func (p *OpReth) Watchdog(out io.Writer, service *service, ctx context.Context) rethURL := fmt.Sprintf("http://localhost:%d", service.MustGetPort("http").HostPort) return watchChainHead(out, rethURL, 2*time.Second) } + +// Bootnode represents a P2P discovery bootnode service that helps other nodes discover each other +// in the network. It runs a Geth bootnode instance that other Ethereum clients can connect to +// for peer discovery. +type Bootnode struct { + // DiscoveryPort is the UDP port used for P2P discovery + DiscoveryPort uint64 + + // PrivateKey is the hex-encoded private key used by the bootnode for P2P communication. + // If not provided, a default key will be used. + PrivateKey string +} + +// Watchdog implements the ServiceWatchdog interface to monitor the bootnode's health. +// It checks if the bootnode is listening for connections on its discovery port. +var _ ServiceWatchdog = &Bootnode{} + +// Run configures and starts the bootnode service using the ethereum/client-go image. +// It sets up the necessary arguments for the bootnode to run, including the private key +// and discovery port. +func (b *Bootnode) Run(service *service, ctx *ExContext) { + // Use the default discovery key if not provided + privateKey := b.PrivateKey + if privateKey == "" { + privateKey = defaultDiscoveryPrivKey + } + + service. + WithImage("ethereum/client-go"). + WithTag("v1.13.0"). + WithEntrypoint("bootnode"). + WithArgs( + "--nodekeyhex", privateKey, + "--addr", fmt.Sprintf("0.0.0.0:%d", b.DiscoveryPort), + "--verbosity", "3", + ) +} + +func (b *Bootnode) Name() string { + return "bootnode" +} diff --git a/internal/recipe_l1.go b/internal/recipe_l1.go index ee267af..3ffa04e 100644 --- a/internal/recipe_l1.go +++ b/internal/recipe_l1.go @@ -23,6 +23,9 @@ type L1Recipe struct { // will run on the host machine. This is useful if you want to bind to the Reth database and you // are running a host machine (i.e Mac) that is differerent from the docker one (Linux) useNativeReth bool + + // bootnodePrivKey is the private key used by the bootnode for P2P discovery + bootnodePrivKey string } func (l *L1Recipe) Name() string { @@ -39,6 +42,7 @@ func (l *L1Recipe) Flags() *flag.FlagSet { flags.BoolVar(&l.useRethForValidation, "use-reth-for-validation", false, "use reth for validation") flags.Uint64Var(&l.secondaryELPort, "secondary-el", 0, "port to use for the secondary builder") flags.BoolVar(&l.useNativeReth, "use-native-reth", false, "use the native reth binary") + flags.StringVar(&l.bootnodePrivKey, "bootnode-privkey", "", "private key for the bootnode (optional)") return flags } @@ -49,18 +53,33 @@ func (l *L1Recipe) Artifacts() *ArtifactsBuilder { return builder } +func (m *Manifest) AddServiceWithDeps(name string, service Service, deps ...string) { + m.AddService(name, service) + for _, dep := range deps { + m.MustGetService(name).DependsOnHealthy(dep) + } +} + func (l *L1Recipe) Apply(ctx *ExContext, artifacts *Artifacts) *Manifest { svcManager := NewManifest(ctx, artifacts.Out) - svcManager.AddService("el", &RethEL{ + // Register bootnode without dependency + bootnode := &Bootnode{ + DiscoveryPort: 30301, + PrivateKey: l.bootnodePrivKey, + } + svcManager.AddService("bootnode", bootnode) + + // Register 'el' service with dependency on bootnode + el := &RethEL{ UseRethForValidation: l.useRethForValidation, UseNativeReth: l.useNativeReth, - }) + } + svcManager.AddServiceWithDeps("el", el, "bootnode") var elService string if l.secondaryELPort != 0 { - // we are going to use the cl-proxy service to connect the beacon node to two builders - // one the 'el' builder and another one the remote one + // Use cl-proxy service to connect the beacon node to two builders elService = "cl-proxy" svcManager.AddService("cl-proxy", &ClProxy{ PrimaryBuilder: "el", @@ -70,22 +89,31 @@ func (l *L1Recipe) Apply(ctx *ExContext, artifacts *Artifacts) *Manifest { elService = "el" } - svcManager.AddService("beacon", &LighthouseBeaconNode{ + // Register beacon with dependency on bootnode + beacon := &LighthouseBeaconNode{ ExecutionNode: elService, MevBoostNode: "mev-boost", - }) - svcManager.AddService("validator", &LighthouseValidator{ + } + svcManager.AddServiceWithDeps("beacon", beacon, "bootnode") + + // Register validator with dependency on beacon + validator := &LighthouseValidator{ BeaconNode: "beacon", - }) + } + svcManager.AddServiceWithDeps("validator", validator, "beacon") mevBoostValidationServer := "" if l.useRethForValidation { mevBoostValidationServer = "el" } - svcManager.AddService("mev-boost", &MevBoostRelay{ + + // Register mev-boost with dependency on beacon + mevBoost := &MevBoostRelay{ BeaconClient: "beacon", ValidationServer: mevBoostValidationServer, - }) + } + svcManager.AddServiceWithDeps("mev-boost", mevBoost, "beacon") + return svcManager }