alloy_node_bindings/nodes/
geth.rs

1//! Utilities for launching a Geth dev-mode instance.
2
3use crate::{
4    utils::{extract_endpoint, extract_value, unused_port},
5    NodeError, NODE_DIAL_LOOP_TIMEOUT, NODE_STARTUP_TIMEOUT,
6};
7use alloy_genesis::{CliqueConfig, Genesis};
8use alloy_primitives::Address;
9use k256::ecdsa::SigningKey;
10use std::{
11    ffi::OsString,
12    fs::{create_dir, File},
13    io::{BufRead, BufReader},
14    path::PathBuf,
15    process::{Child, ChildStderr, Command, Stdio},
16    time::Instant,
17};
18use tempfile::tempdir;
19use url::Url;
20
21/// The exposed APIs
22const API: &str = "eth,net,web3,txpool,admin,personal,miner,debug";
23
24/// The geth command
25const GETH: &str = "geth";
26
27/// Whether or not node is in `dev` mode and configuration options that depend on the mode.
28#[derive(Clone, Copy, Debug, PartialEq, Eq)]
29pub enum NodeMode {
30    /// Options that can be set in dev mode
31    Dev(DevOptions),
32    /// Options that cannot be set in dev mode
33    NonDev(PrivateNetOptions),
34}
35
36impl Default for NodeMode {
37    fn default() -> Self {
38        Self::Dev(Default::default())
39    }
40}
41
42/// Configuration options that can be set in dev mode.
43#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
44pub struct DevOptions {
45    /// The interval at which the dev chain will mine new blocks.
46    pub block_time: Option<u64>,
47}
48
49/// Configuration options that cannot be set in dev mode.
50#[derive(Clone, Copy, Debug, PartialEq, Eq)]
51pub struct PrivateNetOptions {
52    /// The p2p port to use.
53    pub p2p_port: Option<u16>,
54
55    /// Whether or not peer discovery is enabled.
56    pub discovery: bool,
57}
58
59impl Default for PrivateNetOptions {
60    fn default() -> Self {
61        Self { p2p_port: None, discovery: true }
62    }
63}
64
65/// A geth instance. Will close the instance when dropped.
66///
67/// Construct this using [`Geth`].
68#[derive(Debug)]
69pub struct GethInstance {
70    pid: Child,
71    port: u16,
72    p2p_port: Option<u16>,
73    auth_port: Option<u16>,
74    ipc: Option<PathBuf>,
75    data_dir: Option<PathBuf>,
76    genesis: Option<Genesis>,
77    clique_private_key: Option<SigningKey>,
78}
79
80impl GethInstance {
81    /// Returns the port of this instance
82    pub const fn port(&self) -> u16 {
83        self.port
84    }
85
86    /// Returns the p2p port of this instance
87    pub const fn p2p_port(&self) -> Option<u16> {
88        self.p2p_port
89    }
90
91    /// Returns the auth port of this instance
92    pub const fn auth_port(&self) -> Option<u16> {
93        self.auth_port
94    }
95
96    /// Returns the HTTP endpoint of this instance
97    #[doc(alias = "http_endpoint")]
98    pub fn endpoint(&self) -> String {
99        format!("http://localhost:{}", self.port)
100    }
101
102    /// Returns the Websocket endpoint of this instance
103    pub fn ws_endpoint(&self) -> String {
104        format!("ws://localhost:{}", self.port)
105    }
106
107    /// Returns the IPC endpoint of this instance
108    pub fn ipc_endpoint(&self) -> String {
109        self.ipc.clone().map_or_else(|| "geth.ipc".to_string(), |ipc| ipc.display().to_string())
110    }
111
112    /// Returns the HTTP endpoint url of this instance
113    #[doc(alias = "http_endpoint_url")]
114    pub fn endpoint_url(&self) -> Url {
115        Url::parse(&self.endpoint()).unwrap()
116    }
117
118    /// Returns the Websocket endpoint url of this instance
119    pub fn ws_endpoint_url(&self) -> Url {
120        Url::parse(&self.ws_endpoint()).unwrap()
121    }
122
123    /// Returns the path to this instances' data directory
124    pub const fn data_dir(&self) -> Option<&PathBuf> {
125        self.data_dir.as_ref()
126    }
127
128    /// Returns the genesis configuration used to configure this instance
129    pub const fn genesis(&self) -> Option<&Genesis> {
130        self.genesis.as_ref()
131    }
132
133    /// Returns the private key used to configure clique on this instance
134    #[deprecated = "clique support was removed in geth >=1.14"]
135    pub const fn clique_private_key(&self) -> Option<&SigningKey> {
136        self.clique_private_key.as_ref()
137    }
138
139    /// Takes the stderr contained in the child process.
140    ///
141    /// This leaves a `None` in its place, so calling methods that require a stderr to be present
142    /// will fail if called after this.
143    pub fn stderr(&mut self) -> Result<ChildStderr, NodeError> {
144        self.pid.stderr.take().ok_or(NodeError::NoStderr)
145    }
146
147    /// Blocks until geth adds the specified peer, using 20s as the timeout.
148    ///
149    /// Requires the stderr to be present in the `GethInstance`.
150    pub fn wait_to_add_peer(&mut self, id: &str) -> Result<(), NodeError> {
151        let mut stderr = self.pid.stderr.as_mut().ok_or(NodeError::NoStderr)?;
152        let mut err_reader = BufReader::new(&mut stderr);
153        let mut line = String::new();
154        let start = Instant::now();
155
156        while start.elapsed() < NODE_DIAL_LOOP_TIMEOUT {
157            line.clear();
158            err_reader.read_line(&mut line).map_err(NodeError::ReadLineError)?;
159
160            // geth ids are truncated
161            let truncated_id = if id.len() > 16 { &id[..16] } else { id };
162            if line.contains("Adding p2p peer") && line.contains(truncated_id) {
163                return Ok(());
164            }
165        }
166        Err(NodeError::Timeout)
167    }
168}
169
170impl Drop for GethInstance {
171    fn drop(&mut self) {
172        self.pid.kill().expect("could not kill geth");
173    }
174}
175
176/// Builder for launching `geth`.
177///
178/// # Panics
179///
180/// If `spawn` is called without `geth` being available in the user's $PATH
181///
182/// # Example
183///
184/// ```no_run
185/// use alloy_node_bindings::Geth;
186///
187/// let port = 8545u16;
188/// let url = format!("http://localhost:{}", port).to_string();
189///
190/// let geth = Geth::new().port(port).block_time(5000u64).spawn();
191///
192/// drop(geth); // this will kill the instance
193/// ```
194#[derive(Clone, Debug, Default)]
195#[must_use = "This Builder struct does nothing unless it is `spawn`ed"]
196pub struct Geth {
197    program: Option<PathBuf>,
198    port: Option<u16>,
199    authrpc_port: Option<u16>,
200    ipc_path: Option<PathBuf>,
201    ipc_enabled: bool,
202    data_dir: Option<PathBuf>,
203    chain_id: Option<u64>,
204    insecure_unlock: bool,
205    keep_err: bool,
206    genesis: Option<Genesis>,
207    mode: NodeMode,
208    clique_private_key: Option<SigningKey>,
209    args: Vec<OsString>,
210}
211
212impl Geth {
213    /// Creates an empty Geth builder.
214    ///
215    /// The mnemonic is chosen randomly.
216    pub fn new() -> Self {
217        Self::default()
218    }
219
220    /// Creates a Geth builder which will execute `geth` at the given path.
221    ///
222    /// # Example
223    ///
224    /// ```
225    /// use alloy_node_bindings::Geth;
226    /// # fn a() {
227    /// let geth = Geth::at("../go-ethereum/build/bin/geth").spawn();
228    ///
229    /// println!("Geth running at `{}`", geth.endpoint());
230    /// # }
231    /// ```
232    pub fn at(path: impl Into<PathBuf>) -> Self {
233        Self::new().path(path)
234    }
235
236    /// Sets the `path` to the `geth` executable
237    ///
238    /// By default, it's expected that `geth` is in `$PATH`, see also
239    /// [`std::process::Command::new()`]
240    pub fn path<T: Into<PathBuf>>(mut self, path: T) -> Self {
241        self.program = Some(path.into());
242        self
243    }
244
245    /// Puts the `geth` instance in `dev` mode.
246    pub fn dev(mut self) -> Self {
247        self.mode = NodeMode::Dev(Default::default());
248        self
249    }
250
251    /// Returns whether the node is launched in Clique consensus mode.
252    pub const fn is_clique(&self) -> bool {
253        self.clique_private_key.is_some()
254    }
255
256    /// Calculates the address of the Clique consensus address.
257    pub fn clique_address(&self) -> Option<Address> {
258        self.clique_private_key.as_ref().map(|pk| Address::from_public_key(pk.verifying_key()))
259    }
260
261    /// Sets the Clique Private Key to the `geth` executable, which will be later
262    /// loaded on the node.
263    ///
264    /// The address derived from this private key will be used to set the `miner.etherbase` field
265    /// on the node.
266    #[deprecated = "clique support was removed in geth >=1.14"]
267    pub fn set_clique_private_key<T: Into<SigningKey>>(mut self, private_key: T) -> Self {
268        self.clique_private_key = Some(private_key.into());
269        self
270    }
271
272    /// Sets the port which will be used when the `geth-cli` instance is launched.
273    ///
274    /// If port is 0 then the OS will choose a random port.
275    /// [GethInstance::port] will return the port that was chosen.
276    pub fn port<T: Into<u16>>(mut self, port: T) -> Self {
277        self.port = Some(port.into());
278        self
279    }
280
281    /// Sets the port which will be used for incoming p2p connections.
282    ///
283    /// This will put the geth instance into non-dev mode, discarding any previously set dev-mode
284    /// options.
285    pub fn p2p_port(mut self, port: u16) -> Self {
286        match &mut self.mode {
287            NodeMode::Dev(_) => {
288                self.mode = NodeMode::NonDev(PrivateNetOptions {
289                    p2p_port: Some(port),
290                    ..Default::default()
291                })
292            }
293            NodeMode::NonDev(opts) => opts.p2p_port = Some(port),
294        }
295        self
296    }
297
298    /// Sets the block-time which will be used when the `geth-cli` instance is launched.
299    ///
300    /// This will put the geth instance in `dev` mode, discarding any previously set options that
301    /// cannot be used in dev mode.
302    pub const fn block_time(mut self, block_time: u64) -> Self {
303        self.mode = NodeMode::Dev(DevOptions { block_time: Some(block_time) });
304        self
305    }
306
307    /// Sets the chain id for the geth instance.
308    pub const fn chain_id(mut self, chain_id: u64) -> Self {
309        self.chain_id = Some(chain_id);
310        self
311    }
312
313    /// Allow geth to unlock accounts when rpc apis are open.
314    pub const fn insecure_unlock(mut self) -> Self {
315        self.insecure_unlock = true;
316        self
317    }
318
319    /// Enable IPC for the geth instance.
320    pub const fn enable_ipc(mut self) -> Self {
321        self.ipc_enabled = true;
322        self
323    }
324
325    /// Disable discovery for the geth instance.
326    ///
327    /// This will put the geth instance into non-dev mode, discarding any previously set dev-mode
328    /// options.
329    pub fn disable_discovery(mut self) -> Self {
330        self.inner_disable_discovery();
331        self
332    }
333
334    fn inner_disable_discovery(&mut self) {
335        match &mut self.mode {
336            NodeMode::Dev(_) => {
337                self.mode =
338                    NodeMode::NonDev(PrivateNetOptions { discovery: false, ..Default::default() })
339            }
340            NodeMode::NonDev(opts) => opts.discovery = false,
341        }
342    }
343
344    /// Sets the IPC path for the socket.
345    pub fn ipc_path<T: Into<PathBuf>>(mut self, path: T) -> Self {
346        self.ipc_path = Some(path.into());
347        self
348    }
349
350    /// Sets the data directory for geth.
351    pub fn data_dir<T: Into<PathBuf>>(mut self, path: T) -> Self {
352        self.data_dir = Some(path.into());
353        self
354    }
355
356    /// Sets the `genesis.json` for the geth instance.
357    ///
358    /// If this is set, geth will be initialized with `geth init` and the `--datadir` option will be
359    /// set to the same value as `data_dir`.
360    ///
361    /// This is destructive and will overwrite any existing data in the data directory.
362    pub fn genesis(mut self, genesis: Genesis) -> Self {
363        self.genesis = Some(genesis);
364        self
365    }
366
367    /// Sets the port for authenticated RPC connections.
368    pub const fn authrpc_port(mut self, port: u16) -> Self {
369        self.authrpc_port = Some(port);
370        self
371    }
372
373    /// Keep the handle to geth's stderr in order to read from it.
374    ///
375    /// Caution: if the stderr handle isn't used, this can end up blocking.
376    pub const fn keep_stderr(mut self) -> Self {
377        self.keep_err = true;
378        self
379    }
380
381    /// Adds an argument to pass to the `geth`.
382    pub fn push_arg<T: Into<OsString>>(&mut self, arg: T) {
383        self.args.push(arg.into());
384    }
385
386    /// Adds multiple arguments to pass to the `geth`.
387    pub fn extend_args<I, S>(&mut self, args: I)
388    where
389        I: IntoIterator<Item = S>,
390        S: Into<OsString>,
391    {
392        for arg in args {
393            self.push_arg(arg);
394        }
395    }
396
397    /// Adds an argument to pass to `geth`.
398    ///
399    /// Pass any arg that is not supported by the builder.
400    pub fn arg<T: Into<OsString>>(mut self, arg: T) -> Self {
401        self.args.push(arg.into());
402        self
403    }
404
405    /// Adds multiple arguments to pass to `geth`.
406    ///
407    /// Pass any args that is not supported by the builder.
408    pub fn args<I, S>(mut self, args: I) -> Self
409    where
410        I: IntoIterator<Item = S>,
411        S: Into<OsString>,
412    {
413        for arg in args {
414            self = self.arg(arg);
415        }
416        self
417    }
418
419    /// Consumes the builder and spawns `geth`.
420    ///
421    /// # Panics
422    ///
423    /// If spawning the instance fails at any point.
424    #[track_caller]
425    pub fn spawn(self) -> GethInstance {
426        self.try_spawn().unwrap()
427    }
428
429    /// Consumes the builder and spawns `geth`. If spawning fails, returns an error.
430    pub fn try_spawn(mut self) -> Result<GethInstance, NodeError> {
431        let bin_path = self
432            .program
433            .as_ref()
434            .map_or_else(|| GETH.as_ref(), |bin| bin.as_os_str())
435            .to_os_string();
436        let mut cmd = Command::new(&bin_path);
437        // `geth` uses stderr for its logs
438        cmd.stderr(Stdio::piped());
439
440        // If no port provided, let the os chose it for us
441        let mut port = self.port.unwrap_or(0);
442        let port_s = port.to_string();
443
444        // If IPC is not enabled on the builder, disable it.
445        if !self.ipc_enabled {
446            cmd.arg("--ipcdisable");
447        }
448
449        // Open the HTTP API
450        cmd.arg("--http");
451        cmd.arg("--http.port").arg(&port_s);
452        cmd.arg("--http.api").arg(API);
453
454        // Open the WS API
455        cmd.arg("--ws");
456        cmd.arg("--ws.port").arg(port_s);
457        cmd.arg("--ws.api").arg(API);
458
459        // pass insecure unlock flag if set
460        let is_clique = self.is_clique();
461        if self.insecure_unlock || is_clique {
462            cmd.arg("--allow-insecure-unlock");
463        }
464
465        if is_clique {
466            self.inner_disable_discovery();
467        }
468
469        // Set the port for authenticated APIs
470        let authrpc_port = self.authrpc_port.unwrap_or_else(&mut unused_port);
471        cmd.arg("--authrpc.port").arg(authrpc_port.to_string());
472
473        // use geth init to initialize the datadir if the genesis exists
474        if is_clique {
475            let clique_addr = self.clique_address();
476            if let Some(genesis) = &mut self.genesis {
477                // set up a clique config with an instant sealing period and short (8 block) epoch
478                let clique_config = CliqueConfig { period: Some(0), epoch: Some(8) };
479                genesis.config.clique = Some(clique_config);
480
481                let clique_addr = clique_addr.ok_or_else(|| {
482                    NodeError::CliqueAddressError(
483                        "could not calculates the address of the Clique consensus address."
484                            .to_string(),
485                    )
486                })?;
487
488                // set the extraData field
489                let extra_data_bytes =
490                    [&[0u8; 32][..], clique_addr.as_ref(), &[0u8; 65][..]].concat();
491                genesis.extra_data = extra_data_bytes.into();
492
493                // we must set the etherbase if using clique
494                // need to use format! / Debug here because the Address Display impl doesn't show
495                // the entire address
496                cmd.arg("--miner.etherbase").arg(format!("{clique_addr:?}"));
497            }
498
499            let clique_addr = self.clique_address().ok_or_else(|| {
500                NodeError::CliqueAddressError(
501                    "could not calculates the address of the Clique consensus address.".to_string(),
502                )
503            })?;
504
505            self.genesis = Some(Genesis::clique_genesis(
506                self.chain_id.ok_or(NodeError::ChainIdNotSet)?,
507                clique_addr,
508            ));
509
510            // we must set the etherbase if using clique
511            // need to use format! / Debug here because the Address Display impl doesn't show the
512            // entire address
513            cmd.arg("--miner.etherbase").arg(format!("{clique_addr:?}"));
514        }
515
516        if let Some(genesis) = &self.genesis {
517            // create a temp dir to store the genesis file
518            let temp_genesis_dir_path = tempdir().map_err(NodeError::CreateDirError)?.keep();
519
520            // create a temp dir to store the genesis file
521            let temp_genesis_path = temp_genesis_dir_path.join("genesis.json");
522
523            // create the genesis file
524            let mut file = File::create(&temp_genesis_path).map_err(|_| {
525                NodeError::GenesisError("could not create genesis file".to_string())
526            })?;
527
528            // serialize genesis and write to file
529            serde_json::to_writer_pretty(&mut file, &genesis).map_err(|_| {
530                NodeError::GenesisError("could not write genesis to file".to_string())
531            })?;
532
533            let mut init_cmd = Command::new(bin_path);
534            if let Some(data_dir) = &self.data_dir {
535                init_cmd.arg("--datadir").arg(data_dir);
536            }
537
538            // set the stderr to null so we don't pollute the test output
539            init_cmd.stderr(Stdio::null());
540
541            init_cmd.arg("init").arg(temp_genesis_path);
542            let res = init_cmd
543                .spawn()
544                .map_err(NodeError::SpawnError)?
545                .wait()
546                .map_err(NodeError::WaitError)?;
547            // .expect("failed to wait for geth init to exit");
548            if !res.success() {
549                return Err(NodeError::InitError);
550            }
551
552            // clean up the temp dir which is now persisted
553            std::fs::remove_dir_all(temp_genesis_dir_path).map_err(|_| {
554                NodeError::GenesisError("could not remove genesis temp dir".to_string())
555            })?;
556        }
557
558        if let Some(data_dir) = &self.data_dir {
559            cmd.arg("--datadir").arg(data_dir);
560
561            // create the directory if it doesn't exist
562            if !data_dir.exists() {
563                create_dir(data_dir).map_err(NodeError::CreateDirError)?;
564            }
565        }
566
567        // Dev mode with custom block time
568        let mut p2p_port = match self.mode {
569            NodeMode::Dev(DevOptions { block_time }) => {
570                cmd.arg("--dev");
571                if let Some(block_time) = block_time {
572                    cmd.arg("--dev.period").arg(block_time.to_string());
573                }
574                None
575            }
576            NodeMode::NonDev(PrivateNetOptions { p2p_port, discovery }) => {
577                // if no port provided, let the os chose it for us
578                let port = p2p_port.unwrap_or(0);
579                cmd.arg("--port").arg(port.to_string());
580
581                // disable discovery if the flag is set
582                if !discovery {
583                    cmd.arg("--nodiscover");
584                }
585                Some(port)
586            }
587        };
588
589        if let Some(chain_id) = self.chain_id {
590            cmd.arg("--networkid").arg(chain_id.to_string());
591        }
592
593        // debug verbosity is needed to check when peers are added
594        cmd.arg("--verbosity").arg("4");
595
596        if let Some(ipc) = &self.ipc_path {
597            cmd.arg("--ipcpath").arg(ipc);
598        }
599
600        cmd.args(self.args);
601
602        let mut child = cmd.spawn().map_err(NodeError::SpawnError)?;
603
604        let stderr = child.stderr.take().ok_or(NodeError::NoStderr)?;
605
606        let start = Instant::now();
607        let mut reader = BufReader::new(stderr);
608
609        // we shouldn't need to wait for p2p to start if geth is in dev mode - p2p is disabled in
610        // dev mode
611        let mut p2p_started = matches!(self.mode, NodeMode::Dev(_));
612        let mut ports_started = false;
613
614        loop {
615            if start + NODE_STARTUP_TIMEOUT <= Instant::now() {
616                let _ = child.kill();
617                return Err(NodeError::Timeout);
618            }
619
620            let mut line = String::with_capacity(120);
621            reader.read_line(&mut line).map_err(NodeError::ReadLineError)?;
622
623            if matches!(self.mode, NodeMode::NonDev(_)) && line.contains("Started P2P networking") {
624                p2p_started = true;
625            }
626
627            if !matches!(self.mode, NodeMode::Dev(_)) {
628                // try to find the p2p port, if not in dev mode
629                if line.contains("New local node record") {
630                    if let Some(port) = extract_value("tcp=", &line) {
631                        p2p_port = port.parse::<u16>().ok();
632                    }
633                }
634            }
635
636            // geth 1.9.23 uses "server started" while 1.9.18 uses "endpoint opened"
637            // the unauthenticated api is used for regular non-engine API requests
638            if line.contains("HTTP endpoint opened")
639                || (line.contains("HTTP server started") && !line.contains("auth=true"))
640            {
641                // Extracts the address from the output
642                if let Some(addr) = extract_endpoint("endpoint=", &line) {
643                    // use the actual http port
644                    port = addr.port();
645                }
646
647                ports_started = true;
648            }
649
650            // Encountered an error such as Fatal: Error starting protocol stack: listen tcp
651            // 127.0.0.1:8545: bind: address already in use
652            if line.contains("Fatal:") {
653                let _ = child.kill();
654                return Err(NodeError::Fatal(line));
655            }
656
657            // If all ports have started we are ready to be queried.
658            if ports_started && p2p_started {
659                break;
660            }
661        }
662
663        if self.keep_err {
664            // re-attach the stderr handle if requested
665            child.stderr = Some(reader.into_inner());
666        } else {
667            // We need to consume the stderr otherwise geth is non-responsive and RPC server results
668            // in connection refused.
669            // See: <https://github.com/alloy-rs/alloy/issues/2091#issuecomment-2676134147>
670            std::thread::spawn(move || {
671                let mut buf = String::new();
672                loop {
673                    let _ = reader.read_line(&mut buf);
674                }
675            });
676        }
677
678        Ok(GethInstance {
679            pid: child,
680            port,
681            ipc: self.ipc_path,
682            data_dir: self.data_dir,
683            p2p_port,
684            auth_port: self.authrpc_port,
685            genesis: self.genesis,
686            clique_private_key: self.clique_private_key,
687        })
688    }
689}