alloy_node_bindings/nodes/
reth.rs

1//! Utilities for launching a Reth dev-mode instance.
2
3use crate::{utils::extract_endpoint, NodeError, NODE_STARTUP_TIMEOUT};
4use alloy_genesis::Genesis;
5use rand::Rng;
6use std::{
7    ffi::OsString,
8    fs::create_dir,
9    io::{BufRead, BufReader},
10    path::PathBuf,
11    process::{Child, ChildStdout, Command, Stdio},
12    time::Instant,
13};
14use url::Url;
15
16/// The exposed APIs
17const API: &str = "eth,net,web3,txpool,trace,rpc,reth,ots,admin,debug";
18
19/// The reth command
20const RETH: &str = "reth";
21
22/// The default HTTP port for Reth.
23const DEFAULT_HTTP_PORT: u16 = 8545;
24
25/// The default WS port for Reth.
26const DEFAULT_WS_PORT: u16 = 8546;
27
28/// The default auth port for Reth.
29const DEFAULT_AUTH_PORT: u16 = 8551;
30
31/// The default P2P port for Reth.
32const DEFAULT_P2P_PORT: u16 = 30303;
33
34/// A Reth instance. Will close the instance when dropped.
35///
36/// Construct this using [`Reth`].
37#[derive(Debug)]
38pub struct RethInstance {
39    pid: Child,
40    instance: u16,
41    http_port: u16,
42    ws_port: u16,
43    auth_port: Option<u16>,
44    p2p_port: Option<u16>,
45    ipc: Option<PathBuf>,
46    data_dir: Option<PathBuf>,
47    genesis: Option<Genesis>,
48}
49
50impl RethInstance {
51    /// Returns the instance number of this instance.
52    pub const fn instance(&self) -> u16 {
53        self.instance
54    }
55
56    /// Returns the HTTP port of this instance.
57    pub const fn http_port(&self) -> u16 {
58        self.http_port
59    }
60
61    /// Returns the WS port of this instance.
62    pub const fn ws_port(&self) -> u16 {
63        self.ws_port
64    }
65
66    /// Returns the auth port of this instance.
67    pub const fn auth_port(&self) -> Option<u16> {
68        self.auth_port
69    }
70
71    /// Returns the p2p port of this instance.
72    /// If discovery is disabled, this will be `None`.
73    pub const fn p2p_port(&self) -> Option<u16> {
74        self.p2p_port
75    }
76
77    /// Returns the HTTP endpoint of this instance.
78    #[doc(alias = "http_endpoint")]
79    pub fn endpoint(&self) -> String {
80        format!("http://localhost:{}", self.http_port)
81    }
82
83    /// Returns the Websocket endpoint of this instance.
84    pub fn ws_endpoint(&self) -> String {
85        format!("ws://localhost:{}", self.ws_port)
86    }
87
88    /// Returns the IPC endpoint of this instance.
89    pub fn ipc_endpoint(&self) -> String {
90        self.ipc.clone().map_or_else(|| "reth.ipc".to_string(), |ipc| ipc.display().to_string())
91    }
92
93    /// Returns the HTTP endpoint url of this instance.
94    #[doc(alias = "http_endpoint_url")]
95    pub fn endpoint_url(&self) -> Url {
96        Url::parse(&self.endpoint()).unwrap()
97    }
98
99    /// Returns the Websocket endpoint url of this instance.
100    pub fn ws_endpoint_url(&self) -> Url {
101        Url::parse(&self.ws_endpoint()).unwrap()
102    }
103
104    /// Returns the path to this instances' data directory.
105    pub const fn data_dir(&self) -> Option<&PathBuf> {
106        self.data_dir.as_ref()
107    }
108
109    /// Returns the genesis configuration used to configure this instance
110    pub const fn genesis(&self) -> Option<&Genesis> {
111        self.genesis.as_ref()
112    }
113
114    /// Takes the stdout contained in the child process.
115    ///
116    /// This leaves a `None` in its place, so calling methods that require a stdout to be present
117    /// will fail if called after this.
118    pub fn stdout(&mut self) -> Result<ChildStdout, NodeError> {
119        self.pid.stdout.take().ok_or(NodeError::NoStdout)
120    }
121}
122
123impl Drop for RethInstance {
124    fn drop(&mut self) {
125        self.pid.kill().expect("could not kill reth");
126    }
127}
128
129/// Builder for launching `reth`.
130///
131/// # Panics
132///
133/// If `spawn` is called without `reth` being available in the user's $PATH
134///
135/// # Example
136///
137/// ```no_run
138/// use alloy_node_bindings::Reth;
139///
140/// let port = 8545u16;
141/// let url = format!("http://localhost:{}", port).to_string();
142///
143/// let reth = Reth::new().instance(1).block_time("12sec").spawn();
144///
145/// drop(reth); // this will kill the instance
146/// ```
147#[derive(Clone, Debug, Default)]
148#[must_use = "This Builder struct does nothing unless it is `spawn`ed"]
149pub struct Reth {
150    dev: bool,
151    http_port: u16,
152    ws_port: u16,
153    auth_port: u16,
154    p2p_port: u16,
155    block_time: Option<String>,
156    instance: u16,
157    discovery_enabled: bool,
158    program: Option<PathBuf>,
159    ipc_path: Option<PathBuf>,
160    ipc_enabled: bool,
161    data_dir: Option<PathBuf>,
162    chain_or_path: Option<String>,
163    genesis: Option<Genesis>,
164    args: Vec<OsString>,
165    keep_stdout: bool,
166}
167
168impl Reth {
169    /// Creates an empty Reth builder.
170    ///
171    /// The instance number is set to a random number between 1 and 200 by default to reduce the
172    /// odds of port conflicts. This can be changed with [`Reth::instance`]. Set to 0 to use the
173    /// default ports. 200 is the maximum number of instances that can be run set by Reth.
174    pub fn new() -> Self {
175        Self {
176            dev: false,
177            http_port: DEFAULT_HTTP_PORT,
178            ws_port: DEFAULT_WS_PORT,
179            auth_port: DEFAULT_AUTH_PORT,
180            p2p_port: DEFAULT_P2P_PORT,
181            block_time: None,
182            instance: rand::thread_rng().gen_range(1..200),
183            discovery_enabled: true,
184            program: None,
185            ipc_path: None,
186            ipc_enabled: false,
187            data_dir: None,
188            chain_or_path: None,
189            genesis: None,
190            args: Vec::new(),
191            keep_stdout: false,
192        }
193    }
194
195    /// Creates a Reth builder which will execute `reth` at the given path.
196    ///
197    /// # Example
198    ///
199    /// ```
200    /// use alloy_node_bindings::Reth;
201    /// # fn a() {
202    /// let reth = Reth::at("../reth/target/release/reth").spawn();
203    ///
204    /// println!("Reth running at `{}`", reth.endpoint());
205    /// # }
206    /// ```
207    pub fn at(path: impl Into<PathBuf>) -> Self {
208        Self::new().path(path)
209    }
210
211    /// Sets the `path` to the `reth` executable
212    ///
213    /// By default, it's expected that `reth` is in `$PATH`, see also
214    /// [`std::process::Command::new()`]
215    pub fn path<T: Into<PathBuf>>(mut self, path: T) -> Self {
216        self.program = Some(path.into());
217        self
218    }
219
220    /// Enable `dev` mode for the Reth instance.
221    pub const fn dev(mut self) -> Self {
222        self.dev = true;
223        self
224    }
225
226    /// Sets the HTTP port for the Reth instance.
227    /// Note: this resets the instance number to 0 to allow for custom ports.
228    pub const fn http_port(mut self, http_port: u16) -> Self {
229        self.http_port = http_port;
230        self.instance = 0;
231        self
232    }
233
234    /// Sets the WS port for the Reth instance.
235    /// Note: this resets the instance number to 0 to allow for custom ports.
236    pub const fn ws_port(mut self, ws_port: u16) -> Self {
237        self.ws_port = ws_port;
238        self.instance = 0;
239        self
240    }
241
242    /// Sets the auth port for the Reth instance.
243    /// Note: this resets the instance number to 0 to allow for custom ports.
244    pub const fn auth_port(mut self, auth_port: u16) -> Self {
245        self.auth_port = auth_port;
246        self.instance = 0;
247        self
248    }
249
250    /// Sets the p2p port for the Reth instance.
251    /// Note: this resets the instance number to 0 to allow for custom ports.
252    pub const fn p2p_port(mut self, p2p_port: u16) -> Self {
253        self.p2p_port = p2p_port;
254        self.instance = 0;
255        self
256    }
257
258    /// Sets the block time for the Reth instance.
259    /// Parses strings using <https://docs.rs/humantime/latest/humantime/fn.parse_duration.html>
260    /// This is only used if `dev` mode is enabled.
261    pub fn block_time(mut self, block_time: &str) -> Self {
262        self.block_time = Some(block_time.to_string());
263        self
264    }
265
266    /// Disables discovery for the Reth instance.
267    pub const fn disable_discovery(mut self) -> Self {
268        self.discovery_enabled = false;
269        self
270    }
271
272    /// Sets the chain name or path to a chain spec for the Reth instance.
273    /// Passed through to `reth --chain <name-or-path>`.
274    pub fn chain_or_path(mut self, chain_or_path: &str) -> Self {
275        self.chain_or_path = Some(chain_or_path.to_string());
276        self
277    }
278
279    /// Enable IPC for the Reth instance.
280    pub const fn enable_ipc(mut self) -> Self {
281        self.ipc_enabled = true;
282        self
283    }
284
285    /// Sets the instance number for the Reth instance. Set to 0 to use the default ports.
286    /// By default, a random number between 1 and 200 is used.
287    pub const fn instance(mut self, instance: u16) -> Self {
288        self.instance = instance;
289        self
290    }
291
292    /// Sets the IPC path for the socket.
293    pub fn ipc_path<T: Into<PathBuf>>(mut self, path: T) -> Self {
294        self.ipc_path = Some(path.into());
295        self
296    }
297
298    /// Sets the data directory for reth.
299    pub fn data_dir<T: Into<PathBuf>>(mut self, path: T) -> Self {
300        self.data_dir = Some(path.into());
301        self
302    }
303
304    /// Sets the `genesis.json` for the Reth instance.
305    ///
306    /// If this is set, reth will be initialized with `reth init` and the `--datadir` option will be
307    /// set to the same value as `data_dir`.
308    ///
309    /// This is destructive and will overwrite any existing data in the data directory.
310    pub fn genesis(mut self, genesis: Genesis) -> Self {
311        self.genesis = Some(genesis);
312        self
313    }
314
315    /// Keep the handle to reth's stdout in order to read from it.
316    ///
317    /// Caution: if the stdout handle isn't used, this can end up blocking.
318    pub const fn keep_stdout(mut self) -> Self {
319        self.keep_stdout = true;
320        self
321    }
322
323    /// Adds an argument to pass to `reth`.
324    ///
325    /// Pass any arg that is not supported by the builder.
326    pub fn arg<T: Into<OsString>>(mut self, arg: T) -> Self {
327        self.args.push(arg.into());
328        self
329    }
330
331    /// Adds multiple arguments to pass to `reth`.
332    ///
333    /// Pass any args that is not supported by the builder.
334    pub fn args<I, S>(mut self, args: I) -> Self
335    where
336        I: IntoIterator<Item = S>,
337        S: Into<OsString>,
338    {
339        for arg in args {
340            self = self.arg(arg);
341        }
342        self
343    }
344
345    /// Consumes the builder and spawns `reth`.
346    ///
347    /// # Panics
348    ///
349    /// If spawning the instance fails at any point.
350    #[track_caller]
351    pub fn spawn(self) -> RethInstance {
352        self.try_spawn().unwrap()
353    }
354
355    /// Consumes the builder and spawns `reth`. If spawning fails, returns an error.
356    pub fn try_spawn(self) -> Result<RethInstance, NodeError> {
357        let bin_path = self
358            .program
359            .as_ref()
360            .map_or_else(|| RETH.as_ref(), |bin| bin.as_os_str())
361            .to_os_string();
362        let mut cmd = Command::new(&bin_path);
363        // `reth` uses stdout for its logs
364        cmd.stdout(Stdio::piped());
365
366        // Use Reth's `node` subcommand.
367        cmd.arg("node");
368
369        // Set the ports if they are not the default.
370        if self.http_port != DEFAULT_HTTP_PORT {
371            cmd.arg("--http.port").arg(self.http_port.to_string());
372        }
373
374        if self.ws_port != DEFAULT_WS_PORT {
375            cmd.arg("--ws.port").arg(self.ws_port.to_string());
376        }
377
378        if self.auth_port != DEFAULT_AUTH_PORT {
379            cmd.arg("--authrpc.port").arg(self.auth_port.to_string());
380        }
381
382        if self.p2p_port != DEFAULT_P2P_PORT {
383            cmd.arg("--discovery.port").arg(self.p2p_port.to_string());
384        }
385
386        // If the `dev` flag is set, enable it.
387        if self.dev {
388            // Enable the dev mode.
389            // This mode uses a local proof-of-authority consensus engine with either fixed block
390            // times or automatically mined blocks.
391            // Disables network discovery and enables local http server.
392            // Prefunds 20 accounts derived by mnemonic "test test test test test test test test
393            // test test test junk" with 10 000 ETH each.
394            cmd.arg("--dev");
395
396            // If the block time is set, use it.
397            if let Some(block_time) = self.block_time {
398                cmd.arg("--dev.block-time").arg(block_time);
399            }
400        }
401
402        // If IPC is not enabled on the builder, disable it.
403        if !self.ipc_enabled {
404            cmd.arg("--ipcdisable");
405        }
406
407        // Open the HTTP API.
408        cmd.arg("--http");
409        cmd.arg("--http.api").arg(API);
410
411        // Open the WS API.
412        cmd.arg("--ws");
413        cmd.arg("--ws.api").arg(API);
414
415        // Configure the IPC path if it is set.
416        if let Some(ipc) = &self.ipc_path {
417            cmd.arg("--ipcpath").arg(ipc);
418        }
419
420        // If the instance is set, use it.
421        // Set the `instance` to 0 to use the default ports.
422        // By defining a custom `http_port`, `ws_port`, `auth_port`, or `p2p_port`, the instance
423        // number will be set to 0 automatically.
424        if self.instance > 0 {
425            cmd.arg("--instance").arg(self.instance.to_string());
426        }
427
428        if let Some(data_dir) = &self.data_dir {
429            cmd.arg("--datadir").arg(data_dir);
430
431            // create the directory if it doesn't exist
432            if !data_dir.exists() {
433                create_dir(data_dir).map_err(NodeError::CreateDirError)?;
434            }
435        }
436
437        if self.discovery_enabled {
438            // Verbosity is required to read the P2P port from the logs.
439            cmd.arg("--verbosity").arg("-vvv");
440        } else {
441            cmd.arg("--disable-discovery");
442            cmd.arg("--no-persist-peers");
443        }
444
445        if let Some(chain_or_path) = self.chain_or_path {
446            cmd.arg("--chain").arg(chain_or_path);
447        }
448
449        // Disable color output to make parsing logs easier.
450        cmd.arg("--color").arg("never");
451
452        // Add any additional arguments.
453        cmd.args(self.args);
454
455        let mut child = cmd.spawn().map_err(NodeError::SpawnError)?;
456
457        let stdout = child.stdout.take().ok_or(NodeError::NoStdout)?;
458
459        let start = Instant::now();
460        let mut reader = BufReader::new(stdout);
461
462        let mut http_port = 0;
463        let mut ws_port = 0;
464        let mut auth_port = 0;
465        let mut p2p_port = 0;
466
467        let mut ports_started = false;
468        let mut p2p_started = !self.discovery_enabled;
469
470        loop {
471            if start + NODE_STARTUP_TIMEOUT <= Instant::now() {
472                let _ = child.kill();
473                return Err(NodeError::Timeout);
474            }
475
476            let mut line = String::with_capacity(120);
477            reader.read_line(&mut line).map_err(NodeError::ReadLineError)?;
478
479            if line.contains("RPC HTTP server started") {
480                if let Some(addr) = extract_endpoint("url=", &line) {
481                    http_port = addr.port();
482                }
483            }
484
485            if line.contains("RPC WS server started") {
486                if let Some(addr) = extract_endpoint("url=", &line) {
487                    ws_port = addr.port();
488                }
489            }
490
491            if line.contains("RPC auth server started") {
492                if let Some(addr) = extract_endpoint("url=", &line) {
493                    auth_port = addr.port();
494                }
495            }
496
497            // Encountered a critical error, exit early.
498            if line.contains("ERROR") {
499                let _ = child.kill();
500                return Err(NodeError::Fatal(line));
501            }
502
503            if http_port != 0 && ws_port != 0 && auth_port != 0 {
504                ports_started = true;
505            }
506
507            if self.discovery_enabled {
508                if line.contains("Updated local ENR") {
509                    if let Some(port) = extract_endpoint("IpV4 UDP Socket", &line) {
510                        p2p_port = port.port();
511                        p2p_started = true;
512                    }
513                }
514            } else {
515                p2p_started = true;
516            }
517
518            // If all ports have started we are ready to be queried.
519            if ports_started && p2p_started {
520                break;
521            }
522        }
523
524        if self.keep_stdout {
525            // re-attach the stdout handle if requested
526            child.stdout = Some(reader.into_inner());
527        }
528
529        Ok(RethInstance {
530            pid: child,
531            instance: self.instance,
532            http_port,
533            ws_port,
534            p2p_port: (p2p_port != 0).then_some(p2p_port),
535            ipc: self.ipc_path,
536            data_dir: self.data_dir,
537            auth_port: Some(auth_port),
538            genesis: self.genesis,
539        })
540    }
541}