@@ -20,6 +20,7 @@ use serde_json::{json, Value as JsonValue};
2020use std:: net:: SocketAddr ;
2121use std:: path:: PathBuf ;
2222use std:: sync:: Arc ;
23+ use tokio:: sync:: oneshot;
2324use tracing:: { debug, error, warn} ;
2425
2526const JSONRPC_VERSION : & str = "2.0" ;
@@ -36,6 +37,20 @@ pub struct DummyElConfig {
3637 pub jwt_secret_path : Option < PathBuf > ,
3738}
3839
40+ /// Represents a prepared dummy execution layer ready to run
41+ pub struct PreparedDummyEl {
42+ engine_listener : tokio:: net:: TcpListener ,
43+ engine_app : Router ,
44+ rpc_listener : tokio:: net:: TcpListener ,
45+ rpc_app : Router ,
46+ ws_listener : tokio:: net:: TcpListener ,
47+ ws_app : Router ,
48+ metrics_listener : tokio:: net:: TcpListener ,
49+ metrics_app : Router ,
50+ p2p_tcp_task : tokio:: task:: JoinHandle < ( ) > ,
51+ p2p_udp_task : tokio:: task:: JoinHandle < ( ) > ,
52+ }
53+
3954#[ derive( Debug , Clone ) ]
4055struct AppState {
4156 jwt_secret : Option < Vec < u8 > > ,
@@ -322,11 +337,29 @@ fn read_jwt_secret(path: &PathBuf) -> anyhow::Result<Vec<u8>> {
322337 Ok ( bytes)
323338}
324339
325- /// Start the dummy execution layer server
340+ /// Prepare the dummy execution layer for startup
326341///
327- /// This spawns the dummy EL HTTP servers on the configured ports.
328- /// Returns a task handle that should be spawned or awaited.
329- pub async fn start_dummy_el ( config : DummyElConfig ) -> anyhow:: Result < ( ) > {
342+ /// This function binds all necessary ports and prepares the servers,
343+ /// then signals readiness via the oneshot channel before running the servers.
344+ /// The function does not return until the servers are shut down.
345+ pub async fn prepare_and_start_dummy_el (
346+ config : DummyElConfig ,
347+ ready_tx : oneshot:: Sender < ( ) > ,
348+ ) -> anyhow:: Result < ( ) > {
349+ let prepared = prepare_dummy_el ( config) . await ?;
350+
351+ // Signal that we're ready
352+ let _ = ready_tx. send ( ( ) ) ;
353+
354+ // Now run the servers
355+ prepared. run ( ) . await
356+ }
357+
358+ /// Prepare the dummy execution layer server without starting it
359+ ///
360+ /// This binds all ports and prepares the servers but does not start accepting connections.
361+ /// Returns a `PreparedDummyEl` that can be run with the `run()` method.
362+ pub async fn prepare_dummy_el ( config : DummyElConfig ) -> anyhow:: Result < PreparedDummyEl > {
330363 // Read JWT secret if provided
331364 let jwt_secret = match & config. jwt_secret_path {
332365 Some ( path) => match read_jwt_secret ( path) {
@@ -426,22 +459,51 @@ pub async fn start_dummy_el(config: DummyElConfig) -> anyhow::Result<()> {
426459 }
427460 } ) ;
428461
429- debug ! ( "Ready to accept requests on all ports" ) ;
430-
431- // Spawn all servers concurrently
462+ // Bind all servers without starting them
432463 let engine_listener = tokio:: net:: TcpListener :: bind ( engine_addr) . await ?;
433464 let rpc_listener = tokio:: net:: TcpListener :: bind ( rpc_addr) . await ?;
434465 let ws_listener = tokio:: net:: TcpListener :: bind ( ws_addr) . await ?;
435466 let metrics_listener = tokio:: net:: TcpListener :: bind ( metrics_addr) . await ?;
436467
437- tokio:: select! {
438- result = axum:: serve( engine_listener, engine_app) => result?,
439- result = axum:: serve( rpc_listener, rpc_app) => result?,
440- result = axum:: serve( ws_listener, ws_app) => result?,
441- result = axum:: serve( metrics_listener, metrics_app) => result?,
442- _ = p2p_tcp_task => { } ,
443- _ = p2p_udp_task => { } ,
468+ debug ! ( "All listeners bound and ready" ) ;
469+
470+ Ok ( PreparedDummyEl {
471+ engine_listener,
472+ engine_app,
473+ rpc_listener,
474+ rpc_app,
475+ ws_listener,
476+ ws_app,
477+ metrics_listener,
478+ metrics_app,
479+ p2p_tcp_task,
480+ p2p_udp_task,
481+ } )
482+ }
483+
484+ impl PreparedDummyEl {
485+ /// Run the prepared dummy execution layer servers
486+ pub async fn run ( self ) -> anyhow:: Result < ( ) > {
487+ debug ! ( "Running dummy execution layer servers" ) ;
488+
489+ tokio:: select! {
490+ result = axum:: serve( self . engine_listener, self . engine_app) => result?,
491+ result = axum:: serve( self . rpc_listener, self . rpc_app) => result?,
492+ result = axum:: serve( self . ws_listener, self . ws_app) => result?,
493+ result = axum:: serve( self . metrics_listener, self . metrics_app) => result?,
494+ _ = self . p2p_tcp_task => { } ,
495+ _ = self . p2p_udp_task => { } ,
496+ }
497+
498+ Ok ( ( ) )
444499 }
500+ }
445501
446- Ok ( ( ) )
502+ /// Start the dummy execution layer server (legacy function)
503+ ///
504+ /// This is a convenience function that prepares and starts the dummy EL.
505+ /// For more control, use `prepare_dummy_el()` and `prepare_and_start_dummy_el()`.
506+ pub async fn start_dummy_el ( config : DummyElConfig ) -> anyhow:: Result < ( ) > {
507+ let prepared = prepare_dummy_el ( config) . await ?;
508+ prepared. run ( ) . await
447509}
0 commit comments