diff --git a/Cargo.lock b/Cargo.lock index 48a6536..fe57469 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2943,6 +2943,7 @@ dependencies = [ "alloy-primitives", "alloy-signer-local", "clap", + "ev-deployer", "ev-node", "evolve-ev-reth", "eyre", diff --git a/bin/ev-deployer/src/config.rs b/bin/ev-deployer/src/config.rs index ba02a62..1d2f807 100644 --- a/bin/ev-deployer/src/config.rs +++ b/bin/ev-deployer/src/config.rs @@ -5,8 +5,8 @@ use serde::{Deserialize, Serialize}; use std::{collections::HashSet, path::Path}; /// Top-level deploy configuration. -#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] -pub(crate) struct DeployConfig { +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] +pub struct DeployConfig { /// Chain configuration. pub chain: ChainConfig, /// Contract configurations. @@ -15,15 +15,15 @@ pub(crate) struct DeployConfig { } /// Chain-level settings. -#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] -pub(crate) struct ChainConfig { +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] +pub struct ChainConfig { /// The chain ID. pub chain_id: u64, } /// All contract configurations. -#[derive(Debug, Clone, Serialize, Deserialize, Default, PartialEq)] -pub(crate) struct ContractsConfig { +#[derive(Debug, Clone, Serialize, Deserialize, Default, PartialEq, Eq)] +pub struct ContractsConfig { /// `AdminProxy` contract config (optional). pub admin_proxy: Option, /// `Permit2` contract config (optional). @@ -49,8 +49,8 @@ impl ContractsConfig { } /// `AdminProxy` configuration. -#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] -pub(crate) struct AdminProxyConfig { +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] +pub struct AdminProxyConfig { /// Address to deploy at (required for genesis, ignored for deploy). pub address: Option
, /// Owner address. @@ -58,15 +58,15 @@ pub(crate) struct AdminProxyConfig { } /// `Permit2` configuration (Uniswap token approval manager). -#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] -pub(crate) struct Permit2Config { +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] +pub struct Permit2Config { /// Address to deploy at (required for genesis, ignored for deploy). pub address: Option
, } impl DeployConfig { /// Load and validate config from a TOML file. - pub(crate) fn load(path: &Path) -> eyre::Result { + pub fn load(path: &Path) -> eyre::Result { let content = std::fs::read_to_string(path)?; let config: Self = toml::from_str(&content)?; config.validate()?; @@ -101,7 +101,7 @@ impl DeployConfig { } /// Additional validation for genesis mode: all addresses must be specified. - pub(crate) fn validate_for_genesis(&self) -> eyre::Result<()> { + pub fn validate_for_genesis(&self) -> eyre::Result<()> { if let Some(ref ap) = self.contracts.admin_proxy { eyre::ensure!( ap.address.is_some(), diff --git a/bin/ev-deployer/src/contracts/immutables.rs b/bin/ev-deployer/src/contracts/immutables.rs index 0934104..9eab22e 100644 --- a/bin/ev-deployer/src/contracts/immutables.rs +++ b/bin/ev-deployer/src/contracts/immutables.rs @@ -10,7 +10,7 @@ use alloy_primitives::{B256, U256}; /// A single immutable reference inside a bytecode blob. #[derive(Debug, Clone, Copy)] -pub(crate) struct ImmutableRef { +pub struct ImmutableRef { /// Byte offset into the **runtime** bytecode. pub start: usize, /// Number of bytes (always 32 for EVM words). @@ -22,7 +22,7 @@ pub(crate) struct ImmutableRef { /// # Panics /// /// Panics if any reference extends past the end of `bytecode`. -pub(crate) fn patch_bytes(bytecode: &mut [u8], refs: &[ImmutableRef], value: &[u8; 32]) { +pub fn patch_bytes(bytecode: &mut [u8], refs: &[ImmutableRef], value: &[u8; 32]) { for r in refs { assert!( r.start + r.length <= bytecode.len(), @@ -36,7 +36,7 @@ pub(crate) fn patch_bytes(bytecode: &mut [u8], refs: &[ImmutableRef], value: &[u } /// Convenience: patch with an ABI-encoded `uint256`. -pub(crate) fn patch_u256(bytecode: &mut [u8], refs: &[ImmutableRef], val: U256) { +pub fn patch_u256(bytecode: &mut [u8], refs: &[ImmutableRef], val: U256) { let word = B256::from(val); patch_bytes(bytecode, refs, &word.0); } diff --git a/bin/ev-deployer/src/contracts/mod.rs b/bin/ev-deployer/src/contracts/mod.rs index 9e61b58..c99c513 100644 --- a/bin/ev-deployer/src/contracts/mod.rs +++ b/bin/ev-deployer/src/contracts/mod.rs @@ -1,14 +1,15 @@ //! Contract bytecode and storage encoding. -pub(crate) mod admin_proxy; -pub(crate) mod immutables; -pub(crate) mod permit2; +pub mod admin_proxy; +pub mod immutables; +pub mod permit2; use alloy_primitives::{Address, Bytes, B256}; use std::collections::BTreeMap; /// A contract ready to be placed in genesis alloc. -pub(crate) struct GenesisContract { +#[derive(Debug)] +pub struct GenesisContract { /// The address to deploy at. pub address: Address, /// Runtime bytecode. diff --git a/bin/ev-deployer/src/deploy/deployer.rs b/bin/ev-deployer/src/deploy/deployer.rs index a673e2a..8a2812a 100644 --- a/bin/ev-deployer/src/deploy/deployer.rs +++ b/bin/ev-deployer/src/deploy/deployer.rs @@ -12,14 +12,16 @@ use async_trait::async_trait; /// Receipt from a confirmed transaction. #[derive(Debug)] -pub(crate) struct TxReceipt { +pub struct TxReceipt { + /// Hash of the confirmed transaction. pub tx_hash: B256, + /// Whether the transaction executed successfully. pub success: bool, } /// Abstracts on-chain operations for the deploy pipeline. #[async_trait] -pub(crate) trait ChainDeployer: Send + Sync { +pub trait ChainDeployer: Send + Sync { /// Get the chain ID of the connected chain. async fn chain_id(&self) -> eyre::Result; @@ -32,13 +34,19 @@ pub(crate) trait ChainDeployer: Send + Sync { } /// Live deployer using alloy provider + signer. -pub(crate) struct LiveDeployer { +pub struct LiveDeployer { provider: Box, } +impl std::fmt::Debug for LiveDeployer { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("LiveDeployer").finish_non_exhaustive() + } +} + impl LiveDeployer { /// Create a new `LiveDeployer` from an RPC URL and a hex-encoded private key. - pub(crate) fn new(rpc_url: &str, private_key_hex: &str) -> eyre::Result { + pub fn new(rpc_url: &str, private_key_hex: &str) -> eyre::Result { let key_hex = private_key_hex .strip_prefix("0x") .unwrap_or(private_key_hex); diff --git a/bin/ev-deployer/src/deploy/mod.rs b/bin/ev-deployer/src/deploy/mod.rs index 7990f24..5fae444 100644 --- a/bin/ev-deployer/src/deploy/mod.rs +++ b/bin/ev-deployer/src/deploy/mod.rs @@ -1,4 +1,4 @@ -pub(crate) mod create2; -pub(crate) mod deployer; -pub(crate) mod pipeline; -pub(crate) mod state; +pub mod create2; +pub mod deployer; +pub mod pipeline; +pub mod state; diff --git a/bin/ev-deployer/src/deploy/pipeline.rs b/bin/ev-deployer/src/deploy/pipeline.rs index 9db2ead..e03913e 100644 --- a/bin/ev-deployer/src/deploy/pipeline.rs +++ b/bin/ev-deployer/src/deploy/pipeline.rs @@ -13,17 +13,18 @@ use alloy_primitives::{Address, B256}; use std::path::{Path, PathBuf}; /// Configuration for the deploy pipeline. -pub(crate) struct PipelineConfig { +#[derive(Debug)] +pub struct PipelineConfig { + /// Parsed deploy configuration. pub config: DeployConfig, + /// Path to the persistent state file. pub state_path: PathBuf, + /// Optional path to write the address manifest. pub addresses_out: Option, } /// Run the full deploy pipeline. -pub(crate) async fn run( - pipeline_cfg: &PipelineConfig, - deployer: &dyn ChainDeployer, -) -> eyre::Result<()> { +pub async fn run(pipeline_cfg: &PipelineConfig, deployer: &dyn ChainDeployer) -> eyre::Result<()> { // ── Step 1: Init ── eprintln!("[1/4] Connecting to RPC..."); let chain_id = deployer.chain_id().await?; diff --git a/bin/ev-deployer/src/genesis.rs b/bin/ev-deployer/src/genesis.rs index 322f8ca..ed87ba1 100644 --- a/bin/ev-deployer/src/genesis.rs +++ b/bin/ev-deployer/src/genesis.rs @@ -9,7 +9,7 @@ use serde_json::{Map, Value}; use std::path::Path; /// Build the alloc JSON from config. -pub(crate) fn build_alloc(config: &DeployConfig) -> Value { +pub fn build_alloc(config: &DeployConfig) -> Value { let mut alloc = Map::new(); if let Some(ref ap_config) = config.contracts.admin_proxy { @@ -26,14 +26,15 @@ pub(crate) fn build_alloc(config: &DeployConfig) -> Value { } /// Build alloc and merge into an existing genesis JSON file. -pub(crate) fn merge_into( - config: &DeployConfig, - genesis_path: &Path, - force: bool, -) -> eyre::Result { +pub fn merge_into(config: &DeployConfig, genesis_path: &Path, force: bool) -> eyre::Result { let content = std::fs::read_to_string(genesis_path)?; let mut genesis: Value = serde_json::from_str(&content)?; + merge_alloc(config, &mut genesis, force)?; + Ok(genesis) +} +/// Merge deployer contracts into a genesis JSON value in memory. +pub fn merge_alloc(config: &DeployConfig, genesis: &mut Value, force: bool) -> eyre::Result<()> { let alloc = build_alloc(config); let genesis_alloc = genesis @@ -57,7 +58,7 @@ pub(crate) fn merge_into( genesis_alloc.insert(canonical, entry.clone()); } - Ok(genesis) + Ok(()) } fn normalize_addr(addr: &str) -> String { diff --git a/bin/ev-deployer/src/init.rs b/bin/ev-deployer/src/init.rs index f33509a..34a9846 100644 --- a/bin/ev-deployer/src/init.rs +++ b/bin/ev-deployer/src/init.rs @@ -1,14 +1,18 @@ //! Dynamic config template generation for the `init` command. /// Parameters for generating the init template. -pub(crate) struct InitParams { +#[derive(Debug)] +pub struct InitParams { + /// Target chain ID. pub chain_id: u64, + /// Whether to include Permit2 with its canonical address. pub permit2: bool, + /// Optional `AdminProxy` owner address. pub admin_proxy_owner: Option, } /// Generate a TOML config template based on the given parameters. -pub(crate) fn generate_template(params: &InitParams) -> String { +pub fn generate_template(params: &InitParams) -> String { let mut out = String::new(); // Header diff --git a/bin/ev-deployer/src/lib.rs b/bin/ev-deployer/src/lib.rs new file mode 100644 index 0000000..8df9541 --- /dev/null +++ b/bin/ev-deployer/src/lib.rs @@ -0,0 +1,13 @@ +//! EV Deployer — genesis alloc generator for ev-reth contracts. +//! +//! This crate provides both a CLI tool and a library for generating genesis +//! alloc entries from declarative TOML configurations. + +pub mod config; +pub mod contracts; +/// CREATE2 deploy pipeline for live chain deployment. +pub mod deploy; +pub mod genesis; +/// Dynamic config template generation for the `init` command. +pub mod init; +pub mod output; diff --git a/bin/ev-deployer/src/main.rs b/bin/ev-deployer/src/main.rs index b51dc62..092e3ce 100644 --- a/bin/ev-deployer/src/main.rs +++ b/bin/ev-deployer/src/main.rs @@ -1,14 +1,8 @@ //! EV Deployer — genesis alloc generator and live deployer for ev-reth contracts. -mod config; -mod contracts; -mod deploy; -mod genesis; -mod init; -mod output; - use alloy_primitives::Address; use clap::{Parser, Subcommand}; +use ev_deployer::{config, deploy, genesis, init, output}; use std::path::PathBuf; /// EV Deployer: generate genesis alloc or deploy ev-reth contracts. diff --git a/bin/ev-deployer/src/output.rs b/bin/ev-deployer/src/output.rs index 8053180..937aa4c 100644 --- a/bin/ev-deployer/src/output.rs +++ b/bin/ev-deployer/src/output.rs @@ -4,7 +4,7 @@ use crate::config::DeployConfig; use serde_json::{Map, Value}; /// Build an address manifest JSON from config. -pub(crate) fn build_manifest(config: &DeployConfig) -> Value { +pub fn build_manifest(config: &DeployConfig) -> Value { let mut manifest = Map::new(); if let Some(ref ap) = config.contracts.admin_proxy { diff --git a/bin/ev-dev/Cargo.toml b/bin/ev-dev/Cargo.toml index 99a72d1..5a10743 100644 --- a/bin/ev-dev/Cargo.toml +++ b/bin/ev-dev/Cargo.toml @@ -15,6 +15,7 @@ path = "src/main.rs" [dependencies] # Core evolve crates ev-node = { path = "../../crates/node" } +ev-deployer = { path = "../ev-deployer" } evolve-ev-reth = { path = "../../crates/evolve" } # Reth CLI and core dependencies diff --git a/bin/ev-dev/README.md b/bin/ev-dev/README.md index 39a615f..731a6e2 100644 --- a/bin/ev-dev/README.md +++ b/bin/ev-dev/README.md @@ -28,6 +28,7 @@ ev-dev [OPTIONS] | `--block-time` | `1` | Block time in seconds (`0` = mine on transaction) | | `--silent` | `false` | Suppress the startup banner | | `--accounts` | `10` | Number of accounts to display (1-20) | +| `--deploy-config` | — | Path to an ev-deployer TOML config to deploy contracts at genesis | ### Examples @@ -40,8 +41,38 @@ ev-dev --host 0.0.0.0 # Custom port, faster blocks ev-dev --port 9545 --block-time 2 + +# Start with genesis contracts deployed +ev-dev --deploy-config bin/ev-deployer/examples/devnet.toml ``` +## Genesis Contract Deployment + +You can deploy contracts into the genesis state by passing a `--deploy-config` flag pointing to an [ev-deployer](../ev-deployer/README.md) TOML config file. + +```bash +ev-dev --deploy-config path/to/deploy.toml +``` + +When a deploy config is provided, ev-dev will: + +1. Load and validate the config +2. Override the config's `chain_id` to match the devnet genesis (a warning is printed if they differ) +3. Merge the contract alloc entries into the genesis state before starting the node +4. Print the deployed contract addresses in the startup banner + +The startup banner will show an extra section: + +``` +Genesis Contracts (from path/to/deploy.toml) +================== + admin_proxy "0x000000000000000000000000000000000000Ad00" + fee_vault "0x000000000000000000000000000000000000FE00" + ... +``` + +See the [ev-deployer README](../ev-deployer/README.md) for full config reference and available contracts. + ## Chain Details | Property | Value | @@ -204,9 +235,10 @@ ev-dev includes all Evolve customizations out of the box: ev-dev is a thin wrapper around the full `ev-reth` node. On startup it: -1. Writes the embedded devnet genesis to a temp file -2. Creates a temporary data directory (clean state every run) -3. Launches `ev-reth` in `--dev` mode with networking disabled -4. Exposes HTTP and WebSocket RPC on the configured host/port +1. If `--deploy-config` is provided, loads the config and merges contract alloc entries into the genesis +2. Writes the (possibly extended) devnet genesis to a temp file +3. Creates a temporary data directory (clean state every run) +4. Launches `ev-reth` in `--dev` mode with networking disabled +5. Exposes HTTP and WebSocket RPC on the configured host/port Each run starts from a fresh genesis — there is no persistent state between restarts. diff --git a/bin/ev-dev/src/main.rs b/bin/ev-dev/src/main.rs index 7ed4e65..6eeef39 100644 --- a/bin/ev-dev/src/main.rs +++ b/bin/ev-dev/src/main.rs @@ -7,12 +7,13 @@ use alloy_signer_local::{coins_bip39::English, MnemonicBuilder}; use clap::Parser; +use ev_deployer::{config::DeployConfig, genesis::merge_alloc, output::build_manifest}; use evolve_ev_reth::{ config::EvolveConfig, rpc::txpool::{EvolveTxpoolApiImpl, EvolveTxpoolApiServer}, }; use reth_ethereum_cli::Cli; -use std::io::Write; +use std::{io::Write, path::PathBuf}; use tracing::info; use ev_node::{EvolveArgs, EvolveChainSpecParser, EvolveNode}; @@ -55,6 +56,10 @@ struct EvDevArgs { /// Number of accounts to display (1..=20) #[arg(long, default_value_t = 10, value_parser = parse_accounts)] accounts: usize, + + /// Path to an ev-deployer TOML config to deploy contracts at genesis. + #[arg(long, value_name = "PATH")] + deploy_config: Option, } fn derive_keys(count: usize) -> Vec<(String, String)> { @@ -84,7 +89,7 @@ fn chain_id_from_genesis() -> u64 { .expect("genesis must have config.chainId") } -fn print_banner(args: &EvDevArgs) { +fn print_banner(args: &EvDevArgs, deploy_cfg: Option<&DeployConfig>) { let accounts = derive_keys(args.accounts); println!(); @@ -124,6 +129,20 @@ fn print_banner(args: &EvDevArgs) { println!("Mnemonic: {HARDHAT_MNEMONIC}"); println!("Derivation path: m/44'/60'/0'/0/{{index}}"); println!(); + + if let Some(cfg) = deploy_cfg { + let config_path = args.deploy_config.as_ref().unwrap(); + println!("Genesis Contracts (from {})", config_path.display()); + println!("=================="); + let manifest = build_manifest(cfg); + if let Some(obj) = manifest.as_object() { + for (name, addr) in obj { + println!(" {name:20} {addr}"); + } + } + println!(); + } + println!("WARNING: These accounts and keys are publicly known."); println!("Any funds sent to them on mainnet WILL BE LOST."); println!(); @@ -138,15 +157,39 @@ fn main() { let dev_args = EvDevArgs::parse(); + let deploy_cfg = dev_args.deploy_config.as_ref().map(|config_path| { + let mut cfg = DeployConfig::load(config_path) + .unwrap_or_else(|e| panic!("failed to load deploy config: {e}")); + + let genesis_chain_id = chain_id_from_genesis(); + if cfg.chain.chain_id != genesis_chain_id { + eprintln!( + "WARNING: deploy config chain_id ({}) differs from devnet genesis ({}), overriding to {}", + cfg.chain.chain_id, genesis_chain_id, genesis_chain_id + ); + cfg.chain.chain_id = genesis_chain_id; + } + cfg + }); + if !dev_args.silent { - print_banner(&dev_args); + print_banner(&dev_args, deploy_cfg.as_ref()); } + let genesis_json = if let Some(ref cfg) = deploy_cfg { + let mut genesis: serde_json::Value = + serde_json::from_str(DEVNET_GENESIS).expect("valid genesis JSON"); + merge_alloc(cfg, &mut genesis, true).expect("failed to merge deploy config into genesis"); + serde_json::to_string(&genesis).expect("failed to serialize merged genesis") + } else { + DEVNET_GENESIS.to_string() + }; + // Write genesis to a temp file that lives for the process duration let mut genesis_file = tempfile::NamedTempFile::new().expect("failed to create temp genesis file"); genesis_file - .write_all(DEVNET_GENESIS.as_bytes()) + .write_all(genesis_json.as_bytes()) .expect("failed to write genesis"); let genesis_path = genesis_file .path() diff --git a/justfile b/justfile index 757a5b9..941833a 100644 --- a/justfile +++ b/justfile @@ -38,6 +38,14 @@ build-all: build-deployer: {{cargo}} build --release --bin ev-deployer +# Install ev-dev to ~/.cargo/bin +install-ev-dev: + {{cargo}} install --path bin/ev-dev + +# Install ev-deployer to ~/.cargo/bin +install-ev-deployer: + {{cargo}} install --path bin/ev-deployer + # Testing ────────────────────────────────────────────── # Run all tests