feat(providers): add a subset of admin namespace (#1880)
* tell Geth to expose the admin namespace * wip: add admin namespace support * add networking and peer related structs * add rest of chain config fields * add datadir to geth * fix data dir ref * add dev flag to geth * set dev only if block_time is not set * put mutually exclusive options in an enum * make block_time use the devmode enum * add p2p port to geth * add basic impls for admin endpoints * move from_int_or_hex to ethers-core utils * fix nodeinfo protocol field * the type is better represented by a struct which can have either eth or snap * add chain id and discovery toggle for Geth * remove PeerEvent * should re-add when peer event endpoints are implemented * simplify serde options for admin responses * change signature for peer modification apis * these admin apis accept an enode, which _may_ be an enr, but could also be a legacy enode "v4" url. * add note on where `ChainConfig` fields come from * add note on PeerInfo about the source of fields * add admin namespace support to CHANGELOG * update pr number in changelog * cargo fmt * move chainconfig to genesis in utils * accept genesis file in geth * add genesis writing to geth spawn * finally get geth nodes to connect * import io::Read in provider tests * fix PeerInfo and use enode id for provider test * make clippy happy * improve documentation for genesis module * remove not(wasm) attributes on genesis module * remove debugging printlns * remove io::Read from provider tests * add failing post merge test case * add full mainnet nodeinfo for testing * support deserializing json bignums to U256 * the serde_json arbitrary_precision feature is necessary so the serde_json::Number variant of `IntOrHexOrBigNum` can be converted into a string and fed into U256::from_dec_string * fix from_int_or_hex_opt doc string * remove third variant from IntOrHex * unnecessary since serde_json::Number handles smaller ints as well * add comments for ids * fix enode id type in admin_peers provider test * fix admin typo in Cargo.toml Co-authored-by: Georgios Konstantopoulos <me@gakonst.com> * add method to wait on a gethinstance adding a peer * fix dial loop and wait_to_add_peer doc comments * update geth * the build can be updated by changing the GETH_BUILD env var * wait for geth to exit on drop * remove unnecessary wait * fix mid-handshake PeerInfo deserialization * remove println * make tests less flaky * handle discovery with the rest of the non dev opts * dump geth stderr to debug failing ci test * add method which dumps the unread stderr of the geth instance into a string * remove call_raw debug println * bug is due to authrpc endpoint being in use * use unused port when authrpc port is not specified * remove dump_stderr from GethInstance * did not work properly anyways Co-authored-by: Georgios Konstantopoulos <me@gakonst.com>
This commit is contained in:
parent
2d793edc94
commit
4b685621ed
|
@ -13,6 +13,7 @@ env:
|
|||
ETHERSCAN_API_KEY_ETHEREUM: I5BXNZYP5GEDWFINGVEZKYIVU2695NPQZB
|
||||
ETHERSCAN_API_KEY_CELO: B13XSMUT6Q3Q4WZ5DNQR8RXDBA2KNTMT4M
|
||||
GOERLI_PRIVATE_KEY: "fa4a1a79e869a96fcb42727f75e3232d6865a82ea675bb95de967a7fe6a773b2"
|
||||
GETH_BUILD: 1.10.26-e5eb32ac
|
||||
|
||||
jobs:
|
||||
tests:
|
||||
|
@ -36,9 +37,9 @@ jobs:
|
|||
- name: Install geth
|
||||
run: |
|
||||
mkdir -p "$HOME/bin"
|
||||
wget -q https://gethstore.blob.core.windows.net/builds/geth-linux-amd64-1.9.23-8c2f2715.tar.gz
|
||||
tar -xvf geth-linux-amd64-1.9.23-8c2f2715.tar.gz
|
||||
mv geth-linux-amd64-1.9.23-8c2f2715/geth $HOME/bin/geth
|
||||
wget -q https://gethstore.blob.core.windows.net/builds/geth-linux-amd64-$GETH_BUILD.tar.gz
|
||||
tar -xvf geth-linux-amd64-$GETH_BUILD.tar.gz
|
||||
mv geth-linux-amd64-$GETH_BUILD/geth $HOME/bin/geth
|
||||
chmod u+x "$HOME/bin/geth"
|
||||
export PATH=$HOME/bin:$PATH
|
||||
geth version
|
||||
|
@ -79,9 +80,9 @@ jobs:
|
|||
- name: Install geth
|
||||
run: |
|
||||
mkdir -p "$HOME/bin"
|
||||
wget -q https://gethstore.blob.core.windows.net/builds/geth-linux-amd64-1.9.23-8c2f2715.tar.gz
|
||||
tar -xvf geth-linux-amd64-1.9.23-8c2f2715.tar.gz
|
||||
mv geth-linux-amd64-1.9.23-8c2f2715/geth $HOME/bin/geth
|
||||
wget -q https://gethstore.blob.core.windows.net/builds/geth-linux-amd64-$GETH_BUILD.tar.gz
|
||||
tar -xvf geth-linux-amd64-$GETH_BUILD.tar.gz
|
||||
mv geth-linux-amd64-$GETH_BUILD/geth $HOME/bin/geth
|
||||
chmod u+x "$HOME/bin/geth"
|
||||
export PATH=$HOME/bin:$PATH
|
||||
geth version
|
||||
|
@ -201,9 +202,9 @@ jobs:
|
|||
- name: Install geth (for state overrides example)
|
||||
run: |
|
||||
mkdir -p "$HOME/bin"
|
||||
wget -q https://gethstore.blob.core.windows.net/builds/geth-linux-amd64-1.9.23-8c2f2715.tar.gz
|
||||
tar -xvf geth-linux-amd64-1.9.23-8c2f2715.tar.gz
|
||||
mv geth-linux-amd64-1.9.23-8c2f2715/geth $HOME/bin/geth
|
||||
wget -q https://gethstore.blob.core.windows.net/builds/geth-linux-amd64-$GETH_BUILD.tar.gz
|
||||
tar -xvf geth-linux-amd64-$GETH_BUILD.tar.gz
|
||||
mv geth-linux-amd64-$GETH_BUILD/geth $HOME/bin/geth
|
||||
chmod u+x "$HOME/bin/geth"
|
||||
export PATH=$HOME/bin:$PATH
|
||||
geth version
|
||||
|
|
|
@ -221,6 +221,8 @@
|
|||
|
||||
### Unreleased
|
||||
|
||||
- Add a subset of the `admin` namespace
|
||||
[1880](https://github.com/gakonst/ethers-rs/pull/1880)
|
||||
- Return String for net version
|
||||
[1376](https://github.com/gakonst/ethers-rs/pull/1376)
|
||||
- Stream of paginated logs that load logs in small pages
|
||||
|
|
|
@ -1185,6 +1185,25 @@ dependencies = [
|
|||
"cfg-if 1.0.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "enr"
|
||||
version = "0.7.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "492a7e5fc2504d5fdce8e124d3e263b244a68b283cac67a69eda0cd43e0aebad"
|
||||
dependencies = [
|
||||
"base64 0.13.1",
|
||||
"bs58",
|
||||
"bytes",
|
||||
"hex",
|
||||
"k256",
|
||||
"log",
|
||||
"rand 0.8.5",
|
||||
"rlp",
|
||||
"serde",
|
||||
"sha3",
|
||||
"zeroize",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "env_logger"
|
||||
version = "0.10.0"
|
||||
|
@ -1479,6 +1498,7 @@ dependencies = [
|
|||
"auto_impl 1.0.1",
|
||||
"base64 0.13.1",
|
||||
"bytes",
|
||||
"enr",
|
||||
"ethers-core",
|
||||
"futures-channel",
|
||||
"futures-core",
|
||||
|
|
|
@ -27,7 +27,7 @@ tiny-keccak = { version = "2.0.2", default-features = false }
|
|||
# misc
|
||||
chrono = { version = "0.4", default-features = false }
|
||||
serde = { version = "1.0.124", default-features = false, features = ["derive"] }
|
||||
serde_json = { version = "1.0.64", default-features = false }
|
||||
serde_json = { version = "1.0.64", default-features = false, features = ["arbitrary_precision"] }
|
||||
thiserror = { version = "1.0", default-features = false }
|
||||
bytes = { version = "1.3.0", features = ["serde"] }
|
||||
hex = { version = "0.4.3", default-features = false, features = ["std"] }
|
||||
|
|
|
@ -1,7 +1,5 @@
|
|||
use std::str::FromStr;
|
||||
|
||||
use crate::types::U256;
|
||||
use serde::{de::Deserializer, Deserialize, Serialize};
|
||||
use crate::{types::U256, utils::from_int_or_hex};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Deserialize, Serialize, Debug, Clone)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
|
@ -19,19 +17,3 @@ pub struct FeeHistory {
|
|||
#[serde(default)]
|
||||
pub reward: Vec<Vec<U256>>,
|
||||
}
|
||||
|
||||
fn from_int_or_hex<'de, D>(deserializer: D) -> Result<U256, D::Error>
|
||||
where
|
||||
D: Deserializer<'de>,
|
||||
{
|
||||
#[derive(Deserialize)]
|
||||
#[serde(untagged)]
|
||||
enum IntOrHex {
|
||||
Int(u64),
|
||||
Hex(String),
|
||||
}
|
||||
match IntOrHex::deserialize(deserializer)? {
|
||||
IntOrHex::Int(n) => Ok(U256::from(n)),
|
||||
IntOrHex::Hex(s) => U256::from_str(s.as_str()).map_err(serde::de::Error::custom),
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,170 @@
|
|||
use std::collections::HashMap;
|
||||
|
||||
use crate::{
|
||||
types::{Address, Bytes, H256, U256, U64},
|
||||
utils::{from_int_or_hex, from_int_or_hex_opt},
|
||||
};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
/// This represents the chain configuration, specifying the genesis block, header fields, and hard
|
||||
/// fork switch blocks.
|
||||
#[derive(Clone, Debug, Default, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct Genesis {
|
||||
/// The fork configuration for this network.
|
||||
pub config: ChainConfig,
|
||||
|
||||
/// The genesis header nonce.
|
||||
pub nonce: U64,
|
||||
|
||||
/// The genesis header timestamp.
|
||||
pub timestamp: U64,
|
||||
|
||||
/// The genesis header extra data.
|
||||
pub extra_data: Bytes,
|
||||
|
||||
/// The genesis header gas limit.
|
||||
pub gas_limit: U64,
|
||||
|
||||
/// The genesis header difficulty.
|
||||
#[serde(deserialize_with = "from_int_or_hex")]
|
||||
pub difficulty: U256,
|
||||
|
||||
/// The genesis header mix hash.
|
||||
pub mix_hash: H256,
|
||||
|
||||
/// The genesis header coinbase address.
|
||||
pub coinbase: Address,
|
||||
|
||||
/// The initial state of the genesis block.
|
||||
pub alloc: HashMap<Address, GenesisAccount>,
|
||||
}
|
||||
|
||||
/// An account in the state of the genesis block.
|
||||
#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub struct GenesisAccount {
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub nonce: Option<u64>,
|
||||
pub balance: U256,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub code: Option<Bytes>,
|
||||
#[serde(flatten, skip_serializing_if = "Option::is_none")]
|
||||
pub storage: Option<HashMap<H256, H256>>,
|
||||
}
|
||||
|
||||
/// 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)]
|
||||
#[serde(default, rename_all = "camelCase")]
|
||||
pub struct ChainConfig {
|
||||
/// The network's chain ID.
|
||||
pub chain_id: u64,
|
||||
|
||||
/// The homestead switch block (None = no fork, 0 = already homestead).
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub homestead_block: Option<u64>,
|
||||
|
||||
/// The DAO fork switch block (None = no fork).
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub dao_fork_block: Option<u64>,
|
||||
|
||||
/// 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")]
|
||||
pub eip150_block: Option<u64>,
|
||||
|
||||
/// The EIP-150 hard fork hash.
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub eip150_hash: Option<H256>,
|
||||
|
||||
/// The EIP-155 hard fork block.
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub eip155_block: Option<u64>,
|
||||
|
||||
/// The EIP-158 hard fork block.
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub eip158_block: Option<u64>,
|
||||
|
||||
/// The Byzantium hard fork block.
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub byzantium_block: Option<u64>,
|
||||
|
||||
/// The Constantinople hard fork block.
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub constantinople_block: Option<u64>,
|
||||
|
||||
/// The Petersburg hard fork block.
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub petersburg_block: Option<u64>,
|
||||
|
||||
/// The Istanbul hard fork block.
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub istanbul_block: Option<u64>,
|
||||
|
||||
/// The Muir Glacier hard fork block.
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub muir_glacier_block: Option<u64>,
|
||||
|
||||
/// The Berlin hard fork block.
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub berlin_block: Option<u64>,
|
||||
|
||||
/// The London hard fork block.
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub london_block: Option<u64>,
|
||||
|
||||
/// The Arrow Glacier hard fork block.
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub arrow_glacier_block: Option<u64>,
|
||||
|
||||
/// The Gray Glacier hard fork block.
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub gray_glacier_block: Option<u64>,
|
||||
|
||||
/// Virtual fork after the merge to use as a network splitter.
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub merge_netsplit_block: Option<u64>,
|
||||
|
||||
/// The Shanghai hard fork block.
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub shanghai_block: Option<u64>,
|
||||
|
||||
/// The Cancun hard fork block.
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub cancun_block: Option<u64>,
|
||||
|
||||
/// Total difficulty reached that triggers the merge consensus upgrade.
|
||||
#[serde(skip_serializing_if = "Option::is_none", deserialize_with = "from_int_or_hex_opt")]
|
||||
pub terminal_total_difficulty: Option<U256>,
|
||||
|
||||
/// 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<EthashConfig>,
|
||||
|
||||
/// Clique parameters.
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub clique: Option<CliqueConfig>,
|
||||
}
|
||||
|
||||
/// Empty consensus configuration for proof-of-work networks.
|
||||
#[derive(Clone, Debug, Deserialize, Serialize)]
|
||||
pub struct EthashConfig {}
|
||||
|
||||
/// Consensus configuration for Clique.
|
||||
#[derive(Clone, Debug, Deserialize, Serialize)]
|
||||
pub struct CliqueConfig {
|
||||
/// Number of seconds between blocks to enforce.
|
||||
pub period: u64,
|
||||
|
||||
/// Epoch length to reset votes and checkpoints.
|
||||
pub epoch: u64,
|
||||
}
|
|
@ -1,20 +1,39 @@
|
|||
use super::unused_port;
|
||||
use super::{unused_port, Genesis};
|
||||
use crate::types::H256;
|
||||
use std::{
|
||||
env::temp_dir,
|
||||
fs::{create_dir, File},
|
||||
io::{BufRead, BufReader},
|
||||
path::PathBuf,
|
||||
process::{Child, Command},
|
||||
process::{Child, Command, Stdio},
|
||||
time::{Duration, Instant},
|
||||
};
|
||||
|
||||
/// How long we will wait for geth to indicate that it is ready.
|
||||
const GETH_STARTUP_TIMEOUT_MILLIS: u64 = 10_000;
|
||||
|
||||
/// Timeout for waiting for geth to add a peer.
|
||||
const GETH_DIAL_LOOP_TIMEOUT: Duration = Duration::new(20, 0);
|
||||
|
||||
/// The exposed APIs
|
||||
const API: &str = "eth,net,web3,txpool";
|
||||
const API: &str = "eth,net,web3,txpool,admin";
|
||||
|
||||
/// 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`](crate::utils::Geth)
|
||||
|
@ -22,6 +41,8 @@ pub struct GethInstance {
|
|||
pid: Child,
|
||||
port: u16,
|
||||
ipc: Option<PathBuf>,
|
||||
data_dir: Option<PathBuf>,
|
||||
p2p_port: Option<u16>,
|
||||
}
|
||||
|
||||
impl GethInstance {
|
||||
|
@ -30,6 +51,11 @@ impl GethInstance {
|
|||
self.port
|
||||
}
|
||||
|
||||
/// Returns the p2p port of this instance
|
||||
pub fn p2p_port(&self) -> Option<u16> {
|
||||
self.p2p_port
|
||||
}
|
||||
|
||||
/// Returns the HTTP endpoint of this instance
|
||||
pub fn endpoint(&self) -> String {
|
||||
format!("http://localhost:{}", self.port)
|
||||
|
@ -40,9 +66,35 @@ impl GethInstance {
|
|||
format!("ws://localhost:{}", self.port)
|
||||
}
|
||||
|
||||
/// Returns the path to this instances' IPC socket
|
||||
pub fn ipc_path(&self) -> &Option<PathBuf> {
|
||||
&self.ipc
|
||||
}
|
||||
|
||||
/// Returns the path to this instances' data directory
|
||||
pub fn data_dir(&self) -> &Option<PathBuf> {
|
||||
&self.data_dir
|
||||
}
|
||||
|
||||
/// Blocks until geth adds the specified peer, using 20s as the timeout.
|
||||
pub fn wait_to_add_peer(&mut self, id: H256) -> 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 {
|
||||
|
@ -51,6 +103,44 @@ impl Drop for GethInstance {
|
|||
}
|
||||
}
|
||||
|
||||
/// Whether or not geth is in `dev` mode and configuration options that depend on the mode.
|
||||
#[derive(Debug, Clone)]
|
||||
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(Debug, Clone, Default)]
|
||||
pub struct DevOptions {
|
||||
/// The interval at which the dev chain will mine new blocks.
|
||||
pub block_time: Option<u64>,
|
||||
}
|
||||
|
||||
/// Configuration options that cannot be set in dev mode.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct PrivateNetOptions {
|
||||
/// The p2p port to use.
|
||||
pub p2p_port: Option<u16>,
|
||||
|
||||
/// 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
|
||||
|
@ -75,8 +165,12 @@ impl Drop for GethInstance {
|
|||
#[derive(Clone, Default)]
|
||||
pub struct Geth {
|
||||
port: Option<u16>,
|
||||
block_time: Option<u64>,
|
||||
authrpc_port: Option<u16>,
|
||||
ipc_path: Option<PathBuf>,
|
||||
data_dir: Option<PathBuf>,
|
||||
chain_id: Option<u64>,
|
||||
genesis: Option<Genesis>,
|
||||
mode: GethMode,
|
||||
}
|
||||
|
||||
impl Geth {
|
||||
|
@ -93,10 +187,54 @@ impl Geth {
|
|||
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.
|
||||
#[must_use]
|
||||
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.
|
||||
#[must_use]
|
||||
pub fn block_time<T: Into<u64>>(mut self, block_time: T) -> Self {
|
||||
self.block_time = Some(block_time.into());
|
||||
self.mode = GethMode::Dev(DevOptions { block_time: Some(block_time.into()) });
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets the chain id for the geth instance.
|
||||
#[must_use]
|
||||
pub fn chain_id<T: Into<u64>>(mut self, chain_id: T) -> Self {
|
||||
self.chain_id = Some(chain_id.into());
|
||||
self
|
||||
}
|
||||
|
||||
/// Disable discovery for the geth instance.
|
||||
///
|
||||
/// This will put the geth instance into non-dev mode, discarding any previously set dev-mode
|
||||
/// options.
|
||||
#[must_use]
|
||||
pub fn disable_discovery(mut self) -> Self {
|
||||
match self.mode {
|
||||
GethMode::Dev(_) => {
|
||||
self.mode =
|
||||
GethMode::NonDev(PrivateNetOptions { discovery: false, ..Default::default() })
|
||||
}
|
||||
GethMode::NonDev(ref mut opts) => opts.discovery = false,
|
||||
}
|
||||
self
|
||||
}
|
||||
|
||||
|
@ -107,6 +245,32 @@ impl Geth {
|
|||
self
|
||||
}
|
||||
|
||||
/// Sets the data directory for geth.
|
||||
#[must_use]
|
||||
pub fn data_dir<T: Into<PathBuf>>(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.
|
||||
#[must_use]
|
||||
pub fn genesis(mut self, genesis: Genesis) -> Self {
|
||||
self.genesis = Some(genesis);
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets the port for authenticated RPC connections.
|
||||
#[must_use]
|
||||
pub fn authrpc_port(mut self, port: u16) -> Self {
|
||||
self.authrpc_port = Some(port);
|
||||
self
|
||||
}
|
||||
|
||||
/// Consumes the builder and spawns `geth` with stdout redirected
|
||||
/// to /dev/null.
|
||||
pub fn spawn(self) -> GethInstance {
|
||||
|
@ -114,6 +278,7 @@ impl Geth {
|
|||
// geth uses stderr for its logs
|
||||
cmd.stderr(std::process::Stdio::piped());
|
||||
let port = if let Some(port) = self.port { port } else { unused_port() };
|
||||
let authrpc_port = if let Some(port) = self.authrpc_port { port } else { unused_port() };
|
||||
|
||||
// Open the HTTP API
|
||||
cmd.arg("--http");
|
||||
|
@ -125,11 +290,72 @@ impl Geth {
|
|||
cmd.arg("--ws.port").arg(port.to_string());
|
||||
cmd.arg("--ws.api").arg(API);
|
||||
|
||||
// Set the port for authenticated APIs
|
||||
cmd.arg("--authrpc.port").arg(authrpc_port.to_string());
|
||||
|
||||
// use geth init to initialize the datadir if the genesis exists
|
||||
if let Some(genesis) = self.genesis {
|
||||
// create a temp dir to store the genesis file
|
||||
let temp_genesis_path = temp_dir().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(GETH);
|
||||
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);
|
||||
init_cmd
|
||||
.spawn()
|
||||
.expect("failed to spawn geth init")
|
||||
.wait()
|
||||
.expect("failed to wait for geth init to exit");
|
||||
}
|
||||
|
||||
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
|
||||
match self.mode {
|
||||
GethMode::Dev(DevOptions { block_time }) => {
|
||||
cmd.arg("--dev");
|
||||
if let Some(block_time) = self.block_time {
|
||||
if let Some(block_time) = block_time {
|
||||
cmd.arg("--dev.period").arg(block_time.to_string());
|
||||
}
|
||||
}
|
||||
GethMode::NonDev(PrivateNetOptions { p2p_port, discovery }) => {
|
||||
if let Some(p2p_port) = p2p_port {
|
||||
cmd.arg("--port").arg(p2p_port.to_string());
|
||||
}
|
||||
|
||||
// disable discovery if the flag is set
|
||||
if !discovery {
|
||||
cmd.arg("--nodiscover");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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);
|
||||
|
@ -137,10 +363,15 @@ impl Geth {
|
|||
|
||||
let mut child = cmd.spawn().expect("couldnt start geth");
|
||||
|
||||
let stdout = child.stderr.expect("Unable to get stderr for geth child process");
|
||||
let stderr = child.stderr.expect("Unable to get stderr for geth child process");
|
||||
|
||||
let start = Instant::now();
|
||||
let mut reader = BufReader::new(stdout);
|
||||
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 + Duration::from_millis(GETH_STARTUP_TIMEOUT_MILLIS) <= Instant::now() {
|
||||
|
@ -150,14 +381,30 @@ impl Geth {
|
|||
let mut line = String::new();
|
||||
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;
|
||||
}
|
||||
|
||||
// geth 1.9.23 uses "server started" while 1.9.18 uses "endpoint opened"
|
||||
if line.contains("HTTP endpoint opened") || line.contains("HTTP server started") {
|
||||
// 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"))
|
||||
{
|
||||
http_started = true;
|
||||
}
|
||||
|
||||
if p2p_started && http_started {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
child.stderr = Some(reader.into_inner());
|
||||
|
||||
GethInstance { pid: child, port, ipc: self.ipc_path }
|
||||
let p2p_port = match self.mode {
|
||||
GethMode::Dev(_) => None,
|
||||
GethMode::NonDev(PrivateNetOptions { p2p_port, .. }) => p2p_port,
|
||||
};
|
||||
|
||||
GethInstance { pid: child, port, ipc: self.ipc_path, data_dir: self.data_dir, p2p_port }
|
||||
}
|
||||
}
|
||||
|
|
|
@ -10,6 +10,10 @@ mod geth;
|
|||
#[cfg(not(target_arch = "wasm32"))]
|
||||
pub use geth::{Geth, GethInstance};
|
||||
|
||||
/// Utilities for working with a `genesis.json` and other chain config structs.
|
||||
mod genesis;
|
||||
pub use genesis::{ChainConfig, Genesis};
|
||||
|
||||
/// Utilities for launching an anvil instance
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
mod anvil;
|
||||
|
@ -23,6 +27,7 @@ mod hash;
|
|||
pub use hash::{hash_message, id, keccak256, serialize};
|
||||
|
||||
mod units;
|
||||
use serde::{Deserialize, Deserializer};
|
||||
pub use units::Units;
|
||||
|
||||
/// Re-export RLP
|
||||
|
@ -38,6 +43,7 @@ use k256::{ecdsa::SigningKey, PublicKey as K256PublicKey};
|
|||
use std::{
|
||||
convert::{TryFrom, TryInto},
|
||||
fmt,
|
||||
str::FromStr,
|
||||
};
|
||||
use thiserror::Error;
|
||||
|
||||
|
@ -452,6 +458,34 @@ pub fn eip1559_default_estimator(base_fee_per_gas: U256, rewards: Vec<Vec<U256>>
|
|||
(max_fee_per_gas, max_priority_fee_per_gas)
|
||||
}
|
||||
|
||||
/// Deserializes the input into a U256, accepting both 0x-prefixed hex and decimal strings with
|
||||
/// arbitrary precision, defined by serde_json's [`Number`](serde_json::Number).
|
||||
pub fn from_int_or_hex<'de, D>(deserializer: D) -> Result<U256, D::Error>
|
||||
where
|
||||
D: Deserializer<'de>,
|
||||
{
|
||||
#[derive(Deserialize)]
|
||||
#[serde(untagged)]
|
||||
enum IntOrHex {
|
||||
Int(serde_json::Number),
|
||||
Hex(String),
|
||||
}
|
||||
|
||||
match IntOrHex::deserialize(deserializer)? {
|
||||
IntOrHex::Hex(s) => U256::from_str(s.as_str()).map_err(serde::de::Error::custom),
|
||||
IntOrHex::Int(n) => U256::from_dec_str(&n.to_string()).map_err(serde::de::Error::custom),
|
||||
}
|
||||
}
|
||||
|
||||
/// Deserializes the input into an `Option<U256>`, using [`from_int_or_hex`] to deserialize the
|
||||
/// inner value.
|
||||
pub fn from_int_or_hex_opt<'de, D>(deserializer: D) -> Result<Option<U256>, D::Error>
|
||||
where
|
||||
D: Deserializer<'de>,
|
||||
{
|
||||
Ok(Some(from_int_or_hex(deserializer)?))
|
||||
}
|
||||
|
||||
fn estimate_priority_fee(rewards: Vec<Vec<U256>>) -> U256 {
|
||||
let mut rewards: Vec<U256> =
|
||||
rewards.iter().map(|r| r[0]).filter(|r| *r > U256::zero()).collect();
|
||||
|
|
|
@ -35,6 +35,9 @@ futures-timer = { version = "3.0.2", default-features = false }
|
|||
futures-channel = { version = "0.3.16", default-features = false, optional = true }
|
||||
pin-project = { version = "1.0.11", default-features = false }
|
||||
|
||||
# peer-related admin namespace
|
||||
enr = { version = "0.7.0", default-features = false, features = ["k256", "serde"] }
|
||||
|
||||
# tracing
|
||||
tracing = { version = "0.1.37", default-features = false }
|
||||
tracing-futures = { version = "0.2.5", default-features = false, features = ["std-future"] }
|
||||
|
|
|
@ -0,0 +1,402 @@
|
|||
use crate::{H256, U256};
|
||||
use enr::{k256::ecdsa::SigningKey, Enr};
|
||||
use ethers_core::utils::{from_int_or_hex, ChainConfig};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::net::{IpAddr, SocketAddr};
|
||||
|
||||
/// This includes general information about a running node, spanning networking and protocol
|
||||
/// details.
|
||||
#[derive(Clone, Debug, Deserialize, Serialize)]
|
||||
pub struct NodeInfo {
|
||||
/// The node's private key.
|
||||
pub id: H256,
|
||||
|
||||
/// The node's user agent, containing a client name, version, OS, and other metadata.
|
||||
pub name: String,
|
||||
|
||||
/// The enode URL of the connected node.
|
||||
pub enode: String,
|
||||
|
||||
/// The [ENR](https://eips.ethereum.org/EIPS/eip-778) of the running client.
|
||||
pub enr: Enr<SigningKey>,
|
||||
|
||||
/// The IP address of the connected node.
|
||||
pub ip: IpAddr,
|
||||
|
||||
/// The node's listening ports.
|
||||
pub ports: Ports,
|
||||
|
||||
/// The node's listening address.
|
||||
#[serde(rename = "listenAddr")]
|
||||
pub listen_addr: String,
|
||||
|
||||
/// The protocols that the node supports, with protocol metadata.
|
||||
pub protocols: ProtocolInfo,
|
||||
}
|
||||
|
||||
/// Represents a node's discovery and listener ports.
|
||||
#[derive(Clone, Debug, Deserialize, Serialize)]
|
||||
pub struct Ports {
|
||||
/// The node's discovery port.
|
||||
pub discovery: u16,
|
||||
|
||||
/// The node's listener port.
|
||||
pub listener: u16,
|
||||
}
|
||||
|
||||
/// Represents protocols that the connected RPC node supports.
|
||||
///
|
||||
/// This contains protocol information reported by the connected RPC node.
|
||||
#[derive(Clone, Debug, Deserialize, Serialize)]
|
||||
pub struct ProtocolInfo {
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub eth: Option<EthProtocolInfo>,
|
||||
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub snap: Option<SnapProtocolInfo>,
|
||||
}
|
||||
|
||||
/// Represents a short summary of the `eth` sub-protocol metadata known about the host peer.
|
||||
///
|
||||
/// See [geth's `NodeInfo`
|
||||
/// struct](https://github.com/ethereum/go-ethereum/blob/c2e0abce2eedc1ba2a1b32c46fd07ef18a25354a/eth/protocols/eth/handler.go#L129)
|
||||
/// for how these fields are determined.
|
||||
#[derive(Clone, Debug, Deserialize, Serialize)]
|
||||
pub struct EthProtocolInfo {
|
||||
/// The eth network version.
|
||||
pub network: u64,
|
||||
|
||||
/// The total difficulty of the host's blockchain.
|
||||
#[serde(deserialize_with = "from_int_or_hex")]
|
||||
pub difficulty: U256,
|
||||
|
||||
/// The Keccak hash of the host's genesis block.
|
||||
pub genesis: H256,
|
||||
|
||||
/// The chain configuration for the host's fork rules.
|
||||
pub config: ChainConfig,
|
||||
|
||||
/// The hash of the host's best known block.
|
||||
pub head: H256,
|
||||
}
|
||||
|
||||
/// Represents a short summary of the host's `snap` sub-protocol metadata.
|
||||
///
|
||||
/// This is just an empty struct, because [geth's internal representation is
|
||||
/// empty](https://github.com/ethereum/go-ethereum/blob/c2e0abce2eedc1ba2a1b32c46fd07ef18a25354a/eth/protocols/snap/handler.go#L571-L576).
|
||||
#[derive(Clone, Debug, Deserialize, Serialize)]
|
||||
pub struct SnapProtocolInfo {}
|
||||
|
||||
/// Represents the protocols that a peer supports.
|
||||
///
|
||||
/// This differs from [`ProtocolInfo`] in that [`PeerProtocolInfo`] contains protocol information
|
||||
/// gathered from the protocol handshake, and [`ProtocolInfo`] contains information reported by the
|
||||
/// connected RPC node.
|
||||
#[derive(Clone, Debug, Deserialize, Serialize)]
|
||||
pub struct PeerProtocolInfo {
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub eth: Option<EthPeerInfo>,
|
||||
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub snap: Option<SnapPeerInfo>,
|
||||
}
|
||||
|
||||
/// Can contain either eth protocol info or a string "handshake", which geth uses if the peer is
|
||||
/// still completing the handshake for the protocol.
|
||||
#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq)]
|
||||
#[serde(untagged)]
|
||||
pub enum EthPeerInfo {
|
||||
/// The `eth` sub-protocol metadata known about the host peer.
|
||||
Info(Box<EthInfo>),
|
||||
|
||||
/// The string "handshake" if the peer is still completing the handshake for the protocol.
|
||||
#[serde(deserialize_with = "deser_handshake", serialize_with = "ser_handshake")]
|
||||
Handshake,
|
||||
}
|
||||
|
||||
/// Represents a short summary of the `eth` sub-protocol metadata known about a connected peer
|
||||
///
|
||||
/// See [geth's `ethPeerInfo`
|
||||
/// struct](https://github.com/ethereum/go-ethereum/blob/53d1ae096ac0515173e17f0f81a553e5f39027f7/eth/peer.go#L28)
|
||||
/// for how these fields are determined.
|
||||
#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq)]
|
||||
pub struct EthInfo {
|
||||
/// The negotiated eth version.
|
||||
pub version: u64,
|
||||
|
||||
/// The total difficulty of the peer's blockchain.
|
||||
#[serde(deserialize_with = "from_int_or_hex")]
|
||||
pub difficulty: U256,
|
||||
|
||||
/// The hash of the peer's best known block.
|
||||
pub head: H256,
|
||||
}
|
||||
|
||||
/// Can contain either snap protocol info or a string "handshake", which geth uses if the peer is
|
||||
/// still completing the handshake for the protocol.
|
||||
#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq)]
|
||||
#[serde(untagged)]
|
||||
pub enum SnapPeerInfo {
|
||||
/// The `snap` sub-protocol metadata known about the host peer.
|
||||
Info(SnapInfo),
|
||||
|
||||
/// The string "handshake" if the peer is still completing the handshake for the protocol.
|
||||
#[serde(deserialize_with = "deser_handshake", serialize_with = "ser_handshake")]
|
||||
Handshake,
|
||||
}
|
||||
|
||||
/// Represents a short summary of the `snap` sub-protocol metadata known about a connected peer.
|
||||
///
|
||||
/// See [geth's `snapPeerInfo`
|
||||
/// struct](https://github.com/ethereum/go-ethereum/blob/53d1ae096ac0515173e17f0f81a553e5f39027f7/eth/peer.go#L53)
|
||||
/// for how these fields are determined.
|
||||
#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq)]
|
||||
pub struct SnapInfo {
|
||||
/// The negotiated snap version.
|
||||
pub version: u64,
|
||||
}
|
||||
|
||||
/// Represents a short summary of information known about a connected peer.
|
||||
///
|
||||
/// See [geth's `PeerInfo` struct](https://github.com/ethereum/go-ethereum/blob/64dccf7aa411c5c7cd36090c3d9b9892945ae813/p2p/peer.go#L484) for the source of each field.
|
||||
#[derive(Clone, Debug, Deserialize, Serialize)]
|
||||
pub struct PeerInfo {
|
||||
/// The peer's ENR.
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub enr: Option<Enr<SigningKey>>,
|
||||
|
||||
/// The peer's enode URL.
|
||||
pub enode: String,
|
||||
|
||||
/// The peer's enode ID.
|
||||
pub id: String,
|
||||
|
||||
/// The peer's name.
|
||||
pub name: String,
|
||||
|
||||
/// The peer's capabilities.
|
||||
pub caps: Vec<String>,
|
||||
|
||||
/// Networking information about the peer.
|
||||
pub network: PeerNetworkInfo,
|
||||
|
||||
/// The protocols that the peer supports, with protocol metadata.
|
||||
pub protocols: PeerProtocolInfo,
|
||||
}
|
||||
|
||||
/// Represents networking related information about the peer, including details about whether or
|
||||
/// not it is inbound, trusted, or static.
|
||||
#[derive(Clone, Debug, Deserialize, Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct PeerNetworkInfo {
|
||||
/// The local endpoint of the TCP connection.
|
||||
pub local_address: SocketAddr,
|
||||
|
||||
/// The remote endpoint of the TCP connection.
|
||||
pub remote_address: SocketAddr,
|
||||
|
||||
/// Whether or not the peer is inbound.
|
||||
pub inbound: bool,
|
||||
|
||||
/// Whether or not the peer is trusted.
|
||||
pub trusted: bool,
|
||||
|
||||
/// Whether or not the peer is a static peer.
|
||||
#[serde(rename = "static")]
|
||||
pub static_node: bool,
|
||||
}
|
||||
|
||||
fn deser_handshake<'de, D>(deserializer: D) -> Result<(), D::Error>
|
||||
where
|
||||
D: serde::Deserializer<'de>,
|
||||
{
|
||||
let s = String::deserialize(deserializer)?;
|
||||
if s == "handshake" {
|
||||
Ok(())
|
||||
} else {
|
||||
Err(serde::de::Error::custom(
|
||||
"expected \"handshake\" if protocol info did not appear in the response",
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
fn ser_handshake<S>(serializer: S) -> Result<S::Ok, S::Error>
|
||||
where
|
||||
S: serde::Serializer,
|
||||
{
|
||||
serializer.serialize_str("handshake")
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn deserialize_peer_info() {
|
||||
let response = r#"{
|
||||
"enode":"enode://bb37b7302f79e47c1226d6e3ccf0ef6d51146019efdcc1f6e861fd1c1a78d5e84e486225a6a8a503b93d5c50125ee980835c92bde7f7d12f074c16f4e439a578@127.0.0.1:60872",
|
||||
"id":"ca23c04b7e796da5d6a5f04a62b81c88d41b1341537db85a2b6443e838d8339b",
|
||||
"name":"Geth/v1.10.19-stable/darwin-arm64/go1.18.3",
|
||||
"caps":["eth/66","eth/67","snap/1"],
|
||||
"network":{
|
||||
"localAddress":"127.0.0.1:30304",
|
||||
"remoteAddress":"127.0.0.1:60872",
|
||||
"inbound":true,
|
||||
"trusted":false,
|
||||
"static":false
|
||||
},
|
||||
"protocols":{
|
||||
"eth":{
|
||||
"version":67,
|
||||
"difficulty":0,
|
||||
"head":"0xb04009ddf4b0763f42778e7d5937e49bebf1e11b2d26c9dac6cefb5f84b6f8ea"
|
||||
},
|
||||
"snap":{"version":1}
|
||||
}
|
||||
}"#;
|
||||
let peer_info: PeerInfo = serde_json::from_str(response).unwrap();
|
||||
|
||||
assert_eq!(peer_info.enode, "enode://bb37b7302f79e47c1226d6e3ccf0ef6d51146019efdcc1f6e861fd1c1a78d5e84e486225a6a8a503b93d5c50125ee980835c92bde7f7d12f074c16f4e439a578@127.0.0.1:60872");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn deserialize_node_info() {
|
||||
// this response also has an enr
|
||||
let response = r#"{
|
||||
"id":"6e2fe698f3064cd99410926ce16734e35e3cc947d4354461d2594f2d2dd9f7b6",
|
||||
"name":"Geth/v1.10.19-stable/darwin-arm64/go1.18.3",
|
||||
"enode":"enode://d7dfaea49c7ef37701e668652bcf1bc63d3abb2ae97593374a949e175e4ff128730a2f35199f3462a56298b981dfc395a5abebd2d6f0284ffe5bdc3d8e258b86@127.0.0.1:30304?discport=0",
|
||||
"enr":"enr:-Jy4QIvS0dKBLjTTV_RojS8hjriwWsJNHRVyOh4Pk4aUXc5SZjKRVIOeYc7BqzEmbCjLdIY4Ln7x5ZPf-2SsBAc2_zqGAYSwY1zog2V0aMfGhNegsXuAgmlkgnY0gmlwhBiT_DiJc2VjcDI1NmsxoQLX366knH7zdwHmaGUrzxvGPTq7Kul1kzdKlJ4XXk_xKIRzbmFwwIN0Y3CCdmA",
|
||||
"ip":"127.0.0.1",
|
||||
"ports":{
|
||||
"discovery":0,
|
||||
"listener":30304
|
||||
},
|
||||
"listenAddr":"[::]:30304",
|
||||
"protocols":{
|
||||
"eth":{
|
||||
"network":1337,
|
||||
"difficulty":0,
|
||||
"genesis":"0xb04009ddf4b0763f42778e7d5937e49bebf1e11b2d26c9dac6cefb5f84b6f8ea",
|
||||
"config":{
|
||||
"chainId":0,
|
||||
"eip150Hash":"0x0000000000000000000000000000000000000000000000000000000000000000"
|
||||
},
|
||||
"head":"0xb04009ddf4b0763f42778e7d5937e49bebf1e11b2d26c9dac6cefb5f84b6f8ea"
|
||||
},
|
||||
"snap":{}
|
||||
}
|
||||
}"#;
|
||||
|
||||
let _: NodeInfo = serde_json::from_str(response).unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn deserialize_node_info_post_merge() {
|
||||
// this response also has an enr
|
||||
let response = r#"{
|
||||
"id":"6e2fe698f3064cd99410926ce16734e35e3cc947d4354461d2594f2d2dd9f7b6",
|
||||
"name":"Geth/v1.10.19-stable/darwin-arm64/go1.18.3",
|
||||
"enode":"enode://d7dfaea49c7ef37701e668652bcf1bc63d3abb2ae97593374a949e175e4ff128730a2f35199f3462a56298b981dfc395a5abebd2d6f0284ffe5bdc3d8e258b86@127.0.0.1:30304?discport=0",
|
||||
"enr":"enr:-Jy4QIvS0dKBLjTTV_RojS8hjriwWsJNHRVyOh4Pk4aUXc5SZjKRVIOeYc7BqzEmbCjLdIY4Ln7x5ZPf-2SsBAc2_zqGAYSwY1zog2V0aMfGhNegsXuAgmlkgnY0gmlwhBiT_DiJc2VjcDI1NmsxoQLX366knH7zdwHmaGUrzxvGPTq7Kul1kzdKlJ4XXk_xKIRzbmFwwIN0Y3CCdmA",
|
||||
"ip":"127.0.0.1",
|
||||
"ports":{
|
||||
"discovery":0,
|
||||
"listener":30304
|
||||
},
|
||||
"listenAddr":"[::]:30304",
|
||||
"protocols":{
|
||||
"eth":{
|
||||
"network":1337,
|
||||
"difficulty":0,
|
||||
"genesis":"0xb04009ddf4b0763f42778e7d5937e49bebf1e11b2d26c9dac6cefb5f84b6f8ea",
|
||||
"config":{
|
||||
"chainId":0,
|
||||
"eip150Hash":"0x0000000000000000000000000000000000000000000000000000000000000000",
|
||||
"terminalTotalDifficulty":58750000000000000000000,
|
||||
"terminalTotalDifficultyPassed":true,
|
||||
"ethash":{}
|
||||
},
|
||||
"head":"0xb04009ddf4b0763f42778e7d5937e49bebf1e11b2d26c9dac6cefb5f84b6f8ea"
|
||||
},
|
||||
"snap":{}
|
||||
}
|
||||
}"#;
|
||||
|
||||
let _: NodeInfo = serde_json::from_str(response).unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn deserialize_node_info_mainnet_full() {
|
||||
let actual_response = r#"{
|
||||
"id": "74477ca052fcf55ee9eafb369fafdb3e91ad7b64fbd7ae15a4985bfdc43696f2",
|
||||
"name": "Geth/v1.10.26-stable/darwin-arm64/go1.19.3",
|
||||
"enode": "enode://962184c6f2a19e064e2ddf0d5c5a788c8c5ed3a4909b7f75fb4dad967392ff542772bcc498cd7f15e13eecbde830265f379779c6da1f71fb8fe1a4734dfc0a1e@127.0.0.1:13337?discport=0",
|
||||
"enr": "enr:-J-4QFttJyL3f2-B2TQmBZNFxex99TSBv1YtB_8jqUbXWkf6LOREKQAPW2bIn8kJ8QvHbWxCQNFzTX6sehjbrz1ZkSuGAYSyQ0_rg2V0aMrJhPxk7ASDEYwwgmlkgnY0gmlwhH8AAAGJc2VjcDI1NmsxoQKWIYTG8qGeBk4t3w1cWniMjF7TpJCbf3X7Ta2Wc5L_VIRzbmFwwIN0Y3CCNBk",
|
||||
"ip": "127.0.0.1",
|
||||
"ports": {
|
||||
"discovery": 0,
|
||||
"listener": 13337
|
||||
},
|
||||
"listenAddr": "[::]:13337",
|
||||
"protocols": {
|
||||
"eth": {
|
||||
"network": 1337,
|
||||
"difficulty": 17179869184,
|
||||
"genesis": "0xd4e56740f876aef8c010b86a40d5f56745a118d0906a34e69aec8c0db1cb8fa3",
|
||||
"config": {
|
||||
"chainId": 1,
|
||||
"homesteadBlock": 1150000,
|
||||
"daoForkBlock": 1920000,
|
||||
"daoForkSupport": true,
|
||||
"eip150Block": 2463000,
|
||||
"eip150Hash": "0x2086799aeebeae135c246c65021c82b4e15a2c451340993aacfd2751886514f0",
|
||||
"eip155Block": 2675000,
|
||||
"eip158Block": 2675000,
|
||||
"byzantiumBlock": 4370000,
|
||||
"constantinopleBlock": 7280000,
|
||||
"petersburgBlock": 7280000,
|
||||
"istanbulBlock": 9069000,
|
||||
"muirGlacierBlock": 9200000,
|
||||
"berlinBlock": 12244000,
|
||||
"londonBlock": 12965000,
|
||||
"arrowGlacierBlock": 13773000,
|
||||
"grayGlacierBlock": 15050000,
|
||||
"terminalTotalDifficulty": 58750000000000000000000,
|
||||
"terminalTotalDifficultyPassed": true,
|
||||
"ethash": {}
|
||||
},
|
||||
"head": "0xd4e56740f876aef8c010b86a40d5f56745a118d0906a34e69aec8c0db1cb8fa3"
|
||||
},
|
||||
"snap": {}
|
||||
}
|
||||
}"#;
|
||||
|
||||
let _: NodeInfo = serde_json::from_str(actual_response).unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn deserialize_peer_info_handshake() {
|
||||
let response = r#"{
|
||||
"enode": "enode://a997fde0023537ad01e536ebf2eeeb4b4b3d5286707586727b704f32e8e2b4959e08b6db5b27eb6b7e9f6efcbb53657f4e2bd16900aa77a89426dc3382c29ce0@[::1]:60948",
|
||||
"id": "df6f8bc331005962c2ef1f5236486a753bc6b2ddb5ef04370757999d1ca832d4",
|
||||
"name": "Geth/v1.10.26-stable-e5eb32ac/linux-amd64/go1.18.5",
|
||||
"caps": ["eth/66","eth/67","snap/1"],
|
||||
"network":{
|
||||
"localAddress":"[::1]:30304",
|
||||
"remoteAddress":"[::1]:60948",
|
||||
"inbound":true,
|
||||
"trusted":false,
|
||||
"static":false
|
||||
},
|
||||
"protocols":{
|
||||
"eth":"handshake",
|
||||
"snap":"handshake"
|
||||
}
|
||||
}"#;
|
||||
|
||||
let info: PeerInfo = serde_json::from_str(response).unwrap();
|
||||
assert_eq!(info.protocols.eth, Some(EthPeerInfo::Handshake));
|
||||
assert_eq!(info.protocols.snap, Some(SnapPeerInfo::Handshake));
|
||||
}
|
||||
}
|
|
@ -10,6 +10,10 @@ pub use transports::*;
|
|||
mod provider;
|
||||
pub use provider::{is_local_endpoint, FilterKind, Provider, ProviderError, ProviderExt};
|
||||
|
||||
// types for the admin api
|
||||
pub mod admin;
|
||||
pub use admin::{NodeInfo, PeerInfo};
|
||||
|
||||
// ENS support
|
||||
pub mod ens;
|
||||
|
||||
|
@ -488,6 +492,32 @@ pub trait Middleware: Sync + Send + Debug {
|
|||
self.inner().get_proof(from, locations, block).await.map_err(FromErr::from)
|
||||
}
|
||||
|
||||
// Admin namespace
|
||||
|
||||
async fn add_peer(&self, enode_url: String) -> Result<bool, Self::Error> {
|
||||
self.inner().add_peer(enode_url).await.map_err(FromErr::from)
|
||||
}
|
||||
|
||||
async fn add_trusted_peer(&self, enode_url: String) -> Result<bool, Self::Error> {
|
||||
self.inner().add_trusted_peer(enode_url).await.map_err(FromErr::from)
|
||||
}
|
||||
|
||||
async fn node_info(&self) -> Result<NodeInfo, Self::Error> {
|
||||
self.inner().node_info().await.map_err(FromErr::from)
|
||||
}
|
||||
|
||||
async fn peers(&self) -> Result<Vec<PeerInfo>, Self::Error> {
|
||||
self.inner().peers().await.map_err(FromErr::from)
|
||||
}
|
||||
|
||||
async fn remove_peer(&self, enode_url: String) -> Result<bool, Self::Error> {
|
||||
self.inner().remove_peer(enode_url).await.map_err(FromErr::from)
|
||||
}
|
||||
|
||||
async fn remove_trusted_peer(&self, enode_url: String) -> Result<bool, Self::Error> {
|
||||
self.inner().remove_trusted_peer(enode_url).await.map_err(FromErr::from)
|
||||
}
|
||||
|
||||
// Mempool inspection for Geth's API
|
||||
|
||||
async fn txpool_content(&self) -> Result<TxpoolContent, Self::Error> {
|
||||
|
|
|
@ -4,7 +4,7 @@ use crate::{
|
|||
pubsub::{PubsubClient, SubscriptionStream},
|
||||
stream::{FilterWatcher, DEFAULT_LOCAL_POLL_INTERVAL, DEFAULT_POLL_INTERVAL},
|
||||
FromErr, Http as HttpProvider, JsonRpcClient, JsonRpcClientWrapper, LogQuery, MockProvider,
|
||||
PendingTransaction, QuorumProvider, RwClient, SyncingStatus,
|
||||
NodeInfo, PeerInfo, PendingTransaction, QuorumProvider, RwClient, SyncingStatus,
|
||||
};
|
||||
|
||||
#[cfg(all(not(target_arch = "wasm32"), feature = "ws"))]
|
||||
|
@ -798,6 +798,48 @@ impl<P: JsonRpcClient> Middleware for Provider<P> {
|
|||
self.request("eth_getProof", [from, locations, block]).await
|
||||
}
|
||||
|
||||
// Admin namespace
|
||||
|
||||
/// Requests adding the given peer, returning a boolean representing whether or not the peer
|
||||
/// was accepted for tracking.
|
||||
async fn add_peer(&self, enode_url: String) -> Result<bool, Self::Error> {
|
||||
let enode_url = utils::serialize(&enode_url);
|
||||
self.request("admin_addPeer", [enode_url]).await
|
||||
}
|
||||
|
||||
/// Requests adding the given peer as a trusted peer, which the node will always connect to
|
||||
/// even when its peer slots are full.
|
||||
async fn add_trusted_peer(&self, enode_url: String) -> Result<bool, Self::Error> {
|
||||
let enode_url = utils::serialize(&enode_url);
|
||||
self.request("admin_addTrustedPeer", [enode_url]).await
|
||||
}
|
||||
|
||||
/// Returns general information about the node as well as information about the running p2p
|
||||
/// protocols (e.g. `eth`, `snap`).
|
||||
async fn node_info(&self) -> Result<NodeInfo, Self::Error> {
|
||||
self.request("admin_nodeInfo", ()).await
|
||||
}
|
||||
|
||||
/// Returns the list of peers currently connected to the node.
|
||||
async fn peers(&self) -> Result<Vec<PeerInfo>, Self::Error> {
|
||||
self.request("admin_peers", ()).await
|
||||
}
|
||||
|
||||
/// Requests to remove the given peer, returning true if the enode was successfully parsed and
|
||||
/// the peer was removed.
|
||||
async fn remove_peer(&self, enode_url: String) -> Result<bool, Self::Error> {
|
||||
let enode_url = utils::serialize(&enode_url);
|
||||
self.request("admin_removePeer", [enode_url]).await
|
||||
}
|
||||
|
||||
/// Requests to remove the given peer, returning a boolean representing whether or not the
|
||||
/// enode url passed was validated. A return value of `true` does not necessarily mean that the
|
||||
/// peer was disconnected.
|
||||
async fn remove_trusted_peer(&self, enode_url: String) -> Result<bool, Self::Error> {
|
||||
let enode_url = utils::serialize(&enode_url);
|
||||
self.request("admin_removeTrustedPeer", [enode_url]).await
|
||||
}
|
||||
|
||||
////// Ethereum Naming Service
|
||||
// The Ethereum Naming Service (ENS) allows easy to remember and use names to
|
||||
// be assigned to Ethereum addresses. Any provider operation which takes an address
|
||||
|
@ -1753,13 +1795,15 @@ pub mod dev_rpc {
|
|||
#[cfg(test)]
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
mod tests {
|
||||
use std::path::PathBuf;
|
||||
|
||||
use super::*;
|
||||
use crate::Http;
|
||||
use ethers_core::{
|
||||
types::{
|
||||
transaction::eip2930::AccessList, Eip1559TransactionRequest, TransactionRequest, H256,
|
||||
},
|
||||
utils::Anvil,
|
||||
utils::{Anvil, Genesis, Geth, GethInstance},
|
||||
};
|
||||
use futures_util::StreamExt;
|
||||
|
||||
|
@ -2124,4 +2168,151 @@ mod tests {
|
|||
"ens name not found: `ox63616e.eth` resolver (0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2) is invalid."
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn geth_admin_nodeinfo() {
|
||||
// we can't use the test provider because infura does not expose admin endpoints
|
||||
let port = 8546u16;
|
||||
let p2p_listener_port = 13337u16;
|
||||
let authrpc_port = 8552u16;
|
||||
let network = 1337u64;
|
||||
let temp_dir = tempfile::tempdir().unwrap().into_path();
|
||||
|
||||
let (geth, provider) = spawn_geth_and_create_provider(
|
||||
network,
|
||||
port,
|
||||
p2p_listener_port,
|
||||
authrpc_port,
|
||||
Some(temp_dir),
|
||||
None,
|
||||
);
|
||||
|
||||
let info = provider.node_info().await.unwrap();
|
||||
drop(geth);
|
||||
|
||||
// check that the port we set works
|
||||
assert_eq!(info.ports.listener, p2p_listener_port);
|
||||
|
||||
// make sure it is running eth
|
||||
assert!(info.protocols.eth.is_some());
|
||||
|
||||
// check that the network id is correct
|
||||
assert_eq!(info.protocols.eth.unwrap().network, network);
|
||||
}
|
||||
|
||||
/// Spawn a new `GethInstance` without discovery and crate a `Provider` for it.
|
||||
///
|
||||
/// These will all use the same genesis config.
|
||||
fn spawn_geth_and_create_provider(
|
||||
chain_id: u64,
|
||||
rpc_port: u16,
|
||||
p2p_port: u16,
|
||||
authrpc_port: u16,
|
||||
datadir: Option<PathBuf>,
|
||||
genesis: Option<Genesis>,
|
||||
) -> (GethInstance, Provider<HttpProvider>) {
|
||||
let geth = Geth::new()
|
||||
.port(rpc_port)
|
||||
.p2p_port(p2p_port)
|
||||
.authrpc_port(authrpc_port)
|
||||
.chain_id(chain_id)
|
||||
.disable_discovery();
|
||||
|
||||
let geth = match genesis {
|
||||
Some(genesis) => geth.genesis(genesis),
|
||||
None => geth,
|
||||
};
|
||||
|
||||
let geth = match datadir {
|
||||
Some(dir) => geth.data_dir(dir),
|
||||
None => geth,
|
||||
}
|
||||
.spawn();
|
||||
|
||||
let url = format!("http://127.0.0.1:{}", rpc_port);
|
||||
let provider = Provider::try_from(url).unwrap();
|
||||
(geth, provider)
|
||||
}
|
||||
|
||||
/// Spawn a set of [`GethInstance`]s with the list of given data directories and [`Provider`]s
|
||||
/// for those [`GethInstance`]s without discovery, setting sequential ports for their p2p, rpc,
|
||||
/// and authrpc ports.
|
||||
fn spawn_geth_instances(
|
||||
datadirs: Vec<PathBuf>,
|
||||
chain_id: u64,
|
||||
genesis: Option<Genesis>,
|
||||
) -> Vec<(GethInstance, Provider<HttpProvider>)> {
|
||||
let mut geths = Vec::new();
|
||||
let mut p2p_port = 30303;
|
||||
let mut rpc_port = 8545;
|
||||
let mut authrpc_port = 8551;
|
||||
|
||||
for dir in datadirs {
|
||||
let (geth, provider) = spawn_geth_and_create_provider(
|
||||
chain_id,
|
||||
rpc_port,
|
||||
p2p_port,
|
||||
authrpc_port,
|
||||
Some(dir),
|
||||
genesis.clone(),
|
||||
);
|
||||
|
||||
geths.push((geth, provider));
|
||||
|
||||
p2p_port += 1;
|
||||
rpc_port += 1;
|
||||
authrpc_port += 1;
|
||||
}
|
||||
|
||||
geths
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn add_second_geth_peer() {
|
||||
// init each geth directory
|
||||
let dir1 = tempfile::tempdir().unwrap().into_path();
|
||||
let dir2 = tempfile::tempdir().unwrap().into_path();
|
||||
|
||||
// use the default genesis
|
||||
let genesis = utils::Genesis::default();
|
||||
|
||||
// spawn the geths
|
||||
let mut geths = spawn_geth_instances(vec![dir1.clone(), dir2.clone()], 1337, Some(genesis));
|
||||
let (mut first_geth, first_peer) = geths.pop().unwrap();
|
||||
let (second_geth, second_peer) = geths.pop().unwrap();
|
||||
|
||||
// get nodeinfo for each geth instance
|
||||
let first_info = first_peer.node_info().await.unwrap();
|
||||
let second_info = second_peer.node_info().await.unwrap();
|
||||
let first_port = first_info.ports.listener;
|
||||
|
||||
// replace the ip in the enode by putting
|
||||
let first_prefix = first_info.enode.split('@').collect::<Vec<&str>>();
|
||||
|
||||
// create enodes for each geth instance using each id and port
|
||||
let first_enode = format!("{}@localhost:{}", first_prefix.first().unwrap(), first_port);
|
||||
|
||||
// add the first geth as a peer for the second
|
||||
let res = second_peer.add_peer(first_enode).await.unwrap();
|
||||
assert!(res);
|
||||
|
||||
// wait on the listening peer for an incoming connection
|
||||
first_geth.wait_to_add_peer(second_info.id).unwrap();
|
||||
|
||||
// check that second_geth exists in the first_geth peer list
|
||||
let peers = first_peer.peers().await.unwrap();
|
||||
|
||||
drop(first_geth);
|
||||
drop(second_geth);
|
||||
|
||||
// check that the second peer is in the list (it uses an enr so the enr should be Some)
|
||||
assert_eq!(peers.len(), 1);
|
||||
|
||||
let peer = peers.get(0).unwrap();
|
||||
assert_eq!(H256::from_str(&peer.id).unwrap(), second_info.id);
|
||||
|
||||
// remove directories
|
||||
std::fs::remove_dir_all(dir1).unwrap();
|
||||
std::fs::remove_dir_all(dir2).unwrap();
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue