alloy_node_bindings/nodes/
geth.rs1use 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
21const API: &str = "eth,net,web3,txpool,admin,personal,miner,debug";
23
24const GETH: &str = "geth";
26
27#[derive(Clone, Copy, Debug, PartialEq, Eq)]
29pub enum NodeMode {
30 Dev(DevOptions),
32 NonDev(PrivateNetOptions),
34}
35
36impl Default for NodeMode {
37 fn default() -> Self {
38 Self::Dev(Default::default())
39 }
40}
41
42#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
44pub struct DevOptions {
45 pub block_time: Option<u64>,
47}
48
49#[derive(Clone, Copy, Debug, PartialEq, Eq)]
51pub struct PrivateNetOptions {
52 pub p2p_port: Option<u16>,
54
55 pub discovery: bool,
57}
58
59impl Default for PrivateNetOptions {
60 fn default() -> Self {
61 Self { p2p_port: None, discovery: true }
62 }
63}
64
65#[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 pub const fn port(&self) -> u16 {
83 self.port
84 }
85
86 pub const fn p2p_port(&self) -> Option<u16> {
88 self.p2p_port
89 }
90
91 pub const fn auth_port(&self) -> Option<u16> {
93 self.auth_port
94 }
95
96 #[doc(alias = "http_endpoint")]
98 pub fn endpoint(&self) -> String {
99 format!("http://localhost:{}", self.port)
100 }
101
102 pub fn ws_endpoint(&self) -> String {
104 format!("ws://localhost:{}", self.port)
105 }
106
107 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 #[doc(alias = "http_endpoint_url")]
114 pub fn endpoint_url(&self) -> Url {
115 Url::parse(&self.endpoint()).unwrap()
116 }
117
118 pub fn ws_endpoint_url(&self) -> Url {
120 Url::parse(&self.ws_endpoint()).unwrap()
121 }
122
123 pub const fn data_dir(&self) -> Option<&PathBuf> {
125 self.data_dir.as_ref()
126 }
127
128 pub const fn genesis(&self) -> Option<&Genesis> {
130 self.genesis.as_ref()
131 }
132
133 #[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 pub fn stderr(&mut self) -> Result<ChildStderr, NodeError> {
144 self.pid.stderr.take().ok_or(NodeError::NoStderr)
145 }
146
147 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 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#[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 pub fn new() -> Self {
217 Self::default()
218 }
219
220 pub fn at(path: impl Into<PathBuf>) -> Self {
233 Self::new().path(path)
234 }
235
236 pub fn path<T: Into<PathBuf>>(mut self, path: T) -> Self {
241 self.program = Some(path.into());
242 self
243 }
244
245 pub fn dev(mut self) -> Self {
247 self.mode = NodeMode::Dev(Default::default());
248 self
249 }
250
251 pub const fn is_clique(&self) -> bool {
253 self.clique_private_key.is_some()
254 }
255
256 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 #[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 pub fn port<T: Into<u16>>(mut self, port: T) -> Self {
277 self.port = Some(port.into());
278 self
279 }
280
281 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 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 pub const fn chain_id(mut self, chain_id: u64) -> Self {
309 self.chain_id = Some(chain_id);
310 self
311 }
312
313 pub const fn insecure_unlock(mut self) -> Self {
315 self.insecure_unlock = true;
316 self
317 }
318
319 pub const fn enable_ipc(mut self) -> Self {
321 self.ipc_enabled = true;
322 self
323 }
324
325 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 pub fn ipc_path<T: Into<PathBuf>>(mut self, path: T) -> Self {
346 self.ipc_path = Some(path.into());
347 self
348 }
349
350 pub fn data_dir<T: Into<PathBuf>>(mut self, path: T) -> Self {
352 self.data_dir = Some(path.into());
353 self
354 }
355
356 pub fn genesis(mut self, genesis: Genesis) -> Self {
363 self.genesis = Some(genesis);
364 self
365 }
366
367 pub const fn authrpc_port(mut self, port: u16) -> Self {
369 self.authrpc_port = Some(port);
370 self
371 }
372
373 pub const fn keep_stderr(mut self) -> Self {
377 self.keep_err = true;
378 self
379 }
380
381 pub fn push_arg<T: Into<OsString>>(&mut self, arg: T) {
383 self.args.push(arg.into());
384 }
385
386 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 pub fn arg<T: Into<OsString>>(mut self, arg: T) -> Self {
401 self.args.push(arg.into());
402 self
403 }
404
405 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 #[track_caller]
425 pub fn spawn(self) -> GethInstance {
426 self.try_spawn().unwrap()
427 }
428
429 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 cmd.stderr(Stdio::piped());
439
440 let mut port = self.port.unwrap_or(0);
442 let port_s = port.to_string();
443
444 if !self.ipc_enabled {
446 cmd.arg("--ipcdisable");
447 }
448
449 cmd.arg("--http");
451 cmd.arg("--http.port").arg(&port_s);
452 cmd.arg("--http.api").arg(API);
453
454 cmd.arg("--ws");
456 cmd.arg("--ws.port").arg(port_s);
457 cmd.arg("--ws.api").arg(API);
458
459 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 let authrpc_port = self.authrpc_port.unwrap_or_else(&mut unused_port);
471 cmd.arg("--authrpc.port").arg(authrpc_port.to_string());
472
473 if is_clique {
475 let clique_addr = self.clique_address();
476 if let Some(genesis) = &mut self.genesis {
477 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 let extra_data_bytes =
490 [&[0u8; 32][..], clique_addr.as_ref(), &[0u8; 65][..]].concat();
491 genesis.extra_data = extra_data_bytes.into();
492
493 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 cmd.arg("--miner.etherbase").arg(format!("{clique_addr:?}"));
514 }
515
516 if let Some(genesis) = &self.genesis {
517 let temp_genesis_dir_path = tempdir().map_err(NodeError::CreateDirError)?.keep();
519
520 let temp_genesis_path = temp_genesis_dir_path.join("genesis.json");
522
523 let mut file = File::create(&temp_genesis_path).map_err(|_| {
525 NodeError::GenesisError("could not create genesis file".to_string())
526 })?;
527
528 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 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 if !res.success() {
549 return Err(NodeError::InitError);
550 }
551
552 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 if !data_dir.exists() {
563 create_dir(data_dir).map_err(NodeError::CreateDirError)?;
564 }
565 }
566
567 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 let port = p2p_port.unwrap_or(0);
579 cmd.arg("--port").arg(port.to_string());
580
581 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 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 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 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 if line.contains("HTTP endpoint opened")
639 || (line.contains("HTTP server started") && !line.contains("auth=true"))
640 {
641 if let Some(addr) = extract_endpoint("endpoint=", &line) {
643 port = addr.port();
645 }
646
647 ports_started = true;
648 }
649
650 if line.contains("Fatal:") {
653 let _ = child.kill();
654 return Err(NodeError::Fatal(line));
655 }
656
657 if ports_started && p2p_started {
659 break;
660 }
661 }
662
663 if self.keep_err {
664 child.stderr = Some(reader.into_inner());
666 } else {
667 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}