diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..b95b140 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,9 @@ +.git +.github +.claude +target +docs/node_modules +docs/.next +docs/dist +data +.data diff --git a/Cargo.toml b/Cargo.toml index a461c5d..5486f47 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -78,6 +78,8 @@ commonware-codec = "0.0.65" borsh = { features = ["derive"], version = "1.5.5" } clap = { version = "4.5.31", features = ["derive"] } fixed = { version = "1.29", features = ["borsh", "serde"] } +serde = { version = "1.0", features = ["derive"] } +serde_json = "1.0" tracing = "0.1" tracing-subscriber = { version = "0.3", features = ["env-filter"] } tokio = { version = "1.44.2", features = ["full"] } diff --git a/README.md b/README.md index 033da17..355138a 100644 --- a/README.md +++ b/README.md @@ -39,6 +39,26 @@ just node-run Default RPC endpoint: `http://localhost:8545`. +### Run `evd` (and optionally `ev-node`) with Docker Compose + +Run `evd` only: + +```bash +docker compose up --build evd +``` + +Run `evd` + `ev-node`: + +```bash +docker compose -f docker-compose.yml -f docker-compose.ev-node.yml up --build +``` + +Notes: +- `evd` JSON-RPC is exposed on `http://localhost:8545` +- `evd` gRPC is exposed on `localhost:50051` +- `ev-node` gets `EVD_GRPC_ENDPOINT=evd:50051` in the compose network +- if your `ev-node` image needs explicit startup flags, override `command` for the `ev-node` service with an extra compose override file + ## Documentation Read the docs for implementation details instead of this README. diff --git a/bin/evd/Cargo.toml b/bin/evd/Cargo.toml index d223cce..00b44fd 100644 --- a/bin/evd/Cargo.toml +++ b/bin/evd/Cargo.toml @@ -24,7 +24,6 @@ evolve_evnode = { workspace = true, features = ["testapp"] } evolve_testapp.workspace = true evolve_token.workspace = true evolve_scheduler.workspace = true -evolve_fungible_asset.workspace = true evolve_testing.workspace = true evolve_tx_eth.workspace = true evolve_chain_index.workspace = true @@ -42,8 +41,6 @@ tokio.workspace = true tracing.workspace = true tracing-subscriber.workspace = true borsh.workspace = true -serde = { version = "1.0", features = ["derive"] } -serde_json = "1.0" [lints] workspace = true diff --git a/bin/evd/src/main.rs b/bin/evd/src/main.rs index 74af602..d60fcc9 100644 --- a/bin/evd/src/main.rs +++ b/bin/evd/src/main.rs @@ -62,8 +62,6 @@ //! } //! ``` -mod genesis_config; - use std::sync::atomic::{AtomicU64, Ordering}; use std::sync::Arc; use std::time::Duration; @@ -76,8 +74,7 @@ use evolve_chain_index::{ build_index_data, BlockMetadata, ChainIndex, ChainStateProvider, ChainStateProviderConfig, PersistentChainIndex, }; -use evolve_core::runtime_api::ACCOUNT_IDENTIFIER_PREFIX; -use evolve_core::{AccountId, Environment, Message, ReadonlyKV, SdkResult}; +use evolve_core::{AccountId, ReadonlyKV}; use evolve_eth_jsonrpc::{start_server_with_subscriptions, RpcServerConfig, SubscriptionManager}; use evolve_evnode::{EvnodeServer, EvnodeServerConfig, ExecutorServiceConfig, OnBlockExecuted}; use evolve_mempool::{new_shared_mempool, Mempool, SharedMempool}; @@ -86,23 +83,19 @@ use evolve_node::{ GenesisOutput, InitArgs, NodeConfig, RunArgs, }; use evolve_rpc_types::SyncStatus; -use evolve_scheduler::scheduler_account::SchedulerRef; use evolve_server::{ load_chain_state, save_chain_state, BlockBuilder, ChainState, CHAIN_STATE_KEY, }; use evolve_stf_traits::{AccountsCodeStorage, StateChange}; use evolve_storage::{Operation, QmdbStorage, Storage, StorageConfig}; +use evolve_testapp::genesis_config::{load_genesis_config, EvdGenesisConfig, EvdGenesisResult}; use evolve_testapp::{ - build_mempool_stf, default_gas_config, do_genesis_inner, install_account_codes, - PLACEHOLDER_ACCOUNT, + build_mempool_stf, default_gas_config, do_genesis_inner, initialize_custom_genesis_resources, + install_account_codes, PLACEHOLDER_ACCOUNT, }; use evolve_testing::server_mocks::AccountStorageMock; -use evolve_token::account::TokenRef; -use evolve_tx_eth::address_to_account_id; use evolve_tx_eth::TxContext; -use genesis_config::{EvdGenesisConfig, EvdGenesisResult}; - #[derive(Parser)] #[command(name = "evd")] #[command(about = "Evolve node daemon with gRPC execution layer")] @@ -144,25 +137,30 @@ fn main() { Commands::Run(args) => { let config = resolve_node_config(&args.common, &args.native); init_node_tracing(&config.observability.log_level); - let genesis_config = load_genesis_config(args.custom.genesis_file.as_deref()); + let genesis_config = match load_genesis_config(args.custom.genesis_file.as_deref()) { + Ok(genesis_config) => genesis_config, + Err(err) => { + tracing::error!("{err}"); + std::process::exit(2); + } + }; run_node(config, genesis_config); } Commands::Init(args) => { let config = resolve_node_config_init(&args.common); init_node_tracing(&config.observability.log_level); - let genesis_config = load_genesis_config(args.custom.genesis_file.as_deref()); + let genesis_config = match load_genesis_config(args.custom.genesis_file.as_deref()) { + Ok(genesis_config) => genesis_config, + Err(err) => { + tracing::error!("{err}"); + std::process::exit(2); + } + }; init_genesis(&config.storage.path, genesis_config); } } } -fn load_genesis_config(path: Option<&str>) -> Option { - path.map(|p| { - tracing::info!("Loading genesis config from: {}", p); - EvdGenesisConfig::load(p).expect("failed to load genesis config") - }) -} - fn run_node(config: NodeConfig, genesis_config: Option) { tracing::info!("=== Evolve Node Daemon (evd) ==="); @@ -201,7 +199,7 @@ fn run_node(config: NodeConfig, genesis_config: Option) { } None => { tracing::info!("No existing state found, running genesis..."); - let output = run_genesis(&storage, &codes, genesis_config.as_ref()).await; + let output = run_genesis(&storage, &codes, genesis_config.as_ref()); commit_genesis(&storage, output.changes, &output.genesis_result) .await .expect("genesis commit failed"); @@ -439,7 +437,7 @@ fn init_genesis(data_dir: &str, genesis_config: Option) { } let codes = build_codes(); - let output = run_genesis(&storage, &codes, genesis_config.as_ref()).await; + let output = run_genesis(&storage, &codes, genesis_config.as_ref()); commit_genesis(&storage, output.changes, &output.genesis_result) .await @@ -457,48 +455,14 @@ fn build_codes() -> AccountStorageMock { codes } -/// Pre-register an EOA account in storage so genesis can reference it. -fn build_eoa_registration(account_id: AccountId, eth_address: [u8; 20]) -> Vec { - let mut ops = Vec::with_capacity(3); - - // 1. Register account code identifier - let mut key = vec![ACCOUNT_IDENTIFIER_PREFIX]; - key.extend_from_slice(&account_id.as_bytes()); - let value = Message::new(&"EthEoaAccount".to_string()) - .unwrap() - .into_bytes() - .unwrap(); - ops.push(Operation::Set { key, value }); - - // 2. Set nonce = 0 (Item prefix 0) - let mut nonce_key = account_id.as_bytes().to_vec(); - nonce_key.push(0u8); - let nonce_value = Message::new(&0u64).unwrap().into_bytes().unwrap(); - ops.push(Operation::Set { - key: nonce_key, - value: nonce_value, - }); - - // 3. Set eth_address (Item prefix 1) - let mut addr_key = account_id.as_bytes().to_vec(); - addr_key.push(1u8); - let addr_value = Message::new(ð_address).unwrap().into_bytes().unwrap(); - ops.push(Operation::Set { - key: addr_key, - value: addr_value, - }); - - ops -} - /// Run genesis using the default testapp genesis or a custom genesis config. -async fn run_genesis( +fn run_genesis( storage: &S, codes: &AccountStorageMock, genesis_config: Option<&EvdGenesisConfig>, ) -> GenesisOutput { match genesis_config { - Some(config) => run_custom_genesis(storage, codes, config).await, + Some(config) => run_custom_genesis(storage, codes, config), None => run_default_genesis(storage, codes), } } @@ -534,55 +498,19 @@ fn run_default_genesis( } /// Custom genesis with ETH EOA accounts from a genesis JSON file. -async fn run_custom_genesis( +/// +/// Registers funded EOA accounts via `EthEoaAccountRef::initialize` inside +/// `system_exec`, then initializes the token with their balances. +fn run_custom_genesis( storage: &S, codes: &AccountStorageMock, genesis_config: &EvdGenesisConfig, ) -> GenesisOutput { use evolve_core::BlockContext; - // Parse accounts that have a non-zero balance (need pre-registration for genesis funding). - // Other accounts are auto-registered by the STF on their first transaction. - let funded_accounts: Vec<(Address, u128)> = genesis_config - .accounts - .iter() - .filter(|acc| acc.balance > 0) - .map(|acc| { - let addr = acc - .parse_address() - .expect("invalid address in genesis config"); - (addr, acc.balance) - }) - .collect(); - - // Pre-register only funded EOA accounts in storage - let mut pre_ops = Vec::new(); - for (addr, _) in &funded_accounts { - let id = address_to_account_id(*addr); - let addr_bytes: [u8; 20] = addr.into_array(); - pre_ops.extend(build_eoa_registration(id, addr_bytes)); - } - - storage - .batch(pre_ops) - .await - .expect("pre-register EOAs failed"); - storage.commit().await.expect("pre-register commit failed"); - - tracing::info!( - "Pre-registered {} funded EOA accounts:", - funded_accounts.len() - ); - for (i, (addr, balance)) in funded_accounts.iter().enumerate() { - let id = address_to_account_id(*addr); - tracing::info!(" #{:02}: {:?} (0x{:x}) balance={}", i, id, addr, balance); - } - - // Build balances list for genesis token initialization - let balances: Vec<(AccountId, u128)> = funded_accounts - .iter() - .map(|(addr, balance)| (address_to_account_id(*addr), *balance)) - .collect(); + let funded_accounts = genesis_config + .funded_accounts() + .expect("invalid address in genesis config"); let minter = AccountId::new(genesis_config.minter_id); let metadata = genesis_config.token.to_metadata(); @@ -593,7 +521,17 @@ async fn run_custom_genesis( let (genesis_result, state) = stf .system_exec(storage, codes, genesis_block, |env| { - do_custom_genesis(metadata.clone(), balances.clone(), minter, env) + let resources = initialize_custom_genesis_resources( + &funded_accounts, + metadata.clone(), + minter, + env, + )?; + + Ok(EvdGenesisResult { + token: resources.token, + scheduler: resources.scheduler, + }) }) .expect("genesis failed"); @@ -605,23 +543,6 @@ async fn run_custom_genesis( } } -fn do_custom_genesis( - metadata: evolve_fungible_asset::FungibleAssetMetadata, - balances: Vec<(AccountId, u128)>, - minter: AccountId, - env: &mut dyn Environment, -) -> SdkResult { - let token = TokenRef::initialize(metadata, balances, Some(minter), env)?.0; - - let scheduler_acc = SchedulerRef::initialize(vec![], vec![], env)?.0; - scheduler_acc.update_begin_blockers(vec![], env)?; - - Ok(EvdGenesisResult { - token: token.0, - scheduler: scheduler_acc.0, - }) -} - fn compute_block_hash(height: u64, timestamp: u64, parent_hash: B256) -> B256 { let mut data = Vec::with_capacity(48); data.extend_from_slice(&height.to_le_bytes()); diff --git a/bin/testapp/Cargo.toml b/bin/testapp/Cargo.toml index f9a9579..50c97f8 100644 --- a/bin/testapp/Cargo.toml +++ b/bin/testapp/Cargo.toml @@ -23,6 +23,10 @@ evolve_storage.workspace = true evolve_node.workspace = true evolve_mempool.workspace = true evolve_tx_eth.workspace = true +evolve_grpc.workspace = true +evolve_chain_index.workspace = true +evolve_eth_jsonrpc.workspace = true +evolve_rpc_types.workspace = true alloy-primitives.workspace = true alloy-consensus = { workspace = true, features = ["k256"] } k256 = { version = "0.13", features = ["ecdsa", "arithmetic"] } @@ -30,6 +34,8 @@ rand = "0.8" tiny-keccak = { version = "2.0", features = ["keccak"] } borsh.workspace = true +serde.workspace = true +serde_json.workspace = true tracing = { workspace = true } tracing-subscriber = { workspace = true } tokio.workspace = true @@ -48,7 +54,6 @@ commonware-runtime.workspace = true [dev-dependencies] tempfile = "3.8" criterion = "0.5" -serde_json = "1.0" alloy-primitives = { workspace = true } async-trait = { workspace = true } evolve_mempool = { workspace = true } diff --git a/bin/evd/src/genesis_config.rs b/bin/testapp/src/genesis_config.rs similarity index 75% rename from bin/evd/src/genesis_config.rs rename to bin/testapp/src/genesis_config.rs index 3b8dea4..d0a1e75 100644 --- a/bin/evd/src/genesis_config.rs +++ b/bin/testapp/src/genesis_config.rs @@ -3,6 +3,9 @@ use borsh::{BorshDeserialize, BorshSerialize}; use evolve_core::AccountId; use evolve_fungible_asset::FungibleAssetMetadata; use serde::Deserialize; +use std::collections::BTreeSet; + +pub type FundedAccount = ([u8; 20], u128); /// Evd genesis configuration loaded from JSON. #[derive(Deserialize)] @@ -49,12 +52,41 @@ impl EvdGenesisConfig { if self.accounts.is_empty() { return Err("genesis config must have at least one account".into()); } + let mut seen = BTreeSet::new(); for (i, acc) in self.accounts.iter().enumerate() { - acc.parse_address() + let addr = acc + .parse_address() .map_err(|e| format!("account[{}]: {}", i, e))?; + if !seen.insert(addr.into_array()) { + return Err(format!( + "account[{i}]: duplicate eth address '{}'", + acc.eth_address + )); + } } Ok(()) } + + /// Return funded accounts as (eth_address, balance) tuples for genesis execution. + pub fn funded_accounts(&self) -> Result, String> { + self.accounts + .iter() + .filter(|acc| acc.balance > 0) + .map(|acc| { + let addr = acc.parse_address()?; + Ok((addr.into_array(), acc.balance)) + }) + .collect() + } +} + +/// Load an optional genesis config and validate it. +pub fn load_genesis_config(path: Option<&str>) -> Result, String> { + path.map(|p| { + tracing::info!("Loading genesis config from: {}", p); + EvdGenesisConfig::load(p) + }) + .transpose() } impl TokenConfig { @@ -179,6 +211,38 @@ mod tests { let _ = std::fs::remove_file(path); } + #[test] + fn load_fails_for_duplicate_account_address() { + let path = temp_genesis_file( + r#"{ + "token": { + "name": "Evolve", + "symbol": "EV", + "decimals": 6, + "icon_url": "https://example.com/icon.png", + "description": "token" + }, + "minter_id": 100, + "accounts": [ + { + "eth_address": "0x0000000000000000000000000000000000000001", + "balance": 1000 + }, + { + "eth_address": "0x0000000000000000000000000000000000000001", + "balance": 2000 + } + ] + }"#, + ); + + let err = EvdGenesisConfig::load(path.to_str().unwrap()) + .err() + .expect("duplicate address must fail"); + assert!(err.contains("duplicate eth address")); + let _ = std::fs::remove_file(path); + } + #[test] fn parse_address_rejects_invalid_and_accepts_valid() { let bad = AccountConfig { diff --git a/bin/testapp/src/lib.rs b/bin/testapp/src/lib.rs index a1bcfb1..8af70ac 100644 --- a/bin/testapp/src/lib.rs +++ b/bin/testapp/src/lib.rs @@ -1,4 +1,5 @@ pub mod eth_eoa; +pub mod genesis_config; pub mod sim_testing; use crate::eth_eoa::eth_eoa_account::{EthEoaAccount, EthEoaAccountRef}; @@ -77,6 +78,55 @@ pub struct GenesisAccounts { pub scheduler: AccountId, } +/// Shared custom-genesis resources used by evd and testapp binaries. +#[derive(Clone, Copy, Debug)] +pub struct CustomGenesisResources { + pub alice: Option, + pub bob: Option, + pub token: AccountId, + pub scheduler: AccountId, +} + +/// Initialize funded EOAs, token, and scheduler for custom genesis. +pub fn initialize_custom_genesis_resources( + funded_accounts: &[([u8; 20], u128)], + metadata: FungibleAssetMetadata, + minter: AccountId, + env: &mut dyn Environment, +) -> SdkResult { + for (eth_addr, _) in funded_accounts { + EthEoaAccountRef::initialize(*eth_addr, env)?; + } + + let balances: Vec<(AccountId, u128)> = funded_accounts + .iter() + .map(|(eth_addr, balance)| { + let addr = alloy_primitives::Address::from(*eth_addr); + (address_to_account_id(addr), *balance) + }) + .collect(); + + let token = TokenRef::initialize(metadata, balances, Some(minter), env)?.0; + + let scheduler_acc = SchedulerRef::initialize(vec![], vec![], env)?.0; + scheduler_acc.update_begin_blockers(vec![], env)?; + + let alice = funded_accounts + .first() + .map(|(eth_addr, _)| address_to_account_id(alloy_primitives::Address::from(*eth_addr))); + let bob = funded_accounts + .get(1) + .map(|(eth_addr, _)| address_to_account_id(alloy_primitives::Address::from(*eth_addr))) + .or(alice); + + Ok(CustomGenesisResources { + alice, + bob, + token: token.0, + scheduler: scheduler_acc.0, + }) +} + fn parse_genesis_address_env(var: &str) -> Option<[u8; 20]> { use alloy_primitives::Address; use std::str::FromStr; diff --git a/bin/testapp/src/main.rs b/bin/testapp/src/main.rs index d3e6f0a..fb64f69 100644 --- a/bin/testapp/src/main.rs +++ b/bin/testapp/src/main.rs @@ -5,12 +5,14 @@ use evolve_core::ReadonlyKV; use evolve_node::{ init_dev_node, init_tracing as init_node_tracing, resolve_node_config, resolve_node_config_init, run_dev_node_with_rpc_and_mempool_eth, - run_dev_node_with_rpc_and_mempool_mock_storage, GenesisOutput, InitArgs, NoArgs, RunArgs, + run_dev_node_with_rpc_and_mempool_mock_storage, GenesisOutput, InitArgs, RunArgs, }; use evolve_storage::{QmdbStorage, Storage, StorageConfig}; +use evolve_testapp::genesis_config::{load_genesis_config, EvdGenesisConfig}; use evolve_testapp::{ - build_mempool_stf, default_gas_config, do_eth_genesis_inner, install_account_codes, - GenesisAccounts, MempoolStf, PLACEHOLDER_ACCOUNT, + build_mempool_stf, default_gas_config, do_eth_genesis_inner, + initialize_custom_genesis_resources, install_account_codes, GenesisAccounts, MempoolStf, + PLACEHOLDER_ACCOUNT, }; use evolve_testing::server_mocks::AccountStorageMock; @@ -32,13 +34,24 @@ enum Commands { } type TestappRunArgs = RunArgs; -type TestappInitArgs = InitArgs; +type TestappInitArgs = InitArgs; #[derive(Args)] struct TestappRunCustom { /// Use in-memory mock storage instead of persistent storage #[arg(long)] mock_storage: bool, + + /// Path to a genesis JSON file with ETH accounts (uses default Alice/Bob genesis if omitted) + #[arg(long)] + genesis_file: Option, +} + +#[derive(Args)] +struct TestappInitCustom { + /// Path to a genesis JSON file with ETH accounts (uses default Alice/Bob genesis if omitted) + #[arg(long)] + genesis_file: Option, } fn main() { @@ -49,14 +62,25 @@ fn main() { let config = resolve_node_config(&args.common, &args.native); init_node_tracing(&config.observability.log_level); + let genesis_config = match load_genesis_config(args.custom.genesis_file.as_deref()) { + Ok(genesis_config) => genesis_config, + Err(err) => { + tracing::error!("{err}"); + std::process::exit(2); + } + }; + let rpc_config = config.to_rpc_config(); + if args.custom.mock_storage { run_dev_node_with_rpc_and_mempool_mock_storage( &config.storage.path, build_genesis_stf, build_stf_from_genesis, build_codes, - run_genesis_output, + move |stf, codes, storage| { + run_genesis_output(stf, codes, storage, genesis_config.as_ref()) + }, rpc_config, ); } else { @@ -65,7 +89,9 @@ fn main() { build_genesis_stf, build_stf_from_genesis, build_codes, - run_genesis_output, + move |stf, codes, storage| { + run_genesis_output(stf, codes, storage, genesis_config.as_ref()) + }, build_storage, rpc_config, ); @@ -75,11 +101,21 @@ fn main() { let config = resolve_node_config_init(&args.common); init_node_tracing(&config.observability.log_level); + let genesis_config = match load_genesis_config(args.custom.genesis_file.as_deref()) { + Ok(genesis_config) => genesis_config, + Err(err) => { + tracing::error!("{err}"); + std::process::exit(2); + } + }; + init_dev_node( &config.storage.path, build_genesis_stf, build_codes, - run_genesis_output, + move |stf, codes, storage| { + run_genesis_output(stf, codes, storage, genesis_config.as_ref()) + }, build_storage, ); } @@ -104,6 +140,18 @@ fn run_genesis_output( stf: &MempoolStf, codes: &AccountStorageMock, storage: &S, + genesis_config: Option<&EvdGenesisConfig>, +) -> Result, Box> { + match genesis_config { + Some(config) => run_custom_genesis(stf, codes, storage, config), + None => run_default_genesis(stf, codes, storage), + } +} + +fn run_default_genesis( + stf: &MempoolStf, + codes: &AccountStorageMock, + storage: &S, ) -> Result, Box> { use alloy_primitives::Address; use evolve_core::BlockContext; @@ -141,6 +189,49 @@ fn run_genesis_output( }) } +fn run_custom_genesis( + stf: &MempoolStf, + codes: &AccountStorageMock, + storage: &S, + config: &EvdGenesisConfig, +) -> Result, Box> { + use evolve_core::{AccountId, BlockContext}; + + let funded_accounts = config.funded_accounts()?; + + let minter = AccountId::new(config.minter_id); + let metadata = config.token.to_metadata(); + + let genesis_block = BlockContext::new(0, 0); + + let (accounts, state) = stf + .system_exec(storage, codes, genesis_block, |env| { + let resources = initialize_custom_genesis_resources( + &funded_accounts, + metadata.clone(), + minter, + env, + )?; + let alice = resources.alice.unwrap_or(resources.token); + let bob = resources.bob.unwrap_or(alice); + + Ok(GenesisAccounts { + alice, + bob, + atom: resources.token, + scheduler: resources.scheduler, + }) + }) + .map_err(|e| format!("{:?}", e))?; + + let changes = state.into_changes().map_err(|e| format!("{:?}", e))?; + + Ok(GenesisOutput { + genesis_result: accounts, + changes, + }) +} + async fn build_storage( context: commonware_runtime::tokio::Context, config: StorageConfig, diff --git a/crates/app/node/Cargo.toml b/crates/app/node/Cargo.toml index e97ab25..c43d213 100644 --- a/crates/app/node/Cargo.toml +++ b/crates/app/node/Cargo.toml @@ -29,6 +29,7 @@ evolve_stf_traits = { workspace = true } evolve_storage = { workspace = true } evolve_chain_index = { workspace = true } evolve_eth_jsonrpc = { workspace = true } +evolve_grpc = { workspace = true } evolve_rpc_types = { workspace = true } [lints] diff --git a/crates/app/node/src/config.rs b/crates/app/node/src/config.rs index 4159f7a..8a68f45 100644 --- a/crates/app/node/src/config.rs +++ b/crates/app/node/src/config.rs @@ -73,6 +73,7 @@ impl NodeConfig { chain_id: self.chain.chain_id, enabled: self.rpc.enabled, enable_block_indexing: self.rpc.enable_block_indexing, + grpc_addr: Some(self.parsed_grpc_addr()), } } } diff --git a/crates/app/node/src/lib.rs b/crates/app/node/src/lib.rs index 652c9d5..9130d9e 100644 --- a/crates/app/node/src/lib.rs +++ b/crates/app/node/src/lib.rs @@ -23,6 +23,7 @@ use evolve_chain_index::{ChainStateProvider, ChainStateProviderConfig, Persisten use evolve_core::encoding::Encodable; use evolve_core::ReadonlyKV; use evolve_eth_jsonrpc::{start_server_with_subscriptions, RpcServerConfig, SubscriptionManager}; +use evolve_grpc::{GrpcServer, GrpcServerConfig}; use evolve_mempool::{new_shared_mempool, Mempool, MempoolTx, SharedMempool}; use evolve_rpc_types::SyncStatus; use evolve_server::{ @@ -116,6 +117,9 @@ pub struct RpcConfig { pub enabled: bool, /// Whether block indexing is enabled while producing blocks. pub enable_block_indexing: bool, + /// Optional gRPC server address. When set, a gRPC server is started + /// alongside JSON-RPC, sharing the same state provider and subscriptions. + pub grpc_addr: Option, } impl Default for RpcConfig { @@ -125,6 +129,7 @@ impl Default for RpcConfig { chain_id: 1, enabled: true, enable_block_indexing: true, + grpc_addr: None, } } } @@ -155,6 +160,12 @@ impl RpcConfig { self.enable_block_indexing = enabled; self } + + /// Enable the gRPC server on the given address. + pub fn with_grpc(mut self, addr: SocketAddr) -> Self { + self.grpc_addr = Some(addr); + self + } } /// Result of a genesis run, including the state changes to commit. @@ -398,8 +409,8 @@ pub fn run_dev_node_with_rpc< }; let state_provider = ChainStateProvider::with_account_codes( Arc::clone(&chain_index), - state_provider_config, - codes_for_rpc, + state_provider_config.clone(), + Arc::clone(&codes_for_rpc), ); // Start JSON-RPC server @@ -417,6 +428,32 @@ pub fn run_dev_node_with_rpc< .await .expect("failed to start RPC server"); + let grpc_handle = if let Some(grpc_addr) = rpc_config.grpc_addr { + let grpc_state_provider = ChainStateProvider::with_account_codes( + Arc::clone(&chain_index), + state_provider_config, + codes_for_rpc, + ); + let grpc_config = GrpcServerConfig { + addr: grpc_addr, + chain_id: rpc_config.chain_id, + ..Default::default() + }; + tracing::info!("Starting gRPC server on {}", grpc_addr); + let grpc_server = GrpcServer::with_subscription_manager( + grpc_config, + grpc_state_provider, + Arc::clone(&subscriptions), + ); + Some(tokio::spawn(async move { + if let Err(e) = grpc_server.serve().await { + tracing::error!("gRPC server error: {}", e); + } + })) + } else { + None + }; + // Create DevConsensus with RPC support let consensus = DevConsensus::with_rpc( stf, @@ -468,7 +505,7 @@ pub fn run_dev_node_with_rpc< tracing::info!("Saved chain state at height {}", final_height); } - Some(handle) + Some((handle, grpc_handle)) } else { // No RPC - use simple DevConsensus let consensus = DevConsensus::new(stf, storage, codes, dev_config) @@ -515,10 +552,15 @@ pub fn run_dev_node_with_rpc< }; // Stop RPC server if running - if let Some(handle) = rpc_handle { + if let Some((handle, grpc_handle)) = rpc_handle { tracing::info!("Stopping RPC server..."); handle.stop().expect("failed to stop RPC server"); tracing::info!("RPC server stopped"); + if let Some(grpc_handle) = grpc_handle { + tracing::info!("Stopping gRPC server..."); + grpc_handle.abort(); + tracing::info!("gRPC server stopped"); + } } } }); @@ -828,8 +870,8 @@ pub fn run_dev_node_with_rpc_and_mempool_eth< }; let state_provider = ChainStateProvider::with_mempool( Arc::clone(&chain_index), - state_provider_config, - codes_for_rpc, + state_provider_config.clone(), + Arc::clone(&codes_for_rpc), mempool.clone(), ); @@ -847,6 +889,34 @@ pub fn run_dev_node_with_rpc_and_mempool_eth< .await .expect("failed to start RPC server"); + // Start gRPC server if configured + let grpc_handle = if let Some(grpc_addr) = rpc_config.grpc_addr { + let grpc_state_provider = ChainStateProvider::with_mempool( + Arc::clone(&chain_index), + state_provider_config, + codes_for_rpc, + mempool.clone(), + ); + let grpc_config = GrpcServerConfig { + addr: grpc_addr, + chain_id: rpc_config.chain_id, + ..Default::default() + }; + tracing::info!("Starting gRPC server on {}", grpc_addr); + let grpc_server = GrpcServer::with_subscription_manager( + grpc_config, + grpc_state_provider, + Arc::clone(&subscriptions), + ); + Some(tokio::spawn(async move { + if let Err(e) = grpc_server.serve().await { + tracing::error!("gRPC server error: {}", e); + } + })) + } else { + None + }; + let consensus = DevConsensus::with_rpc_and_mempool( stf, storage, @@ -895,7 +965,7 @@ pub fn run_dev_node_with_rpc_and_mempool_eth< tracing::info!("Saved chain state at height {}", final_height); } - Some(handle) + Some((handle, grpc_handle)) } else { let consensus = DevConsensus::with_mempool(stf, storage, codes, dev_config, mempool) .with_block_archive(archive_cb); @@ -939,10 +1009,15 @@ pub fn run_dev_node_with_rpc_and_mempool_eth< None }; - if let Some(handle) = rpc_handle { + if let Some((handle, grpc_handle)) = rpc_handle { tracing::info!("Stopping RPC server..."); handle.stop().expect("failed to stop RPC server"); tracing::info!("RPC server stopped"); + if let Some(grpc_handle) = grpc_handle { + tracing::info!("Stopping gRPC server..."); + grpc_handle.abort(); + tracing::info!("gRPC server stopped"); + } } } }); diff --git a/docker-compose.ev-node.yml b/docker-compose.ev-node.yml new file mode 100644 index 0000000..c6dfe36 --- /dev/null +++ b/docker-compose.ev-node.yml @@ -0,0 +1,7 @@ +services: + ev-node: + image: ghcr.io/evstack/ev-node-grpc:latest + depends_on: + - evd + environment: + EVD_GRPC_ENDPOINT: evd:50051 diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..d92806a --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,22 @@ +services: + evd: + build: + context: . + dockerfile: docker/evd/Dockerfile + image: evolve/evd:local + command: + - run + - --grpc-addr + - 0.0.0.0:50051 + - --rpc-addr + - 0.0.0.0:8545 + - --data-dir + - /var/lib/evolve/data + ports: + - "8545:8545" + - "50051:50051" + volumes: + - evd-data:/var/lib/evolve/data + +volumes: + evd-data: diff --git a/docker/evd/Dockerfile b/docker/evd/Dockerfile new file mode 100644 index 0000000..0d390ca --- /dev/null +++ b/docker/evd/Dockerfile @@ -0,0 +1,33 @@ +FROM rust:bookworm AS builder + +WORKDIR /app + +RUN apt-get update \ + && apt-get install -y --no-install-recommends protobuf-compiler libprotobuf-dev \ + && rm -rf /var/lib/apt/lists/* + +COPY . . + +RUN cargo build -p evd --release + +FROM debian:bookworm-slim AS runtime + +RUN apt-get update \ + && apt-get install -y --no-install-recommends ca-certificates \ + && rm -rf /var/lib/apt/lists/* \ + && groupadd --system evolve \ + && useradd --system --gid evolve --create-home --home-dir /home/evolve evolve \ + && mkdir -p /var/lib/evolve/data \ + && chown -R evolve:evolve /var/lib/evolve /home/evolve + +WORKDIR /app + +COPY --from=builder /app/target/release/evd /usr/local/bin/evd + +RUN chown evolve:evolve /usr/local/bin/evd /app + +USER evolve:evolve + +EXPOSE 8545 50051 + +ENTRYPOINT ["/usr/local/bin/evd"]