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
17const GETH_STARTUP_TIMEOUT_MILLIS: u64 = 10_000;
19
20const GETH_DIAL_LOOP_TIMEOUT: Duration = Duration::new(20, 0);
22
23const API: &str = "eth,net,web3,txpool,admin,personal,miner,debug";
25
26const GETH: &str = "geth";
28
29#[derive(Debug)]
31pub enum GethInstanceError {
32 Timeout(String),
34
35 ReadLineError(std::io::Error),
37
38 NoStderr,
40}
41
42pub 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 pub fn port(&self) -> u16 {
58 self.port
59 }
60
61 pub fn p2p_port(&self) -> Option<u16> {
63 self.p2p_port
64 }
65
66 pub fn endpoint(&self) -> String {
68 format!("http://localhost:{}", self.port)
69 }
70
71 pub fn ws_endpoint(&self) -> String {
73 format!("ws://localhost:{}", self.port)
74 }
75
76 pub fn ipc_path(&self) -> &Option<PathBuf> {
78 &self.ipc
79 }
80
81 pub fn data_dir(&self) -> &Option<PathBuf> {
83 &self.data_dir
84 }
85
86 pub fn genesis(&self) -> &Option<Genesis> {
88 &self.genesis
89 }
90
91 pub fn clique_private_key(&self) -> &Option<SigningKey> {
93 &self.clique_private_key
94 }
95
96 pub fn stderr(&mut self) -> Result<ChildStderr, GethInstanceError> {
101 self.pid.stderr.take().ok_or(GethInstanceError::NoStderr)
102 }
103
104 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 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#[derive(Debug, Clone)]
135pub enum GethMode {
136 Dev(DevOptions),
138 NonDev(PrivateNetOptions),
140}
141
142impl Default for GethMode {
143 fn default() -> Self {
144 Self::Dev(Default::default())
145 }
146}
147
148#[derive(Debug, Clone, Default)]
150pub struct DevOptions {
151 pub block_time: Option<u64>,
153}
154
155#[derive(Debug, Clone)]
157pub struct PrivateNetOptions {
158 pub p2p_port: Option<u16>,
160
161 pub discovery: bool,
163}
164
165impl Default for PrivateNetOptions {
166 fn default() -> Self {
167 Self { p2p_port: None, discovery: true }
168 }
169}
170
171#[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 pub fn new() -> Self {
210 Self::default()
211 }
212
213 pub fn at(path: impl Into<PathBuf>) -> Self {
226 Self::new().path(path)
227 }
228
229 pub fn is_clique(&self) -> bool {
231 self.clique_private_key.is_some()
232 }
233
234 #[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 #[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 #[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 #[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 #[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 #[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 #[must_use]
296 pub fn insecure_unlock(mut self) -> Self {
297 self.insecure_unlock = true;
298 self
299 }
300
301 #[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 #[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 #[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 #[must_use]
342 pub fn genesis(mut self, genesis: Genesis) -> Self {
343 self.genesis = Some(genesis);
344 self
345 }
346
347 #[must_use]
349 pub fn authrpc_port(mut self, port: u16) -> Self {
350 self.authrpc_port = Some(port);
351 self
352 }
353
354 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 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 cmd.arg("--http");
366 cmd.arg("--http.port").arg(port.to_string());
367 cmd.arg("--http.api").arg(API);
368
369 cmd.arg("--ws");
371 cmd.arg("--ws.port").arg(port.to_string());
372 cmd.arg("--ws.api").arg(API);
373
374 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 cmd.arg("--authrpc.port").arg(authrpc_port.to_string());
386
387 if let Some(ref mut genesis) = self.genesis {
389 if is_clique {
390 use super::CliqueConfig;
391 let clique_config = CliqueConfig { period: Some(0), epoch: Some(8) };
393 genesis.config.clique = Some(clique_config);
394
395 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 let temp_genesis_dir_path =
418 tempdir().expect("should be able to create temp dir for genesis init").into_path();
419
420 let temp_genesis_path = temp_genesis_dir_path.join("genesis.json");
422
423 let mut file = File::create(&temp_genesis_path).expect("could not create genesis file");
425
426 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 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 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 if !data_dir.exists() {
455 create_dir(data_dir).expect("could not create data dir");
456 }
457 }
458
459 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 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 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 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 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#[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 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 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 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}