From 5ed60f827f3bd0211855919c08504cc4701e8353 Mon Sep 17 00:00:00 2001 From: DaniPopes <57450786+DaniPopes@users.noreply.github.com> Date: Sun, 14 Jan 2024 04:20:07 +0100 Subject: [PATCH] feat: add `alloy-node-bindings` (#111) * feat: fork from ethers * feat: migrate * chore: lints * chore: lints * chore: clippy * chore: rename to node-bindings --- .github/workflows/ci.yml | 5 +- Cargo.toml | 3 +- crates/node-bindings/Cargo.toml | 22 + crates/node-bindings/README.md | 3 + crates/node-bindings/src/anvil.rs | 332 ++++++++ crates/node-bindings/src/genesis.rs | 995 ++++++++++++++++++++++ crates/node-bindings/src/geth.rs | 680 +++++++++++++++ crates/node-bindings/src/lib.rs | 76 ++ crates/node-bindings/src/serde_helpers.rs | 65 ++ crates/providers/Cargo.toml | 2 +- crates/providers/src/provider.rs | 4 +- crates/rpc-client/Cargo.toml | 9 +- crates/rpc-client/tests/it/ipc.rs | 2 +- 13 files changed, 2187 insertions(+), 11 deletions(-) create mode 100644 crates/node-bindings/Cargo.toml create mode 100644 crates/node-bindings/README.md create mode 100644 crates/node-bindings/src/anvil.rs create mode 100644 crates/node-bindings/src/genesis.rs create mode 100644 crates/node-bindings/src/geth.rs create mode 100644 crates/node-bindings/src/lib.rs create mode 100644 crates/node-bindings/src/serde_helpers.rs diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index a9f1e5f7511..5ea86c2dfd5 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -60,11 +60,12 @@ jobs: - name: cargo hack run: | cargo hack check --workspace --target wasm32-unknown-unknown \ - --exclude alloy-transport-ipc \ --exclude alloy-signer \ --exclude alloy-signer-aws \ --exclude alloy-signer-ledger \ - --exclude alloy-signer-trezor + --exclude alloy-signer-trezor \ + --exclude alloy-node-bindings \ + --exclude alloy-transport-ipc feature-checks: runs-on: ubuntu-latest diff --git a/Cargo.toml b/Cargo.toml index cc92d8e2ef9..3d29ee2c745 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -21,10 +21,11 @@ alloy-consensus = { version = "0.1.0", path = "crates/consensus" } alloy-eips = { version = "0.1.0", path = "crates/eips" } alloy-json-rpc = { version = "0.1.0", path = "crates/json-rpc" } alloy-network = { version = "0.1.0", path = "crates/network" } +alloy-node-bindings = { version = "0.1.0", path = "crates/node-bindings" } alloy-pubsub = { version = "0.1.0", path = "crates/pubsub" } alloy-rpc-client = { version = "0.1.0", path = "crates/rpc-client" } -alloy-rpc-types = { version = "0.1.0", path = "crates/rpc-types" } alloy-rpc-trace-types = { version = "0.1.0", path = "crates/rpc-trace-types" } +alloy-rpc-types = { version = "0.1.0", path = "crates/rpc-types" } alloy-signer = { version = "0.1.0", path = "crates/signer" } alloy-signer-aws = { version = "0.1.0", path = "crates/signer-aws" } alloy-signer-ledger = { version = "0.1.0", path = "crates/signer-ledger" } diff --git a/crates/node-bindings/Cargo.toml b/crates/node-bindings/Cargo.toml new file mode 100644 index 00000000000..fc75eff14e3 --- /dev/null +++ b/crates/node-bindings/Cargo.toml @@ -0,0 +1,22 @@ +[package] +name = "alloy-node-bindings" +description = "Ethereum execution-layer client bindings" + +version.workspace = true +edition.workspace = true +rust-version.workspace = true +authors.workspace = true +license.workspace = true +homepage.workspace = true +repository.workspace = true +exclude.workspace = true + +[dependencies] +alloy-primitives = { workspace = true, features = ["k256", "serde"] } +k256.workspace = true +serde_json.workspace = true +serde.workspace = true +tempfile.workspace = true + +[dev-dependencies] +rand.workspace = true diff --git a/crates/node-bindings/README.md b/crates/node-bindings/README.md new file mode 100644 index 00000000000..a9195594d68 --- /dev/null +++ b/crates/node-bindings/README.md @@ -0,0 +1,3 @@ +# alloy-node-bindings + +Ethereum execution-layer client bindings. diff --git a/crates/node-bindings/src/anvil.rs b/crates/node-bindings/src/anvil.rs new file mode 100644 index 00000000000..c242d91c8c3 --- /dev/null +++ b/crates/node-bindings/src/anvil.rs @@ -0,0 +1,332 @@ +//! Utilities for launching an Anvil instance. + +use crate::unused_port; +use alloy_primitives::{hex, Address}; +use k256::{ecdsa::SigningKey, SecretKey as K256SecretKey}; +use std::{ + io::{BufRead, BufReader}, + path::PathBuf, + process::{Child, Command}, + time::{Duration, Instant}, +}; + +/// How long we will wait for anvil to indicate that it is ready. +const ANVIL_STARTUP_TIMEOUT_MILLIS: u64 = 10_000; + +/// An anvil CLI instance. Will close the instance when dropped. +/// +/// Construct this using [`Anvil`]. +#[derive(Debug)] +pub struct AnvilInstance { + pid: Child, + private_keys: Vec, + addresses: Vec
, + port: u16, + chain_id: Option, +} + +impl AnvilInstance { + /// Returns the private keys used to instantiate this instance + pub fn keys(&self) -> &[K256SecretKey] { + &self.private_keys + } + + /// Returns the addresses used to instantiate this instance + pub fn addresses(&self) -> &[Address] { + &self.addresses + } + + /// Returns the port of this instance + pub fn port(&self) -> u16 { + self.port + } + + /// Returns the chain of the anvil instance + pub fn chain_id(&self) -> u64 { + const ANVIL_HARDHAT_CHAIN_ID: u64 = 31_337; + self.chain_id.unwrap_or(ANVIL_HARDHAT_CHAIN_ID) + } + + /// Returns the HTTP endpoint of this instance + pub fn endpoint(&self) -> String { + format!("http://localhost:{}", self.port) + } + + /// Returns the Websocket endpoint of this instance + pub fn ws_endpoint(&self) -> String { + format!("ws://localhost:{}", self.port) + } +} + +impl Drop for AnvilInstance { + fn drop(&mut self) { + self.pid.kill().expect("could not kill anvil"); + } +} + +/// Builder for launching `anvil`. +/// +/// # Panics +/// +/// If `spawn` is called without `anvil` being available in the user's $PATH +/// +/// # Example +/// +/// ```no_run +/// use alloy_node_bindings::Anvil; +/// +/// let port = 8545u16; +/// let url = format!("http://localhost:{}", port).to_string(); +/// +/// let anvil = Anvil::new() +/// .port(port) +/// .mnemonic("abstract vacuum mammal awkward pudding scene penalty purchase dinner depart evoke puzzle") +/// .spawn(); +/// +/// drop(anvil); // this will kill the instance +/// ``` +#[derive(Debug, Clone, Default)] +#[must_use = "This Builder struct does nothing unless it is `spawn`ed"] +pub struct Anvil { + program: Option, + port: Option, + block_time: Option, + chain_id: Option, + mnemonic: Option, + fork: Option, + fork_block_number: Option, + args: Vec, + timeout: Option, +} + +impl Anvil { + /// Creates an empty Anvil builder. + /// The default port is 8545. The mnemonic is chosen randomly. + /// + /// # Example + /// + /// ``` + /// # use alloy_node_bindings::Anvil; + /// fn a() { + /// let anvil = Anvil::default().spawn(); + /// + /// println!("Anvil running at `{}`", anvil.endpoint()); + /// # } + /// ``` + pub fn new() -> Self { + Self::default() + } + + /// Creates an Anvil builder which will execute `anvil` at the given path. + /// + /// # Example + /// + /// ``` + /// # use alloy_node_bindings::Anvil; + /// fn a() { + /// let anvil = Anvil::at("~/.foundry/bin/anvil").spawn(); + /// + /// println!("Anvil running at `{}`", anvil.endpoint()); + /// # } + /// ``` + pub fn at(path: impl Into) -> Self { + Self::new().path(path) + } + + /// Sets the `path` to the `anvil` cli + /// + /// By default, it's expected that `anvil` is in `$PATH`, see also + /// [`std::process::Command::new()`] + pub fn path>(mut self, path: T) -> Self { + self.program = Some(path.into()); + self + } + + /// Sets the port which will be used when the `anvil` instance is launched. + pub fn port>(mut self, port: T) -> Self { + self.port = Some(port.into()); + self + } + + /// Sets the chain_id the `anvil` instance will use. + pub fn chain_id>(mut self, chain_id: T) -> Self { + self.chain_id = Some(chain_id.into()); + self + } + + /// Sets the mnemonic which will be used when the `anvil` instance is launched. + pub fn mnemonic>(mut self, mnemonic: T) -> Self { + self.mnemonic = Some(mnemonic.into()); + self + } + + /// Sets the block-time in seconds which will be used when the `anvil` instance is launched. + pub fn block_time>(mut self, block_time: T) -> Self { + self.block_time = Some(block_time.into()); + self + } + + /// Sets the `fork-block-number` which will be used in addition to [`Self::fork`]. + /// + /// **Note:** if set, then this requires `fork` to be set as well + pub fn fork_block_number>(mut self, fork_block_number: T) -> Self { + self.fork_block_number = Some(fork_block_number.into()); + self + } + + /// Sets the `fork` argument to fork from another currently running Ethereum client + /// at a given block. Input should be the HTTP location and port of the other client, + /// e.g. `http://localhost:8545`. You can optionally specify the block to fork from + /// using an @ sign: `http://localhost:8545@1599200` + pub fn fork>(mut self, fork: T) -> Self { + self.fork = Some(fork.into()); + self + } + + /// Adds an argument to pass to the `anvil`. + pub fn arg>(mut self, arg: T) -> Self { + self.args.push(arg.into()); + self + } + + /// Adds multiple arguments to pass to the `anvil`. + pub fn args(mut self, args: I) -> Self + where + I: IntoIterator, + S: Into, + { + for arg in args { + self = self.arg(arg); + } + self + } + + /// Sets the timeout which will be used when the `anvil` instance is launched. + pub fn timeout>(mut self, timeout: T) -> Self { + self.timeout = Some(timeout.into()); + self + } + + /// Consumes the builder and spawns `anvil`. + /// + /// # Panics + /// + /// If spawning the instance fails at any point. + #[track_caller] + pub fn spawn(self) -> AnvilInstance { + let mut cmd = if let Some(ref prg) = self.program { + Command::new(prg) + } else { + Command::new("anvil") + }; + cmd.stdout(std::process::Stdio::piped()).stderr(std::process::Stdio::inherit()); + let port = if let Some(port) = self.port { port } else { unused_port() }; + cmd.arg("-p").arg(port.to_string()); + + if let Some(mnemonic) = self.mnemonic { + cmd.arg("-m").arg(mnemonic); + } + + if let Some(chain_id) = self.chain_id { + cmd.arg("--chain-id").arg(chain_id.to_string()); + } + + if let Some(block_time) = self.block_time { + cmd.arg("-b").arg(block_time.to_string()); + } + + if let Some(fork) = self.fork { + cmd.arg("-f").arg(fork); + } + + if let Some(fork_block_number) = self.fork_block_number { + cmd.arg("--fork-block-number").arg(fork_block_number.to_string()); + } + + cmd.args(self.args); + + let mut child = cmd.spawn().expect("couldnt start anvil"); + + let stdout = child.stdout.take().expect("Unable to get stdout for anvil child process"); + + let start = Instant::now(); + let mut reader = BufReader::new(stdout); + + let mut private_keys = Vec::new(); + let mut addresses = Vec::new(); + let mut is_private_key = false; + let mut chain_id = None; + loop { + if start + Duration::from_millis(self.timeout.unwrap_or(ANVIL_STARTUP_TIMEOUT_MILLIS)) + <= Instant::now() + { + panic!("Timed out waiting for anvil to start. Is anvil installed?") + } + + let mut line = String::new(); + reader.read_line(&mut line).expect("Failed to read line from anvil process"); + if line.contains("Listening on") { + break; + } + + if line.starts_with("Private Keys") { + is_private_key = true; + } + + if is_private_key && line.starts_with('(') { + let key_str = line + .split("0x") + .last() + .unwrap_or_else(|| panic!("could not parse private key: {}", line)) + .trim(); + let key_hex = hex::decode(key_str).expect("could not parse as hex"); + let key = K256SecretKey::from_bytes((&key_hex[..]).into()) + .expect("did not get private key"); + addresses.push(Address::from_public_key(SigningKey::from(&key).verifying_key())); + private_keys.push(key); + } + + if let Some(start_chain_id) = line.find("Chain ID:") { + let rest = &line[start_chain_id + "Chain ID:".len()..]; + if let Ok(chain) = rest.split_whitespace().next().unwrap_or("").parse::() { + chain_id = Some(chain); + }; + } + } + + AnvilInstance { + pid: child, + private_keys, + addresses, + port, + chain_id: self.chain_id.or(chain_id), + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn can_launch_anvil() { + let _ = Anvil::new().spawn(); + } + + #[test] + fn can_launch_anvil_with_more_accounts() { + let _ = Anvil::new().arg("--accounts").arg("20").spawn(); + } + + #[test] + fn assert_chain_id() { + let anvil = Anvil::new().fork("https://rpc.ankr.com/eth").spawn(); + assert_eq!(anvil.chain_id(), 1); + } + + #[test] + fn assert_chain_id_without_rpc() { + let anvil = Anvil::new().spawn(); + assert_eq!(anvil.chain_id(), 31337); + } +} diff --git a/crates/node-bindings/src/genesis.rs b/crates/node-bindings/src/genesis.rs new file mode 100644 index 00000000000..79ebc44d728 --- /dev/null +++ b/crates/node-bindings/src/genesis.rs @@ -0,0 +1,995 @@ +//! Utilities for working with a `genesis.json` and other chain config structs. + +use crate::{from_bytes_to_h256, serde_helpers::deserialize_stringified_u64_opt}; +use alloy_primitives::{Address, Bytes, B256, U256, U64}; +use serde::{Deserialize, Serialize}; +use std::collections::HashMap; + +/// This represents the chain configuration, specifying the genesis block, header fields, and hard +/// fork switch blocks. +#[derive(Clone, Debug, Default, Serialize, Deserialize, PartialEq, Eq)] +#[serde(rename_all = "camelCase")] +pub struct Genesis { + /// The fork configuration for this network. + #[serde(default)] + pub config: ChainConfig, + + /// The genesis header nonce. + #[serde(default)] + pub nonce: U64, + + /// The genesis header timestamp. + #[serde(default)] + pub timestamp: U64, + + /// The genesis header extra data. + #[serde(default)] + pub extra_data: Bytes, + + /// The genesis header gas limit. + #[serde(default)] + pub gas_limit: U64, + + /// The genesis header difficulty. + pub difficulty: U256, + + /// The genesis header mix hash. + #[serde(default)] + pub mix_hash: B256, + + /// The genesis header coinbase address. + #[serde(default)] + pub coinbase: Address, + + /// The initial state of the genesis block. + pub alloc: HashMap, + + // The following fields are only included for tests, and should not be used in real genesis + // blocks. + /// The block number + #[serde(skip_serializing_if = "Option::is_none", default)] + pub number: Option, + + /// The block gas gasUsed + #[serde(skip_serializing_if = "Option::is_none", default)] + pub gas_used: Option, + + /// The block parent hash + #[serde(skip_serializing_if = "Option::is_none", default)] + pub parent_hash: Option, + + /// The base fee + #[serde(skip_serializing_if = "Option::is_none", default)] + pub base_fee_per_gas: Option, +} + +impl Genesis { + /// Creates a chain config using the given chain id. + /// and funds the given address with max coins. + /// + /// Enables all hard forks up to London at genesis. + pub fn new(chain_id: u64, signer_addr: Address) -> Genesis { + // set up a clique config with an instant sealing period and short (8 block) epoch + let clique_config = CliqueConfig { period: Some(0), epoch: Some(8) }; + + let config = ChainConfig { + chain_id, + eip155_block: Some(0), + eip150_block: Some(0), + eip158_block: Some(0), + + homestead_block: Some(0), + byzantium_block: Some(0), + constantinople_block: Some(0), + petersburg_block: Some(0), + istanbul_block: Some(0), + muir_glacier_block: Some(0), + berlin_block: Some(0), + london_block: Some(0), + clique: Some(clique_config), + ..Default::default() + }; + + // fund account + let mut alloc = HashMap::new(); + alloc.insert( + signer_addr, + GenesisAccount { balance: U256::MAX, nonce: None, code: None, storage: None }, + ); + + // put signer address in the extra data, padded by the required amount of zeros + // Clique issue: https://github.com/ethereum/EIPs/issues/225 + // Clique EIP: https://eips.ethereum.org/EIPS/eip-225 + // + // The first 32 bytes are vanity data, so we will populate it with zeros + // This is followed by the signer address, which is 20 bytes + // There are 65 bytes of zeros after the signer address, which is usually populated with the + // proposer signature. Because the genesis does not have a proposer signature, it will be + // populated with zeros. + let extra_data_bytes = [&[0u8; 32][..], signer_addr.as_slice(), &[0u8; 65][..]].concat(); + let extra_data = Bytes::from(extra_data_bytes); + + Genesis { + config, + alloc, + difficulty: U256::from(1), + gas_limit: U64::from(5000000), + extra_data, + ..Default::default() + } + } +} + +/// An account in the state of the genesis block. +#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)] +pub struct GenesisAccount { + /// The nonce of the account. + #[serde( + skip_serializing_if = "Option::is_none", + deserialize_with = "deserialize_stringified_u64_opt", + default + )] + pub nonce: Option, + /// The balance of the account. + pub balance: U256, + /// The code of the account, if any. + #[serde(skip_serializing_if = "Option::is_none", default)] + pub code: Option, + /// The storage of the account, if any. + #[serde( + skip_serializing_if = "Option::is_none", + deserialize_with = "from_unformatted_hex_map", + default + )] + pub storage: Option>, +} + +/// Represents a node's chain configuration. +/// +/// See [geth's `ChainConfig` +/// struct](https://github.com/ethereum/go-ethereum/blob/64dccf7aa411c5c7cd36090c3d9b9892945ae813/params/config.go#L349) +/// for the source of each field. +#[derive(Clone, Debug, Deserialize, Serialize, Default, PartialEq, Eq)] +#[serde(default, rename_all = "camelCase")] +pub struct ChainConfig { + /// The network's chain ID. + #[serde(default = "one")] + pub chain_id: u64, + + /// The homestead switch block (None = no fork, 0 = already homestead). + #[serde( + skip_serializing_if = "Option::is_none", + deserialize_with = "deserialize_stringified_u64_opt" + )] + pub homestead_block: Option, + + /// The DAO fork switch block (None = no fork). + #[serde( + skip_serializing_if = "Option::is_none", + deserialize_with = "deserialize_stringified_u64_opt" + )] + pub dao_fork_block: Option, + + /// Whether or not the node supports the DAO hard-fork. + pub dao_fork_support: bool, + + /// The EIP-150 hard fork block (None = no fork). + #[serde( + skip_serializing_if = "Option::is_none", + deserialize_with = "deserialize_stringified_u64_opt" + )] + pub eip150_block: Option, + + /// The EIP-150 hard fork hash. + #[serde(skip_serializing_if = "Option::is_none")] + pub eip150_hash: Option, + + /// The EIP-155 hard fork block. + #[serde( + skip_serializing_if = "Option::is_none", + deserialize_with = "deserialize_stringified_u64_opt" + )] + pub eip155_block: Option, + + /// The EIP-158 hard fork block. + #[serde( + skip_serializing_if = "Option::is_none", + deserialize_with = "deserialize_stringified_u64_opt" + )] + pub eip158_block: Option, + + /// The Byzantium hard fork block. + #[serde( + skip_serializing_if = "Option::is_none", + deserialize_with = "deserialize_stringified_u64_opt" + )] + pub byzantium_block: Option, + + /// The Constantinople hard fork block. + #[serde( + skip_serializing_if = "Option::is_none", + deserialize_with = "deserialize_stringified_u64_opt" + )] + pub constantinople_block: Option, + + /// The Petersburg hard fork block. + #[serde( + skip_serializing_if = "Option::is_none", + deserialize_with = "deserialize_stringified_u64_opt" + )] + pub petersburg_block: Option, + + /// The Istanbul hard fork block. + #[serde( + skip_serializing_if = "Option::is_none", + deserialize_with = "deserialize_stringified_u64_opt" + )] + pub istanbul_block: Option, + + /// The Muir Glacier hard fork block. + #[serde( + skip_serializing_if = "Option::is_none", + deserialize_with = "deserialize_stringified_u64_opt" + )] + pub muir_glacier_block: Option, + + /// The Berlin hard fork block. + #[serde( + skip_serializing_if = "Option::is_none", + deserialize_with = "deserialize_stringified_u64_opt" + )] + pub berlin_block: Option, + + /// The London hard fork block. + #[serde( + skip_serializing_if = "Option::is_none", + deserialize_with = "deserialize_stringified_u64_opt" + )] + pub london_block: Option, + + /// The Arrow Glacier hard fork block. + #[serde( + skip_serializing_if = "Option::is_none", + deserialize_with = "deserialize_stringified_u64_opt" + )] + pub arrow_glacier_block: Option, + + /// The Gray Glacier hard fork block. + #[serde( + skip_serializing_if = "Option::is_none", + deserialize_with = "deserialize_stringified_u64_opt" + )] + pub gray_glacier_block: Option, + + /// Virtual fork after the merge to use as a network splitter. + #[serde( + skip_serializing_if = "Option::is_none", + deserialize_with = "deserialize_stringified_u64_opt" + )] + pub merge_netsplit_block: Option, + + /// Shanghai switch time. + #[serde( + skip_serializing_if = "Option::is_none", + deserialize_with = "deserialize_stringified_u64_opt" + )] + pub shanghai_time: Option, + + /// Cancun switch time. + #[serde( + skip_serializing_if = "Option::is_none", + deserialize_with = "deserialize_stringified_u64_opt" + )] + pub cancun_time: Option, + + /// Total difficulty reached that triggers the merge consensus upgrade. + #[serde(skip_serializing_if = "Option::is_none", deserialize_with = "deserialize_ttd")] + pub terminal_total_difficulty: Option, + + /// A flag specifying that the network already passed the terminal total difficulty. Its + /// purpose is to disable legacy sync without having seen the TTD locally. + pub terminal_total_difficulty_passed: bool, + + /// Ethash parameters. + #[serde(skip_serializing_if = "Option::is_none")] + pub ethash: Option, + + /// Clique parameters. + #[serde(skip_serializing_if = "Option::is_none")] + pub clique: Option, +} + +// used only for serde +#[inline] +const fn one() -> u64 { + 1 +} + +/// Empty consensus configuration for proof-of-work networks. +#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq)] +#[allow(missing_copy_implementations)] +pub struct EthashConfig {} + +/// Consensus configuration for Clique. +#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq)] +#[allow(missing_copy_implementations)] +pub struct CliqueConfig { + /// Number of seconds between blocks to enforce. + #[serde( + default, + skip_serializing_if = "Option::is_none", + deserialize_with = "deserialize_stringified_u64_opt" + )] + pub period: Option, + + /// Epoch length to reset votes and checkpoints. + #[serde( + default, + skip_serializing_if = "Option::is_none", + deserialize_with = "deserialize_stringified_u64_opt" + )] + pub epoch: Option, +} + +fn deserialize_ttd<'de, D>(deserializer: D) -> Result, D::Error> +where + D: serde::Deserializer<'de>, +{ + match Option::::deserialize(deserializer)? { + None => Ok(None), + Some(val) => { + if let Some(num) = val.as_str() { + return num.parse().map(Some).map_err(serde::de::Error::custom); + } + + if let serde_json::Value::Number(num) = val { + // geth serializes ttd as number, for mainnet this exceeds u64 which serde is unable + // to deserialize as integer + if num.as_f64() == Some(5.875e22) { + Ok(Some(U256::from(58750000000000000000000u128))) + } else { + num.as_u64() + .map(U256::from) + .ok_or_else(|| { + serde::de::Error::custom(format!("expected a number got {num}")) + }) + .map(Some) + } + } else { + Err(serde::de::Error::custom(format!("expected a number, got {:?}", val))) + } + } + } +} + +/// Deserializes the input into an Option>, using from_unformatted_hex to +/// deserialize the keys and values. +fn from_unformatted_hex_map<'de, D: serde::Deserializer<'de>>( + deserializer: D, +) -> Result>, D::Error> { + let map = Option::>::deserialize(deserializer)?; + match map { + Some(mut map) => { + let mut res_map = HashMap::new(); + for (k, v) in map.drain() { + let k_deserialized = from_bytes_to_h256::<'de, D>(k)?; + let v_deserialized = from_bytes_to_h256::<'de, D>(v)?; + res_map.insert(k_deserialized, v_deserialized); + } + Ok(Some(res_map)) + } + None => Ok(None), + } +} + +#[cfg(test)] +mod tests { + use super::*; + use std::{collections::HashMap, str::FromStr}; + + #[test] + fn parse_hive_genesis() { + let geth_genesis = r#" + { + "difficulty": "0x20000", + "gasLimit": "0x1", + "alloc": {}, + "config": { + "ethash": {}, + "chainId": 1 + } + } + "#; + + let _genesis: Genesis = serde_json::from_str(geth_genesis).unwrap(); + } + + #[test] + fn parse_hive_clique_smoke_genesis() { + let geth_genesis = r#" + { + "difficulty": "0x1", + "gasLimit": "0x400000", + "extraData": "0x0000000000000000000000000000000000000000000000000000000000000000658bdf435d810c91414ec09147daa6db624063790000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", + "mixHash": "0x0000000000000000000000000000000000000000000000000000000000000000", + "nonce": "0x0", + "timestamp": "0x5c51a607", + "alloc": {} + } + "#; + + let _genesis: Genesis = serde_json::from_str(geth_genesis).unwrap(); + } + + #[test] + fn parse_non_hex_prefixed_balance() { + // tests that we can parse balance / difficulty fields that are either hex or decimal + let example_balance_json = r#" + { + "nonce": "0x0000000000000042", + "difficulty": "34747478", + "mixHash": "0x123456789abcdef123456789abcdef123456789abcdef123456789abcdef1234", + "coinbase": "0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "timestamp": "0x123456", + "parentHash": "0x0000000000000000000000000000000000000000000000000000000000000000", + "extraData": "0xfafbfcfd", + "gasLimit": "0x2fefd8", + "alloc": { + "0x3E951C9f69a06Bc3AD71fF7358DbC56bEd94b9F2": { + "balance": "1000000000000000000000000000" + }, + "0xe228C30d4e5245f967ac21726d5412dA27aD071C": { + "balance": "1000000000000000000000000000" + }, + "0xD59Ce7Ccc6454a2D2C2e06bbcf71D0Beb33480eD": { + "balance": "1000000000000000000000000000" + }, + "0x1CF4D54414eF51b41f9B2238c57102ab2e61D1F2": { + "balance": "1000000000000000000000000000" + }, + "0x249bE3fDEd872338C733cF3975af9736bdCb9D4D": { + "balance": "1000000000000000000000000000" + }, + "0x3fCd1bff94513712f8cD63d1eD66776A67D5F78e": { + "balance": "1000000000000000000000000000" + } + }, + "config": { + "ethash": {}, + "chainId": 10, + "homesteadBlock": 0, + "eip150Block": 0, + "eip155Block": 0, + "eip158Block": 0, + "byzantiumBlock": 0, + "constantinopleBlock": 0, + "petersburgBlock": 0, + "istanbulBlock": 0 + } + } + "#; + + let genesis: Genesis = serde_json::from_str(example_balance_json).unwrap(); + + // check difficulty against hex ground truth + let expected_difficulty = U256::from(0x2123456); + assert_eq!(expected_difficulty, genesis.difficulty); + + // check all alloc balances + let dec_balance = U256::from(1000000000000000000000000000_u128); + for alloc in &genesis.alloc { + assert_eq!(alloc.1.balance, dec_balance); + } + } + + #[test] + fn parse_hive_rpc_genesis() { + let geth_genesis = r#" + { + "config": { + "chainId": 7, + "homesteadBlock": 0, + "eip150Block": 0, + "eip150Hash": "0x5de1ee4135274003348e80b788e5afa4b18b18d320a5622218d5c493fedf5689", + "eip155Block": 0, + "eip158Block": 0 + }, + "coinbase": "0x0000000000000000000000000000000000000000", + "difficulty": "0x20000", + "extraData": "0x0000000000000000000000000000000000000000000000000000000000000000658bdf435d810c91414ec09147daa6db624063790000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", + "gasLimit": "0x2fefd8", + "nonce": "0x0000000000000000", + "timestamp": "0x1234", + "alloc": { + "cf49fda3be353c69b41ed96333cd24302da4556f": { + "balance": "0x123450000000000000000" + }, + "0161e041aad467a890839d5b08b138c1e6373072": { + "balance": "0x123450000000000000000" + }, + "87da6a8c6e9eff15d703fc2773e32f6af8dbe301": { + "balance": "0x123450000000000000000" + }, + "b97de4b8c857e4f6bc354f226dc3249aaee49209": { + "balance": "0x123450000000000000000" + }, + "c5065c9eeebe6df2c2284d046bfc906501846c51": { + "balance": "0x123450000000000000000" + }, + "0000000000000000000000000000000000000314": { + "balance": "0x0", + "code": "0x60606040526000357c0100000000000000000000000000000000000000000000000000000000900463ffffffff168063a223e05d1461006a578063abd1a0cf1461008d578063abfced1d146100d4578063e05c914a14610110578063e6768b451461014c575b610000565b346100005761007761019d565b6040518082815260200191505060405180910390f35b34610000576100be600480803573ffffffffffffffffffffffffffffffffffffffff169060200190919050506101a3565b6040518082815260200191505060405180910390f35b346100005761010e600480803573ffffffffffffffffffffffffffffffffffffffff169060200190919080359060200190919050506101ed565b005b346100005761014a600480803590602001909190803573ffffffffffffffffffffffffffffffffffffffff16906020019091905050610236565b005b346100005761017960048080359060200190919080359060200190919080359060200190919050506103c4565b60405180848152602001838152602001828152602001935050505060405180910390f35b60005481565b6000600160008373ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff1681526020019081526020016000205490505b919050565b80600160008473ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff168152602001908152602001600020819055505b5050565b7f6031a8d62d7c95988fa262657cd92107d90ed96e08d8f867d32f26edfe85502260405180905060405180910390a17f47e2689743f14e97f7dcfa5eec10ba1dff02f83b3d1d4b9c07b206cbbda66450826040518082815260200191505060405180910390a1817fa48a6b249a5084126c3da369fbc9b16827ead8cb5cdc094b717d3f1dcd995e2960405180905060405180910390a27f7890603b316f3509577afd111710f9ebeefa15e12f72347d9dffd0d65ae3bade81604051808273ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff16815260200191505060405180910390a18073ffffffffffffffffffffffffffffffffffffffff167f7efef9ea3f60ddc038e50cccec621f86a0195894dc0520482abf8b5c6b659e4160405180905060405180910390a28181604051808381526020018273ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff1681526020019250505060405180910390a05b5050565b6000600060008585859250925092505b935093509390505600a165627a7a72305820aaf842d0d0c35c45622c5263cbb54813d2974d3999c8c38551d7c613ea2bc1170029", + "storage": { + "0x0000000000000000000000000000000000000000000000000000000000000000": "0x1234", + "0x6661e9d6d8b923d5bbaab1b96e1dd51ff6ea2a93520fdc9eb75d059238b8c5e9": "0x01" + } + }, + "0000000000000000000000000000000000000315": { + "balance": "0x9999999999999999999999999999999", + "code": "0x60606040526000357c0100000000000000000000000000000000000000000000000000000000900463ffffffff168063ef2769ca1461003e575b610000565b3461000057610078600480803573ffffffffffffffffffffffffffffffffffffffff1690602001909190803590602001909190505061007a565b005b8173ffffffffffffffffffffffffffffffffffffffff166108fc829081150290604051809050600060405180830381858888f1935050505015610106578173ffffffffffffffffffffffffffffffffffffffff167f30a3c50752f2552dcc2b93f5b96866280816a986c0c0408cb6778b9fa198288f826040518082815260200191505060405180910390a25b5b50505600a165627a7a72305820637991fabcc8abad4294bf2bb615db78fbec4edff1635a2647d3894e2daf6a610029" + } + } + } + "#; + + let _genesis: Genesis = serde_json::from_str(geth_genesis).unwrap(); + } + + #[test] + fn parse_hive_graphql_genesis() { + let geth_genesis = r#" + { + "config" : {}, + "coinbase" : "0x8888f1f195afa192cfee860698584c030f4c9db1", + "difficulty" : "0x020000", + "extraData" : "0x42", + "gasLimit" : "0x2fefd8", + "mixHash" : "0x2c85bcbce56429100b2108254bb56906257582aeafcbd682bc9af67a9f5aee46", + "nonce" : "0x78cc16f7b4f65485", + "parentHash" : "0x0000000000000000000000000000000000000000000000000000000000000000", + "timestamp" : "0x54c98c81", + "alloc" : { + "a94f5374fce5edbc8e2a8697c15331677e6ebf0b": { + "balance" : "0x09184e72a000" + } + } + } + "#; + + let _genesis: Genesis = serde_json::from_str(geth_genesis).unwrap(); + } + + #[test] + fn parse_hive_engine_genesis() { + let geth_genesis = r#" + { + "config": { + "chainId": 7, + "homesteadBlock": 0, + "eip150Block": 0, + "eip150Hash": "0x5de1ee4135274003348e80b788e5afa4b18b18d320a5622218d5c493fedf5689", + "eip155Block": 0, + "eip158Block": 0, + "byzantiumBlock": 0, + "constantinopleBlock": 0, + "petersburgBlock": 0, + "istanbulBlock": 0, + "muirGlacierBlock": 0, + "berlinBlock": 0, + "yolov2Block": 0, + "yolov3Block": 0, + "londonBlock": 0 + }, + "coinbase": "0x0000000000000000000000000000000000000000", + "difficulty": "0x30000", + "extraData": "0x0000000000000000000000000000000000000000000000000000000000000000658bdf435d810c91414ec09147daa6db624063790000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", + "gasLimit": "0x2fefd8", + "nonce": "0x0000000000000000", + "timestamp": "0x1234", + "alloc": { + "cf49fda3be353c69b41ed96333cd24302da4556f": { + "balance": "0x123450000000000000000" + }, + "0161e041aad467a890839d5b08b138c1e6373072": { + "balance": "0x123450000000000000000" + }, + "87da6a8c6e9eff15d703fc2773e32f6af8dbe301": { + "balance": "0x123450000000000000000" + }, + "b97de4b8c857e4f6bc354f226dc3249aaee49209": { + "balance": "0x123450000000000000000" + }, + "c5065c9eeebe6df2c2284d046bfc906501846c51": { + "balance": "0x123450000000000000000" + }, + "0000000000000000000000000000000000000314": { + "balance": "0x0", + "code": "0x60606040526000357c0100000000000000000000000000000000000000000000000000000000900463ffffffff168063a223e05d1461006a578063abd1a0cf1461008d578063abfced1d146100d4578063e05c914a14610110578063e6768b451461014c575b610000565b346100005761007761019d565b6040518082815260200191505060405180910390f35b34610000576100be600480803573ffffffffffffffffffffffffffffffffffffffff169060200190919050506101a3565b6040518082815260200191505060405180910390f35b346100005761010e600480803573ffffffffffffffffffffffffffffffffffffffff169060200190919080359060200190919050506101ed565b005b346100005761014a600480803590602001909190803573ffffffffffffffffffffffffffffffffffffffff16906020019091905050610236565b005b346100005761017960048080359060200190919080359060200190919080359060200190919050506103c4565b60405180848152602001838152602001828152602001935050505060405180910390f35b60005481565b6000600160008373ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff1681526020019081526020016000205490505b919050565b80600160008473ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff168152602001908152602001600020819055505b5050565b7f6031a8d62d7c95988fa262657cd92107d90ed96e08d8f867d32f26edfe85502260405180905060405180910390a17f47e2689743f14e97f7dcfa5eec10ba1dff02f83b3d1d4b9c07b206cbbda66450826040518082815260200191505060405180910390a1817fa48a6b249a5084126c3da369fbc9b16827ead8cb5cdc094b717d3f1dcd995e2960405180905060405180910390a27f7890603b316f3509577afd111710f9ebeefa15e12f72347d9dffd0d65ae3bade81604051808273ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff16815260200191505060405180910390a18073ffffffffffffffffffffffffffffffffffffffff167f7efef9ea3f60ddc038e50cccec621f86a0195894dc0520482abf8b5c6b659e4160405180905060405180910390a28181604051808381526020018273ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff1681526020019250505060405180910390a05b5050565b6000600060008585859250925092505b935093509390505600a165627a7a72305820aaf842d0d0c35c45622c5263cbb54813d2974d3999c8c38551d7c613ea2bc1170029", + "storage": { + "0x0000000000000000000000000000000000000000000000000000000000000000": "0x1234", + "0x6661e9d6d8b923d5bbaab1b96e1dd51ff6ea2a93520fdc9eb75d059238b8c5e9": "0x01" + } + }, + "0000000000000000000000000000000000000315": { + "balance": "0x9999999999999999999999999999999", + "code": "0x60606040526000357c0100000000000000000000000000000000000000000000000000000000900463ffffffff168063ef2769ca1461003e575b610000565b3461000057610078600480803573ffffffffffffffffffffffffffffffffffffffff1690602001909190803590602001909190505061007a565b005b8173ffffffffffffffffffffffffffffffffffffffff166108fc829081150290604051809050600060405180830381858888f1935050505015610106578173ffffffffffffffffffffffffffffffffffffffff167f30a3c50752f2552dcc2b93f5b96866280816a986c0c0408cb6778b9fa198288f826040518082815260200191505060405180910390a25b5b50505600a165627a7a72305820637991fabcc8abad4294bf2bb615db78fbec4edff1635a2647d3894e2daf6a610029" + }, + "0000000000000000000000000000000000000316": { + "balance": "0x0", + "code": "0x444355" + }, + "0000000000000000000000000000000000000317": { + "balance": "0x0", + "code": "0x600160003555" + } + } + } + "#; + + let _genesis: Genesis = serde_json::from_str(geth_genesis).unwrap(); + } + + #[test] + fn parse_hive_devp2p_genesis() { + let geth_genesis = r#" + { + "config": { + "chainId": 19763, + "homesteadBlock": 0, + "eip150Block": 0, + "eip155Block": 0, + "eip158Block": 0, + "byzantiumBlock": 0, + "ethash": {} + }, + "nonce": "0xdeadbeefdeadbeef", + "timestamp": "0x0", + "extraData": "0x0000000000000000000000000000000000000000000000000000000000000000", + "gasLimit": "0x80000000", + "difficulty": "0x20000", + "mixHash": "0x0000000000000000000000000000000000000000000000000000000000000000", + "coinbase": "0x0000000000000000000000000000000000000000", + "alloc": { + "71562b71999873db5b286df957af199ec94617f7": { + "balance": "0xffffffffffffffffffffffffff" + } + }, + "number": "0x0", + "gasUsed": "0x0", + "parentHash": "0x0000000000000000000000000000000000000000000000000000000000000000" + } + "#; + + let _genesis: Genesis = serde_json::from_str(geth_genesis).unwrap(); + } + + #[test] + fn parse_execution_apis_genesis() { + let geth_genesis = r#" + { + "config": { + "chainId": 1337, + "homesteadBlock": 0, + "eip150Block": 0, + "eip150Hash": "0x0000000000000000000000000000000000000000000000000000000000000000", + "eip155Block": 0, + "eip158Block": 0, + "byzantiumBlock": 0, + "constantinopleBlock": 0, + "petersburgBlock": 0, + "istanbulBlock": 0, + "muirGlacierBlock": 0, + "berlinBlock": 0, + "londonBlock": 0, + "arrowGlacierBlock": 0, + "grayGlacierBlock": 0, + "shanghaiTime": 0, + "terminalTotalDifficulty": 0, + "terminalTotalDifficultyPassed": true, + "ethash": {} + }, + "nonce": "0x0", + "timestamp": "0x0", + "extraData": "0x", + "gasLimit": "0x4c4b40", + "difficulty": "0x1", + "mixHash": "0x0000000000000000000000000000000000000000000000000000000000000000", + "coinbase": "0x0000000000000000000000000000000000000000", + "alloc": { + "658bdf435d810c91414ec09147daa6db62406379": { + "balance": "0x487a9a304539440000" + }, + "aa00000000000000000000000000000000000000": { + "code": "0x6042", + "storage": { + "0x0000000000000000000000000000000000000000000000000000000000000000": "0x0000000000000000000000000000000000000000000000000000000000000000", + "0x0100000000000000000000000000000000000000000000000000000000000000": "0x0100000000000000000000000000000000000000000000000000000000000000", + "0x0200000000000000000000000000000000000000000000000000000000000000": "0x0200000000000000000000000000000000000000000000000000000000000000", + "0x0300000000000000000000000000000000000000000000000000000000000000": "0x0000000000000000000000000000000000000000000000000000000000000303" + }, + "balance": "0x1", + "nonce": "0x1" + }, + "bb00000000000000000000000000000000000000": { + "code": "0x600154600354", + "storage": { + "0x0000000000000000000000000000000000000000000000000000000000000000": "0x0000000000000000000000000000000000000000000000000000000000000000", + "0x0100000000000000000000000000000000000000000000000000000000000000": "0x0100000000000000000000000000000000000000000000000000000000000000", + "0x0200000000000000000000000000000000000000000000000000000000000000": "0x0200000000000000000000000000000000000000000000000000000000000000", + "0x0300000000000000000000000000000000000000000000000000000000000000": "0x0000000000000000000000000000000000000000000000000000000000000303" + }, + "balance": "0x2", + "nonce": "0x1" + } + }, + "number": "0x0", + "gasUsed": "0x0", + "parentHash": "0x0000000000000000000000000000000000000000000000000000000000000000", + "baseFeePerGas": "0x3b9aca00" + } + "#; + + let genesis: Genesis = serde_json::from_str(geth_genesis).unwrap(); + + // ensure the test fields are parsed correctly + assert_eq!(genesis.base_fee_per_gas, Some(U256::from(1000000000))); + assert_eq!(genesis.number, Some(U64::ZERO)); + assert_eq!(genesis.gas_used, Some(U64::ZERO)); + assert_eq!(genesis.parent_hash, Some(B256::ZERO)); + } + + #[test] + fn parse_hive_rpc_genesis_full() { + let geth_genesis = r#" + { + "config": { + "clique": { + "period": 1 + }, + "chainId": 7, + "homesteadBlock": 0, + "eip150Block": 0, + "eip155Block": 0, + "eip158Block": 0 + }, + "coinbase": "0x0000000000000000000000000000000000000000", + "difficulty": "0x020000", + "extraData": "0x0000000000000000000000000000000000000000000000000000000000000000658bdf435d810c91414ec09147daa6db624063790000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", + "gasLimit": "0x2fefd8", + "nonce": "0x0000000000000000", + "timestamp": "0x1234", + "alloc": { + "cf49fda3be353c69b41ed96333cd24302da4556f": { + "balance": "0x123450000000000000000" + }, + "0161e041aad467a890839d5b08b138c1e6373072": { + "balance": "0x123450000000000000000" + }, + "87da6a8c6e9eff15d703fc2773e32f6af8dbe301": { + "balance": "0x123450000000000000000" + }, + "b97de4b8c857e4f6bc354f226dc3249aaee49209": { + "balance": "0x123450000000000000000" + }, + "c5065c9eeebe6df2c2284d046bfc906501846c51": { + "balance": "0x123450000000000000000" + }, + "0000000000000000000000000000000000000314": { + "balance": "0x0", + "code": "0x60606040526000357c0100000000000000000000000000000000000000000000000000000000900463ffffffff168063a223e05d1461006a578063abd1a0cf1461008d578063abfced1d146100d4578063e05c914a14610110578063e6768b451461014c575b610000565b346100005761007761019d565b6040518082815260200191505060405180910390f35b34610000576100be600480803573ffffffffffffffffffffffffffffffffffffffff169060200190919050506101a3565b6040518082815260200191505060405180910390f35b346100005761010e600480803573ffffffffffffffffffffffffffffffffffffffff169060200190919080359060200190919050506101ed565b005b346100005761014a600480803590602001909190803573ffffffffffffffffffffffffffffffffffffffff16906020019091905050610236565b005b346100005761017960048080359060200190919080359060200190919080359060200190919050506103c4565b60405180848152602001838152602001828152602001935050505060405180910390f35b60005481565b6000600160008373ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff1681526020019081526020016000205490505b919050565b80600160008473ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff168152602001908152602001600020819055505b5050565b7f6031a8d62d7c95988fa262657cd92107d90ed96e08d8f867d32f26edfe85502260405180905060405180910390a17f47e2689743f14e97f7dcfa5eec10ba1dff02f83b3d1d4b9c07b206cbbda66450826040518082815260200191505060405180910390a1817fa48a6b249a5084126c3da369fbc9b16827ead8cb5cdc094b717d3f1dcd995e2960405180905060405180910390a27f7890603b316f3509577afd111710f9ebeefa15e12f72347d9dffd0d65ae3bade81604051808273ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff16815260200191505060405180910390a18073ffffffffffffffffffffffffffffffffffffffff167f7efef9ea3f60ddc038e50cccec621f86a0195894dc0520482abf8b5c6b659e4160405180905060405180910390a28181604051808381526020018273ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff1681526020019250505060405180910390a05b5050565b6000600060008585859250925092505b935093509390505600a165627a7a72305820aaf842d0d0c35c45622c5263cbb54813d2974d3999c8c38551d7c613ea2bc1170029", + "storage": { + "0x0000000000000000000000000000000000000000000000000000000000000000": "0x1234", + "0x6661e9d6d8b923d5bbaab1b96e1dd51ff6ea2a93520fdc9eb75d059238b8c5e9": "0x01" + } + }, + "0000000000000000000000000000000000000315": { + "balance": "0x9999999999999999999999999999999", + "code": "0x60606040526000357c0100000000000000000000000000000000000000000000000000000000900463ffffffff168063ef2769ca1461003e575b610000565b3461000057610078600480803573ffffffffffffffffffffffffffffffffffffffff1690602001909190803590602001909190505061007a565b005b8173ffffffffffffffffffffffffffffffffffffffff166108fc829081150290604051809050600060405180830381858888f1935050505015610106578173ffffffffffffffffffffffffffffffffffffffff167f30a3c50752f2552dcc2b93f5b96866280816a986c0c0408cb6778b9fa198288f826040518082815260200191505060405180910390a25b5b50505600a165627a7a72305820637991fabcc8abad4294bf2bb615db78fbec4edff1635a2647d3894e2daf6a610029" + } + }, + "mixHash": "0x0000000000000000000000000000000000000000000000000000000000000000", + "parentHash": "0x0000000000000000000000000000000000000000000000000000000000000000" + } + "#; + + let genesis: Genesis = serde_json::from_str(geth_genesis).unwrap(); + let alloc_entry = genesis + .alloc + .get(&Address::from_str("0000000000000000000000000000000000000314").unwrap()) + .expect("missing account for parsed genesis"); + let storage = alloc_entry.storage.as_ref().expect("missing storage for parsed genesis"); + let expected_storage = HashMap::from_iter(vec![ + ( + B256::from_str( + "0x0000000000000000000000000000000000000000000000000000000000000000", + ) + .unwrap(), + B256::from_str( + "0x0000000000000000000000000000000000000000000000000000000000001234", + ) + .unwrap(), + ), + ( + B256::from_str( + "0x6661e9d6d8b923d5bbaab1b96e1dd51ff6ea2a93520fdc9eb75d059238b8c5e9", + ) + .unwrap(), + B256::from_str( + "0x0000000000000000000000000000000000000000000000000000000000000001", + ) + .unwrap(), + ), + ]); + assert_eq!(storage, &expected_storage); + + let expected_code = Bytes::from_str("0x60606040526000357c0100000000000000000000000000000000000000000000000000000000900463ffffffff168063a223e05d1461006a578063abd1a0cf1461008d578063abfced1d146100d4578063e05c914a14610110578063e6768b451461014c575b610000565b346100005761007761019d565b6040518082815260200191505060405180910390f35b34610000576100be600480803573ffffffffffffffffffffffffffffffffffffffff169060200190919050506101a3565b6040518082815260200191505060405180910390f35b346100005761010e600480803573ffffffffffffffffffffffffffffffffffffffff169060200190919080359060200190919050506101ed565b005b346100005761014a600480803590602001909190803573ffffffffffffffffffffffffffffffffffffffff16906020019091905050610236565b005b346100005761017960048080359060200190919080359060200190919080359060200190919050506103c4565b60405180848152602001838152602001828152602001935050505060405180910390f35b60005481565b6000600160008373ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff1681526020019081526020016000205490505b919050565b80600160008473ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff168152602001908152602001600020819055505b5050565b7f6031a8d62d7c95988fa262657cd92107d90ed96e08d8f867d32f26edfe85502260405180905060405180910390a17f47e2689743f14e97f7dcfa5eec10ba1dff02f83b3d1d4b9c07b206cbbda66450826040518082815260200191505060405180910390a1817fa48a6b249a5084126c3da369fbc9b16827ead8cb5cdc094b717d3f1dcd995e2960405180905060405180910390a27f7890603b316f3509577afd111710f9ebeefa15e12f72347d9dffd0d65ae3bade81604051808273ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff16815260200191505060405180910390a18073ffffffffffffffffffffffffffffffffffffffff167f7efef9ea3f60ddc038e50cccec621f86a0195894dc0520482abf8b5c6b659e4160405180905060405180910390a28181604051808381526020018273ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff1681526020019250505060405180910390a05b5050565b6000600060008585859250925092505b935093509390505600a165627a7a72305820aaf842d0d0c35c45622c5263cbb54813d2974d3999c8c38551d7c613ea2bc1170029").unwrap(); + let code = alloc_entry.code.as_ref().expect("missing code for parsed genesis"); + assert_eq!(code, &expected_code); + } + + #[test] + fn test_hive_smoke_alloc_deserialize() { + let hive_genesis = r#" + { + "nonce": "0x0000000000000042", + "difficulty": "0x2123456", + "mixHash": "0x123456789abcdef123456789abcdef123456789abcdef123456789abcdef1234", + "coinbase": "0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "timestamp": "0x123456", + "parentHash": "0x0000000000000000000000000000000000000000000000000000000000000000", + "extraData": "0xfafbfcfd", + "gasLimit": "0x2fefd8", + "alloc": { + "dbdbdb2cbd23b783741e8d7fcf51e459b497e4a6": { + "balance": "0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff" + }, + "e6716f9544a56c530d868e4bfbacb172315bdead": { + "balance": "0x11", + "code": "0x12" + }, + "b9c015918bdaba24b4ff057a92a3873d6eb201be": { + "balance": "0x21", + "storage": { + "0x0000000000000000000000000000000000000000000000000000000000000001": "0x22" + } + }, + "1a26338f0d905e295fccb71fa9ea849ffa12aaf4": { + "balance": "0x31", + "nonce": "0x32" + }, + "0000000000000000000000000000000000000001": { + "balance": "0x41" + }, + "0000000000000000000000000000000000000002": { + "balance": "0x51" + }, + "0000000000000000000000000000000000000003": { + "balance": "0x61" + }, + "0000000000000000000000000000000000000004": { + "balance": "0x71" + } + }, + "config": { + "ethash": {}, + "chainId": 10, + "homesteadBlock": 0, + "eip150Block": 0, + "eip155Block": 0, + "eip158Block": 0, + "byzantiumBlock": 0, + "constantinopleBlock": 0, + "petersburgBlock": 0, + "istanbulBlock": 0 + } + } + "#; + + let expected_genesis = Genesis { + nonce: U64::from(0x0000000000000042), + difficulty: U256::from(0x2123456), + mix_hash: B256::from_str("0x123456789abcdef123456789abcdef123456789abcdef123456789abcdef1234").unwrap(), + coinbase: Address::from_str("0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa").unwrap(), + timestamp: U64::from(0x123456), + parent_hash: Some(B256::from_str("0x0000000000000000000000000000000000000000000000000000000000000000").unwrap()), + extra_data: Bytes::from_str("0xfafbfcfd").unwrap(), + gas_limit: U64::from(0x2fefd8), + alloc: HashMap::from_iter(vec![ + ( + Address::from_str("0xdbdbdb2cbd23b783741e8d7fcf51e459b497e4a6").unwrap(), + GenesisAccount { + balance: U256::from_str("0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff").unwrap(), + nonce: None, + code: None, + storage: None, + }, + ), + ( + Address::from_str("0xe6716f9544a56c530d868e4bfbacb172315bdead").unwrap(), + GenesisAccount { + balance: U256::from_str("0x11").unwrap(), + nonce: None, + code: Some(Bytes::from_str("0x12").unwrap()), + storage: None, + }, + ), + ( + Address::from_str("0xb9c015918bdaba24b4ff057a92a3873d6eb201be").unwrap(), + GenesisAccount { + balance: U256::from_str("0x21").unwrap(), + nonce: None, + code: None, + storage: Some(HashMap::from_iter(vec![ + ( + B256::from_str("0x0000000000000000000000000000000000000000000000000000000000000001").unwrap(), + B256::from_str("0x0000000000000000000000000000000000000000000000000000000000000022").unwrap(), + ), + ])), + }, + ), + ( + Address::from_str("0x1a26338f0d905e295fccb71fa9ea849ffa12aaf4").unwrap(), + GenesisAccount { + balance: U256::from_str("0x31").unwrap(), + nonce: Some(0x32u64), + code: None, + storage: None, + }, + ), + ( + Address::from_str("0x0000000000000000000000000000000000000001").unwrap(), + GenesisAccount { + balance: U256::from_str("0x41").unwrap(), + nonce: None, + code: None, + storage: None, + }, + ), + ( + Address::from_str("0x0000000000000000000000000000000000000002").unwrap(), + GenesisAccount { + balance: U256::from_str("0x51").unwrap(), + nonce: None, + code: None, + storage: None, + }, + ), + ( + Address::from_str("0x0000000000000000000000000000000000000003").unwrap(), + GenesisAccount { + balance: U256::from_str("0x61").unwrap(), + nonce: None, + code: None, + storage: None, + }, + ), + ( + Address::from_str("0x0000000000000000000000000000000000000004").unwrap(), + GenesisAccount { + balance: U256::from_str("0x71").unwrap(), + nonce: None, + code: None, + storage: None, + }, + ), + ]), + config: ChainConfig { + ethash: Some(EthashConfig{}), + chain_id: 10, + homestead_block: Some(0), + eip150_block: Some(0), + eip155_block: Some(0), + eip158_block: Some(0), + byzantium_block: Some(0), + constantinople_block: Some(0), + petersburg_block: Some(0), + istanbul_block: Some(0), + ..Default::default() + }, + ..Default::default() + }; + + let deserialized_genesis: Genesis = serde_json::from_str(hive_genesis).unwrap(); + assert_eq!(deserialized_genesis, expected_genesis, "deserialized genesis {deserialized_genesis:#?} does not match expected {expected_genesis:#?}"); + } +} diff --git a/crates/node-bindings/src/geth.rs b/crates/node-bindings/src/geth.rs new file mode 100644 index 00000000000..41d802c53bb --- /dev/null +++ b/crates/node-bindings/src/geth.rs @@ -0,0 +1,680 @@ +//! Utilities for launching a go-ethereum dev-mode instance. + +use crate::{unused_port, CliqueConfig, Genesis}; +use alloy_primitives::{hex, Address, Bytes, B256}; +use k256::ecdsa::SigningKey; +use std::{ + borrow::Cow, + fs::{create_dir, File}, + io::{BufRead, BufReader}, + net::SocketAddr, + path::PathBuf, + process::{Child, ChildStderr, Command, Stdio}, + time::{Duration, Instant}, +}; +use tempfile::tempdir; + +/// How long we will wait for geth to indicate that it is ready. +const GETH_STARTUP_TIMEOUT: Duration = Duration::from_secs(10); + +/// Timeout for waiting for geth to add a peer. +const GETH_DIAL_LOOP_TIMEOUT: Duration = Duration::from_secs(20); + +/// The exposed APIs +const API: &str = "eth,net,web3,txpool,admin,personal,miner,debug"; + +/// The geth command +const GETH: &str = "geth"; + +/// Errors that can occur when working with the [`GethInstance`]. +#[derive(Debug)] +pub enum GethInstanceError { + /// Timed out waiting for a message from geth's stderr. + Timeout(String), + + /// A line could not be read from the geth stderr. + ReadLineError(std::io::Error), + + /// The child geth process's stderr was not captured. + NoStderr, +} + +/// A geth instance. Will close the instance when dropped. +/// +/// Construct this using [`Geth`]. +#[derive(Debug)] +pub struct GethInstance { + pid: Child, + port: u16, + ipc: Option, + data_dir: Option, + p2p_port: Option, + genesis: Option, + clique_private_key: Option, +} + +impl GethInstance { + /// Returns the port of this instance + pub fn port(&self) -> u16 { + self.port + } + + /// Returns the p2p port of this instance + pub fn p2p_port(&self) -> Option { + self.p2p_port + } + + /// Returns the HTTP endpoint of this instance + pub fn endpoint(&self) -> String { + format!("http://localhost:{}", self.port) + } + + /// Returns the Websocket endpoint of this instance + pub fn ws_endpoint(&self) -> String { + format!("ws://localhost:{}", self.port) + } + + /// Returns the path to this instances' IPC socket + pub fn ipc_path(&self) -> &Option { + &self.ipc + } + + /// Returns the path to this instances' data directory + pub fn data_dir(&self) -> &Option { + &self.data_dir + } + + /// Returns the genesis configuration used to configure this instance + pub fn genesis(&self) -> &Option { + &self.genesis + } + + /// Returns the private key used to configure clique on this instance + pub fn clique_private_key(&self) -> &Option { + &self.clique_private_key + } + + /// Takes the stderr contained in the child process. + /// + /// This leaves a `None` in its place, so calling methods that require a stderr to be present + /// will fail if called after this. + pub fn stderr(&mut self) -> Result { + self.pid.stderr.take().ok_or(GethInstanceError::NoStderr) + } + + /// Blocks until geth adds the specified peer, using 20s as the timeout. + /// + /// Requires the stderr to be present in the `GethInstance`. + pub fn wait_to_add_peer(&mut self, id: B256) -> Result<(), GethInstanceError> { + let mut stderr = self.pid.stderr.as_mut().ok_or(GethInstanceError::NoStderr)?; + let mut err_reader = BufReader::new(&mut stderr); + let mut line = String::new(); + let start = Instant::now(); + + while start.elapsed() < GETH_DIAL_LOOP_TIMEOUT { + line.clear(); + err_reader.read_line(&mut line).map_err(GethInstanceError::ReadLineError)?; + + // geth ids are trunated + let truncated_id = hex::encode(&id.0[..8]); + if line.contains("Adding p2p peer") && line.contains(&truncated_id) { + return Ok(()); + } + } + Err(GethInstanceError::Timeout("Timed out waiting for geth to add a peer".into())) + } +} + +impl Drop for GethInstance { + fn drop(&mut self) { + self.pid.kill().expect("could not kill geth"); + } +} + +/// Whether or not geth is in `dev` mode and configuration options that depend on the mode. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum GethMode { + /// Options that can be set in dev mode + Dev(DevOptions), + /// Options that cannot be set in dev mode + NonDev(PrivateNetOptions), +} + +impl Default for GethMode { + fn default() -> Self { + Self::Dev(Default::default()) + } +} + +/// Configuration options that can be set in dev mode. +#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)] +pub struct DevOptions { + /// The interval at which the dev chain will mine new blocks. + pub block_time: Option, +} + +/// Configuration options that cannot be set in dev mode. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub struct PrivateNetOptions { + /// The p2p port to use. + pub p2p_port: Option, + + /// Whether or not peer discovery is enabled. + pub discovery: bool, +} + +impl Default for PrivateNetOptions { + fn default() -> Self { + Self { p2p_port: None, discovery: true } + } +} + +/// Builder for launching `geth`. +/// +/// # Panics +/// +/// If `spawn` is called without `geth` being available in the user's $PATH +/// +/// # Example +/// +/// ```no_run +/// use alloy_node_bindings::Geth; +/// +/// let port = 8545u16; +/// let url = format!("http://localhost:{}", port).to_string(); +/// +/// let geth = Geth::new().port(port).block_time(5000u64).spawn(); +/// +/// drop(geth); // this will kill the instance +/// ``` +#[derive(Clone, Debug, Default)] +#[must_use = "This Builder struct does nothing unless it is `spawn`ed"] +pub struct Geth { + program: Option, + port: Option, + authrpc_port: Option, + ipc_path: Option, + data_dir: Option, + chain_id: Option, + insecure_unlock: bool, + genesis: Option, + mode: GethMode, + clique_private_key: Option, +} + +impl Geth { + /// Creates an empty Geth builder. + /// + /// The mnemonic is chosen randomly. + pub fn new() -> Self { + Self::default() + } + + /// Creates a Geth builder which will execute `geth` at the given path. + /// + /// # Example + /// + /// ``` + /// use alloy_node_bindings::Geth; + /// # fn a() { + /// let geth = Geth::at("../go-ethereum/build/bin/geth").spawn(); + /// + /// println!("Geth running at `{}`", geth.endpoint()); + /// # } + /// ``` + pub fn at(path: impl Into) -> Self { + Self::new().path(path) + } + + /// Returns whether the node is launched in Clique consensus mode. + pub fn is_clique(&self) -> bool { + self.clique_private_key.is_some() + } + + /// Calculates the address of the Clique consensus address. + pub fn clique_address(&self) -> Option
{ + self.clique_private_key.as_ref().map(|pk| Address::from_public_key(pk.verifying_key())) + } + + /// Sets the `path` to the `geth` executable + /// + /// By default, it's expected that `geth` is in `$PATH`, see also + /// [`std::process::Command::new()`] + pub fn path>(mut self, path: T) -> Self { + self.program = Some(path.into()); + self + } + + /// Sets the Clique Private Key to the `geth` executable, which will be later + /// loaded on the node. + /// + /// The address derived from this private key will be used to set the `miner.etherbase` field + /// on the node. + pub fn set_clique_private_key>(mut self, private_key: T) -> Self { + self.clique_private_key = Some(private_key.into()); + self + } + + /// Sets the port which will be used when the `geth-cli` instance is launched. + /// + /// If port is 0 then the OS will choose a random port. + /// [GethInstance::port] will return the port that was chosen. + pub fn port>(mut self, port: T) -> Self { + self.port = Some(port.into()); + self + } + + /// Sets the port which will be used for incoming p2p connections. + /// + /// This will put the geth instance into non-dev mode, discarding any previously set dev-mode + /// options. + pub fn p2p_port(mut self, port: u16) -> Self { + match self.mode { + GethMode::Dev(_) => { + self.mode = GethMode::NonDev(PrivateNetOptions { + p2p_port: Some(port), + ..Default::default() + }) + } + GethMode::NonDev(ref mut opts) => opts.p2p_port = Some(port), + } + self + } + + /// Sets the block-time which will be used when the `geth-cli` instance is launched. + /// + /// This will put the geth instance in `dev` mode, discarding any previously set options that + /// cannot be used in dev mode. + pub fn block_time>(mut self, block_time: T) -> Self { + self.mode = GethMode::Dev(DevOptions { block_time: Some(block_time.into()) }); + self + } + + /// Sets the chain id for the geth instance. + pub fn chain_id>(mut self, chain_id: T) -> Self { + self.chain_id = Some(chain_id.into()); + self + } + + /// Allow geth to unlock accounts when rpc apis are open. + pub fn insecure_unlock(mut self) -> Self { + self.insecure_unlock = true; + self + } + + /// Disable discovery for the geth instance. + /// + /// This will put the geth instance into non-dev mode, discarding any previously set dev-mode + /// options. + pub fn disable_discovery(mut self) -> Self { + self.inner_disable_discovery(); + self + } + + fn inner_disable_discovery(&mut self) { + match self.mode { + GethMode::Dev(_) => { + self.mode = + GethMode::NonDev(PrivateNetOptions { discovery: false, ..Default::default() }) + } + GethMode::NonDev(ref mut opts) => opts.discovery = false, + } + } + + /// Manually sets the IPC path for the socket manually. + pub fn ipc_path>(mut self, path: T) -> Self { + self.ipc_path = Some(path.into()); + self + } + + /// Sets the data directory for geth. + pub fn data_dir>(mut self, path: T) -> Self { + self.data_dir = Some(path.into()); + self + } + + /// Sets the `genesis.json` for the geth instance. + /// + /// If this is set, geth will be initialized with `geth init` and the `--datadir` option will be + /// set to the same value as `data_dir`. + /// + /// This is destructive and will overwrite any existing data in the data directory. + pub fn genesis(mut self, genesis: Genesis) -> Self { + self.genesis = Some(genesis); + self + } + + /// Sets the port for authenticated RPC connections. + pub fn authrpc_port(mut self, port: u16) -> Self { + self.authrpc_port = Some(port); + self + } + + /// Consumes the builder and spawns `geth`. + /// + /// # Panics + /// + /// If spawning the instance fails at any point. + #[track_caller] + pub fn spawn(mut self) -> GethInstance { + let bin_path = match self.program.as_ref() { + Some(bin) => bin.as_os_str(), + None => GETH.as_ref(), + } + .to_os_string(); + let mut cmd = Command::new(&bin_path); + // geth uses stderr for its logs + cmd.stderr(Stdio::piped()); + + // If no port provided, let the os chose it for us + let mut port = self.port.unwrap_or(0); + let port_s = port.to_string(); + + // Open the HTTP API + cmd.arg("--http"); + cmd.arg("--http.port").arg(&port_s); + cmd.arg("--http.api").arg(API); + + // Open the WS API + cmd.arg("--ws"); + cmd.arg("--ws.port").arg(port_s); + cmd.arg("--ws.api").arg(API); + + // pass insecure unlock flag if set + let is_clique = self.is_clique(); + if self.insecure_unlock || is_clique { + cmd.arg("--allow-insecure-unlock"); + } + + if is_clique { + self.inner_disable_discovery(); + } + + // Set the port for authenticated APIs + let authrpc_port = self.authrpc_port.unwrap_or_else(&mut unused_port); + cmd.arg("--authrpc.port").arg(authrpc_port.to_string()); + + // use geth init to initialize the datadir if the genesis exists + if is_clique { + let clique_addr = self.clique_address(); + if let Some(genesis) = &mut self.genesis { + // set up a clique config with an instant sealing period and short (8 block) epoch + let clique_config = CliqueConfig { period: Some(0), epoch: Some(8) }; + genesis.config.clique = Some(clique_config); + + let clique_addr = clique_addr.expect("is_clique == true"); + + // set the extraData field + let extra_data_bytes = + [&[0u8; 32][..], clique_addr.as_ref(), &[0u8; 65][..]].concat(); + let extra_data = Bytes::from(extra_data_bytes); + genesis.extra_data = extra_data; + + // we must set the etherbase if using clique + // need to use format! / Debug here because the Address Display impl doesn't show + // the entire address + cmd.arg("--miner.etherbase").arg(format!("{clique_addr:?}")); + } + + let clique_addr = self.clique_address().expect("is_clique == true"); + + self.genesis = Some(Genesis::new( + self.chain_id.expect("chain id must be set in clique mode"), + clique_addr, + )); + + // we must set the etherbase if using clique + // need to use format! / Debug here because the Address Display impl doesn't show the + // entire address + cmd.arg("--miner.etherbase").arg(format!("{clique_addr:?}")); + } + + if let Some(ref genesis) = self.genesis { + // create a temp dir to store the genesis file + let temp_genesis_dir_path = + tempdir().expect("should be able to create temp dir for genesis init").into_path(); + + // create a temp dir to store the genesis file + let temp_genesis_path = temp_genesis_dir_path.join("genesis.json"); + + // create the genesis file + let mut file = File::create(&temp_genesis_path).expect("could not create genesis file"); + + // serialize genesis and write to file + serde_json::to_writer_pretty(&mut file, &genesis) + .expect("could not write genesis to file"); + + let mut init_cmd = Command::new(bin_path); + if let Some(ref data_dir) = self.data_dir { + init_cmd.arg("--datadir").arg(data_dir); + } + + // set the stderr to null so we don't pollute the test output + init_cmd.stderr(Stdio::null()); + + init_cmd.arg("init").arg(temp_genesis_path); + let res = init_cmd + .spawn() + .expect("failed to spawn geth init") + .wait() + .expect("failed to wait for geth init to exit"); + if !res.success() { + panic!("geth init failed"); + } + + // clean up the temp dir which is now persisted + std::fs::remove_dir_all(temp_genesis_dir_path) + .expect("could not remove genesis temp dir"); + } + + if let Some(ref data_dir) = self.data_dir { + cmd.arg("--datadir").arg(data_dir); + + // create the directory if it doesn't exist + if !data_dir.exists() { + create_dir(data_dir).expect("could not create data dir"); + } + } + + // Dev mode with custom block time + let mut p2p_port = match self.mode { + GethMode::Dev(DevOptions { block_time }) => { + cmd.arg("--dev"); + if let Some(block_time) = block_time { + cmd.arg("--dev.period").arg(block_time.to_string()); + } + None + } + GethMode::NonDev(PrivateNetOptions { p2p_port, discovery }) => { + // if no port provided, let the os chose it for us + let port = p2p_port.unwrap_or(0); + cmd.arg("--port").arg(port.to_string()); + + // disable discovery if the flag is set + if !discovery { + cmd.arg("--nodiscover"); + } + Some(port) + } + }; + + if let Some(chain_id) = self.chain_id { + cmd.arg("--networkid").arg(chain_id.to_string()); + } + + // debug verbosity is needed to check when peers are added + cmd.arg("--verbosity").arg("4"); + + if let Some(ref ipc) = self.ipc_path { + cmd.arg("--ipcpath").arg(ipc); + } + + let mut child = cmd.spawn().expect("couldnt start geth"); + + let stderr = child.stderr.expect("Unable to get stderr for geth child process"); + + let start = Instant::now(); + let mut reader = BufReader::new(stderr); + + // we shouldn't need to wait for p2p to start if geth is in dev mode - p2p is disabled in + // dev mode + let mut p2p_started = matches!(self.mode, GethMode::Dev(_)); + let mut http_started = false; + + loop { + if start + GETH_STARTUP_TIMEOUT <= Instant::now() { + panic!("Timed out waiting for geth to start. Is geth installed?") + } + + let mut line = String::with_capacity(120); + reader.read_line(&mut line).expect("Failed to read line from geth process"); + + if matches!(self.mode, GethMode::NonDev(_)) && line.contains("Started P2P networking") { + p2p_started = true; + } + + if !matches!(self.mode, GethMode::Dev(_)) { + // try to find the p2p port, if not in dev mode + if line.contains("New local node record") { + if let Some(port) = extract_value("tcp=", &line) { + p2p_port = port.parse::().ok(); + } + } + } + + // geth 1.9.23 uses "server started" while 1.9.18 uses "endpoint opened" + // the unauthenticated api is used for regular non-engine API requests + if line.contains("HTTP endpoint opened") + || (line.contains("HTTP server started") && !line.contains("auth=true")) + { + // Extracts the address from the output + if let Some(addr) = extract_endpoint(&line) { + // use the actual http port + port = addr.port(); + } + + http_started = true; + } + + // Encountered an error such as Fatal: Error starting protocol stack: listen tcp + // 127.0.0.1:8545: bind: address already in use + if line.contains("Fatal:") { + panic!("{line}"); + } + + if p2p_started && http_started { + break; + } + } + + child.stderr = Some(reader.into_inner()); + + GethInstance { + pid: child, + port, + ipc: self.ipc_path, + data_dir: self.data_dir, + p2p_port, + genesis: self.genesis, + clique_private_key: self.clique_private_key, + } + } +} + +// extracts the value for the given key and line +fn extract_value<'a>(key: &str, line: &'a str) -> Option<&'a str> { + let mut key = Cow::from(key); + if !key.ends_with('=') { + key = Cow::from(format!("{}=", key)); + } + line.find(key.as_ref()).map(|pos| { + let start = pos + key.len(); + let end = line[start..].find(' ').map(|i| start + i).unwrap_or(line.len()); + line[start..end].trim() + }) +} + +// extracts the value for the given key and line +fn extract_endpoint(line: &str) -> Option { + let val = extract_value("endpoint=", line)?; + val.parse::().ok() +} + +// These tests should use a different datadir for each `Geth` spawned +#[cfg(test)] +mod tests { + use super::*; + use std::path::Path; + + #[test] + fn test_extract_address() { + let line = "INFO [07-01|13:20:42.774] HTTP server started endpoint=127.0.0.1:8545 auth=false prefix= cors= vhosts=localhost"; + assert_eq!(extract_endpoint(line), Some(SocketAddr::from(([127, 0, 0, 1], 8545)))); + } + + #[test] + fn port_0() { + run_with_tempdir(|_| { + let _geth = Geth::new().disable_discovery().port(0u16).spawn(); + }); + } + + /// Allows running tests with a temporary directory, which is cleaned up after the function is + /// called. + /// + /// Helps with tests that spawn a helper instance, which has to be dropped before the temporary + /// directory is cleaned up. + #[track_caller] + fn run_with_tempdir(f: impl Fn(&Path)) { + let temp_dir = tempfile::tempdir().unwrap(); + let temp_dir_path = temp_dir.path(); + f(temp_dir_path); + #[cfg(not(windows))] + temp_dir.close().unwrap(); + } + + #[test] + fn p2p_port() { + run_with_tempdir(|temp_dir_path| { + let geth = Geth::new().disable_discovery().data_dir(temp_dir_path).spawn(); + let p2p_port = geth.p2p_port(); + assert!(p2p_port.is_some()); + }); + } + + #[test] + fn explicit_p2p_port() { + run_with_tempdir(|temp_dir_path| { + // if a p2p port is explicitly set, it should be used + let geth = Geth::new().p2p_port(1234).data_dir(temp_dir_path).spawn(); + let p2p_port = geth.p2p_port(); + assert_eq!(p2p_port, Some(1234)); + }); + } + + #[test] + fn dev_mode() { + run_with_tempdir(|temp_dir_path| { + // dev mode should not have a p2p port, and dev should be the default + let geth = Geth::new().data_dir(temp_dir_path).spawn(); + let p2p_port = geth.p2p_port(); + assert!(p2p_port.is_none(), "{p2p_port:?}"); + }) + } + + #[test] + fn clique_correctly_configured() { + run_with_tempdir(|temp_dir_path| { + let private_key = SigningKey::random(&mut rand::thread_rng()); + let geth = Geth::new() + .set_clique_private_key(private_key) + .chain_id(1337u64) + .data_dir(temp_dir_path) + .spawn(); + + assert!(geth.p2p_port.is_some()); + assert!(geth.clique_private_key().is_some()); + assert!(geth.genesis().is_some()); + }) + } +} diff --git a/crates/node-bindings/src/lib.rs b/crates/node-bindings/src/lib.rs new file mode 100644 index 00000000000..3947783acf9 --- /dev/null +++ b/crates/node-bindings/src/lib.rs @@ -0,0 +1,76 @@ +#![doc = include_str!("../README.md")] +#![doc( + html_logo_url = "https://raw.githubusercontent.com/alloy-rs/core/main/assets/alloy.jpg", + html_favicon_url = "https://raw.githubusercontent.com/alloy-rs/core/main/assets/favicon.ico" +)] +#![warn( + missing_copy_implementations, + missing_debug_implementations, + missing_docs, + unreachable_pub, + rustdoc::all +)] +#![cfg_attr(not(test), warn(unused_crate_dependencies))] +#![deny(unused_must_use, rust_2018_idioms)] +#![cfg_attr(docsrs, feature(doc_cfg, doc_auto_cfg))] + +use alloy_primitives::{Bytes, B256, U256}; +use serde::Deserializer; + +pub mod anvil; +pub use anvil::{Anvil, AnvilInstance}; + +pub mod geth; +pub use geth::{Geth, GethInstance}; + +mod genesis; +pub use genesis::{ChainConfig, CliqueConfig, EthashConfig, Genesis, GenesisAccount}; + +pub mod serde_helpers; + +/// 1 Ether = 1e18 Wei == 0x0de0b6b3a7640000 Wei +pub const WEI_IN_ETHER: U256 = U256::from_limbs([0x0de0b6b3a7640000, 0x0, 0x0, 0x0]); + +/// The number of blocks from the past for which the fee rewards are fetched for fee estimation. +pub const EIP1559_FEE_ESTIMATION_PAST_BLOCKS: u64 = 10; +/// The default percentile of gas premiums that are fetched for fee estimation. +pub const EIP1559_FEE_ESTIMATION_REWARD_PERCENTILE: f64 = 5.0; +/// The default max priority fee per gas, used in case the base fee is within a threshold. +pub const EIP1559_FEE_ESTIMATION_DEFAULT_PRIORITY_FEE: u64 = 3_000_000_000; +/// The threshold for base fee below which we use the default priority fee, and beyond which we +/// estimate an appropriate value for priority fee. +pub const EIP1559_FEE_ESTIMATION_PRIORITY_FEE_TRIGGER: u64 = 100_000_000_000; +/// The threshold max change/difference (in %) at which we will ignore the fee history values +/// under it. +pub const EIP1559_FEE_ESTIMATION_THRESHOLD_MAX_CHANGE: i64 = 200; + +/// Converts a Bytes value into a B256, accepting inputs that are less than 32 bytes long. These +/// inputs will be left padded with zeros. +pub fn from_bytes_to_h256<'de, D>(bytes: Bytes) -> Result +where + D: Deserializer<'de>, +{ + if bytes.0.len() > 32 { + return Err(serde::de::Error::custom("input too long to be a B256")); + } + + // left pad with zeros to 32 bytes + let mut padded = [0u8; 32]; + padded[32 - bytes.0.len()..].copy_from_slice(&bytes.0); + + // then convert to B256 without a panic + Ok(B256::from_slice(&padded)) +} + +/// A bit of hack to find an unused TCP port. +/// +/// Does not guarantee that the given port is unused after the function exists, just that it was +/// unused before the function started (i.e., it does not reserve a port). +fn unused_port() -> u16 { + let listener = std::net::TcpListener::bind("127.0.0.1:0") + .expect("Failed to create TCP listener to find unused port"); + + let local_addr = + listener.local_addr().expect("Failed to read TCP listener local_addr to find unused port"); + local_addr.port() +} diff --git a/crates/node-bindings/src/serde_helpers.rs b/crates/node-bindings/src/serde_helpers.rs new file mode 100644 index 00000000000..d647b003601 --- /dev/null +++ b/crates/node-bindings/src/serde_helpers.rs @@ -0,0 +1,65 @@ +//! Some convenient serde helpers + +use alloy_primitives::U256; +use serde::{Deserialize, Deserializer}; + +/// Supports parsing u64 +/// +/// See +pub fn deserialize_stringified_u64<'de, D>(deserializer: D) -> Result +where + D: Deserializer<'de>, +{ + let num = U256::deserialize(deserializer)?; + num.try_into().map_err(serde::de::Error::custom) +} + +/// Supports parsing u64 +/// +/// See +pub fn deserialize_stringified_u64_opt<'de, D>(deserializer: D) -> Result, D::Error> +where + D: Deserializer<'de>, +{ + if let Some(num) = Option::::deserialize(deserializer)? { + num.try_into().map(Some).map_err(serde::de::Error::custom) + } else { + Ok(None) + } +} + +/// Helper type to deserialize sequence of numbers +#[derive(Deserialize)] +#[serde(untagged)] +enum NumericSeq { + Seq([U256; 1]), + U256(U256), + Num(u64), +} + +/// Deserializes a number from hex or int +pub fn deserialize_number<'de, D>(deserializer: D) -> Result +where + D: Deserializer<'de>, +{ + U256::deserialize(deserializer) +} + +/// Deserializes a number from hex or int, but optionally +pub fn deserialize_number_opt<'de, D>(deserializer: D) -> Result, D::Error> +where + D: Deserializer<'de>, +{ + Option::::deserialize(deserializer) +} + +/// Deserializes single integer params: `1, [1], ["0x01"]` +pub fn deserialize_number_seq<'de, D>(deserializer: D) -> Result +where + D: Deserializer<'de>, +{ + Ok(match NumericSeq::deserialize(deserializer)? { + NumericSeq::Seq([n]) | NumericSeq::U256(n) => n, + NumericSeq::Num(n) => U256::from(n), + }) +} diff --git a/crates/providers/Cargo.toml b/crates/providers/Cargo.toml index 8fcdfc9369f..24aceeaa6f1 100644 --- a/crates/providers/Cargo.toml +++ b/crates/providers/Cargo.toml @@ -26,8 +26,8 @@ reqwest.workspace = true auto_impl = "1.1.0" [dev-dependencies] +alloy-node-bindings.workspace = true tokio = { version = "1.33.0", features = ["macros"] } -ethers-core = "2.0.10" [features] anvil = [] diff --git a/crates/providers/src/provider.rs b/crates/providers/src/provider.rs index 4f175885d3b..84e671f5317 100644 --- a/crates/providers/src/provider.rs +++ b/crates/providers/src/provider.rs @@ -516,14 +516,14 @@ impl<'a> TryFrom<&'a String> for Provider> { } #[cfg(test)] -mod providers_test { +mod tests { use crate::{ provider::{Provider, TempProvider}, utils, }; + use alloy_node_bindings::Anvil; use alloy_primitives::{address, b256, bytes, U256, U64}; use alloy_rpc_types::{Block, BlockNumberOrTag, Filter}; - use ethers_core::utils::Anvil; #[tokio::test] async fn gets_block_number() { diff --git a/crates/rpc-client/Cargo.toml b/crates/rpc-client/Cargo.toml index 1b822c6afe6..5d62220fd98 100644 --- a/crates/rpc-client/Cargo.toml +++ b/crates/rpc-client/Cargo.toml @@ -35,12 +35,13 @@ alloy-transport-ipc = { workspace = true, optional = true } [dev-dependencies] alloy-primitives.workspace = true +alloy-node-bindings.workspace = true +alloy-transport-ipc = { workspace = true, features = ["mock"] } alloy-transport-ws.workspace = true + +tempfile = "3" test-log = { version = "0.2.13", default-features = false, features = ["trace"] } tracing-subscriber = { version = "0.3.17", features = ["std", "env-filter"] } -ethers-core = "2.0.10" -alloy-transport-ipc = { workspace = true, features = ["mock"] } -tempfile = "3" [features] default = ["reqwest"] @@ -48,4 +49,4 @@ reqwest = ["dep:url", "dep:reqwest", "alloy-transport-http/reqwest"] hyper = ["dep:url", "dep:hyper", "alloy-transport-http/hyper"] pubsub = ["dep:tokio", "dep:alloy-pubsub", "dep:alloy-primitives"] ws = ["pubsub", "dep:alloy-transport-ws"] -ipc = ["pubsub", "dep:alloy-transport-ipc"] \ No newline at end of file +ipc = ["pubsub", "dep:alloy-transport-ipc"] diff --git a/crates/rpc-client/tests/it/ipc.rs b/crates/rpc-client/tests/it/ipc.rs index 6de72a1459f..53de95b909b 100644 --- a/crates/rpc-client/tests/it/ipc.rs +++ b/crates/rpc-client/tests/it/ipc.rs @@ -1,8 +1,8 @@ +use alloy_node_bindings::{Geth, GethInstance}; use alloy_primitives::U64; use alloy_pubsub::PubSubFrontend; use alloy_rpc_client::{ClientBuilder, RpcCall, RpcClient}; use alloy_transport_ipc::IpcConnect; -use ethers_core::utils::{Geth, GethInstance}; use std::borrow::Cow; use tempfile::NamedTempFile;