ethers_core/utils/
geth.rs

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