From 41f21a2bc59526ec37965f6de6512d603cb5eee7 Mon Sep 17 00:00:00 2001 From: tac0turtle Date: Thu, 26 Feb 2026 17:10:10 +0100 Subject: [PATCH 1/5] Add Docker Compose stack for evd and update branch changes --- .dockerignore | 9 ++ README.md | 21 ++++ bin/evd/Cargo.toml | 3 - bin/evd/src/main.rs | 130 ++++++--------------- bin/testapp/Cargo.toml | 7 +- bin/{evd => testapp}/src/genesis_config.rs | 0 bin/testapp/src/lib.rs | 1 + bin/testapp/src/main.rs | 129 +++++++++++++++++++- crates/app/node/Cargo.toml | 1 + crates/app/node/src/config.rs | 1 + crates/app/node/src/lib.rs | 41 ++++++- docker-compose.ev-node.yml | 7 ++ docker-compose.yml | 22 ++++ docker/evd/Dockerfile | 25 ++++ 14 files changed, 289 insertions(+), 108 deletions(-) create mode 100644 .dockerignore rename bin/{evd => testapp}/src/genesis_config.rs (100%) create mode 100644 docker-compose.ev-node.yml create mode 100644 docker-compose.yml create mode 100644 docker/evd/Dockerfile 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/README.md b/README.md index 033da17..657e7db 100644 --- a/README.md +++ b/README.md @@ -39,6 +39,27 @@ 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 +EV_NODE_IMAGE= \ +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..1f29312 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}; @@ -92,6 +89,7 @@ use evolve_server::{ }; use evolve_stf_traits::{AccountsCodeStorage, StateChange}; use evolve_storage::{Operation, QmdbStorage, Storage, StorageConfig}; +use evolve_testapp::genesis_config::{EvdGenesisConfig, EvdGenesisResult}; use evolve_testapp::{ build_mempool_stf, default_gas_config, do_genesis_inner, install_account_codes, PLACEHOLDER_ACCOUNT, @@ -101,8 +99,6 @@ 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")] @@ -201,7 +197,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 +435,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 +453,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,16 +496,18 @@ 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; + use evolve_testapp::eth_eoa::eth_eoa_account::EthEoaAccountRef; - // 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 + let funded_accounts: Vec<([u8; 20], u128)> = genesis_config .accounts .iter() .filter(|acc| acc.balance > 0) @@ -551,39 +515,10 @@ async fn run_custom_genesis( let addr = acc .parse_address() .expect("invalid address in genesis config"); - (addr, acc.balance) + (addr.into_array(), 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 minter = AccountId::new(genesis_config.minter_id); let metadata = genesis_config.token.to_metadata(); @@ -593,7 +528,27 @@ 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) + 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 = Address::from(*eth_addr); + (address_to_account_id(addr), *balance) + }) + .collect(); + + let token = TokenRef::initialize(metadata.clone(), 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, + }) }) .expect("genesis failed"); @@ -605,23 +560,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..49b71e7 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 = { version = "1.0", features = ["derive"] } +serde_json = "1.0" 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 100% rename from bin/evd/src/genesis_config.rs rename to bin/testapp/src/genesis_config.rs diff --git a/bin/testapp/src/lib.rs b/bin/testapp/src/lib.rs index a1bcfb1..ef33742 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}; diff --git a/bin/testapp/src/main.rs b/bin/testapp/src/main.rs index d3e6f0a..b6a76f8 100644 --- a/bin/testapp/src/main.rs +++ b/bin/testapp/src/main.rs @@ -1,13 +1,16 @@ //! Evolve Dev Node entrypoint. +use std::net::SocketAddr; + use clap::{Args, Parser, Subcommand}; 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::EvdGenesisConfig; use evolve_testapp::{ build_mempool_stf, default_gas_config, do_eth_genesis_inner, install_account_codes, GenesisAccounts, MempoolStf, PLACEHOLDER_ACCOUNT, @@ -32,13 +35,28 @@ 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, + + /// Enable gRPC server on this address (e.g. 127.0.0.1:9545) + #[arg(long)] + grpc_addr: 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 +67,22 @@ fn main() { let config = resolve_node_config(&args.common, &args.native); init_node_tracing(&config.observability.log_level); - let rpc_config = config.to_rpc_config(); + let genesis_config = load_genesis_config(args.custom.genesis_file.as_deref()); + + let mut rpc_config = config.to_rpc_config(); + if let Some(grpc_addr) = args.custom.grpc_addr { + rpc_config.grpc_addr = Some(grpc_addr); + } + 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 +91,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,17 +103,28 @@ fn main() { 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()); + 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, ); } } } +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 build_codes() -> AccountStorageMock { let mut codes = AccountStorageMock::default(); install_account_codes(&mut codes); @@ -104,6 +143,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 +192,72 @@ fn run_genesis_output( }) } +fn run_custom_genesis( + stf: &MempoolStf, + codes: &AccountStorageMock, + storage: &S, + config: &EvdGenesisConfig, +) -> Result, Box> { + use evolve_core::{AccountId, BlockContext}; + use evolve_scheduler::scheduler_account::SchedulerRef; + use evolve_testapp::eth_eoa::eth_eoa_account::EthEoaAccountRef; + use evolve_token::account::TokenRef; + use evolve_tx_eth::address_to_account_id; + + let funded_accounts: Vec<([u8; 20], u128)> = config + .accounts + .iter() + .filter(|acc| acc.balance > 0) + .map(|acc| { + let addr = acc + .parse_address() + .expect("invalid address in genesis config"); + (addr.into_array(), acc.balance) + }) + .collect(); + + 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| { + // Register funded EOA accounts through the STF environment + 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.clone(), balances, Some(minter), env)?.0; + + let scheduler_acc = SchedulerRef::initialize(vec![], vec![], env)?.0; + scheduler_acc.update_begin_blockers(vec![], env)?; + + Ok(GenesisAccounts { + alice: token.0, + bob: token.0, + atom: token.0, + scheduler: scheduler_acc.0, + }) + }) + .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..8361fa2 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: None, } } } diff --git a/crates/app/node/src/lib.rs b/crates/app/node/src/lib.rs index 652c9d5..d083bc7 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. @@ -828,8 +839,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 +858,32 @@ pub fn run_dev_node_with_rpc_and_mempool_eth< .await .expect("failed to start RPC server"); + // Start gRPC server if configured + 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), + ); + tokio::spawn(async move { + if let Err(e) = grpc_server.serve().await { + tracing::error!("gRPC server error: {}", e); + } + }); + } + let consensus = DevConsensus::with_rpc_and_mempool( stf, storage, diff --git a/docker-compose.ev-node.yml b/docker-compose.ev-node.yml new file mode 100644 index 0000000..66d2832 --- /dev/null +++ b/docker-compose.ev-node.yml @@ -0,0 +1,7 @@ +services: + ev-node: + image: ${EV_NODE_IMAGE:?Set EV_NODE_IMAGE to your ev-node container image} + 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..27b50e8 --- /dev/null +++ b/docker/evd/Dockerfile @@ -0,0 +1,25 @@ +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/* + +WORKDIR /app + +COPY --from=builder /app/target/release/evd /usr/local/bin/evd + +EXPOSE 8545 50051 + +ENTRYPOINT ["/usr/local/bin/evd"] From 06ec43d613aa2f1b2a58da90031dbc25354137b6 Mon Sep 17 00:00:00 2001 From: tac0turtle Date: Thu, 26 Feb 2026 17:29:17 +0100 Subject: [PATCH 2/5] Use ev-node-grpc image as default in compose --- README.md | 6 ++++++ docker-compose.ev-node.yml | 2 +- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 657e7db..b6f17d0 100644 --- a/README.md +++ b/README.md @@ -49,6 +49,12 @@ docker compose up --build evd Run `evd` + `ev-node`: +```bash +docker compose -f docker-compose.yml -f docker-compose.ev-node.yml up --build +``` + +Override image if needed: + ```bash EV_NODE_IMAGE= \ docker compose -f docker-compose.yml -f docker-compose.ev-node.yml up --build diff --git a/docker-compose.ev-node.yml b/docker-compose.ev-node.yml index 66d2832..b77d11d 100644 --- a/docker-compose.ev-node.yml +++ b/docker-compose.ev-node.yml @@ -1,6 +1,6 @@ services: ev-node: - image: ${EV_NODE_IMAGE:?Set EV_NODE_IMAGE to your ev-node container image} + image: ${EV_NODE_IMAGE:-ghcr.io/evstack/ev-node-grpc:latest} depends_on: - evd environment: From 26ac5cdb53562cde4256483083704a0a19fbbbcc Mon Sep 17 00:00:00 2001 From: tac0turtle Date: Thu, 26 Feb 2026 17:31:54 +0100 Subject: [PATCH 3/5] Remove EV_NODE_IMAGE override from compose setup --- README.md | 7 ------- docker-compose.ev-node.yml | 2 +- 2 files changed, 1 insertion(+), 8 deletions(-) diff --git a/README.md b/README.md index b6f17d0..355138a 100644 --- a/README.md +++ b/README.md @@ -53,13 +53,6 @@ Run `evd` + `ev-node`: docker compose -f docker-compose.yml -f docker-compose.ev-node.yml up --build ``` -Override image if needed: - -```bash -EV_NODE_IMAGE= \ -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` diff --git a/docker-compose.ev-node.yml b/docker-compose.ev-node.yml index b77d11d..c6dfe36 100644 --- a/docker-compose.ev-node.yml +++ b/docker-compose.ev-node.yml @@ -1,6 +1,6 @@ services: ev-node: - image: ${EV_NODE_IMAGE:-ghcr.io/evstack/ev-node-grpc:latest} + image: ghcr.io/evstack/ev-node-grpc:latest depends_on: - evd environment: From 08c3ae6a8bf39f02c82743a7e647b05a0c743321 Mon Sep 17 00:00:00 2001 From: tac0turtle Date: Thu, 26 Feb 2026 17:52:18 +0100 Subject: [PATCH 4/5] fix: resolve clap grpc_addr name collision in testapp CLI The duplicate grpc_addr field in TestappRunCustom clashed with the same field in NativeRunConfigArgs, causing a runtime panic. Use the existing --grpc-addr from NativeRunConfigArgs wired through NodeConfig. Co-Authored-By: Claude Opus 4.6 --- bin/testapp/src/main.rs | 10 +--------- 1 file changed, 1 insertion(+), 9 deletions(-) diff --git a/bin/testapp/src/main.rs b/bin/testapp/src/main.rs index b6a76f8..12b5e8e 100644 --- a/bin/testapp/src/main.rs +++ b/bin/testapp/src/main.rs @@ -1,7 +1,5 @@ //! Evolve Dev Node entrypoint. -use std::net::SocketAddr; - use clap::{Args, Parser, Subcommand}; use evolve_core::ReadonlyKV; use evolve_node::{ @@ -46,10 +44,6 @@ struct TestappRunCustom { /// Path to a genesis JSON file with ETH accounts (uses default Alice/Bob genesis if omitted) #[arg(long)] genesis_file: Option, - - /// Enable gRPC server on this address (e.g. 127.0.0.1:9545) - #[arg(long)] - grpc_addr: Option, } #[derive(Args)] @@ -70,9 +64,7 @@ fn main() { let genesis_config = load_genesis_config(args.custom.genesis_file.as_deref()); let mut rpc_config = config.to_rpc_config(); - if let Some(grpc_addr) = args.custom.grpc_addr { - rpc_config.grpc_addr = Some(grpc_addr); - } + rpc_config.grpc_addr = Some(config.parsed_grpc_addr()); if args.custom.mock_storage { run_dev_node_with_rpc_and_mempool_mock_storage( From bb0149feebc2f157bfd64c0bd586f744275038ef Mon Sep 17 00:00:00 2001 From: tac0turtle Date: Fri, 27 Feb 2026 11:47:22 +0100 Subject: [PATCH 5/5] fix: address PR review issues for genesis, grpc, and docker --- Cargo.toml | 2 + bin/evd/src/main.rs | 75 +++++++++++---------------- bin/testapp/Cargo.toml | 4 +- bin/testapp/src/genesis_config.rs | 66 +++++++++++++++++++++++- bin/testapp/src/lib.rs | 49 ++++++++++++++++++ bin/testapp/src/main.rs | 84 ++++++++++++------------------- crates/app/node/src/config.rs | 2 +- crates/app/node/src/lib.rs | 58 +++++++++++++++++---- docker/evd/Dockerfile | 10 +++- 9 files changed, 238 insertions(+), 112 deletions(-) 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/bin/evd/src/main.rs b/bin/evd/src/main.rs index 1f29312..d60fcc9 100644 --- a/bin/evd/src/main.rs +++ b/bin/evd/src/main.rs @@ -83,20 +83,17 @@ 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::{EvdGenesisConfig, EvdGenesisResult}; +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; #[derive(Parser)] @@ -140,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) ==="); @@ -505,19 +507,10 @@ fn run_custom_genesis( genesis_config: &EvdGenesisConfig, ) -> GenesisOutput { use evolve_core::BlockContext; - use evolve_testapp::eth_eoa::eth_eoa_account::EthEoaAccountRef; - - let funded_accounts: Vec<([u8; 20], u128)> = genesis_config - .accounts - .iter() - .filter(|acc| acc.balance > 0) - .map(|acc| { - let addr = acc - .parse_address() - .expect("invalid address in genesis config"); - (addr.into_array(), acc.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(); @@ -528,26 +521,16 @@ fn run_custom_genesis( let (genesis_result, state) = stf .system_exec(storage, codes, genesis_block, |env| { - 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 = Address::from(*eth_addr); - (address_to_account_id(addr), *balance) - }) - .collect(); - - let token = TokenRef::initialize(metadata.clone(), balances, Some(minter), env)?.0; - - let scheduler_acc = SchedulerRef::initialize(vec![], vec![], env)?.0; - scheduler_acc.update_begin_blockers(vec![], env)?; + let resources = initialize_custom_genesis_resources( + &funded_accounts, + metadata.clone(), + minter, + env, + )?; Ok(EvdGenesisResult { - token: token.0, - scheduler: scheduler_acc.0, + token: resources.token, + scheduler: resources.scheduler, }) }) .expect("genesis failed"); diff --git a/bin/testapp/Cargo.toml b/bin/testapp/Cargo.toml index 49b71e7..50c97f8 100644 --- a/bin/testapp/Cargo.toml +++ b/bin/testapp/Cargo.toml @@ -34,8 +34,8 @@ rand = "0.8" tiny-keccak = { version = "2.0", features = ["keccak"] } borsh.workspace = true -serde = { version = "1.0", features = ["derive"] } -serde_json = "1.0" +serde.workspace = true +serde_json.workspace = true tracing = { workspace = true } tracing-subscriber = { workspace = true } tokio.workspace = true diff --git a/bin/testapp/src/genesis_config.rs b/bin/testapp/src/genesis_config.rs index 3b8dea4..d0a1e75 100644 --- a/bin/testapp/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 ef33742..8af70ac 100644 --- a/bin/testapp/src/lib.rs +++ b/bin/testapp/src/lib.rs @@ -78,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 12b5e8e..fb64f69 100644 --- a/bin/testapp/src/main.rs +++ b/bin/testapp/src/main.rs @@ -8,10 +8,11 @@ use evolve_node::{ run_dev_node_with_rpc_and_mempool_mock_storage, GenesisOutput, InitArgs, RunArgs, }; use evolve_storage::{QmdbStorage, Storage, StorageConfig}; -use evolve_testapp::genesis_config::EvdGenesisConfig; +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; @@ -61,10 +62,15 @@ fn main() { 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); + } + }; - let mut rpc_config = config.to_rpc_config(); - rpc_config.grpc_addr = Some(config.parsed_grpc_addr()); + let rpc_config = config.to_rpc_config(); if args.custom.mock_storage { run_dev_node_with_rpc_and_mempool_mock_storage( @@ -95,7 +101,13 @@ fn main() { 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_dev_node( &config.storage.path, @@ -110,13 +122,6 @@ fn main() { } } -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 build_codes() -> AccountStorageMock { let mut codes = AccountStorageMock::default(); install_account_codes(&mut codes); @@ -191,22 +196,8 @@ fn run_custom_genesis( config: &EvdGenesisConfig, ) -> Result, Box> { use evolve_core::{AccountId, BlockContext}; - use evolve_scheduler::scheduler_account::SchedulerRef; - use evolve_testapp::eth_eoa::eth_eoa_account::EthEoaAccountRef; - use evolve_token::account::TokenRef; - use evolve_tx_eth::address_to_account_id; - - let funded_accounts: Vec<([u8; 20], u128)> = config - .accounts - .iter() - .filter(|acc| acc.balance > 0) - .map(|acc| { - let addr = acc - .parse_address() - .expect("invalid address in genesis config"); - (addr.into_array(), acc.balance) - }) - .collect(); + + let funded_accounts = config.funded_accounts()?; let minter = AccountId::new(config.minter_id); let metadata = config.token.to_metadata(); @@ -215,29 +206,20 @@ fn run_custom_genesis( let (accounts, state) = stf .system_exec(storage, codes, genesis_block, |env| { - // Register funded EOA accounts through the STF environment - 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.clone(), balances, Some(minter), env)?.0; - - let scheduler_acc = SchedulerRef::initialize(vec![], vec![], env)?.0; - scheduler_acc.update_begin_blockers(vec![], 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: token.0, - bob: token.0, - atom: token.0, - scheduler: scheduler_acc.0, + alice, + bob, + atom: resources.token, + scheduler: resources.scheduler, }) }) .map_err(|e| format!("{:?}", e))?; diff --git a/crates/app/node/src/config.rs b/crates/app/node/src/config.rs index 8361fa2..8a68f45 100644 --- a/crates/app/node/src/config.rs +++ b/crates/app/node/src/config.rs @@ -73,7 +73,7 @@ impl NodeConfig { chain_id: self.chain.chain_id, enabled: self.rpc.enabled, enable_block_indexing: self.rpc.enable_block_indexing, - grpc_addr: None, + grpc_addr: Some(self.parsed_grpc_addr()), } } } diff --git a/crates/app/node/src/lib.rs b/crates/app/node/src/lib.rs index d083bc7..9130d9e 100644 --- a/crates/app/node/src/lib.rs +++ b/crates/app/node/src/lib.rs @@ -409,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 @@ -428,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, @@ -479,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) @@ -526,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"); + } } } }); @@ -859,7 +890,7 @@ pub fn run_dev_node_with_rpc_and_mempool_eth< .expect("failed to start RPC server"); // Start gRPC server if configured - if let Some(grpc_addr) = rpc_config.grpc_addr { + 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, @@ -877,12 +908,14 @@ pub fn run_dev_node_with_rpc_and_mempool_eth< grpc_state_provider, Arc::clone(&subscriptions), ); - tokio::spawn(async move { + 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, @@ -932,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); @@ -976,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/evd/Dockerfile b/docker/evd/Dockerfile index 27b50e8..0d390ca 100644 --- a/docker/evd/Dockerfile +++ b/docker/evd/Dockerfile @@ -14,12 +14,20 @@ 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/* + && 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"]