refactor: move cli utils to new reth-cli-utils crate (#790)

* Move bin/src/util to reth-cli-utils

* Add reth-cli-utils to workspace members

* Fix imports in bin/src

* Create reth-cli-utils crate

* Add utils import
This commit is contained in:
David Kulman
2023-01-10 01:13:41 +01:00
committed by GitHub
parent 7c9c2fea50
commit 515590faa8
14 changed files with 87 additions and 38 deletions

View File

@ -0,0 +1,24 @@
[package]
name = "reth-cli-utils"
version = "0.1.0"
edition = "2021"
license = "MIT"
repository = "https://github.com/paradigmxyz/reth"
readme = "README.md"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
reth-primitives = { path = "../../primitives" }
reth-consensus = { path = "../../consensus", features = ["serde"] }
reth-db = {path = "../../storage/db", features = ["mdbx", "test-utils"] }
serde = "1.0"
serde_json = "1.0"
eyre = "0.6.8"
shellexpand = "2.1"
walkdir = "2.3"
tracing = "0.1"
tracing-subscriber = { version = "0.3", features = ["env-filter"] }

View File

@ -0,0 +1,89 @@
use reth_primitives::{
utils::serde_helpers::deserialize_stringified_u64, Address, Bytes, Header, H256, U256,
};
use serde::{Deserialize, Serialize};
use std::{collections::HashMap, path::PathBuf};
/// Defines a chain, including it's genesis block, chain ID and fork block numbers.
#[derive(Clone, Debug, Default, Serialize, Deserialize)]
pub struct ChainSpecification {
/// Consensus configuration.
#[serde(rename = "config")]
pub consensus: reth_consensus::Config,
/// The genesis block of the chain.
#[serde(flatten)]
pub genesis: Genesis,
}
/// The genesis block specification.
#[derive(Clone, Debug, Default, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct Genesis {
/// The genesis header nonce.
#[serde(deserialize_with = "deserialize_stringified_u64")]
pub nonce: u64,
/// The genesis header timestamp.
#[serde(deserialize_with = "deserialize_stringified_u64")]
pub timestamp: u64,
/// The genesis header extra data.
pub extra_data: Bytes,
/// The genesis header gas limit.
#[serde(deserialize_with = "deserialize_stringified_u64")]
pub gas_limit: u64,
/// The genesis header difficulty.
pub difficulty: U256,
/// The genesis header mix hash.
pub mix_hash: H256,
/// The genesis header coinbase address.
pub coinbase: Address,
/// The genesis state root.
pub state_root: H256,
/// The initial state of accounts in the genesis block.
pub alloc: HashMap<Address, GenesisAccount>,
}
impl From<Genesis> for Header {
fn from(genesis: Genesis) -> Header {
Header {
gas_limit: genesis.gas_limit,
difficulty: genesis.difficulty,
nonce: genesis.nonce,
extra_data: genesis.extra_data,
state_root: genesis.state_root,
timestamp: genesis.timestamp,
mix_hash: genesis.mix_hash,
beneficiary: genesis.coinbase,
..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 at genesis.
#[serde(skip_serializing_if = "Option::is_none")]
pub nonce: Option<u64>,
/// The balance of the account at genesis.
pub balance: U256,
}
/// Clap value parser for [ChainSpecification]s that takes either a built-in chainspec or the path
/// to a custom one.
pub fn chain_spec_value_parser(s: &str) -> Result<ChainSpecification, eyre::Error> {
Ok(match s {
"mainnet" => {
serde_json::from_str(include_str!("../../../../bin/reth/res/chainspec/mainnet.json"))?
}
"goerli" => {
serde_json::from_str(include_str!("../../../../bin/reth/res/chainspec/goerli.json"))?
}
"sepolia" => {
serde_json::from_str(include_str!("../../../../bin/reth/res/chainspec/sepolia.json"))?
}
_ => {
let raw = std::fs::read_to_string(PathBuf::from(shellexpand::full(s)?.into_owned()))?;
serde_json::from_str(&raw)?
}
})
}

View File

@ -0,0 +1,61 @@
use crate::chainspec::Genesis;
use reth_db::{
cursor::DbCursorRO,
database::Database,
mdbx::{Env, WriteMap},
tables,
transaction::{DbTx, DbTxMut},
};
use reth_primitives::{Account, Header, H256};
use std::{path::Path, sync::Arc};
use tracing::debug;
/// Opens up an existing database or creates a new one at the specified path.
pub fn init_db<P: AsRef<Path>>(path: P) -> eyre::Result<Env<WriteMap>> {
std::fs::create_dir_all(path.as_ref())?;
let db = reth_db::mdbx::Env::<reth_db::mdbx::WriteMap>::open(
path.as_ref(),
reth_db::mdbx::EnvKind::RW,
)?;
db.create_tables()?;
Ok(db)
}
/// Write the genesis block if it has not already been written
#[allow(clippy::field_reassign_with_default)]
pub fn init_genesis<DB: Database>(db: Arc<DB>, genesis: Genesis) -> Result<H256, reth_db::Error> {
let tx = db.tx()?;
if let Some((_, hash)) = tx.cursor::<tables::CanonicalHeaders>()?.first()? {
debug!("Genesis already written, skipping.");
return Ok(hash)
}
drop(tx);
debug!("Writing genesis block.");
let tx = db.tx_mut()?;
// Insert account state
for (address, account) in &genesis.alloc {
tx.put::<tables::PlainAccountState>(
*address,
Account {
nonce: account.nonce.unwrap_or_default(),
balance: account.balance,
bytecode_hash: None,
},
)?;
}
// Insert header
let header: Header = genesis.into();
let hash = header.hash_slow();
tx.put::<tables::CanonicalHeaders>(0, hash)?;
tx.put::<tables::HeaderNumbers>(hash, 0)?;
tx.put::<tables::BlockBodies>((0, hash).into(), Default::default())?;
tx.put::<tables::BlockTransitionIndex>((0, hash).into(), 0)?;
tx.put::<tables::HeaderTD>((0, hash).into(), header.difficulty.into())?;
tx.put::<tables::Headers>((0, hash).into(), header)?;
tx.commit()?;
Ok(hash)
}

154
crates/cli/utils/src/lib.rs Normal file
View File

@ -0,0 +1,154 @@
#![warn(missing_docs, unreachable_pub)]
#![deny(unused_must_use, rust_2018_idioms)]
#![doc(test(
no_crate_inject,
attr(deny(warnings, rust_2018_idioms), allow(dead_code, unused_variables))
))]
//! Utility functions.
use reth_primitives::{BlockHashOrNumber, H256};
use std::{
env::VarError,
net::{SocketAddr, ToSocketAddrs},
path::{Path, PathBuf},
str::FromStr,
};
use walkdir::{DirEntry, WalkDir};
/// Utilities for parsing chainspecs
pub mod chainspec;
/// Utilities for initializing parts of the chain
pub mod init;
/// Finds all files in a directory with a given postfix.
pub fn find_all_files_with_postfix(path: &Path, postfix: &str) -> Vec<PathBuf> {
WalkDir::new(path)
.into_iter()
.filter_map(|e| e.ok())
.filter(|e| e.file_name().to_string_lossy().ends_with(postfix))
.map(DirEntry::into_path)
.collect::<Vec<PathBuf>>()
}
/// Parses a user-specified path with support for environment variables and common shorthands (e.g.
/// ~ for the user's home directory).
pub fn parse_path(value: &str) -> Result<PathBuf, shellexpand::LookupError<VarError>> {
shellexpand::full(value).map(|path| PathBuf::from(path.into_owned()))
}
/// Parse [BlockHashOrNumber]
pub fn hash_or_num_value_parser(value: &str) -> Result<BlockHashOrNumber, eyre::Error> {
match H256::from_str(value) {
Ok(hash) => Ok(BlockHashOrNumber::Hash(hash)),
Err(_) => Ok(BlockHashOrNumber::Number(value.parse()?)),
}
}
/// Parse a [SocketAddr] from a `str`.
///
/// The following formats are checked:
///
/// - If the value can be parsed as a `u16` or starts with `:` it is considered a port, and the
/// hostname is set to `localhost`.
/// - If the value contains `:` it is assumed to be the format `<host>:<port>`
/// - Otherwise it is assumed to be a hostname
///
/// An error is returned if the value is empty.
pub fn parse_socket_address(value: &str) -> Result<SocketAddr, eyre::Error> {
if value.is_empty() {
eyre::bail!("Cannot parse socket address from an empty string");
}
if value.starts_with(':') || value.parse::<u16>().is_ok() {
("localhost", 9000).to_socket_addrs()
} else if value.contains(':') {
value.to_socket_addrs()
} else {
(value, 9000).to_socket_addrs()
}?
.next()
.ok_or_else(|| eyre::eyre!("Could not parse socket address from {}", value))
}
/// Tracing utility
pub mod reth_tracing {
use tracing::Subscriber;
use tracing_subscriber::{prelude::*, EnvFilter};
/// Tracing modes
pub enum TracingMode {
/// Enable all traces.
All,
/// Enable debug traces.
Debug,
/// Enable info traces.
Info,
/// Enable warn traces.
Warn,
/// Enable error traces.
Error,
/// Disable tracing.
Silent,
}
impl TracingMode {
fn into_env_filter(self) -> EnvFilter {
match self {
Self::All => EnvFilter::new("reth=trace"),
Self::Debug => EnvFilter::new("reth=debug"),
Self::Info => EnvFilter::new("reth=info"),
Self::Warn => EnvFilter::new("reth=warn"),
Self::Error => EnvFilter::new("reth=error"),
Self::Silent => EnvFilter::new(""),
}
}
}
impl From<u8> for TracingMode {
fn from(value: u8) -> Self {
match value {
0 => Self::Error,
1 => Self::Warn,
2 => Self::Info,
3 => Self::Debug,
_ => Self::All,
}
}
}
/// Build subscriber
// TODO: JSON/systemd support
pub fn build_subscriber(mods: TracingMode) -> impl Subscriber {
// TODO: Auto-detect
let no_color = std::env::var("RUST_LOG_STYLE").map(|val| val == "never").unwrap_or(false);
let with_target = std::env::var("RUST_LOG_TARGET").map(|val| val != "0").unwrap_or(false);
// Take env over config
let filter = if std::env::var(EnvFilter::DEFAULT_ENV).unwrap_or_default().is_empty() {
mods.into_env_filter()
} else {
EnvFilter::from_default_env()
};
tracing_subscriber::registry()
.with(tracing_subscriber::fmt::layer().with_ansi(!no_color).with_target(with_target))
.with(filter)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn parse_socket_addresses() {
for value in ["localhost:9000", ":9000", "9000", "localhost"] {
let socket_addr = parse_socket_address(value)
.expect(&format!("could not parse socket address: {}", value));
assert!(socket_addr.ip().is_loopback());
assert_eq!(socket_addr.port(), 9000);
}
}
}