Skip to main content

cbrzn_ethers_core/utils/
geth.rs

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