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
18const GETH_STARTUP_TIMEOUT: Duration = Duration::from_secs(10);
20
21const GETH_DIAL_LOOP_TIMEOUT: Duration = Duration::from_secs(20);
23
24const API: &str = "eth,net,web3,txpool,admin,personal,miner,debug";
26
27const GETH: &str = "geth";
29
30#[derive(Debug)]
32pub enum GethInstanceError {
33 Timeout(String),
35
36 ReadLineError(std::io::Error),
38
39 NoStderr,
41}
42
43#[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 pub fn port(&self) -> u16 {
60 self.port
61 }
62
63 pub fn p2p_port(&self) -> Option<u16> {
65 self.p2p_port
66 }
67
68 pub fn endpoint(&self) -> String {
70 format!("http://localhost:{}", self.port)
71 }
72
73 pub fn ws_endpoint(&self) -> String {
75 format!("ws://localhost:{}", self.port)
76 }
77
78 pub fn ipc_path(&self) -> &Option<PathBuf> {
80 &self.ipc
81 }
82
83 pub fn data_dir(&self) -> &Option<PathBuf> {
85 &self.data_dir
86 }
87
88 pub fn genesis(&self) -> &Option<Genesis> {
90 &self.genesis
91 }
92
93 pub fn clique_private_key(&self) -> &Option<SigningKey> {
95 &self.clique_private_key
96 }
97
98 pub fn stderr(&mut self) -> Result<ChildStderr, GethInstanceError> {
103 self.pid.stderr.take().ok_or(GethInstanceError::NoStderr)
104 }
105
106 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 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#[derive(Clone, Copy, Debug, PartialEq, Eq)]
137pub enum GethMode {
138 Dev(DevOptions),
140 NonDev(PrivateNetOptions),
142}
143
144impl Default for GethMode {
145 fn default() -> Self {
146 Self::Dev(Default::default())
147 }
148}
149
150#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
152pub struct DevOptions {
153 pub block_time: Option<u64>,
155}
156
157#[derive(Clone, Copy, Debug, PartialEq, Eq)]
159pub struct PrivateNetOptions {
160 pub p2p_port: Option<u16>,
162
163 pub discovery: bool,
165}
166
167impl Default for PrivateNetOptions {
168 fn default() -> Self {
169 Self { p2p_port: None, discovery: true }
170 }
171}
172
173#[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 pub fn new() -> Self {
214 Self::default()
215 }
216
217 pub fn at(path: impl Into<PathBuf>) -> Self {
230 Self::new().path(path)
231 }
232
233 pub fn is_clique(&self) -> bool {
235 self.clique_private_key.is_some()
236 }
237
238 pub fn path<T: Into<PathBuf>>(mut self, path: T) -> Self {
243 self.program = Some(path.into());
244 self
245 }
246
247 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 pub fn port<T: Into<u16>>(mut self, port: T) -> Self {
262 self.port = Some(port.into());
263 self
264 }
265
266 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 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 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 pub fn insecure_unlock(mut self) -> Self {
300 self.insecure_unlock = true;
301 self
302 }
303
304 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 pub fn ipc_path<T: Into<PathBuf>>(mut self, path: T) -> Self {
325 self.ipc_path = Some(path.into());
326 self
327 }
328
329 pub fn data_dir<T: Into<PathBuf>>(mut self, path: T) -> Self {
331 self.data_dir = Some(path.into());
332 self
333 }
334
335 pub fn genesis(mut self, genesis: Genesis) -> Self {
342 self.genesis = Some(genesis);
343 self
344 }
345
346 pub fn authrpc_port(mut self, port: u16) -> Self {
348 self.authrpc_port = Some(port);
349 self
350 }
351
352 #[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 cmd.stderr(Stdio::piped());
367
368 let mut port = self.port.unwrap_or(0);
370 let port_s = port.to_string();
371
372 cmd.arg("--http");
374 cmd.arg("--http.port").arg(&port_s);
375 cmd.arg("--http.api").arg(API);
376
377 cmd.arg("--ws");
379 cmd.arg("--ws.port").arg(port_s);
380 cmd.arg("--ws.api").arg(API);
381
382 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 let authrpc_port = self.authrpc_port.unwrap_or_else(&mut unused_port);
394 cmd.arg("--authrpc.port").arg(authrpc_port.to_string());
395
396 if is_clique {
398 if let Some(genesis) = &mut self.genesis {
399 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 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 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 cmd.arg("--miner.etherbase").arg(format!("{clique_addr:?}"));
431 }
432
433 if let Some(ref genesis) = self.genesis {
434 let temp_genesis_dir_path =
436 tempdir().expect("should be able to create temp dir for genesis init").into_path();
437
438 let temp_genesis_path = temp_genesis_dir_path.join("genesis.json");
440
441 let mut file = File::create(&temp_genesis_path).expect("could not create genesis file");
443
444 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 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 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 if !data_dir.exists() {
476 create_dir(data_dir).expect("could not create data dir");
477 }
478 }
479
480 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 let port = p2p_port.unwrap_or(0);
492 cmd.arg("--port").arg(port.to_string());
493
494 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 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 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 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 if line.contains("HTTP endpoint opened") ||
549 (line.contains("HTTP server started") && !line.contains("auth=true"))
550 {
551 if let Some(addr) = extract_endpoint(&line) {
553 port = addr.port();
555 }
556
557 http_started = true;
558 }
559
560 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
585fn 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
598fn extract_endpoint(line: &str) -> Option<SocketAddr> {
600 let val = extract_value("endpoint=", line)?;
601 val.parse::<SocketAddr>().ok()
602}
603
604#[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 #[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 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 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}