Skip to main content

alloy_node_bindings/nodes/
reth.rs

1//! Utilities for launching a Reth dev-mode instance.
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,
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 used to configure this instance
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    /// This leaves a `None` in its place, so calling methods that require a stdout to be present
126    /// will fail if called after this.
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/// # Panics
141///
142/// If `spawn` is called without `reth` being available in the user's $PATH
143///
144/// # Example
145///
146/// ```no_run
147/// use alloy_node_bindings::Reth;
148///
149/// let port = 8545u16;
150/// let url = format!("http://localhost:{}", port).to_string();
151///
152/// let reth = Reth::new().instance(1).block_time("12sec").spawn();
153///
154/// drop(reth); // this will kill the instance
155/// ```
156#[derive(Clone, Debug, Default)]
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 Reth {
179    /// Creates an empty Reth builder.
180    ///
181    /// The instance number is set to a random number between 1 and 200 by default to reduce the
182    /// odds of port conflicts. This can be changed with [`Reth::instance`]. Set to 0 to use the
183    /// default ports. 200 is the maximum number of instances that can be run set by Reth.
184    pub fn new() -> Self {
185        Self {
186            dev: false,
187            host: None,
188            http_port: DEFAULT_HTTP_PORT,
189            ws_port: DEFAULT_WS_PORT,
190            auth_port: DEFAULT_AUTH_PORT,
191            p2p_port: DEFAULT_P2P_PORT,
192            block_time: None,
193            instance: rand::thread_rng().gen_range(1..200),
194            discovery_enabled: true,
195            program: None,
196            ipc_path: None,
197            ipc_enabled: false,
198            data_dir: None,
199            chain_or_path: None,
200            genesis: None,
201            args: Vec::new(),
202            keep_stdout: false,
203        }
204    }
205
206    /// Creates a Reth builder which will execute `reth` at the given path.
207    ///
208    /// # Example
209    ///
210    /// ```
211    /// use alloy_node_bindings::Reth;
212    /// # fn a() {
213    /// let reth = Reth::at("../reth/target/release/reth").spawn();
214    ///
215    /// println!("Reth running at `{}`", reth.endpoint());
216    /// # }
217    /// ```
218    pub fn at(path: impl Into<PathBuf>) -> Self {
219        Self::new().path(path)
220    }
221
222    /// Sets the `path` to the `reth` executable
223    ///
224    /// By default, it's expected that `reth` is in `$PATH`, see also
225    /// [`std::process::Command::new()`]
226    pub fn path<T: Into<PathBuf>>(mut self, path: T) -> Self {
227        self.program = Some(path.into());
228        self
229    }
230
231    /// Enable `dev` mode for the Reth instance.
232    pub const fn dev(mut self) -> Self {
233        self.dev = true;
234        self
235    }
236
237    /// Sets the host which will be used when the `reth` instance is launched.
238    ///
239    /// Defaults to `localhost`.
240    pub fn host<T: Into<String>>(mut self, host: T) -> Self {
241        self.host = Some(host.into());
242        self
243    }
244
245    /// Sets the HTTP port for the Reth instance.
246    /// Note: this resets the instance number to 0 to allow for custom ports.
247    pub const fn http_port(mut self, http_port: u16) -> Self {
248        self.http_port = http_port;
249        self.instance = 0;
250        self
251    }
252
253    /// Sets the WS port for the Reth instance.
254    /// Note: this resets the instance number to 0 to allow for custom ports.
255    pub const fn ws_port(mut self, ws_port: u16) -> Self {
256        self.ws_port = ws_port;
257        self.instance = 0;
258        self
259    }
260
261    /// Sets the auth port for the Reth instance.
262    /// Note: this resets the instance number to 0 to allow for custom ports.
263    pub const fn auth_port(mut self, auth_port: u16) -> Self {
264        self.auth_port = auth_port;
265        self.instance = 0;
266        self
267    }
268
269    /// Sets the p2p port for the Reth instance.
270    /// Note: this resets the instance number to 0 to allow for custom ports.
271    pub const fn p2p_port(mut self, p2p_port: u16) -> Self {
272        self.p2p_port = p2p_port;
273        self.instance = 0;
274        self
275    }
276
277    /// Sets the block time for the Reth instance.
278    /// Parses strings using <https://docs.rs/humantime/latest/humantime/fn.parse_duration.html>
279    /// This is only used if `dev` mode is enabled.
280    pub fn block_time(mut self, block_time: &str) -> Self {
281        self.block_time = Some(block_time.to_string());
282        self
283    }
284
285    /// Disables discovery for the Reth instance.
286    pub const fn disable_discovery(mut self) -> Self {
287        self.discovery_enabled = false;
288        self
289    }
290
291    /// Sets the chain name or path to a chain spec for the Reth instance.
292    /// Passed through to `reth --chain <name-or-path>`.
293    pub fn chain_or_path(mut self, chain_or_path: &str) -> Self {
294        self.chain_or_path = Some(chain_or_path.to_string());
295        self
296    }
297
298    /// Enable IPC for the Reth instance.
299    pub const fn enable_ipc(mut self) -> Self {
300        self.ipc_enabled = true;
301        self
302    }
303
304    /// Sets the instance number for the Reth instance. Set to 0 to use the default ports.
305    /// By default, a random number between 1 and 200 is used.
306    pub const fn instance(mut self, instance: u16) -> Self {
307        self.instance = instance;
308        self
309    }
310
311    /// Sets the IPC path for the socket.
312    ///
313    /// This also enables IPC, as setting a path implies the intent to use IPC.
314    pub fn ipc_path<T: Into<PathBuf>>(mut self, path: T) -> Self {
315        self.ipc_path = Some(path.into());
316        self.ipc_enabled = true;
317        self
318    }
319
320    /// Sets the data directory for reth.
321    pub fn data_dir<T: Into<PathBuf>>(mut self, path: T) -> Self {
322        self.data_dir = Some(path.into());
323        self
324    }
325
326    /// Sets the `genesis.json` for the Reth instance.
327    ///
328    /// If this is set, reth will be initialized with `reth init` and the `--datadir` option will be
329    /// set to the same value as `data_dir`.
330    ///
331    /// This is destructive and will overwrite any existing data in the data directory.
332    pub fn genesis(mut self, genesis: Genesis) -> Self {
333        self.genesis = Some(genesis);
334        self
335    }
336
337    /// Keep the handle to reth's stdout in order to read from it.
338    ///
339    /// Caution: if the stdout handle isn't used, this can end up blocking.
340    pub const fn keep_stdout(mut self) -> Self {
341        self.keep_stdout = true;
342        self
343    }
344
345    /// Adds an argument to pass to `reth`.
346    ///
347    /// Pass any arg that is not supported by the builder.
348    pub fn arg<T: Into<OsString>>(mut self, arg: T) -> Self {
349        self.args.push(arg.into());
350        self
351    }
352
353    /// Adds multiple arguments to pass to `reth`.
354    ///
355    /// Pass any args that is not supported by the builder.
356    pub fn args<I, S>(mut self, args: I) -> Self
357    where
358        I: IntoIterator<Item = S>,
359        S: Into<OsString>,
360    {
361        for arg in args {
362            self = self.arg(arg);
363        }
364        self
365    }
366
367    /// Consumes the builder and spawns `reth`.
368    ///
369    /// # Panics
370    ///
371    /// If spawning the instance fails at any point.
372    #[track_caller]
373    pub fn spawn(self) -> RethInstance {
374        self.try_spawn().unwrap()
375    }
376
377    /// Consumes the builder and spawns `reth`. If spawning fails, returns an error.
378    pub fn try_spawn(self) -> Result<RethInstance, NodeError> {
379        let bin_path = self
380            .program
381            .as_ref()
382            .map_or_else(|| RETH.as_ref(), |bin| bin.as_os_str())
383            .to_os_string();
384        let mut cmd = Command::new(&bin_path);
385        // `reth` uses stdout for its logs
386        cmd.stdout(Stdio::piped());
387
388        // Use Reth's `node` subcommand.
389        cmd.arg("node");
390
391        // Set the ports if they are not the default.
392        if self.http_port != DEFAULT_HTTP_PORT {
393            cmd.arg("--http.port").arg(self.http_port.to_string());
394        }
395
396        if self.ws_port != DEFAULT_WS_PORT {
397            cmd.arg("--ws.port").arg(self.ws_port.to_string());
398        }
399
400        if self.auth_port != DEFAULT_AUTH_PORT {
401            cmd.arg("--authrpc.port").arg(self.auth_port.to_string());
402        }
403
404        if self.p2p_port != DEFAULT_P2P_PORT {
405            cmd.arg("--discovery.port").arg(self.p2p_port.to_string());
406        }
407
408        // If the `dev` flag is set, enable it.
409        if self.dev {
410            // Enable the dev mode.
411            // This mode uses a local proof-of-authority consensus engine with either fixed block
412            // times or automatically mined blocks.
413            // Disables network discovery and enables local http server.
414            // Prefunds 20 accounts derived by mnemonic "test test test test test test test test
415            // test test test junk" with 10 000 ETH each.
416            cmd.arg("--dev");
417
418            // If the block time is set, use it.
419            if let Some(block_time) = self.block_time {
420                cmd.arg("--dev.block-time").arg(block_time);
421            }
422        }
423
424        // If IPC is not enabled on the builder, disable it.
425        if !self.ipc_enabled {
426            cmd.arg("--ipcdisable");
427        }
428
429        // Open the HTTP API.
430        cmd.arg("--http");
431        cmd.arg("--http.api").arg(API);
432
433        if let Some(ref host) = self.host {
434            cmd.arg("--http.addr").arg(host);
435        }
436
437        // Open the WS API.
438        cmd.arg("--ws");
439        cmd.arg("--ws.api").arg(API);
440
441        if let Some(ref host) = self.host {
442            cmd.arg("--ws.addr").arg(host);
443        }
444
445        // Configure the IPC path if it is set.
446        if let Some(ipc) = &self.ipc_path {
447            cmd.arg("--ipcpath").arg(ipc);
448        }
449
450        // If the instance is set, use it.
451        // Set the `instance` to 0 to use the default ports.
452        // By defining a custom `http_port`, `ws_port`, `auth_port`, or `p2p_port`, the instance
453        // number will be set to 0 automatically.
454        if self.instance > 0 {
455            cmd.arg("--instance").arg(self.instance.to_string());
456        }
457
458        if let Some(data_dir) = &self.data_dir {
459            cmd.arg("--datadir").arg(data_dir);
460
461            // create the directory if it doesn't exist
462            if !data_dir.exists() {
463                create_dir(data_dir).map_err(NodeError::CreateDirError)?;
464            }
465        }
466
467        if self.discovery_enabled {
468            // Verbosity is required to read the P2P port from the logs.
469            cmd.arg("--verbosity").arg("-vvv");
470        } else {
471            cmd.arg("--disable-discovery");
472            cmd.arg("--no-persist-peers");
473        }
474
475        if let Some(chain_or_path) = self.chain_or_path {
476            cmd.arg("--chain").arg(chain_or_path);
477        }
478
479        // Disable color output to make parsing logs easier.
480        cmd.arg("--color").arg("never");
481
482        // Add any additional arguments.
483        cmd.args(self.args);
484
485        let mut child = cmd.spawn().map_err(NodeError::SpawnError)?;
486
487        let stdout = child.stdout.take().ok_or(NodeError::NoStdout)?;
488
489        let start = Instant::now();
490        let mut reader = BufReader::new(stdout);
491
492        let mut http_port = 0;
493        let mut ws_port = 0;
494        let mut auth_port = 0;
495        let mut p2p_port = 0;
496
497        let mut ports_started = false;
498        let mut p2p_started = !self.discovery_enabled;
499
500        loop {
501            if start + NODE_STARTUP_TIMEOUT <= Instant::now() {
502                let _ = child.kill();
503                return Err(NodeError::Timeout);
504            }
505
506            let mut line = String::with_capacity(120);
507            reader.read_line(&mut line).map_err(NodeError::ReadLineError)?;
508
509            if line.contains("RPC HTTP server started") {
510                if let Some(addr) = extract_endpoint("url=", &line) {
511                    http_port = addr.port();
512                }
513            }
514
515            if line.contains("RPC WS server started") {
516                if let Some(addr) = extract_endpoint("url=", &line) {
517                    ws_port = addr.port();
518                }
519            }
520
521            if line.contains("RPC auth server started") {
522                if let Some(addr) = extract_endpoint("url=", &line) {
523                    auth_port = addr.port();
524                }
525            }
526
527            // Encountered a critical error, exit early.
528            if line.contains("ERROR") {
529                let _ = child.kill();
530                return Err(NodeError::Fatal(line));
531            }
532
533            if http_port != 0 && ws_port != 0 && auth_port != 0 {
534                ports_started = true;
535            }
536
537            if self.discovery_enabled {
538                if line.contains("Updated local ENR") {
539                    if let Some(port) = extract_endpoint("IpV4 UDP Socket", &line) {
540                        p2p_port = port.port();
541                        p2p_started = true;
542                    }
543                }
544            } else {
545                p2p_started = true;
546            }
547
548            // If all ports have started we are ready to be queried.
549            if ports_started && p2p_started {
550                break;
551            }
552        }
553
554        if self.keep_stdout {
555            // re-attach the stdout handle if requested
556            child.stdout = Some(reader.into_inner());
557        }
558
559        Ok(RethInstance {
560            pid: child,
561            host: self.host.unwrap_or_else(|| "localhost".to_string()),
562            instance: self.instance,
563            http_port,
564            ws_port,
565            p2p_port: (p2p_port != 0).then_some(p2p_port),
566            ipc: self.ipc_path,
567            data_dir: self.data_dir,
568            auth_port: Some(auth_port),
569            genesis: self.genesis,
570        })
571    }
572}
573
574#[cfg(test)]
575mod tests {
576    use super::*;
577
578    #[test]
579    fn can_set_host() {
580        let reth = Reth::new().host("0.0.0.0").dev().try_spawn();
581        if let Ok(reth) = reth {
582            assert_eq!(reth.host(), "0.0.0.0");
583            assert!(reth.endpoint().starts_with("http://0.0.0.0:"));
584            assert!(reth.ws_endpoint().starts_with("ws://0.0.0.0:"));
585        }
586    }
587
588    #[test]
589    fn default_host_is_localhost() {
590        let reth = Reth::new().dev().try_spawn();
591        if let Ok(reth) = reth {
592            assert_eq!(reth.host(), "localhost");
593            assert!(reth.endpoint().starts_with("http://localhost:"));
594            assert!(reth.ws_endpoint().starts_with("ws://localhost:"));
595        }
596    }
597}