Skip to main content

alloy_node_bindings/nodes/
reth.rs

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