From 41f21a2bc59526ec37965f6de6512d603cb5eee7 Mon Sep 17 00:00:00 2001 From: tac0turtle Date: Thu, 26 Feb 2026 17:10:10 +0100 Subject: [PATCH 01/10] 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 02/10] 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 03/10] 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 04/10] 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 604ee1e39da31153358cc1728459d20268ef2126 Mon Sep 17 00:00:00 2001 From: tac0turtle Date: Fri, 27 Feb 2026 10:24:49 +0100 Subject: [PATCH 05/10] strict v2 account identity: registry-only address resolution --- bin/evd/src/main.rs | 165 ++++++++++-- bin/testapp/src/eth_eoa.rs | 15 +- bin/testapp/src/genesis_config.rs | 7 + bin/testapp/src/lib.rs | 20 +- bin/testapp/src/main.rs | 132 +++++++++- bin/testapp/src/sim_testing.rs | 18 +- bin/testapp/tests/mempool_e2e.rs | 26 +- bin/txload/src/main.rs | 4 +- crates/app/sdk/core/src/lib.rs | 245 ++++++++++-------- crates/app/sdk/core/src/low_level.rs | 22 +- crates/app/sdk/core/src/runtime_api.rs | 19 +- crates/app/sdk/core/src/storage_api.rs | 2 +- crates/app/sdk/stf_traits/src/lib.rs | 25 ++ crates/app/sdk/testing/src/lib.rs | 6 +- crates/app/stf/src/handlers.rs | 14 +- crates/app/stf/src/lib.rs | 41 ++- crates/app/tx/eth/src/eoa_registry.rs | 249 +++++++++++++++++++ crates/app/tx/eth/src/lib.rs | 12 +- crates/app/tx/eth/src/mempool.rs | 56 +++-- crates/app/tx/eth/src/traits.rs | 171 +++++-------- crates/app/tx/eth/tests/integration_tests.rs | 12 +- crates/rpc/types/Cargo.toml | 1 + crates/rpc/types/src/lib.rs | 35 +-- crates/testing/debugger/src/breakpoints.rs | 9 +- crates/testing/debugger/src/inspector.rs | 7 +- crates/testing/debugger/src/trace.rs | 28 +-- crates/testing/simulator/src/eth_eoa.rs | 4 +- 27 files changed, 976 insertions(+), 369 deletions(-) create mode 100644 crates/app/tx/eth/src/eoa_registry.rs diff --git a/bin/evd/src/main.rs b/bin/evd/src/main.rs index 1f29312..b121927 100644 --- a/bin/evd/src/main.rs +++ b/bin/evd/src/main.rs @@ -72,7 +72,7 @@ use commonware_runtime::tokio::{Config as TokioConfig, Runner}; use commonware_runtime::{Runner as RunnerTrait, Spawner}; use evolve_chain_index::{ build_index_data, BlockMetadata, ChainIndex, ChainStateProvider, ChainStateProviderConfig, - PersistentChainIndex, + PersistentChainIndex, StateQuerier, StorageStateQuerier, }; use evolve_core::{AccountId, ReadonlyKV}; use evolve_eth_jsonrpc::{start_server_with_subscriptions, RpcServerConfig, SubscriptionManager}; @@ -91,13 +91,13 @@ 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, + build_mempool_stf, default_gas_config, do_eth_genesis_inner, 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 evolve_tx_eth::{register_runtime_contract_account, resolve_or_create_eoa_account}; #[derive(Parser)] #[command(name = "evd")] @@ -240,12 +240,17 @@ fn run_node(config: NodeConfig, genesis_config: Option) { sync_status: SyncStatus::NotSyncing(false), }; + let state_querier: Arc = Arc::new(StorageStateQuerier::new( + storage.clone(), + genesis_result.token, + )); let state_provider = ChainStateProvider::with_mempool( Arc::clone(chain_index.as_ref().expect("chain index required for RPC")), state_provider_config, codes_for_rpc, mempool.clone(), - ); + ) + .with_state_querier(state_querier); let rpc_addr = config.parsed_rpc_addr(); let server_config = RpcServerConfig { @@ -465,27 +470,40 @@ fn run_genesis( } } -/// Default genesis using testapp's `do_genesis_inner` (sequential account IDs). +/// Default genesis using ETH-address-derived AccountIds for EOA balances. fn run_default_genesis( storage: &S, codes: &AccountStorageMock, ) -> GenesisOutput { use evolve_core::BlockContext; + use std::str::FromStr; - tracing::info!("Running default testapp genesis..."); + tracing::info!("Running default ETH-mapped genesis..."); let gas_config = default_gas_config(); let stf = build_mempool_stf(gas_config, PLACEHOLDER_ACCOUNT); let genesis_block = BlockContext::new(0, 0); + let alice_eth_address = std::env::var("GENESIS_ALICE_ETH_ADDRESS") + .ok() + .and_then(|s| Address::from_str(s.trim()).ok()) + .map(Into::into) + .unwrap_or([0xAA; 20]); + let bob_eth_address = std::env::var("GENESIS_BOB_ETH_ADDRESS") + .ok() + .and_then(|s| Address::from_str(s.trim()).ok()) + .map(Into::into) + .unwrap_or([0xBB; 20]); let (accounts, state) = stf - .system_exec(storage, codes, genesis_block, |env| do_genesis_inner(env)) + .system_exec(storage, codes, genesis_block, |env| { + do_eth_genesis_inner(alice_eth_address, bob_eth_address, env) + }) .expect("genesis failed"); let changes = state.into_changes().expect("failed to get state changes"); let genesis_result = EvdGenesisResult { - token: accounts.atom, + token: accounts.evolve, scheduler: accounts.scheduler, }; @@ -497,15 +515,13 @@ fn run_default_genesis( /// Custom genesis with ETH EOA accounts from a genesis JSON file. /// -/// Registers funded EOA accounts via `EthEoaAccountRef::initialize` inside -/// `system_exec`, then initializes the token with their balances. +/// Funds balances at ETH-address-derived AccountIds. 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; let funded_accounts: Vec<([u8; 20], u128)> = genesis_config .accounts @@ -528,21 +544,21 @@ 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(); + .map( + |(eth_addr, balance)| -> evolve_core::SdkResult<(AccountId, u128)> { + let addr = Address::from(*eth_addr); + Ok((resolve_or_create_eoa_account(addr, env)?, *balance)) + }, + ) + .collect::>>()?; let token = TokenRef::initialize(metadata.clone(), balances, Some(minter), env)?.0; + let _token_eth_addr = register_runtime_contract_account(token.0, env)?; let scheduler_acc = SchedulerRef::initialize(vec![], vec![], env)?.0; + let _scheduler_eth_addr = register_runtime_contract_account(scheduler_acc.0, env)?; scheduler_acc.update_begin_blockers(vec![], env)?; Ok(EvdGenesisResult { @@ -606,3 +622,112 @@ fn state_changes_to_operations(changes: Vec) -> Vec { }) .collect() } + +#[cfg(test)] +mod tests { + use super::*; + use evolve_core::encoding::Encodable; + use evolve_core::runtime_api::ACCOUNT_IDENTIFIER_PREFIX; + use evolve_core::Message; + use evolve_storage::MockStorage; + use std::collections::BTreeMap; + + struct EnvVarGuard { + key: &'static str, + old: Option, + } + + impl EnvVarGuard { + fn set(key: &'static str, value: &str) -> Self { + let old = std::env::var(key).ok(); + std::env::set_var(key, value); + Self { key, old } + } + } + + impl Drop for EnvVarGuard { + fn drop(&mut self) { + if let Some(value) = &self.old { + std::env::set_var(self.key, value); + } else { + std::env::remove_var(self.key); + } + } + } + + fn apply_changes_to_map(changes: Vec) -> BTreeMap, Vec> { + let mut out = BTreeMap::new(); + for change in changes { + match change { + StateChange::Set { key, value } => { + out.insert(key, value); + } + StateChange::Remove { key } => { + out.remove(&key); + } + } + } + out + } + + fn read_token_balance( + state: &BTreeMap, Vec>, + token_account_id: AccountId, + account_id: AccountId, + ) -> u128 { + let mut key = token_account_id.as_bytes().to_vec(); + key.push(1u8); // Token::balances storage prefix + key.extend(account_id.encode().expect("encode account id")); + + match state.get(&key) { + Some(value) => Message::from_bytes(value.clone()) + .get::() + .expect("decode balance"), + None => 0, + } + } + + fn eoa_account_ids(state: &BTreeMap, Vec>) -> Vec { + state + .iter() + .filter_map(|(key, value)| { + if key.len() != 33 || key[0] != ACCOUNT_IDENTIFIER_PREFIX { + return None; + } + let code_id = Message::from_bytes(value.clone()).get::().ok()?; + if code_id != "EthEoaAccount" { + return None; + } + let account_bytes: [u8; 32] = key[1..33].try_into().ok()?; + Some(AccountId::from_bytes(account_bytes)) + }) + .collect() + } + + #[test] + fn default_genesis_funds_eth_mapped_sender_account() { + let _alice_addr = EnvVarGuard::set( + "GENESIS_ALICE_ETH_ADDRESS", + "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266", + ); + let _bob_addr = EnvVarGuard::set( + "GENESIS_BOB_ETH_ADDRESS", + "0x70997970C51812dc3A010C7d01b50e0d17dc79C8", + ); + let _alice_bal = EnvVarGuard::set("GENESIS_ALICE_TOKEN_BALANCE", "1234"); + let _bob_bal = EnvVarGuard::set("GENESIS_BOB_TOKEN_BALANCE", "5678"); + + let storage = MockStorage::new(); + let codes = build_codes(); + let output = run_default_genesis(&storage, &codes); + let state = apply_changes_to_map(output.changes); + + let eoa_ids = eoa_account_ids(&state); + assert_eq!(eoa_ids.len(), 2); + assert!(eoa_ids.iter().any(|id| read_token_balance( + &state, + output.genesis_result.token, + *id + ) == 1234)); + } +} diff --git a/bin/testapp/src/eth_eoa.rs b/bin/testapp/src/eth_eoa.rs index b0ab9d7..d1263d3 100644 --- a/bin/testapp/src/eth_eoa.rs +++ b/bin/testapp/src/eth_eoa.rs @@ -22,7 +22,6 @@ pub mod eth_eoa_account { use evolve_collections::item::Item; use evolve_core::{AccountId, Environment, Message, SdkResult}; use evolve_macros::{exec, init, query}; - use evolve_tx_eth::address_to_account_id; use evolve_tx_eth::TxContext; /// An Ethereum-compatible externally owned account. @@ -93,18 +92,20 @@ pub mod eth_eoa_account { /// - Just increments nonce (test mode, no signature verification) #[exec] fn authenticate(&self, tx: Message, env: &mut dyn Environment) -> SdkResult<()> { + let expected_address = self.eth_address.may_get(env)?.unwrap_or([0u8; 20]); + + if let Ok(sender_address) = tx.get::<[u8; 20]>() { + if sender_address != expected_address { + return Err(evolve_core::ErrorCode::new(0x51)); // Sender mismatch + } // Fast path: validator passes sender AccountId directly. - if let Ok(sender_id) = tx.get::() { - let expected_address = self.eth_address.may_get(env)?.unwrap_or([0u8; 20]); - let expected_id = - address_to_account_id(alloy_primitives::Address::from(expected_address)); - if sender_id != expected_id { + } else if let Ok(sender_id) = tx.get::() { + if sender_id != env.whoami() { return Err(evolve_core::ErrorCode::new(0x51)); // Sender mismatch } // Backward-compatible fallback for older validator payloads. } else if let Ok(mempool_tx) = tx.get::() { let sender_bytes: [u8; 20] = mempool_tx.sender_address().into(); - let expected_address = self.eth_address.may_get(env)?.unwrap_or([0u8; 20]); if sender_bytes != expected_address { return Err(evolve_core::ErrorCode::new(0x51)); // Sender mismatch } diff --git a/bin/testapp/src/genesis_config.rs b/bin/testapp/src/genesis_config.rs index 3b8dea4..b13bc69 100644 --- a/bin/testapp/src/genesis_config.rs +++ b/bin/testapp/src/genesis_config.rs @@ -2,6 +2,7 @@ use alloy_primitives::Address; use borsh::{BorshDeserialize, BorshSerialize}; use evolve_core::AccountId; use evolve_fungible_asset::FungibleAssetMetadata; +use evolve_node::HasTokenAccountId; use serde::Deserialize; /// Evd genesis configuration loaded from JSON. @@ -34,6 +35,12 @@ pub struct EvdGenesisResult { pub scheduler: AccountId, } +impl HasTokenAccountId for EvdGenesisResult { + fn token_account_id(&self) -> AccountId { + self.token + } +} + impl EvdGenesisConfig { /// Load genesis config from a JSON file. pub fn load(path: &str) -> Result { diff --git a/bin/testapp/src/lib.rs b/bin/testapp/src/lib.rs index ef33742..ebeffb5 100644 --- a/bin/testapp/src/lib.rs +++ b/bin/testapp/src/lib.rs @@ -6,6 +6,7 @@ use crate::eth_eoa::eth_eoa_account::{EthEoaAccount, EthEoaAccountRef}; use evolve_authentication::AuthenticationTxValidator; use evolve_core::{AccountId, BlockContext, Environment, InvokeResponse, ReadonlyKV, SdkResult}; use evolve_fungible_asset::FungibleAssetMetadata; +use evolve_node::HasTokenAccountId; use evolve_scheduler::scheduler_account::{Scheduler, SchedulerRef}; use evolve_scheduler::server::{SchedulerBeginBlocker, SchedulerEndBlocker}; use evolve_server::Block; @@ -13,8 +14,8 @@ use evolve_stf::execution_state::ExecutionState; use evolve_stf::{Stf, StorageGasConfig}; use evolve_stf_traits::{AccountsCodeStorage, PostTxExecution, WritableAccountsCodeStorage}; use evolve_token::account::{Token, TokenRef}; -use evolve_tx_eth::address_to_account_id; use evolve_tx_eth::TxContext; +use evolve_tx_eth::{register_runtime_contract_account, resolve_or_create_eoa_account}; pub const MINTER: AccountId = AccountId::new(100_002); @@ -78,6 +79,12 @@ pub struct GenesisAccounts { pub scheduler: AccountId, } +impl HasTokenAccountId for GenesisAccounts { + fn token_account_id(&self) -> AccountId { + self.atom + } +} + fn parse_genesis_address_env(var: &str) -> Option<[u8; 20]> { use alloy_primitives::Address; use std::str::FromStr; @@ -117,9 +124,11 @@ pub fn do_genesis_with_addresses( env, )? .0; + let _atom_eth_addr = register_runtime_contract_account(atom.0, env)?; // Create scheduler (no begin blockers needed for block info anymore) let scheduler_acc = SchedulerRef::initialize(vec![], vec![], env)?.0; + let _scheduler_eth_addr = register_runtime_contract_account(scheduler_acc.0, env)?; // Update scheduler's account's list. scheduler_acc.update_begin_blockers(vec![], env)?; @@ -177,10 +186,9 @@ pub fn do_eth_genesis_inner( use alloy_primitives::Address; use std::str::FromStr; - // Convert Ethereum addresses to AccountIds - // (accounts should already be registered in storage) - let alice_id = address_to_account_id(Address::from(alice_eth_address)); - let bob_id = address_to_account_id(Address::from(bob_eth_address)); + // Resolve/create canonical EOA accounts from full 20-byte ETH addresses. + let alice_id = resolve_or_create_eoa_account(Address::from(alice_eth_address), env)?; + let bob_id = resolve_or_create_eoa_account(Address::from(bob_eth_address), env)?; let alice_balance = std::env::var("GENESIS_ALICE_TOKEN_BALANCE") .ok() .and_then(|v| u128::from_str(v.trim()).ok()) @@ -204,9 +212,11 @@ pub fn do_eth_genesis_inner( env, )? .0; + let _evolve_eth_addr = register_runtime_contract_account(evolve.0, env)?; // Create scheduler let scheduler_acc = SchedulerRef::initialize(vec![], vec![], env)?.0; + let _scheduler_eth_addr = register_runtime_contract_account(scheduler_acc.0, env)?; scheduler_acc.update_begin_blockers(vec![], env)?; Ok(EthGenesisAccounts { diff --git a/bin/testapp/src/main.rs b/bin/testapp/src/main.rs index 12b5e8e..ebaf32f 100644 --- a/bin/testapp/src/main.rs +++ b/bin/testapp/src/main.rs @@ -192,9 +192,8 @@ fn run_custom_genesis( ) -> 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; + use evolve_tx_eth::{register_runtime_contract_account, resolve_or_create_eoa_account}; let funded_accounts: Vec<([u8; 20], u128)> = config .accounts @@ -215,22 +214,21 @@ 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(); + .map( + |(eth_addr, balance)| -> evolve_core::SdkResult<(AccountId, u128)> { + let addr = alloy_primitives::Address::from(*eth_addr); + Ok((resolve_or_create_eoa_account(addr, env)?, *balance)) + }, + ) + .collect::>>()?; let token = TokenRef::initialize(metadata.clone(), balances, Some(minter), env)?.0; + let _token_eth_addr = register_runtime_contract_account(token.0, env)?; let scheduler_acc = SchedulerRef::initialize(vec![], vec![], env)?.0; + let _scheduler_eth_addr = register_runtime_contract_account(scheduler_acc.0, env)?; scheduler_acc.update_begin_blockers(vec![], env)?; Ok(GenesisAccounts { @@ -259,3 +257,113 @@ async fn build_storage( .await .map_err(|e| Box::new(e) as _) } + +#[cfg(test)] +mod tests { + use super::*; + use evolve_core::encoding::Encodable; + use evolve_core::runtime_api::ACCOUNT_IDENTIFIER_PREFIX; + use evolve_core::AccountId; + use evolve_core::Message; + use evolve_storage::MockStorage; + use evolve_testapp::genesis_config::{AccountConfig, TokenConfig}; + use std::collections::BTreeMap; + + fn apply_changes_to_map( + changes: Vec, + ) -> BTreeMap, Vec> { + let mut out = BTreeMap::new(); + for change in changes { + match change { + evolve_stf_traits::StateChange::Set { key, value } => { + out.insert(key, value); + } + evolve_stf_traits::StateChange::Remove { key } => { + out.remove(&key); + } + } + } + out + } + + fn read_token_balance( + state: &BTreeMap, Vec>, + token_account_id: AccountId, + account_id: AccountId, + ) -> u128 { + let mut key = token_account_id.as_bytes().to_vec(); + key.push(1u8); // Token::balances storage prefix + key.extend(account_id.encode().expect("encode account id")); + + match state.get(&key) { + Some(value) => Message::from_bytes(value.clone()) + .get::() + .expect("decode balance"), + None => 0, + } + } + + fn count_registered_code_id( + state: &BTreeMap, Vec>, + expected_code_id: &str, + ) -> usize { + state + .iter() + .filter(|(key, value)| { + if key.len() != 33 || key[0] != ACCOUNT_IDENTIFIER_PREFIX { + return false; + } + Message::from_bytes((*value).clone()) + .get::() + .map(|code_id| code_id == expected_code_id) + .unwrap_or(false) + }) + .count() + } + + #[test] + fn custom_genesis_funds_registry_resolved_eoa_account() { + let stf = build_genesis_stf(); + let codes = build_codes(); + let storage = MockStorage::new(); + + let config = EvdGenesisConfig { + token: TokenConfig { + name: "evolve".to_string(), + symbol: "ev".to_string(), + decimals: 6, + icon_url: "https://example.com/icon.png".to_string(), + description: "token".to_string(), + }, + minter_id: 100_002, + accounts: vec![AccountConfig { + eth_address: "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266".to_string(), + balance: 777, + }], + }; + + let output = run_custom_genesis(&stf, &codes, &storage, &config).expect("custom genesis"); + let state = apply_changes_to_map(output.changes); + + let mapped_id = state + .iter() + .find_map(|(key, value)| { + if key.len() != 33 || key[0] != ACCOUNT_IDENTIFIER_PREFIX { + return None; + } + let code_id = Message::from_bytes((*value).clone()).get::().ok()?; + if code_id != "EthEoaAccount" { + return None; + } + let id_bytes: [u8; 32] = key[1..33].try_into().ok()?; + Some(AccountId::from_bytes(id_bytes)) + }) + .expect("eoa account id"); + assert_eq!( + read_token_balance(&state, output.genesis_result.atom, mapped_id), + 777 + ); + + assert_eq!(count_registered_code_id(&state, "EthEoaAccount"), 1); + } +} diff --git a/bin/testapp/src/sim_testing.rs b/bin/testapp/src/sim_testing.rs index 930ffcf..f33d4fe 100644 --- a/bin/testapp/src/sim_testing.rs +++ b/bin/testapp/src/sim_testing.rs @@ -21,7 +21,9 @@ use evolve_stf::results::BlockResult; use evolve_stf_traits::{Block as BlockTrait, Transaction}; use evolve_testing::server_mocks::AccountStorageMock; use evolve_token::account::TokenRef; -use evolve_tx_eth::{account_id_to_address, address_to_account_id}; +use evolve_tx_eth::{ + derive_eth_eoa_account_id, derive_runtime_contract_address, register_runtime_contract_account, +}; use evolve_tx_eth::{EthGateway, TxContext}; use k256::ecdsa::{signature::hazmat::PrehashSigner, SigningKey, VerifyingKey}; use std::collections::BTreeMap; @@ -331,8 +333,8 @@ impl SimTestApp { let alice_address = get_address(&alice_key); let bob_address = get_address(&bob_key); - let alice_id = address_to_account_id(alice_address); - let bob_id = address_to_account_id(bob_address); + let alice_id = derive_eth_eoa_account_id(alice_address); + let bob_id = derive_eth_eoa_account_id(bob_address); register_account_code_identifier(&mut sim, alice_id, "EthEoaAccount") .expect("register alice code"); @@ -355,10 +357,12 @@ impl SimTestApp { env, )? .0; + let _atom_eth_addr = register_runtime_contract_account(atom.0, env)?; let scheduler = evolve_scheduler::scheduler_account::SchedulerRef::initialize(vec![], vec![], env)? .0; + let _scheduler_eth_addr = register_runtime_contract_account(scheduler.0, env)?; scheduler.update_begin_blockers(vec![], env)?; Ok(GenesisAccounts { @@ -414,7 +418,7 @@ impl SimTestApp { calldata.extend_from_slice(&selector); calldata.extend_from_slice(&args); - let to = account_id_to_address(token_account); + let to = derive_runtime_contract_address(token_account); Some(create_signed_tx( signing_key, self.chain_id, @@ -724,7 +728,7 @@ impl SimTestApp { /// Create an EOA with a specific Ethereum address. pub fn create_eoa_with_address(&mut self, eth_address: [u8; 20]) -> AccountId { - let account_id = address_to_account_id(alloy_primitives::Address::from(eth_address)); + let account_id = derive_eth_eoa_account_id(alloy_primitives::Address::from(eth_address)); register_account_code_identifier(&mut self.sim, account_id, "EthEoaAccount") .expect("register eoa code"); init_eth_eoa_storage(&mut self.sim, account_id, eth_address).expect("init eoa storage"); @@ -736,7 +740,7 @@ impl SimTestApp { let signing_key = generate_signing_key(&mut self.sim, MAX_SIGNING_KEY_ATTEMPTS) .expect("failed to generate signing key"); let address = get_address(&signing_key); - let account_id = address_to_account_id(address); + let account_id = derive_eth_eoa_account_id(address); self.signers.insert(account_id, signing_key); self.nonces.entry(account_id).or_insert(0); account_id @@ -747,7 +751,7 @@ impl SimTestApp { let signing_key = generate_signing_key(&mut self.sim, MAX_SIGNING_KEY_ATTEMPTS) .expect("failed to generate signing key"); let address = get_address(&signing_key); - let account_id = address_to_account_id(address); + let account_id = derive_eth_eoa_account_id(address); register_account_code_identifier(&mut self.sim, account_id, code_id) .expect("register account code"); self.signers.insert(account_id, signing_key); diff --git a/bin/testapp/tests/mempool_e2e.rs b/bin/testapp/tests/mempool_e2e.rs index 8653f68..e64860a 100644 --- a/bin/testapp/tests/mempool_e2e.rs +++ b/bin/testapp/tests/mempool_e2e.rs @@ -18,7 +18,7 @@ use evolve_testapp::{ build_mempool_stf, default_gas_config, do_eth_genesis, install_account_codes, }; use evolve_testing::server_mocks::AccountStorageMock; -use evolve_tx_eth::{account_id_to_address, EthGateway}; +use evolve_tx_eth::{derive_runtime_contract_address, EthGateway}; use k256::ecdsa::{signature::hazmat::PrehashSigner, SigningKey, VerifyingKey}; use rand::rngs::OsRng; use std::collections::BTreeMap; @@ -271,21 +271,9 @@ async fn test_token_transfer_e2e() { let mut codes = AccountStorageMock::new(); install_account_codes(&mut codes); - // Derive account IDs from Ethereum addresses - let alice_account_id = evolve_tx_eth::address_to_account_id(alice_address); - let bob_account_id = evolve_tx_eth::address_to_account_id(bob_address); - - // Create initial storage and pre-populate ETH EOA account data + // Create initial storage let init_storage = AsyncMockStorage::new(); - // Register account code identifiers (global storage) - init_storage.register_account_code(alice_account_id, "EthEoaAccount"); - init_storage.register_account_code(bob_account_id, "EthEoaAccount"); - - // Initialize account storage (nonce, eth_address) - init_storage.init_eth_eoa_storage(alice_account_id, alice_address.into()); - init_storage.init_eth_eoa_storage(bob_account_id, bob_address.into()); - // Build STF for TxContext let gas_config = default_gas_config(); let stf = build_mempool_stf(gas_config.clone(), AccountId::new(0)); @@ -318,6 +306,14 @@ async fn test_token_transfer_e2e() { let dev = handles.dev; let mempool = handles.mempool; + let alice_account_id = + evolve_tx_eth::lookup_account_id_in_storage(dev.storage(), alice_address) + .expect("lookup alice id") + .expect("alice id exists"); + let bob_account_id = evolve_tx_eth::lookup_account_id_in_storage(dev.storage(), bob_address) + .expect("lookup bob id") + .expect("bob id exists"); + // Read initial state let alice_nonce_before = read_nonce(dev.storage(), alice_account_id); let alice_balance_before = @@ -350,7 +346,7 @@ async fn test_token_transfer_e2e() { calldata.extend_from_slice(&args); // Get token's Ethereum address - let token_address = account_id_to_address(genesis_accounts.evolve); + let token_address = derive_runtime_contract_address(genesis_accounts.evolve); println!("\nTransaction details:"); println!(" Token account: {:?}", genesis_accounts.evolve); diff --git a/bin/txload/src/main.rs b/bin/txload/src/main.rs index 3fc18ad..3ecaec6 100644 --- a/bin/txload/src/main.rs +++ b/bin/txload/src/main.rs @@ -8,7 +8,7 @@ use alloy_consensus::{SignableTransaction, TxEip1559}; use alloy_primitives::{keccak256, Address, Bytes, PrimitiveSignature, TxKind, B256, U256}; use clap::Parser; use evolve_core::AccountId; -use evolve_tx_eth::address_to_account_id; +use evolve_tx_eth::derive_eth_eoa_account_id; use k256::ecdsa::{signature::hazmat::PrehashSigner, SigningKey, VerifyingKey}; use rand::RngCore; use serde_json::{json, Value}; @@ -378,7 +378,7 @@ async fn run_loadtest(config: LoadtestConfig) -> Result<(), String> { wallets.push(WorkerWallet { signing_key: key, address, - account_id: address_to_account_id(address), + account_id: derive_eth_eoa_account_id(address), next_nonce: 0, }); } diff --git a/crates/app/sdk/core/src/lib.rs b/crates/app/sdk/core/src/lib.rs index 4722a71..f8da4d3 100644 --- a/crates/app/sdk/core/src/lib.rs +++ b/crates/app/sdk/core/src/lib.rs @@ -38,36 +38,81 @@ pub type SdkResult = Result; #[cfg(not(feature = "error-decode"))] pub type SdkResult = Result; +/// Canonical 32-byte account identity. +/// +/// Byte representation is the only canonical form. No numeric interpretation is +/// required for correctness. #[derive( Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Debug, BorshSerialize, BorshDeserialize, )] -pub struct AccountId(u128); +pub struct AccountId([u8; 32]); impl AccountId { - pub fn invalid() -> AccountId { - AccountId(u128::MAX) + /// Reserved invalid sentinel (all 0xFF). + pub const fn invalid() -> AccountId { + AccountId([0xFF; 32]) } -} -impl AccountId { - pub fn increase(&self) -> Self { - Self(self.0 + 1) + /// Construct from raw canonical bytes. + pub const fn from_bytes(bytes: [u8; 32]) -> Self { + Self(bytes) } -} -impl AccountId { + /// Return raw canonical bytes. + pub const fn to_bytes(self) -> [u8; 32] { + self.0 + } + + /// Borrow raw canonical bytes. + pub const fn as_bytes(&self) -> [u8; 32] { + self.0 + } + + /// Backward-compatible constructor from `u128`. + /// + /// Encodes into the lower 16 bytes in big-endian order. pub const fn new(u: u128) -> Self { - Self(u) + let mut out = [0u8; 32]; + let bytes = u.to_be_bytes(); + out[16] = bytes[0]; + out[17] = bytes[1]; + out[18] = bytes[2]; + out[19] = bytes[3]; + out[20] = bytes[4]; + out[21] = bytes[5]; + out[22] = bytes[6]; + out[23] = bytes[7]; + out[24] = bytes[8]; + out[25] = bytes[9]; + out[26] = bytes[10]; + out[27] = bytes[11]; + out[28] = bytes[12]; + out[29] = bytes[13]; + out[30] = bytes[14]; + out[31] = bytes[15]; + Self(out) } + /// Legacy extractor for compatibility where a numeric ID is still required. pub const fn inner(&self) -> u128 { - self.0 + u128::from_be_bytes([ + self.0[16], self.0[17], self.0[18], self.0[19], self.0[20], self.0[21], self.0[22], + self.0[23], self.0[24], self.0[25], self.0[26], self.0[27], self.0[28], self.0[29], + self.0[30], self.0[31], + ]) } -} -impl AccountId { - pub fn as_bytes(&self) -> Vec { - self.0.to_be_bytes().into() + /// Increment account ID bytes in big-endian order (wraps on overflow). + pub fn increase(&self) -> Self { + let mut out = self.0; + for i in (0..32).rev() { + let (next, carry) = out[i].overflowing_add(1); + out[i] = next; + if !carry { + break; + } + } + Self(out) } } @@ -145,33 +190,6 @@ pub trait InvokableMessage: Encodable + Clone { const FUNCTION_IDENTIFIER_NAME: &'static str; } -/// A macro that ensures a condition holds true. If not, returns an error. -/// -/// # Usage -/// -/// ```rust -/// # use std::error::Error; -/// # -/// # // Suppose we have an enum for our errors: -/// #[derive(Debug, )] -/// enum MyError { -/// SomeError, -/// AnotherError, -/// } -/// -/// // Return type must be Result -/// fn example_function(value: i32) -> Result<(), MyError> { -/// use evolve_core::ensure; -/// -/// ensure!(value > 10, MyError::SomeError); -/// // Proceed if the condition is satisfied -/// Ok(()) -/// } -/// # -/// # fn main() { -/// # example_function(11).unwrap(); -/// # } -/// ``` #[macro_export] macro_rules! ensure { ($cond:expr, $err:expr) => { @@ -225,85 +243,116 @@ mod tests { } } - #[derive(Debug, Clone, PartialEq, BorshSerialize, BorshDeserialize)] - struct TestPayload { - value: u32, - name: String, - } + impl Environment for TestEnv { + fn do_exec( + &mut self, + _to: AccountId, + _data: &InvokeRequest, + _funds: Vec, + ) -> SdkResult { + Err(ERR_UNKNOWN_FUNCTION) + } - #[test] - fn test_invoke_request_encode_decode() { - let function_id: u64 = 42; - let human_name = "test_function"; - let payload = TestPayload { - value: 123, - name: "test".to_string(), - }; + fn emit_event(&mut self, _name: &str, _data: &[u8]) -> SdkResult<()> { + Ok(()) + } - // Create InvokeRequest using the public API - let message = Message::new(&payload).expect("message creation should succeed"); - let request = InvokeRequest::new_from_message(human_name, function_id, message); + fn unique_id(&mut self) -> SdkResult<[u8; 32]> { + Ok([0u8; 32]) + } + } - // Verify accessors work - assert_eq!(request.function(), function_id); - assert_eq!(request.human_name(), human_name); + #[derive(Clone, BorshSerialize, BorshDeserialize)] + struct DummyMessage { + value: u64, + } - // Verify payload can be retrieved - let retrieved: TestPayload = request.get().expect("get should succeed"); - assert_eq!(retrieved, payload); + impl InvokableMessage for DummyMessage { + const FUNCTION_IDENTIFIER: u64 = 42; + const FUNCTION_IDENTIFIER_NAME: &'static str = "dummy"; + } + + #[test] + fn test_message_roundtrip() { + let msg = DummyMessage { value: 123 }; + let encoded = msg.encode().unwrap(); + let decoded = borsh::from_slice::(&encoded).unwrap(); + assert_eq!(decoded.value, 123); } #[test] fn test_one_coin_success() { - let mut funds = BTreeMap::new(); - funds.insert( - 0, - FungibleAsset { - asset_id: AccountId::new(1), - amount: 100, - }, - ); let env = TestEnv { - whoami: AccountId::new(10), - sender: AccountId::new(20), - funds: funds.into_values().collect(), + whoami: AccountId::new(1), + sender: AccountId::new(2), + funds: vec![FungibleAsset { + asset_id: AccountId::new(10), + amount: 100, + }], }; - let coin = one_coin(&env).expect("one_coin should succeed with exactly one fund"); - assert_eq!(coin.asset_id, AccountId::new(1)); + let coin = one_coin(&env).unwrap(); + assert_eq!(coin.asset_id, AccountId::new(10)); assert_eq!(coin.amount, 100); } #[test] - fn test_one_coin_fails_with_zero_funds() { + fn test_one_coin_error() { let env = TestEnv { - whoami: AccountId::new(10), - sender: AccountId::new(20), + whoami: AccountId::new(1), + sender: AccountId::new(2), funds: vec![], }; - let err = one_coin(&env).expect_err("one_coin should fail with zero funds"); + let err = one_coin(&env).unwrap_err(); assert_eq!(err, ERR_ONE_COIN); } #[test] - fn test_one_coin_fails_with_multiple_funds() { - let env = TestEnv { - whoami: AccountId::new(10), - sender: AccountId::new(20), - funds: vec![ - FungibleAsset { - asset_id: AccountId::new(1), - amount: 10, - }, - FungibleAsset { - asset_id: AccountId::new(2), - amount: 20, - }, - ], - }; + fn test_storage_key_size_constant() { + assert_eq!(MAX_STORAGE_KEY_SIZE, 254); + } - let err = one_coin(&env).expect_err("one_coin should fail with multiple funds"); - assert_eq!(err, ERR_ONE_COIN); + #[test] + fn test_account_id_u128_compat() { + let id = AccountId::new(42u128); + assert_eq!(id.inner(), 42u128); + + let mut expected = [0u8; 32]; + expected[31] = 42; + assert_eq!(id.as_bytes(), expected); + } + + #[test] + fn test_account_id_increase() { + let a = AccountId::from_bytes([0u8; 32]); + let b = a.increase(); + let mut expected = [0u8; 32]; + expected[31] = 1; + assert_eq!(b.as_bytes(), expected); + } + + #[test] + fn test_ensure_macro() { + fn test_fn(val: i32) -> SdkResult<()> { + ensure!(val > 10, ERR_UNAUTHORIZED); + Ok(()) + } + + assert!(test_fn(11).is_ok()); + assert_eq!(test_fn(5).unwrap_err(), ERR_UNAUTHORIZED); + } + + #[test] + fn test_message_btreemap_roundtrip() { + let mut map = BTreeMap::new(); + map.insert("a".to_string(), 1u64); + map.insert("b".to_string(), 2u64); + + let msg = Message::new(&map).unwrap(); + let decoded: BTreeMap = msg.get().unwrap(); + + assert_eq!(decoded.get("a"), Some(&1u64)); + assert_eq!(decoded.get("b"), Some(&2u64)); } } diff --git a/crates/app/sdk/core/src/low_level.rs b/crates/app/sdk/core/src/low_level.rs index 0cfb50b..545043c 100644 --- a/crates/app/sdk/core/src/low_level.rs +++ b/crates/app/sdk/core/src/low_level.rs @@ -1,6 +1,7 @@ use crate::encoding::{Decodable, Encodable}; use crate::runtime_api::{ - CreateAccountRequest, CreateAccountResponse, MigrateRequest, RUNTIME_ACCOUNT_ID, + CreateAccountRequest, CreateAccountResponse, MigrateRequest, RegisterAccountAtIdRequest, + RegisterAccountAtIdResponse, RUNTIME_ACCOUNT_ID, }; use crate::{ AccountId, Environment, EnvironmentQuery, FungibleAsset, InvokableMessage, InvokeRequest, @@ -64,3 +65,22 @@ pub fn migrate_account( env.do_exec(RUNTIME_ACCOUNT_ID, &invoke_request, funds)? .get() } + +pub fn register_account_at_id( + account_id: AccountId, + code_id: String, + init_msg: &Req, + env: &mut dyn Environment, +) -> SdkResult<()> { + let _: RegisterAccountAtIdResponse = exec_account( + RUNTIME_ACCOUNT_ID, + &RegisterAccountAtIdRequest { + account_id, + code_id, + init_message: Message::new(init_msg)?, + }, + vec![], + env, + )?; + Ok(()) +} diff --git a/crates/app/sdk/core/src/runtime_api.rs b/crates/app/sdk/core/src/runtime_api.rs index 2495104..d1edf4b 100644 --- a/crates/app/sdk/core/src/runtime_api.rs +++ b/crates/app/sdk/core/src/runtime_api.rs @@ -3,7 +3,7 @@ use crate::{AccountId, InvokableMessage, InvokeRequest, InvokeResponse}; use borsh::{BorshDeserialize, BorshSerialize}; pub const ACCOUNT_IDENTIFIER_PREFIX: u8 = 0; pub const ACCOUNT_IDENTIFIER_SINGLETON_PREFIX: u8 = 1; -pub const RUNTIME_ACCOUNT_ID: AccountId = AccountId(0); +pub const RUNTIME_ACCOUNT_ID: AccountId = AccountId::from_bytes([0u8; 32]); /// Storage key for consensus parameters. /// This is a well-known key that nodes read to validate blocks during sync. @@ -26,6 +26,21 @@ pub struct CreateAccountResponse { pub init_response: Message, } +#[derive(BorshDeserialize, BorshSerialize, Clone)] +pub struct RegisterAccountAtIdRequest { + pub account_id: AccountId, + pub code_id: String, + pub init_message: Message, +} + +impl InvokableMessage for RegisterAccountAtIdRequest { + const FUNCTION_IDENTIFIER: u64 = 2; + const FUNCTION_IDENTIFIER_NAME: &'static str = "register_account_at_id"; +} + +#[derive(BorshDeserialize, BorshSerialize, Clone)] +pub struct RegisterAccountAtIdResponse {} + #[derive(BorshDeserialize, BorshSerialize, Clone)] pub struct MigrateRequest { pub account_id: AccountId, @@ -34,7 +49,7 @@ pub struct MigrateRequest { } impl InvokableMessage for MigrateRequest { - const FUNCTION_IDENTIFIER: u64 = 2; + const FUNCTION_IDENTIFIER: u64 = 3; const FUNCTION_IDENTIFIER_NAME: &'static str = "migrate"; } diff --git a/crates/app/sdk/core/src/storage_api.rs b/crates/app/sdk/core/src/storage_api.rs index e0aa2fd..31f7a6c 100644 --- a/crates/app/sdk/core/src/storage_api.rs +++ b/crates/app/sdk/core/src/storage_api.rs @@ -1,7 +1,7 @@ use crate::{AccountId, InvokableMessage, Message}; use borsh::{BorshDeserialize, BorshSerialize}; -pub const STORAGE_ACCOUNT_ID: AccountId = AccountId(1); +pub const STORAGE_ACCOUNT_ID: AccountId = AccountId::new(1); #[derive(BorshDeserialize, BorshSerialize, Clone)] pub struct StorageGetRequest { diff --git a/crates/app/sdk/stf_traits/src/lib.rs b/crates/app/sdk/stf_traits/src/lib.rs index 3bd12d3..71abfd9 100644 --- a/crates/app/sdk/stf_traits/src/lib.rs +++ b/crates/app/sdk/stf_traits/src/lib.rs @@ -24,10 +24,35 @@ pub trait Transaction { fn funds(&self) -> &[FungibleAsset]; fn compute_identifier(&self) -> [u8; 32]; + /// Resolve the canonical sender account ID in the current state context. + /// + /// Default behavior is the static sender embedded in the transaction. + /// ETH transactions can override this to use address->account registry lookup. + fn resolve_sender_account(&self, _env: &mut dyn Environment) -> SdkResult { + Ok(self.sender()) + } + + /// Resolve the canonical recipient account ID in the current state context. + /// + /// Default behavior is the static recipient embedded in the transaction. + fn resolve_recipient_account(&self, _env: &mut dyn Environment) -> SdkResult { + Ok(self.recipient()) + } + /// Optional sender bootstrap primitive for STF account auto-registration. fn sender_bootstrap(&self) -> Option { None } + + /// Optional original 20-byte sender address (for ETH-compatible indexing). + fn sender_eth_address(&self) -> Option<[u8; 20]> { + None + } + + /// Optional original 20-byte recipient address (for ETH-compatible indexing). + fn recipient_eth_address(&self) -> Option<[u8; 20]> { + None + } } /// Provides the message payload used for account-level authentication. diff --git a/crates/app/sdk/testing/src/lib.rs b/crates/app/sdk/testing/src/lib.rs index fcdcadb..2989aca 100644 --- a/crates/app/sdk/testing/src/lib.rs +++ b/crates/app/sdk/testing/src/lib.rs @@ -111,7 +111,7 @@ impl MockEnv { StorageSetRequest::FUNCTION_IDENTIFIER => { let storage_set: StorageSetRequest = request.get()?; - let mut key = self.whoami.as_bytes(); + let mut key = self.whoami.as_bytes().to_vec(); key.extend(storage_set.key); self.state.insert(key, storage_set.value.as_vec()?); @@ -120,7 +120,7 @@ impl MockEnv { } StorageRemoveRequest::FUNCTION_IDENTIFIER => { let storage_remove: StorageRemoveRequest = request.get()?; - let mut key = self.whoami.as_bytes(); + let mut key = self.whoami.as_bytes().to_vec(); key.extend(storage_remove.key); self.state.remove(&key); Ok(InvokeResponse::new(&StorageRemoveResponse {})?) @@ -134,7 +134,7 @@ impl MockEnv { StorageGetRequest::FUNCTION_IDENTIFIER => { let storage_get: StorageGetRequest = request.get()?; - let mut key = storage_get.account_id.as_bytes(); + let mut key = storage_get.account_id.as_bytes().to_vec(); key.extend(storage_get.key); let value = self.state.get(&key).cloned(); diff --git a/crates/app/stf/src/handlers.rs b/crates/app/stf/src/handlers.rs index d161fc6..7a3a735 100644 --- a/crates/app/stf/src/handlers.rs +++ b/crates/app/stf/src/handlers.rs @@ -11,7 +11,8 @@ use crate::errors::{ERR_ACCOUNT_DOES_NOT_EXIST, ERR_CODE_NOT_FOUND, ERR_SAME_COD use crate::invoker::Invoker; use crate::runtime_api_impl; use evolve_core::runtime_api::{ - CreateAccountRequest, CreateAccountResponse, MigrateRequest, RUNTIME_ACCOUNT_ID, + CreateAccountRequest, CreateAccountResponse, MigrateRequest, RegisterAccountAtIdRequest, + RegisterAccountAtIdResponse, RUNTIME_ACCOUNT_ID, }; use evolve_core::storage_api::{ StorageGetRequest, StorageGetResponse, StorageRemoveRequest, StorageRemoveResponse, @@ -56,6 +57,11 @@ pub fn handle_system_exec( }; Ok(InvokeResponse::new(&resp)?) } + RegisterAccountAtIdRequest::FUNCTION_IDENTIFIER => { + let req: RegisterAccountAtIdRequest = request.get()?; + invoker.register_account_at_id(req.account_id, &req.code_id, req.init_message)?; + Ok(InvokeResponse::new(&RegisterAccountAtIdResponse {})?) + } MigrateRequest::FUNCTION_IDENTIFIER => { // exec on behalf of runtime the migration request, runtime has the money // so runtime needs to send the money to the account, so here we simulate that @@ -114,7 +120,7 @@ pub fn handle_storage_exec( StorageSetRequest::FUNCTION_IDENTIFIER => { let storage_set: StorageSetRequest = request.get()?; - let mut key = invoker.whoami.as_bytes(); + let mut key = invoker.whoami.as_bytes().to_vec(); key.extend(storage_set.key); // increase gas costs @@ -127,7 +133,7 @@ pub fn handle_storage_exec( } StorageRemoveRequest::FUNCTION_IDENTIFIER => { let storage_remove: StorageRemoveRequest = request.get()?; - let mut key = invoker.whoami.as_bytes(); + let mut key = invoker.whoami.as_bytes().to_vec(); key.extend(storage_remove.key); invoker.gas_counter.consume_remove_gas(&key)?; invoker.storage.remove(&key)?; @@ -145,7 +151,7 @@ pub fn handle_storage_query( StorageGetRequest::FUNCTION_IDENTIFIER => { let storage_get: StorageGetRequest = request.get()?; - let mut key = storage_get.account_id.as_bytes(); + let mut key = storage_get.account_id.as_bytes().to_vec(); key.extend(storage_get.key); let value = invoker.storage.get(&key)?; diff --git a/crates/app/stf/src/lib.rs b/crates/app/stf/src/lib.rs index f7dec10..5872627 100644 --- a/crates/app/stf/src/lib.rs +++ b/crates/app/stf/src/lib.rs @@ -348,7 +348,7 @@ mod model_tests { } fn account_storage_key(account: AccountId, key: &[u8]) -> Vec { - let mut out = account.as_bytes(); + let mut out = account.as_bytes().to_vec(); out.extend_from_slice(key); out } @@ -1266,11 +1266,31 @@ where storage_gas_config: gas_config, }; + let resolved_sender = { + let mut resolve_ctx = + Invoker::new_for_begin_block(state, codes, &mut gas_counter, block); + let sender = match tx.resolve_sender_account(&mut resolve_ctx) { + Ok(sender) => sender, + Err(err) => { + drop(resolve_ctx); + let gas_used = gas_counter.gas_used(); + let events = state.pop_events(); + return TxResult { + events, + gas_used, + response: Err(err), + }; + } + }; + drop(resolve_ctx); + sender + }; + // Auto-register sender when transaction provides a bootstrap primitive. if let Some(bootstrap) = tx.sender_bootstrap() { let mut reg_ctx = Invoker::new_for_begin_block(state, codes, &mut gas_counter, block); if let Err(err) = reg_ctx.register_account_at_id( - tx.sender(), + resolved_sender, bootstrap.account_code_id, bootstrap.init_message, ) { @@ -1313,9 +1333,22 @@ where // exec tx - transforms validation context to execution context // while preserving the same underlying state - let mut ctx = ctx.into_new_exec(tx.sender()); + let mut ctx = ctx.into_new_exec(resolved_sender); - let response = ctx.do_exec(tx.recipient(), tx.request(), tx.funds().to_vec()); + let recipient = match tx.resolve_recipient_account(&mut ctx) { + Ok(recipient) => recipient, + Err(err) => { + drop(ctx); + let gas_used = gas_counter.gas_used(); + let events = state.pop_events(); + return TxResult { + events, + gas_used, + response: Err(err), + }; + } + }; + let response = ctx.do_exec(recipient, tx.request(), tx.funds().to_vec()); // Run post-tx handler (e.g., for fee collection, logging, etc.) // The handler can observe the result and make additional state changes diff --git a/crates/app/tx/eth/src/eoa_registry.rs b/crates/app/tx/eth/src/eoa_registry.rs new file mode 100644 index 0000000..0f3578c --- /dev/null +++ b/crates/app/tx/eth/src/eoa_registry.rs @@ -0,0 +1,249 @@ +use crate::traits::{derive_eth_eoa_account_id, derive_runtime_contract_address}; +use alloy_primitives::Address; +use evolve_core::low_level::{exec_account, query_account, register_account_at_id}; +use evolve_core::runtime_api::RUNTIME_ACCOUNT_ID; +use evolve_core::storage_api::{ + StorageGetRequest, StorageGetResponse, StorageSetRequest, StorageSetResponse, + STORAGE_ACCOUNT_ID, +}; +use evolve_core::{AccountId, Environment, EnvironmentQuery, Message, ReadonlyKV, SdkResult}; + +const EOA_ADDR_TO_ID_PREFIX: &[u8] = b"registry/eoa/eth/a2i/"; +const EOA_ID_TO_ADDR_PREFIX: &[u8] = b"registry/eoa/eth/i2a/"; +const CONTRACT_ADDR_TO_ID_PREFIX: &[u8] = b"registry/contract/runtime/a2i/"; +const CONTRACT_ID_TO_ADDR_PREFIX: &[u8] = b"registry/contract/runtime/i2a/"; +const ETH_EOA_CODE_ID: &str = "EthEoaAccount"; + +fn addr_to_id_key(address: Address) -> Vec { + let mut key = Vec::with_capacity(EOA_ADDR_TO_ID_PREFIX.len() + 20); + key.extend_from_slice(EOA_ADDR_TO_ID_PREFIX); + key.extend_from_slice(address.as_slice()); + key +} + +fn id_to_addr_key(account_id: AccountId) -> Vec { + let account_bytes = account_id.as_bytes(); + let mut key = Vec::with_capacity(EOA_ID_TO_ADDR_PREFIX.len() + account_bytes.len()); + key.extend_from_slice(EOA_ID_TO_ADDR_PREFIX); + key.extend_from_slice(&account_bytes); + key +} + +fn contract_addr_to_id_key(address: Address) -> Vec { + let mut key = Vec::with_capacity(CONTRACT_ADDR_TO_ID_PREFIX.len() + 20); + key.extend_from_slice(CONTRACT_ADDR_TO_ID_PREFIX); + key.extend_from_slice(address.as_slice()); + key +} + +fn contract_id_to_addr_key(account_id: AccountId) -> Vec { + let account_bytes = account_id.as_bytes(); + let mut key = Vec::with_capacity(CONTRACT_ID_TO_ADDR_PREFIX.len() + account_bytes.len()); + key.extend_from_slice(CONTRACT_ID_TO_ADDR_PREFIX); + key.extend_from_slice(&account_bytes); + key +} + +fn decode_account_id(message: Message) -> SdkResult { + message.get::() +} + +pub fn lookup_account_id_in_env( + address: Address, + env: &mut dyn EnvironmentQuery, +) -> SdkResult> { + let response: StorageGetResponse = query_account( + STORAGE_ACCOUNT_ID, + &StorageGetRequest { + account_id: RUNTIME_ACCOUNT_ID, + key: addr_to_id_key(address), + }, + env, + )?; + match response.value { + Some(raw) => Ok(Some(decode_account_id(raw)?)), + None => Ok(None), + } +} + +pub fn lookup_address_in_env( + account_id: AccountId, + env: &mut dyn EnvironmentQuery, +) -> SdkResult> { + let response: StorageGetResponse = query_account( + STORAGE_ACCOUNT_ID, + &StorageGetRequest { + account_id: RUNTIME_ACCOUNT_ID, + key: id_to_addr_key(account_id), + }, + env, + )?; + match response.value { + Some(raw) => { + let bytes = raw.get::<[u8; 20]>()?; + Ok(Some(Address::from(bytes))) + } + None => Ok(None), + } +} + +pub fn lookup_account_id_in_storage( + storage: &S, + address: Address, +) -> SdkResult> { + let mut full_key = RUNTIME_ACCOUNT_ID.as_bytes().to_vec(); + full_key.extend_from_slice(&addr_to_id_key(address)); + match storage.get(&full_key)? { + Some(raw) => Ok(Some(decode_account_id(Message::from_bytes(raw))?)), + None => Ok(None), + } +} + +pub fn lookup_contract_account_id_in_env( + address: Address, + env: &mut dyn EnvironmentQuery, +) -> SdkResult> { + let response: StorageGetResponse = query_account( + STORAGE_ACCOUNT_ID, + &StorageGetRequest { + account_id: RUNTIME_ACCOUNT_ID, + key: contract_addr_to_id_key(address), + }, + env, + )?; + match response.value { + Some(raw) => Ok(Some(decode_account_id(raw)?)), + None => Ok(None), + } +} + +pub fn lookup_contract_account_id_in_storage( + storage: &S, + address: Address, +) -> SdkResult> { + let mut full_key = RUNTIME_ACCOUNT_ID.as_bytes().to_vec(); + full_key.extend_from_slice(&contract_addr_to_id_key(address)); + match storage.get(&full_key)? { + Some(raw) => Ok(Some(decode_account_id(Message::from_bytes(raw))?)), + None => Ok(None), + } +} + +pub fn lookup_address_in_storage( + storage: &S, + account_id: AccountId, +) -> SdkResult> { + let mut full_key = RUNTIME_ACCOUNT_ID.as_bytes().to_vec(); + full_key.extend_from_slice(&id_to_addr_key(account_id)); + match storage.get(&full_key)? { + Some(raw) => { + let bytes = Message::from_bytes(raw).get::<[u8; 20]>()?; + Ok(Some(Address::from(bytes))) + } + None => Ok(None), + } +} + +fn set_mapping( + address: Address, + account_id: AccountId, + env: &mut dyn Environment, +) -> SdkResult<()> { + if let Some(existing) = lookup_account_id_in_env(address, env)? { + if existing != account_id { + return Err(evolve_core::ErrorCode::new(0x5A)); + } + } + if let Some(existing_addr) = lookup_address_in_env(account_id, env)? { + if existing_addr != address { + return Err(evolve_core::ErrorCode::new(0x5A)); + } + } + + let _resp: StorageSetResponse = exec_account( + STORAGE_ACCOUNT_ID, + &StorageSetRequest { + key: addr_to_id_key(address), + value: Message::new(&account_id)?, + }, + vec![], + env, + )?; + let _resp: StorageSetResponse = exec_account( + STORAGE_ACCOUNT_ID, + &StorageSetRequest { + key: id_to_addr_key(account_id), + value: Message::new(&address.into_array())?, + }, + vec![], + env, + )?; + Ok(()) +} + +pub fn resolve_or_create_eoa_account( + address: Address, + env: &mut dyn Environment, +) -> SdkResult { + if let Some(account_id) = lookup_account_id_in_env(address, env)? { + return Ok(account_id); + } + + let account_id = derive_eth_eoa_account_id(address); + register_account_at_id( + account_id, + ETH_EOA_CODE_ID.to_string(), + &address.into_array(), + env, + )?; + + set_mapping(address, account_id, env)?; + Ok(account_id) +} + +pub fn register_runtime_contract_account( + account_id: AccountId, + env: &mut dyn Environment, +) -> SdkResult
{ + let address = derive_runtime_contract_address(account_id); + if let Some(existing) = lookup_contract_account_id_in_env(address, env)? { + if existing != account_id { + return Err(evolve_core::ErrorCode::new(0x5A)); + } + } + + let response: StorageGetResponse = query_account( + STORAGE_ACCOUNT_ID, + &StorageGetRequest { + account_id: RUNTIME_ACCOUNT_ID, + key: contract_id_to_addr_key(account_id), + }, + env, + )?; + if let Some(raw) = response.value { + let existing = raw.get::<[u8; 20]>()?; + if existing != address.into_array() { + return Err(evolve_core::ErrorCode::new(0x5A)); + } + } + + let _resp: StorageSetResponse = exec_account( + STORAGE_ACCOUNT_ID, + &StorageSetRequest { + key: contract_addr_to_id_key(address), + value: Message::new(&account_id)?, + }, + vec![], + env, + )?; + let _resp: StorageSetResponse = exec_account( + STORAGE_ACCOUNT_ID, + &StorageSetRequest { + key: contract_id_to_addr_key(account_id), + value: Message::new(&address.into_array())?, + }, + vec![], + env, + )?; + Ok(address) +} diff --git a/crates/app/tx/eth/src/lib.rs b/crates/app/tx/eth/src/lib.rs index 09d4411..af88ccc 100644 --- a/crates/app/tx/eth/src/lib.rs +++ b/crates/app/tx/eth/src/lib.rs @@ -35,6 +35,7 @@ pub mod decoder; pub mod envelope; +pub mod eoa_registry; pub mod error; pub mod ethereum; pub mod gateway; @@ -45,9 +46,18 @@ pub mod verifier; // Re-export main types pub use decoder::TypedTxDecoder; pub use envelope::{tx_type, TxEnvelope}; +pub use eoa_registry::{ + lookup_account_id_in_env, lookup_account_id_in_storage, lookup_address_in_env, + lookup_address_in_storage, lookup_contract_account_id_in_env, + lookup_contract_account_id_in_storage, register_runtime_contract_account, + resolve_or_create_eoa_account, +}; pub use error::*; pub use ethereum::{SignedEip1559Tx, SignedLegacyTx}; pub use gateway::{EthGateway, GatewayError}; pub use mempool::TxContext; -pub use traits::{account_id_to_address, address_to_account_id, TypedTransaction}; +pub use traits::{ + derive_eth_eoa_account_id, derive_runtime_contract_account_id, derive_runtime_contract_address, + derive_system_account_id, TypedTransaction, +}; pub use verifier::{EcdsaVerifier, SignatureVerifierRegistry}; diff --git a/crates/app/tx/eth/src/mempool.rs b/crates/app/tx/eth/src/mempool.rs index f24760e..2e44801 100644 --- a/crates/app/tx/eth/src/mempool.rs +++ b/crates/app/tx/eth/src/mempool.rs @@ -13,12 +13,15 @@ use alloy_primitives::{Address, B256}; use evolve_core::encoding::{Decodable, Encodable}; -use evolve_core::{AccountId, FungibleAsset, InvokeRequest, Message, SdkResult}; +use evolve_core::{AccountId, Environment, FungibleAsset, InvokeRequest, Message, SdkResult}; use evolve_mempool::{GasPriceOrdering, MempoolTx, SenderKey}; -use evolve_stf_traits::{AuthenticationPayload, SenderBootstrap, Transaction}; +use evolve_stf_traits::{AuthenticationPayload, Transaction}; use crate::envelope::TxEnvelope; -use crate::traits::{address_to_account_id, TypedTransaction}; +use crate::eoa_registry::{ + lookup_account_id_in_env, lookup_contract_account_id_in_env, resolve_or_create_eoa_account, +}; +use crate::traits::TypedTransaction; /// A verified transaction ready for mempool storage. /// @@ -28,10 +31,6 @@ use crate::traits::{address_to_account_id, TypedTransaction}; pub struct TxContext { /// The original Ethereum transaction envelope. envelope: TxEnvelope, - /// Sender account ID (derived from address). - sender_id: AccountId, - /// Recipient account ID (derived from address). - recipient_id: AccountId, /// The invoke request to execute (derived by evolve_tx). invoke_request: InvokeRequest, /// Gas price for ordering (effective gas price). @@ -43,9 +42,7 @@ impl TxContext { /// /// Returns `None` if the transaction has no recipient (contract creation). pub fn new(envelope: TxEnvelope, base_fee: u128) -> Option { - let sender_id = address_to_account_id(envelope.sender()); - let recipient = envelope.to()?; - let recipient_id = address_to_account_id(recipient); + envelope.to()?; let invoke_request = envelope.to_invoke_requests().into_iter().next()?; @@ -54,8 +51,6 @@ impl TxContext { Some(Self { envelope, - sender_id, - recipient_id, invoke_request, effective_gas_price, }) @@ -114,11 +109,29 @@ impl MempoolTx for TxContext { impl Transaction for TxContext { fn sender(&self) -> AccountId { - self.sender_id + AccountId::invalid() + } + + fn resolve_sender_account(&self, env: &mut dyn Environment) -> SdkResult { + resolve_or_create_eoa_account(self.sender_address(), env) } fn recipient(&self) -> AccountId { - self.recipient_id + AccountId::invalid() + } + + fn resolve_recipient_account(&self, env: &mut dyn Environment) -> SdkResult { + let to = self + .envelope + .to() + .ok_or_else(|| evolve_core::ErrorCode::new(0x50))?; + if let Some(account_id) = lookup_account_id_in_env(to, env)? { + return Ok(account_id); + } + if let Some(account_id) = lookup_contract_account_id_in_env(to, env)? { + return Ok(account_id); + } + Err(evolve_core::ErrorCode::new(0x52)) } fn request(&self) -> &InvokeRequest { @@ -138,18 +151,19 @@ impl Transaction for TxContext { self.envelope.tx_hash().0 } - fn sender_bootstrap(&self) -> Option { - let eth_address: [u8; 20] = self.sender_address().into(); - Some(SenderBootstrap { - account_code_id: "EthEoaAccount", - init_message: Message::new(ð_address).ok()?, - }) + fn sender_eth_address(&self) -> Option<[u8; 20]> { + Some(self.sender_address().into()) + } + + fn recipient_eth_address(&self) -> Option<[u8; 20]> { + self.envelope.to().map(Into::into) } } impl AuthenticationPayload for TxContext { fn authentication_payload(&self) -> SdkResult { - Message::new(&self.sender_id) + let sender: [u8; 20] = self.sender_address().into(); + Message::new(&sender) } } diff --git a/crates/app/tx/eth/src/traits.rs b/crates/app/tx/eth/src/traits.rs index cc4fdf7..93bcb53 100644 --- a/crates/app/tx/eth/src/traits.rs +++ b/crates/app/tx/eth/src/traits.rs @@ -1,145 +1,100 @@ -//! Core traits for typed transactions. +//! Core traits and canonical identity derivation for typed transactions. -use alloy_primitives::{Address, B256}; +use alloy_primitives::{keccak256, Address, B256}; use evolve_core::{AccountId, InvokeRequest}; +const DOMAIN_EOA_ETH_V1: &[u8] = b"eoa:eth:v1"; +const DOMAIN_CONTRACT_RUNTIME_V1: &[u8] = b"contract:runtime:v1"; +const DOMAIN_SYSTEM_V1: &[u8] = b"system:v1"; +const DOMAIN_CONTRACT_ADDR_RUNTIME_V1: &[u8] = b"contract:addr:runtime:v1"; + +/// Derive a canonical ETH EOA account ID from a 20-byte address. +pub fn derive_eth_eoa_account_id(addr: Address) -> AccountId { + derive_account_id(DOMAIN_EOA_ETH_V1, addr.as_slice()) +} + +/// Derive a canonical runtime contract account ID from creation entropy. +pub fn derive_runtime_contract_account_id(creation_entropy: &[u8]) -> AccountId { + derive_account_id(DOMAIN_CONTRACT_RUNTIME_V1, creation_entropy) +} + +/// Derive a canonical system/module account ID from a module name. +pub fn derive_system_account_id(module_name: &str) -> AccountId { + derive_account_id(DOMAIN_SYSTEM_V1, module_name.as_bytes()) +} + +/// Deterministic runtime contract address used for explicit registry mappings. +pub fn derive_runtime_contract_address(account_id: AccountId) -> Address { + let mut input = Vec::with_capacity(DOMAIN_CONTRACT_ADDR_RUNTIME_V1.len() + 32); + input.extend_from_slice(DOMAIN_CONTRACT_ADDR_RUNTIME_V1); + input.extend_from_slice(&account_id.as_bytes()); + let digest = keccak256(&input); + Address::from_slice(&digest.as_slice()[12..32]) +} + +fn derive_account_id(domain_tag: &[u8], payload: &[u8]) -> AccountId { + let mut input = Vec::with_capacity(domain_tag.len() + payload.len()); + input.extend_from_slice(domain_tag); + input.extend_from_slice(payload); + AccountId::from_bytes(keccak256(&input).0) +} + /// Core trait that all transaction types must implement. -/// -/// This trait defines the common interface for all transaction types, -/// whether they are standard Ethereum transactions or custom Evolve types. pub trait TypedTransaction: Send + Sync { - /// Returns the EIP-2718 transaction type byte. - /// - /// - `0x00-0x7f`: Reserved for Ethereum standard types - /// - `0x80-0xff`: Available for custom/L2 types fn tx_type(&self) -> u8; - - /// Returns the sender address. - /// - /// For signed transactions, this is recovered from the signature. fn sender(&self) -> Address; - - /// Returns the transaction hash. fn tx_hash(&self) -> B256; - - /// Returns the gas limit for this transaction. fn gas_limit(&self) -> u64; - - /// Returns the chain ID for replay protection (EIP-155). - /// - /// Returns `None` for legacy transactions without EIP-155. fn chain_id(&self) -> Option; - - /// Returns the nonce for this transaction. fn nonce(&self) -> u64; - - /// Returns the recipient address, if any. - /// - /// Returns `None` for contract creation transactions. fn to(&self) -> Option
; - - /// Returns the value (in wei) being transferred. fn value(&self) -> alloy_primitives::U256; - - /// Returns the input data for this transaction. fn input(&self) -> &[u8]; - - /// Converts the transaction to internal InvokeRequest(s). - /// - /// Standard Ethereum transactions produce a single InvokeRequest. - /// Batch transactions may produce multiple. fn to_invoke_requests(&self) -> Vec; - /// Converts the sender Address to an AccountId. - /// - /// Uses the lower 128 bits of the address for compatibility. fn sender_account_id(&self) -> AccountId { - address_to_account_id(self.sender()) + derive_eth_eoa_account_id(self.sender()) } - /// Converts the recipient Address to an AccountId, if present. fn recipient_account_id(&self) -> Option { - self.to().map(address_to_account_id) + self.to().map(derive_eth_eoa_account_id) } } -/// Converts an Ethereum Address (20 bytes) to an AccountId (u128). -/// -/// Takes the last 16 bytes of the address. This mapping is reversible: -/// `address_to_account_id(account_id_to_address(id)) == id` -/// -/// This allows contract accounts to be addressed via Ethereum transactions. -/// The first 4 bytes of the address are discarded, so addresses that only -/// differ in those bytes will map to the same AccountId. -pub fn address_to_account_id(addr: Address) -> AccountId { - let bytes = addr.as_slice(); - let mut id_bytes = [0u8; 16]; - id_bytes.copy_from_slice(&bytes[4..]); - AccountId::new(u128::from_be_bytes(id_bytes)) -} - -/// Converts an AccountId to an Ethereum Address. -/// -/// Pads with zeros in the first 4 bytes. This is the inverse of `address_to_account_id`: -/// `address_to_account_id(account_id_to_address(id)) == id` -/// -/// For EOA addresses derived from public keys (which have random first 4 bytes), -/// this won't recover the original address. But for contract addresses that were -/// created from AccountIds, this is a perfect round-trip. -pub fn account_id_to_address(id: AccountId) -> Address { - let id_bytes = id.as_bytes(); - let mut addr_bytes = [0u8; 20]; - // Copy 16 bytes of AccountId to last 16 bytes of address - addr_bytes[4..20].copy_from_slice(&id_bytes[..16]); - Address::from_slice(&addr_bytes) -} - #[cfg(test)] mod tests { use super::*; #[test] - fn test_account_id_to_address_encodes_id_bytes() { - let id = AccountId::new(0x112233445566778899aabbccddeeff00); - let addr = account_id_to_address(id); - assert_eq!(&addr.as_slice()[4..], &id.as_bytes()[..16]); + fn test_eth_eoa_derivation_is_deterministic() { + let addr = Address::from([0x11; 20]); + assert_eq!( + derive_eth_eoa_account_id(addr), + derive_eth_eoa_account_id(addr) + ); } #[test] - fn test_account_id_address_round_trip() { - // For any AccountId, converting to address and back should give the same ID - let id = AccountId::new(0x112233445566778899aabbccddeeff00); - let addr = account_id_to_address(id); - let id_back = address_to_account_id(addr); - assert_eq!(id, id_back); - - // Test with various values - for i in [0u128, 1, u128::MAX, 0xdeadbeef, 12345678901234567890] { - let id = AccountId::new(i); - let addr = account_id_to_address(id); - let id_back = address_to_account_id(addr); - assert_eq!(id, id_back, "round-trip failed for id={}", i); - } + fn test_domains_are_separated() { + let payload = [0x22; 20]; + let eoa = derive_account_id(DOMAIN_EOA_ETH_V1, &payload); + let contract = derive_account_id(DOMAIN_CONTRACT_RUNTIME_V1, &payload); + assert_ne!(eoa, contract); } #[test] - fn test_address_to_account_id_first_4_bytes_ignored() { - // Addresses that only differ in the first 4 bytes map to the same AccountId. - // This is the trade-off for reversibility with account_id_to_address. - let mut bytes1 = [0u8; 20]; - let mut bytes2 = [0u8; 20]; - bytes1[0] = 0x01; - bytes2[0] = 0x02; - let addr1 = Address::from_slice(&bytes1); - let addr2 = Address::from_slice(&bytes2); - - // These collide - this is expected and documented behavior - assert_eq!(address_to_account_id(addr1), address_to_account_id(addr2)); + fn test_system_derivation() { + let a = derive_system_account_id("runtime"); + let b = derive_system_account_id("storage"); + assert_ne!(a, b); + } - // But addresses differing in the last 16 bytes do NOT collide - let mut bytes3 = [0u8; 20]; - bytes3[19] = 0x01; - let addr3 = Address::from_slice(&bytes3); - assert_ne!(address_to_account_id(addr1), address_to_account_id(addr3)); + #[test] + fn test_runtime_contract_address_is_deterministic() { + let id = AccountId::new(0x112233u128); + let a = derive_runtime_contract_address(id); + let b = derive_runtime_contract_address(id); + assert_eq!(a, b); + assert_ne!(derive_eth_eoa_account_id(a), id); } } diff --git a/crates/app/tx/eth/tests/integration_tests.rs b/crates/app/tx/eth/tests/integration_tests.rs index 0c9111e..c390e61 100644 --- a/crates/app/tx/eth/tests/integration_tests.rs +++ b/crates/app/tx/eth/tests/integration_tests.rs @@ -364,15 +364,15 @@ fn test_registry_verifies_correct_chain() { #[test] fn test_address_to_account_id_preserves_uniqueness() { - use evolve_tx_eth::address_to_account_id; + use evolve_tx_eth::derive_eth_eoa_account_id; let addr1 = Address::repeat_byte(0x11); let addr2 = Address::repeat_byte(0x22); let addr3 = Address::repeat_byte(0x33); - let id1 = address_to_account_id(addr1); - let id2 = address_to_account_id(addr2); - let id3 = address_to_account_id(addr3); + let id1 = derive_eth_eoa_account_id(addr1); + let id2 = derive_eth_eoa_account_id(addr2); + let id3 = derive_eth_eoa_account_id(addr3); // All should be unique assert_ne!(id1, id2); @@ -382,11 +382,11 @@ fn test_address_to_account_id_preserves_uniqueness() { #[test] fn test_sender_account_id_matches_address_conversion() { - use evolve_tx_eth::address_to_account_id; + use evolve_tx_eth::derive_eth_eoa_account_id; let signing_key = SigningKey::random(&mut OsRng); let sender = get_address(&signing_key); - let expected_id = address_to_account_id(sender); + let expected_id = derive_eth_eoa_account_id(sender); let tx = TxLegacy { chain_id: Some(1), diff --git a/crates/rpc/types/Cargo.toml b/crates/rpc/types/Cargo.toml index 56c7ebb..cb3c421 100644 --- a/crates/rpc/types/Cargo.toml +++ b/crates/rpc/types/Cargo.toml @@ -9,6 +9,7 @@ rust-version.workspace = true [dependencies] evolve_core.workspace = true +evolve_tx_eth.workspace = true alloy-primitives = { version = "0.8", features = ["serde"] } serde = { version = "1.0", features = ["derive"] } diff --git a/crates/rpc/types/src/lib.rs b/crates/rpc/types/src/lib.rs index 5d0fa00..d974ab7 100644 --- a/crates/rpc/types/src/lib.rs +++ b/crates/rpc/types/src/lib.rs @@ -22,27 +22,16 @@ pub struct ChainConfig { pub chain_id: u64, } -/// Convert an evolve AccountId to an Ethereum address. +/// Convert an internal AccountId to a deterministic compatibility address. /// -/// AccountId is u128, Address is 20 bytes. We take the lower 20 bytes. +/// This is one-way and is intended for indexing/display fallbacks only. pub fn account_id_to_address(account_id: evolve_core::AccountId) -> Address { - let bytes = account_id.as_bytes(); - // AccountId::as_bytes() returns big-endian u128 (16 bytes) - // Pad to 20 bytes by prepending 4 zero bytes - let mut addr_bytes = [0u8; 20]; - addr_bytes[4..].copy_from_slice(&bytes); - Address::from(addr_bytes) + evolve_tx_eth::derive_runtime_contract_address(account_id) } -/// Convert an Ethereum address to an evolve AccountId. -/// -/// Takes the lower 16 bytes of the address as a u128. +/// Derive canonical ETH-EOA AccountId from an Ethereum address. pub fn address_to_account_id(address: Address) -> evolve_core::AccountId { - let bytes = address.as_slice(); - // Take last 16 bytes (address is 20 bytes) - let mut id_bytes = [0u8; 16]; - id_bytes.copy_from_slice(&bytes[4..]); - evolve_core::AccountId::new(u128::from_be_bytes(id_bytes)) + evolve_tx_eth::derive_eth_eoa_account_id(address) } /// Sync status for eth_syncing response. @@ -211,7 +200,7 @@ mod proptests { use proptest::prelude::*; fn arb_account_id() -> impl Strategy { - any::().prop_map(evolve_core::AccountId::new) + any::<[u8; 32]>().prop_map(evolve_core::AccountId::from_bytes) } fn arb_address() -> impl Strategy { @@ -295,14 +284,14 @@ mod proptests { } proptest! { - // ==================== AccountId <-> Address conversion ==================== - // Tests our custom conversion logic between Evolve AccountId and Ethereum Address + #[test] + fn prop_account_id_to_address_is_deterministic(id in arb_account_id()) { + prop_assert_eq!(account_id_to_address(id), account_id_to_address(id)); + } #[test] - fn prop_account_id_to_address_roundtrip(id in arb_account_id()) { - let address = account_id_to_address(id); - let recovered = address_to_account_id(address); - prop_assert_eq!(id, recovered); + fn prop_address_to_account_id_is_deterministic(address in arb_address()) { + prop_assert_eq!(address_to_account_id(address), address_to_account_id(address)); } // ==================== Custom serde implementations ==================== diff --git a/crates/testing/debugger/src/breakpoints.rs b/crates/testing/debugger/src/breakpoints.rs index 536c920..a737a17 100644 --- a/crates/testing/debugger/src/breakpoints.rs +++ b/crates/testing/debugger/src/breakpoints.rs @@ -8,11 +8,8 @@ use evolve_core::AccountId; use serde::{Deserialize, Serialize}; /// Convert AccountId to serializable bytes. -fn account_to_bytes(id: AccountId) -> [u8; 16] { - let bytes = id.as_bytes(); - let mut arr = [0u8; 16]; - arr.copy_from_slice(&bytes[..16]); - arr +fn account_to_bytes(id: AccountId) -> [u8; 32] { + id.as_bytes() } /// A breakpoint condition. @@ -25,7 +22,7 @@ pub enum Breakpoint { OnTx([u8; 32]), /// Break on any transaction involving this account (stored as bytes). - OnAccount([u8; 16]), + OnAccount([u8; 32]), /// Break when a specific storage key is modified. OnStorageKey(Vec), diff --git a/crates/testing/debugger/src/inspector.rs b/crates/testing/debugger/src/inspector.rs index 4829d86..53131f7 100644 --- a/crates/testing/debugger/src/inspector.rs +++ b/crates/testing/debugger/src/inspector.rs @@ -10,11 +10,8 @@ use evolve_core::AccountId; use std::collections::HashMap; /// Convert AccountId to serializable bytes. -fn account_to_bytes(id: AccountId) -> [u8; 16] { - let bytes = id.as_bytes(); - let mut arr = [0u8; 16]; - arr.copy_from_slice(&bytes[..16]); - arr +fn account_to_bytes(id: AccountId) -> [u8; 32] { + id.as_bytes() } /// Inspector for examining trace and state. diff --git a/crates/testing/debugger/src/trace.rs b/crates/testing/debugger/src/trace.rs index 1759ff9..9743caa 100644 --- a/crates/testing/debugger/src/trace.rs +++ b/crates/testing/debugger/src/trace.rs @@ -11,18 +11,14 @@ use serde::{Deserialize, Serialize}; use std::collections::HashMap; /// Convert AccountId to serializable bytes. -fn account_to_bytes(id: AccountId) -> [u8; 16] { - let bytes = id.as_bytes(); - let mut arr = [0u8; 16]; - arr.copy_from_slice(&bytes[..16]); - arr +fn account_to_bytes(id: AccountId) -> [u8; 32] { + id.as_bytes() } /// Convert bytes back to AccountId. #[allow(dead_code)] -fn bytes_to_account(bytes: [u8; 16]) -> AccountId { - let value = u128::from_be_bytes(bytes); - AccountId::new(value) +fn bytes_to_account(bytes: [u8; 32]) -> AccountId { + AccountId::from_bytes(bytes) } /// A complete execution trace capturing all state transitions. @@ -226,10 +222,10 @@ pub enum TraceEvent { /// Transaction execution started. TxStart { tx_id: [u8; 32], - /// Sender account ID as bytes (u128 big-endian). - sender: [u8; 16], - /// Recipient account ID as bytes (u128 big-endian). - recipient: [u8; 16], + /// Sender account ID as canonical 32 bytes. + sender: [u8; 32], + /// Recipient account ID as canonical 32 bytes. + recipient: [u8; 32], event_index: usize, }, @@ -243,10 +239,10 @@ pub enum TraceEvent { /// A call was made between accounts. Call { - /// Caller account ID as bytes (u128 big-endian). - from: [u8; 16], - /// Callee account ID as bytes (u128 big-endian). - to: [u8; 16], + /// Caller account ID as canonical 32 bytes. + from: [u8; 32], + /// Callee account ID as canonical 32 bytes. + to: [u8; 32], function_id: u64, data_hash: [u8; 32], event_index: usize, diff --git a/crates/testing/simulator/src/eth_eoa.rs b/crates/testing/simulator/src/eth_eoa.rs index 3dddc61..60720d0 100644 --- a/crates/testing/simulator/src/eth_eoa.rs +++ b/crates/testing/simulator/src/eth_eoa.rs @@ -33,14 +33,14 @@ pub fn init_eth_eoa_storage( account_id: AccountId, eth_address: [u8; 20], ) -> Result<(), ErrorCode> { - let mut nonce_key = account_id.as_bytes(); + let mut nonce_key = account_id.as_bytes().to_vec(); nonce_key.push(0u8); let nonce_value = Message::new(&0u64) .expect("encode nonce") .into_bytes() .expect("nonce bytes"); - let mut addr_key = account_id.as_bytes(); + let mut addr_key = account_id.as_bytes().to_vec(); addr_key.push(1u8); let addr_value = Message::new(ð_address) .expect("encode eth address") From c5176cab696f0d4e18e34493640fe0a5cd785294 Mon Sep 17 00:00:00 2001 From: tac0turtle Date: Fri, 27 Feb 2026 10:27:06 +0100 Subject: [PATCH 06/10] include remaining chain-index and node changes --- crates/app/node/src/lib.rs | 73 +++++++++-- .../evolve_authentication/src/lib.rs | 3 +- crates/rpc/chain-index/src/integration.rs | 36 ++++-- crates/rpc/chain-index/src/lib.rs | 2 + crates/rpc/chain-index/src/provider.rs | 48 ++++--- crates/rpc/chain-index/src/querier.rs | 121 ++++++++++++++++++ docker/testapp/Dockerfile | 25 ++++ 7 files changed, 271 insertions(+), 37 deletions(-) create mode 100644 crates/rpc/chain-index/src/querier.rs create mode 100644 docker/testapp/Dockerfile diff --git a/crates/app/node/src/lib.rs b/crates/app/node/src/lib.rs index d083bc7..8fa288b 100644 --- a/crates/app/node/src/lib.rs +++ b/crates/app/node/src/lib.rs @@ -19,9 +19,12 @@ use alloy_primitives::U256; use borsh::{BorshDeserialize, BorshSerialize}; use commonware_runtime::tokio::{Config as TokioConfig, Context as TokioContext, Runner}; use commonware_runtime::{Runner as RunnerTrait, Spawner}; -use evolve_chain_index::{ChainStateProvider, ChainStateProviderConfig, PersistentChainIndex}; +use evolve_chain_index::{ + ChainStateProvider, ChainStateProviderConfig, PersistentChainIndex, StateQuerier, + StorageStateQuerier, +}; use evolve_core::encoding::Encodable; -use evolve_core::ReadonlyKV; +use evolve_core::{AccountId, 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}; @@ -176,6 +179,14 @@ pub struct GenesisOutput { pub changes: Vec, } +/// Trait for extracting the token account ID from a genesis result. +/// +/// Implementing this trait on your genesis result type enables +/// `eth_getBalance` queries via the RPC server. +pub trait HasTokenAccountId { + fn token_account_id(&self) -> AccountId; +} + type RuntimeContext = TokioContext; /// Build the block archive callback. @@ -244,7 +255,14 @@ pub fn run_dev_node< Codes: AccountsCodeStorage + Send + Sync + 'static, S: ReadonlyKV + Storage + Clone + Send + Sync + 'static, Stf: StfExecutor + Send + Sync + 'static, - G: BorshSerialize + BorshDeserialize + Clone + Debug + Send + Sync + 'static, + G: BorshSerialize + + BorshDeserialize + + Clone + + Debug + + HasTokenAccountId + + Send + + Sync + + 'static, BuildGenesisStf: Fn() -> Stf + Send + Sync + 'static, BuildStf: Fn(&G) -> Stf + Send + Sync + 'static, BuildCodes: Fn() -> Codes + Clone + Send + Sync + 'static, @@ -293,7 +311,14 @@ pub fn run_dev_node_with_rpc< Codes: AccountsCodeStorage + Send + Sync + 'static, S: ReadonlyKV + Storage + Clone + Send + Sync + 'static, Stf: StfExecutor + Send + Sync + 'static, - G: BorshSerialize + BorshDeserialize + Clone + Debug + Send + Sync + 'static, + G: BorshSerialize + + BorshDeserialize + + Clone + + Debug + + HasTokenAccountId + + Send + + Sync + + 'static, BuildGenesisStf: Fn() -> Stf + Send + Sync + 'static, BuildStf: Fn(&G) -> Stf + Send + Sync + 'static, BuildCodes: Fn() -> Codes + Clone + Send + Sync + 'static, @@ -407,11 +432,16 @@ pub fn run_dev_node_with_rpc< gas_price: U256::ZERO, sync_status: SyncStatus::NotSyncing(false), }; + let state_querier: Arc = Arc::new(StorageStateQuerier::new( + storage.clone(), + genesis_result.token_account_id(), + )); let state_provider = ChainStateProvider::with_account_codes( Arc::clone(&chain_index), state_provider_config, codes_for_rpc, - ); + ) + .with_state_querier(state_querier); // Start JSON-RPC server let server_config = RpcServerConfig { @@ -729,7 +759,14 @@ pub fn run_dev_node_with_rpc_and_mempool_eth< Codes: AccountsCodeStorage + Send + Sync + 'static, S: ReadonlyKV + Storage + Clone + Send + Sync + 'static, Stf: StfExecutor + Send + Sync + 'static, - G: BorshSerialize + BorshDeserialize + Clone + Debug + Send + Sync + 'static, + G: BorshSerialize + + BorshDeserialize + + Clone + + Debug + + HasTokenAccountId + + Send + + Sync + + 'static, BuildGenesisStf: Fn() -> Stf + Send + Sync + 'static, BuildStf: Fn(&G) -> Stf + Send + Sync + 'static, BuildCodes: Fn() -> Codes + Clone + Send + Sync + 'static, @@ -837,12 +874,22 @@ pub fn run_dev_node_with_rpc_and_mempool_eth< gas_price: U256::ZERO, sync_status: SyncStatus::NotSyncing(false), }; + + // Create state querier for balance/nonce reads + let state_querier: Arc = Arc::new( + StorageStateQuerier::new( + storage.clone(), + genesis_result.token_account_id(), + ), + ); + let state_provider = ChainStateProvider::with_mempool( Arc::clone(&chain_index), state_provider_config.clone(), Arc::clone(&codes_for_rpc), mempool.clone(), - ); + ) + .with_state_querier(Arc::clone(&state_querier)); let server_config = RpcServerConfig { http_addr: rpc_config.http_addr, @@ -865,7 +912,8 @@ pub fn run_dev_node_with_rpc_and_mempool_eth< state_provider_config, codes_for_rpc, mempool.clone(), - ); + ) + .with_state_querier(state_querier); let grpc_config = GrpcServerConfig { addr: grpc_addr, chain_id: rpc_config.chain_id, @@ -1004,7 +1052,14 @@ pub fn run_dev_node_with_rpc_and_mempool_mock_storage< ) where Codes: AccountsCodeStorage + Send + Sync + 'static, Stf: StfExecutor + Send + Sync + 'static, - G: BorshSerialize + BorshDeserialize + Clone + Debug + Send + Sync + 'static, + G: BorshSerialize + + BorshDeserialize + + Clone + + Debug + + HasTokenAccountId + + Send + + Sync + + 'static, BuildGenesisStf: Fn() -> Stf + Send + Sync + 'static, BuildStf: Fn(&G) -> Stf + Send + Sync + 'static, BuildCodes: Fn() -> Codes + Clone + Send + Sync + 'static, diff --git a/crates/app/sdk/standards/evolve_authentication/src/lib.rs b/crates/app/sdk/standards/evolve_authentication/src/lib.rs index 5b77135..2058114 100644 --- a/crates/app/sdk/standards/evolve_authentication/src/lib.rs +++ b/crates/app/sdk/standards/evolve_authentication/src/lib.rs @@ -42,8 +42,9 @@ impl TxV for AuthenticationTxValidator { fn validate_tx(&self, tx: &T, env: &mut dyn Environment) -> SdkResult<()> { + let sender_account = tx.resolve_sender_account(env)?; // trigger authentication - auth_interface::AuthenticationInterfaceRef::new(tx.sender()) + auth_interface::AuthenticationInterfaceRef::new(sender_account) .authenticate(tx.authentication_payload()?, env) .map_err(|e| { if e == ERR_UNKNOWN_FUNCTION { diff --git a/crates/rpc/chain-index/src/integration.rs b/crates/rpc/chain-index/src/integration.rs index e880d13..3bb298c 100644 --- a/crates/rpc/chain-index/src/integration.rs +++ b/crates/rpc/chain-index/src/integration.rs @@ -192,14 +192,21 @@ fn build_stored_transaction( transaction_index: u32, chain_id: u64, ) -> StoredTransaction { - let from = account_id_to_address(tx.sender()); + let from = tx + .sender_eth_address() + .map(Address::from) + .unwrap_or_else(|| account_id_to_address(tx.sender())); let to = { - let recipient = tx.recipient(); - // Check if recipient is the invalid/zero account - if recipient == AccountId::invalid() { - None + if let Some(recipient) = tx.recipient_eth_address() { + Some(Address::from(recipient)) } else { - Some(account_id_to_address(recipient)) + let recipient = tx.recipient(); + // Check if recipient is the invalid/zero account + if recipient == AccountId::invalid() { + None + } else { + Some(account_id_to_address(recipient)) + } } }; @@ -242,13 +249,20 @@ fn build_stored_receipt( transaction_index: u32, cumulative_gas_used: u64, ) -> StoredReceipt { - let from = account_id_to_address(tx.sender()); + let from = tx + .sender_eth_address() + .map(Address::from) + .unwrap_or_else(|| account_id_to_address(tx.sender())); let to = { - let recipient = tx.recipient(); - if recipient == AccountId::invalid() { - None + if let Some(recipient) = tx.recipient_eth_address() { + Some(Address::from(recipient)) } else { - Some(account_id_to_address(recipient)) + let recipient = tx.recipient(); + if recipient == AccountId::invalid() { + None + } else { + Some(account_id_to_address(recipient)) + } } }; diff --git a/crates/rpc/chain-index/src/lib.rs b/crates/rpc/chain-index/src/lib.rs index 947553e..789ba89 100644 --- a/crates/rpc/chain-index/src/lib.rs +++ b/crates/rpc/chain-index/src/lib.rs @@ -34,6 +34,7 @@ pub mod error; pub mod index; pub mod integration; pub mod provider; +pub mod querier; pub mod types; pub use cache::ChainCache; @@ -41,4 +42,5 @@ pub use error::{ChainIndexError, ChainIndexResult}; pub use index::{ChainIndex, PersistentChainIndex}; pub use integration::{build_index_data, event_to_stored_log, index_block, BlockMetadata}; pub use provider::{ChainStateProvider, ChainStateProviderConfig, NoopAccountCodes}; +pub use querier::{StateQuerier, StorageStateQuerier}; pub use types::*; diff --git a/crates/rpc/chain-index/src/provider.rs b/crates/rpc/chain-index/src/provider.rs index 06acef4..d82d5d4 100644 --- a/crates/rpc/chain-index/src/provider.rs +++ b/crates/rpc/chain-index/src/provider.rs @@ -18,6 +18,7 @@ use tokio::time::timeout; use crate::error::ChainIndexError; use crate::index::ChainIndex; +use crate::querier::StateQuerier; use evolve_core::schema::AccountSchema; use evolve_core::AccountCode; use evolve_eth_jsonrpc::error::RpcError; @@ -131,6 +132,8 @@ pub struct ChainStateProvider< mempool: Option>>, /// Optional ingress verifier for transaction decode/verify. verifier: Option>, + /// Optional state querier for balance/nonce/call queries. + state_querier: Option>, /// Cached module identifiers for schema introspection endpoints. module_ids_cache: parking_lot::RwLock>>, /// Cached per-module schema lookups. @@ -165,6 +168,7 @@ impl ChainStateProvider { account_codes: Arc::new(NoopAccountCodes), mempool: None, verifier: None, + state_querier: None, module_ids_cache: parking_lot::RwLock::new(None), module_schema_cache: parking_lot::RwLock::new(BTreeMap::new()), all_schemas_cache: parking_lot::RwLock::new(None), @@ -185,6 +189,7 @@ impl ChainStateProvider ChainStateProvider ChainStateProvider Option<&SharedMempool>> { self.mempool.as_ref() } + + /// Attach a state querier for balance/nonce/call queries. + pub fn with_state_querier(mut self, querier: Arc) -> Self { + self.state_querier = Some(querier); + self + } } impl From for RpcError { @@ -363,36 +375,40 @@ impl St } } - async fn get_balance(&self, _address: Address, _block: Option) -> Result { - // TODO: Implement state queries via Storage + STF - // For now, return zero - Ok(U256::ZERO) + async fn get_balance(&self, address: Address, _block: Option) -> Result { + match &self.state_querier { + Some(q) => q.get_balance(address).await, + None => Ok(U256::ZERO), + } } async fn get_transaction_count( &self, - _address: Address, + address: Address, _block: Option, ) -> Result { - // TODO: Implement nonce queries via Storage + STF - // For now, return zero - Ok(0) + match &self.state_querier { + Some(q) => q.get_transaction_count(address).await, + None => Ok(0), + } } - async fn call(&self, _request: &CallRequest, _block: Option) -> Result { - // TODO: Implement via STF::query() - // For now, return empty - Ok(Bytes::new()) + async fn call(&self, request: &CallRequest, _block: Option) -> Result { + match &self.state_querier { + Some(q) => q.call(request).await, + None => Ok(Bytes::new()), + } } async fn estimate_gas( &self, - _request: &CallRequest, + request: &CallRequest, _block: Option, ) -> Result { - // TODO: Implement via STF with gas tracking - // For now, return default gas - Ok(21000) + match &self.state_querier { + Some(q) => q.estimate_gas(request).await, + None => Ok(21000), + } } async fn get_logs(&self, filter: &LogFilter) -> Result, RpcError> { diff --git a/crates/rpc/chain-index/src/querier.rs b/crates/rpc/chain-index/src/querier.rs new file mode 100644 index 0000000..41168fa --- /dev/null +++ b/crates/rpc/chain-index/src/querier.rs @@ -0,0 +1,121 @@ +//! State querier for reading account balances and nonces from storage. +//! +//! This module provides direct storage reads for RPC state queries +//! (eth_getBalance, eth_getTransactionCount) without going through +//! the full STF execution pipeline. + +use alloy_primitives::{Address, Bytes, U256}; +use async_trait::async_trait; + +use evolve_core::encoding::Encodable; +use evolve_core::{AccountId, Message, ReadonlyKV}; +use evolve_eth_jsonrpc::error::RpcError; +use evolve_rpc_types::CallRequest; +use evolve_tx_eth::lookup_account_id_in_storage; + +/// Trait for querying on-chain state (balance, nonce). +/// +/// Implementors hold a reference to storage and know how to +/// map Ethereum addresses to Evolve account state. +#[async_trait] +pub trait StateQuerier: Send + Sync { + /// Get the token balance for an Ethereum address. + async fn get_balance(&self, address: Address) -> Result; + + /// Get the transaction count (nonce) for an Ethereum address. + async fn get_transaction_count(&self, address: Address) -> Result; + + /// Execute a read-only call. + async fn call(&self, request: &CallRequest) -> Result; + + /// Estimate gas for a transaction. + async fn estimate_gas(&self, request: &CallRequest) -> Result; +} + +/// State querier that reads directly from storage. +/// +/// Uses the known storage key layout to read token balances and nonces +/// without invoking the STF. This is the same key format used by the +/// `#[account_impl]` macro: +/// - Nonce: `account_id_bytes ++ [0]` (EthEoaAccount storage prefix 0) +/// - Balance: `token_id_bytes ++ [1] ++ encode(account_id)` (Token storage prefix 1) +pub struct StorageStateQuerier { + storage: S, + token_account_id: AccountId, +} + +impl StorageStateQuerier { + pub fn new(storage: S, token_account_id: AccountId) -> Self { + Self { + storage, + token_account_id, + } + } + + fn read_nonce(&self, account_id: AccountId) -> Result { + let mut key = account_id.as_bytes().to_vec(); + key.push(0u8); // EthEoaAccount::nonce storage prefix + match self + .storage + .get(&key) + .map_err(|e| RpcError::InternalError(format!("storage read: {:?}", e)))? + { + Some(value) => Message::from_bytes(value) + .get::() + .map_err(|e| RpcError::InternalError(format!("decode nonce: {:?}", e))), + None => Ok(0), + } + } + + fn read_balance(&self, account_id: AccountId) -> Result { + let mut key = self.token_account_id.as_bytes().to_vec(); + key.push(1u8); // Token::balances storage prefix + key.extend( + account_id + .encode() + .map_err(|e| RpcError::InternalError(format!("encode account id: {:?}", e)))?, + ); + match self + .storage + .get(&key) + .map_err(|e| RpcError::InternalError(format!("storage read: {:?}", e)))? + { + Some(value) => Message::from_bytes(value) + .get::() + .map_err(|e| RpcError::InternalError(format!("decode balance: {:?}", e))), + None => Ok(0), + } + } +} + +#[async_trait] +impl StateQuerier for StorageStateQuerier { + async fn get_balance(&self, address: Address) -> Result { + let Some(account_id) = lookup_account_id_in_storage(&self.storage, address) + .map_err(|e| RpcError::InternalError(format!("lookup account id: {:?}", e)))? + else { + return Ok(U256::ZERO); + }; + let balance = self.read_balance(account_id)?; + Ok(U256::from(balance)) + } + + async fn get_transaction_count(&self, address: Address) -> Result { + let Some(account_id) = lookup_account_id_in_storage(&self.storage, address) + .map_err(|e| RpcError::InternalError(format!("lookup account id: {:?}", e)))? + else { + return Ok(0); + }; + self.read_nonce(account_id) + } + + async fn call(&self, _request: &CallRequest) -> Result { + // TODO: Implement via STF::query() + Ok(Bytes::new()) + } + + async fn estimate_gas(&self, _request: &CallRequest) -> Result { + // TODO: Implement via STF with gas tracking + Ok(21000) + } +} diff --git a/docker/testapp/Dockerfile b/docker/testapp/Dockerfile new file mode 100644 index 0000000..f5980b8 --- /dev/null +++ b/docker/testapp/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 evolve_testapp --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/testapp /usr/local/bin/testapp + +EXPOSE 8545 50051 + +ENTRYPOINT ["/usr/local/bin/testapp"] From 17f913934fae45d1d6597efc5b2e98218d77fd81 Mon Sep 17 00:00:00 2001 From: tac0turtle Date: Fri, 27 Feb 2026 14:39:23 +0100 Subject: [PATCH 07/10] Address review feedback and stabilize node/rpc flows --- bin/evd/src/main.rs | 527 ++++++++++++---------- bin/testapp/src/main.rs | 29 +- bin/testapp/tests/mempool_e2e.rs | 244 +++++----- crates/app/node/src/lib.rs | 5 +- crates/app/stf/src/lib.rs | 184 ++++---- crates/app/tx/eth/src/eoa_registry.rs | 9 +- crates/app/tx/eth/src/error.rs | 17 + crates/app/tx/eth/src/mempool.rs | 12 +- crates/rpc/chain-index/src/integration.rs | 56 +-- crates/rpc/chain-index/src/provider.rs | 36 +- crates/rpc/chain-index/src/querier.rs | 21 +- crates/testing/simulator/src/eth_eoa.rs | 6 +- docker/testapp/Dockerfile | 28 +- 13 files changed, 647 insertions(+), 527 deletions(-) diff --git a/bin/evd/src/main.rs b/bin/evd/src/main.rs index 6dc1f84..15f6b81 100644 --- a/bin/evd/src/main.rs +++ b/bin/evd/src/main.rs @@ -163,257 +163,307 @@ fn main() { } } -fn run_node(config: NodeConfig, genesis_config: Option) { - tracing::info!("=== Evolve Node Daemon (evd) ==="); +type TokioContext = commonware_runtime::tokio::Context; +type NodeStorage = QmdbStorage; +type SharedChainIndex = Arc; +type RpcMempool = SharedMempool>; - std::fs::create_dir_all(&config.storage.path).expect("failed to create data directory"); +struct RpcRuntimeHandle { + stop_fn: Option>, +} - let storage_config = StorageConfig { - path: config.storage.path.clone().into(), - ..Default::default() - }; +impl RpcRuntimeHandle { + fn new(stop_fn: impl FnOnce() + Send + 'static) -> Self { + Self { + stop_fn: Some(Box::new(stop_fn)), + } + } - let runtime_config = TokioConfig::default() - .with_storage_directory(&config.storage.path) - .with_worker_threads(4); + fn stop(mut self) { + if let Some(stop_fn) = self.stop_fn.take() { + stop_fn(); + } + } +} - let runner = Runner::new(runtime_config); +async fn init_storage_and_genesis( + context: TokioContext, + storage_config: StorageConfig, + genesis_config: Option, +) -> (NodeStorage, EvdGenesisResult, u64) { + let storage = QmdbStorage::new(context, storage_config) + .await + .expect("failed to create storage"); - runner.start(move |context| { - async move { - let context_for_shutdown = context.clone(); + let codes = build_codes(); + tracing::info!("Installed account codes: {:?}", codes.list_identifiers()); - // Initialize QMDB storage - let storage = QmdbStorage::new(context, storage_config) + match load_chain_state::(&storage) { + Some(state) => { + tracing::info!("Resuming from existing state at height {}", state.height); + (storage, state.genesis_result, state.height) + } + None => { + tracing::info!("No existing state found, running genesis..."); + let output = run_genesis(&storage, &codes, genesis_config.as_ref()); + commit_genesis(&storage, output.changes, &output.genesis_result) .await - .expect("failed to create storage"); - - // Set up account codes - let codes = build_codes(); - tracing::info!("Installed account codes: {:?}", codes.list_identifiers()); - - // Load or run genesis - let (genesis_result, initial_height) = - match load_chain_state::(&storage) { - Some(state) => { - tracing::info!("Resuming from existing state at height {}", state.height); - (state.genesis_result, state.height) - } - None => { - tracing::info!("No existing state found, running genesis..."); - let output = run_genesis(&storage, &codes, genesis_config.as_ref()); - commit_genesis(&storage, output.changes, &output.genesis_result) - .await - .expect("genesis commit failed"); - tracing::info!("Genesis complete: {:?}", output.genesis_result); - (output.genesis_result, 1) - } - }; - - // Build STF with scheduler from genesis - let gas_config = default_gas_config(); - let stf = build_mempool_stf(gas_config, genesis_result.scheduler); - - // Create shared mempool - let mempool: SharedMempool> = new_shared_mempool(); - // Create chain index backed by SQLite (only when needed) - let chain_index = if config.rpc.enabled || config.rpc.enable_block_indexing { - let chain_index_db_path = - std::path::PathBuf::from(&config.storage.path).join("chain-index.sqlite"); - let index = Arc::new( - PersistentChainIndex::new(&chain_index_db_path) - .expect("failed to open chain index database"), - ); - if let Err(e) = index.initialize() { - tracing::warn!("Failed to initialize chain index: {:?}", e); - } - Some(index) - } else { - None - }; + .expect("genesis commit failed"); + tracing::info!("Genesis complete: {:?}", output.genesis_result); + (storage, output.genesis_result, 1) + } + } +} - // Set up JSON-RPC server if enabled - let rpc_handle = if config.rpc.enabled { - let subscriptions = Arc::new(SubscriptionManager::new()); - let codes_for_rpc = Arc::new(build_codes()); - - let state_provider_config = ChainStateProviderConfig { - chain_id: config.chain.chain_id, - protocol_version: "0x1".to_string(), - gas_price: U256::ZERO, - sync_status: SyncStatus::NotSyncing(false), - }; - - let state_querier: Arc = Arc::new(StorageStateQuerier::new( - storage.clone(), - genesis_result.token, - )); - let state_provider = ChainStateProvider::with_mempool( - Arc::clone(chain_index.as_ref().expect("chain index required for RPC")), - state_provider_config, - codes_for_rpc, - mempool.clone(), - ) - .with_state_querier(state_querier); - - let rpc_addr = config.parsed_rpc_addr(); - let server_config = RpcServerConfig { - http_addr: rpc_addr, - chain_id: config.chain.chain_id, - }; - - tracing::info!("Starting JSON-RPC server on {}", rpc_addr); - let handle = start_server_with_subscriptions( - server_config, - state_provider, - Arc::clone(&subscriptions), - ) - .await - .expect("failed to start RPC server"); +fn init_chain_index(config: &NodeConfig) -> Option { + if !config.rpc.enabled && !config.rpc.enable_block_indexing { + return None; + } - Some(handle) - } else { - None - }; + let chain_index_db_path = + std::path::PathBuf::from(&config.storage.path).join("chain-index.sqlite"); + let index = Arc::new( + PersistentChainIndex::new(&chain_index_db_path) + .expect("failed to open chain index database"), + ); + if let Err(err) = index.initialize() { + tracing::warn!("Failed to initialize chain index: {:?}", err); + } + Some(index) +} - // Shared state for the block callback - let parent_hash = Arc::new(std::sync::RwLock::new(B256::ZERO)); - let current_height = Arc::new(AtomicU64::new(initial_height)); - - // Build the OnBlockExecuted callback: commits state to storage + indexes blocks - let storage_for_callback = storage.clone(); - let chain_index_for_callback = chain_index.clone(); - let parent_hash_for_callback = Arc::clone(&parent_hash); - let current_height_for_callback = Arc::clone(¤t_height); - let callback_chain_id = config.chain.chain_id; - let executor_config = ExecutorServiceConfig::default(); - let callback_max_gas = executor_config.max_gas; - let callback_indexing_enabled = config.rpc.enable_block_indexing; - - let on_block_executed: OnBlockExecuted = Arc::new(move |info| { - // 1. Commit state changes to QmdbStorage - let operations = state_changes_to_operations(info.state_changes); - - let commit_hash = futures::executor::block_on(async { - storage_for_callback - .batch(operations) - .await - .expect("storage batch failed"); - storage_for_callback - .commit() - .await - .expect("storage commit failed") - }); - let state_root = B256::from_slice(commit_hash.as_bytes()); - - // 2. Compute block hash and build metadata - let prev_parent = *parent_hash_for_callback.read().unwrap(); - let block_hash = compute_block_hash(info.height, info.timestamp, prev_parent); - - let metadata = BlockMetadata::new( - block_hash, - prev_parent, - state_root, - info.timestamp, - callback_max_gas, - Address::ZERO, - callback_chain_id, - ); - - // 3. Reconstruct block and index it - let block = BlockBuilder::::new() - .number(info.height) - .timestamp(info.timestamp) - .transactions(info.transactions) - .build(); - - let (stored_block, stored_txs, stored_receipts) = - build_index_data(&block, &info.block_result, &metadata); - - if let Some(ref chain_index) = chain_index_for_callback { - if callback_indexing_enabled { - if let Err(e) = - chain_index.store_block(stored_block, stored_txs, stored_receipts) - { - tracing::warn!("Failed to index block {}: {:?}", info.height, e); - } else { - tracing::debug!( - "Indexed block {} (hash={}, state_root={})", - info.height, - block_hash, - state_root - ); - } - } - } +async fn start_rpc_server( + config: &NodeConfig, + storage: NodeStorage, + mempool: RpcMempool, + chain_index: &Option, + token_account_id: AccountId, +) -> Option { + if !config.rpc.enabled { + return None; + } - // 4. Update parent hash and height for next block - *parent_hash_for_callback.write().unwrap() = block_hash; - current_height_for_callback.store(info.height, Ordering::SeqCst); - }); - - // Configure gRPC server - let grpc_config = EvnodeServerConfig { - addr: config.parsed_grpc_addr(), - enable_gzip: config.grpc.enable_gzip, - max_message_size: config.grpc_max_message_size_usize(), - executor_config, - }; + let subscriptions = Arc::new(SubscriptionManager::new()); + let codes_for_rpc = Arc::new(build_codes()); + let state_provider_config = ChainStateProviderConfig { + chain_id: config.chain.chain_id, + protocol_version: "0x1".to_string(), + gas_price: U256::ZERO, + sync_status: SyncStatus::NotSyncing(false), + }; - let grpc_addr = config.parsed_grpc_addr(); - tracing::info!("Starting gRPC server on {}", grpc_addr); - tracing::info!("Configuration:"); - tracing::info!(" - Chain ID: {}", config.chain.chain_id); - tracing::info!(" - gRPC compression: {}", config.grpc.enable_gzip); - tracing::info!(" - JSON-RPC: {}", config.rpc.enabled); - tracing::info!(" - Block indexing: {}", config.rpc.enable_block_indexing); - tracing::info!(" - Initial height: {}", initial_height); - - // Create gRPC server with mempool and block callback - let server = EvnodeServer::with_mempool( - grpc_config, - stf, - storage.clone(), - build_codes(), - mempool, - ) - .with_on_block_executed(on_block_executed); - - tracing::info!("Server ready. Press Ctrl+C to stop."); - - // Run gRPC server with shutdown handling - tokio::select! { - result = server.serve() => { - if let Err(e) = result { - tracing::error!("gRPC server error: {}", e); - } - } - _ = tokio::signal::ctrl_c() => { - tracing::info!("Received Ctrl+C, shutting down..."); - context_for_shutdown - .stop(0, Some(Duration::from_secs(config.operations.shutdown_timeout_secs))) - .await - .expect("shutdown failed"); + let state_querier: Arc = + Arc::new(StorageStateQuerier::new(storage, token_account_id)); + let state_provider = ChainStateProvider::with_mempool( + Arc::clone(chain_index.as_ref().expect("chain index required for RPC")), + state_provider_config, + codes_for_rpc, + mempool, + ) + .with_state_querier(state_querier); + + let rpc_addr = config.parsed_rpc_addr(); + let server_config = RpcServerConfig { + http_addr: rpc_addr, + chain_id: config.chain.chain_id, + }; + + tracing::info!("Starting JSON-RPC server on {}", rpc_addr); + let handle = + start_server_with_subscriptions(server_config, state_provider, Arc::clone(&subscriptions)) + .await + .expect("failed to start RPC server"); + + Some(RpcRuntimeHandle::new(move || { + handle.stop().expect("failed to stop RPC server"); + })) +} + +fn build_on_block_executed( + storage: NodeStorage, + chain_index: Option, + initial_height: u64, + callback_chain_id: u64, + callback_max_gas: u64, + callback_indexing_enabled: bool, +) -> (OnBlockExecuted, Arc) { + let parent_hash = Arc::new(std::sync::RwLock::new(B256::ZERO)); + let current_height = Arc::new(AtomicU64::new(initial_height)); + let parent_hash_for_callback = Arc::clone(&parent_hash); + let current_height_for_callback = Arc::clone(¤t_height); + + let on_block_executed: OnBlockExecuted = Arc::new(move |info| { + let operations = state_changes_to_operations(info.state_changes); + let commit_hash = futures::executor::block_on(async { + storage + .batch(operations) + .await + .expect("storage batch failed"); + storage.commit().await.expect("storage commit failed") + }); + let state_root = B256::from_slice(commit_hash.as_bytes()); + + let prev_parent = *parent_hash_for_callback.read().unwrap(); + let block_hash = compute_block_hash(info.height, info.timestamp, prev_parent); + let metadata = BlockMetadata::new( + block_hash, + prev_parent, + state_root, + info.timestamp, + callback_max_gas, + Address::ZERO, + callback_chain_id, + ); + + let block = BlockBuilder::::new() + .number(info.height) + .timestamp(info.timestamp) + .transactions(info.transactions) + .build(); + let (stored_block, stored_txs, stored_receipts) = + build_index_data(&block, &info.block_result, &metadata); + + if callback_indexing_enabled { + if let Some(ref index) = chain_index { + if let Err(err) = index.store_block(stored_block, stored_txs, stored_receipts) { + tracing::warn!("Failed to index block {}: {:?}", info.height, err); + } else { + tracing::debug!( + "Indexed block {} (hash={}, state_root={})", + info.height, + block_hash, + state_root + ); } } + } - // Save chain state with actual committed height - let chain_state = ChainState { - height: current_height.load(Ordering::SeqCst), - genesis_result, - }; - if let Err(e) = save_chain_state(&storage, &chain_state).await { - tracing::error!("Failed to save chain state: {}", e); - } + *parent_hash_for_callback.write().unwrap() = block_hash; + current_height_for_callback.store(info.height, Ordering::SeqCst); + }); - // Stop RPC server - if let Some(handle) = rpc_handle { - tracing::info!("Stopping JSON-RPC server..."); - handle.stop().expect("failed to stop RPC server"); - } + (on_block_executed, current_height) +} + +fn log_server_configuration(config: &NodeConfig, initial_height: u64) { + let grpc_addr = config.parsed_grpc_addr(); + tracing::info!("Starting gRPC server on {}", grpc_addr); + tracing::info!("Configuration:"); + tracing::info!(" - Chain ID: {}", config.chain.chain_id); + tracing::info!(" - gRPC compression: {}", config.grpc.enable_gzip); + tracing::info!(" - JSON-RPC: {}", config.rpc.enabled); + tracing::info!(" - Block indexing: {}", config.rpc.enable_block_indexing); + tracing::info!(" - Initial height: {}", initial_height); +} - tracing::info!("Shutdown complete."); +async fn run_server_with_shutdown( + serve_future: F, + context_for_shutdown: TokioContext, + shutdown_timeout_secs: u64, +) where + F: std::future::Future>, + E: std::fmt::Display, +{ + tokio::pin!(serve_future); + tokio::select! { + result = &mut serve_future => { + if let Err(err) = result { + tracing::error!("gRPC server error: {}", err); + } + } + _ = tokio::signal::ctrl_c() => { + tracing::info!("Received Ctrl+C, shutting down..."); + context_for_shutdown + .stop(0, Some(Duration::from_secs(shutdown_timeout_secs))) + .await + .expect("shutdown failed"); } + } +} + +async fn persist_chain_state( + storage: &NodeStorage, + current_height: &Arc, + genesis_result: EvdGenesisResult, +) { + let chain_state = ChainState { + height: current_height.load(Ordering::SeqCst), + genesis_result, + }; + if let Err(err) = save_chain_state(storage, &chain_state).await { + tracing::error!("Failed to save chain state: {}", err); + } +} + +fn stop_rpc_server(rpc_handle: Option) { + if let Some(handle) = rpc_handle { + tracing::info!("Stopping JSON-RPC server..."); + handle.stop(); + } +} + +fn run_node(config: NodeConfig, genesis_config: Option) { + tracing::info!("=== Evolve Node Daemon (evd) ==="); + std::fs::create_dir_all(&config.storage.path).expect("failed to create data directory"); + + let storage_config = StorageConfig { + path: config.storage.path.clone().into(), + ..Default::default() + }; + let runtime_config = TokioConfig::default() + .with_storage_directory(&config.storage.path) + .with_worker_threads(4); + let runner = Runner::new(runtime_config); + + runner.start(move |context| async move { + let context_for_shutdown = context.clone(); + let (storage, genesis_result, initial_height) = + init_storage_and_genesis(context, storage_config, genesis_config).await; + + let stf = build_mempool_stf(default_gas_config(), genesis_result.scheduler); + let mempool: RpcMempool = new_shared_mempool(); + let chain_index = init_chain_index(&config); + let rpc_handle = start_rpc_server( + &config, + storage.clone(), + mempool.clone(), + &chain_index, + genesis_result.token, + ) + .await; + + let executor_config = ExecutorServiceConfig::default(); + let (on_block_executed, current_height) = build_on_block_executed( + storage.clone(), + chain_index, + initial_height, + config.chain.chain_id, + executor_config.max_gas, + config.rpc.enable_block_indexing, + ); + log_server_configuration(&config, initial_height); + + let grpc_config = EvnodeServerConfig { + addr: config.parsed_grpc_addr(), + enable_gzip: config.grpc.enable_gzip, + max_message_size: config.grpc_max_message_size_usize(), + executor_config, + }; + let server = + EvnodeServer::with_mempool(grpc_config, stf, storage.clone(), build_codes(), mempool) + .with_on_block_executed(on_block_executed); + + tracing::info!("Server ready. Press Ctrl+C to stop."); + run_server_with_shutdown( + server.serve(), + context_for_shutdown, + config.operations.shutdown_timeout_secs, + ) + .await; + + persist_chain_state(&storage, ¤t_height, genesis_result).await; + stop_rpc_server(rpc_handle); + tracing::info!("Shutdown complete."); }); } @@ -634,17 +684,26 @@ mod tests { use evolve_core::Message; use evolve_storage::MockStorage; use std::collections::BTreeMap; + use std::sync::{Mutex, MutexGuard}; + + static ENV_VAR_LOCK: Mutex<()> = Mutex::new(()); struct EnvVarGuard { key: &'static str, old: Option, + _guard: MutexGuard<'static, ()>, } impl EnvVarGuard { fn set(key: &'static str, value: &str) -> Self { + let guard = ENV_VAR_LOCK.lock().expect("env var lock poisoned"); let old = std::env::var(key).ok(); std::env::set_var(key, value); - Self { key, old } + Self { + key, + old, + _guard: guard, + } } } diff --git a/bin/testapp/src/main.rs b/bin/testapp/src/main.rs index fbf581d..f10fef1 100644 --- a/bin/testapp/src/main.rs +++ b/bin/testapp/src/main.rs @@ -61,7 +61,13 @@ 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()); @@ -94,7 +100,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, build_genesis_stf, @@ -108,11 +120,12 @@ fn main() { } } -fn load_genesis_config(path: Option<&str>) -> Option { +fn load_genesis_config(path: Option<&str>) -> Result, String> { path.map(|p| { tracing::info!("Loading genesis config from: {}", p); - EvdGenesisConfig::load(p).expect("failed to load genesis config") + EvdGenesisConfig::load(p) }) + .transpose() } fn build_codes() -> AccountStorageMock { @@ -198,12 +211,10 @@ fn run_custom_genesis( .iter() .filter(|acc| acc.balance > 0) .map(|acc| { - let addr = acc - .parse_address() - .expect("invalid address in genesis config"); - (addr.into_array(), acc.balance) + acc.parse_address() + .map(|addr| (addr.into_array(), acc.balance)) }) - .collect(); + .collect::, _>>()?; let minter = AccountId::new(config.minter_id); let metadata = config.token.to_metadata(); diff --git a/bin/testapp/tests/mempool_e2e.rs b/bin/testapp/tests/mempool_e2e.rs index e64860a..22fb06f 100644 --- a/bin/testapp/tests/mempool_e2e.rs +++ b/bin/testapp/tests/mempool_e2e.rs @@ -8,23 +8,27 @@ //! 5. Token balances are updated correctly use alloy_consensus::{SignableTransaction, TxEip1559}; -use alloy_primitives::{Bytes, PrimitiveSignature, TxKind, U256}; +use alloy_primitives::{Address, Bytes, PrimitiveSignature, TxKind, B256, U256}; use async_trait::async_trait; use evolve_core::{AccountId, ErrorCode, ReadonlyKV}; -use evolve_node::build_dev_node_with_mempool; +use evolve_node::{build_dev_node_with_mempool, DevNodeMempoolHandles}; use evolve_server::DevConfig; +use evolve_simulator::{generate_signing_key, SimConfig, Simulator}; use evolve_storage::{CommitHash, Operation}; use evolve_testapp::{ build_mempool_stf, default_gas_config, do_eth_genesis, install_account_codes, + EthGenesisAccounts, MempoolStf, }; use evolve_testing::server_mocks::AccountStorageMock; -use evolve_tx_eth::{derive_runtime_contract_address, EthGateway}; +use evolve_tx_eth::{derive_runtime_contract_address, EthGateway, TxContext}; use k256::ecdsa::{signature::hazmat::PrehashSigner, SigningKey, VerifyingKey}; -use rand::rngs::OsRng; use std::collections::BTreeMap; use std::sync::RwLock; use tiny_keccak::{Hasher, Keccak}; +type TestNodeHandles = + DevNodeMempoolHandles; + // ============================================================================ // Test Infrastructure // ============================================================================ @@ -249,36 +253,25 @@ fn read_token_balance( // E2E Test // ============================================================================ -/// End-to-end test of token transfer via Ethereum transaction. -/// -/// This test verifies: -/// 1. Alice signs a tx calling token.transfer(bob, 100) -/// 2. Transaction is submitted to mempool -/// 3. DevConsensus produces a block -/// 4. Transaction is authenticated (signature verified, nonce incremented) -/// 5. Token transfer executes (Alice balance decreases, Bob balance increases) -#[tokio::test] -async fn test_token_transfer_e2e() { - let chain_id = 1337u64; - - // Create signing keys for Alice and Bob - let alice_key = SigningKey::random(&mut OsRng); - let bob_key = SigningKey::random(&mut OsRng); - let alice_address = get_address(&alice_key); - let bob_address = get_address(&bob_key); +fn deterministic_signing_keys() -> (SigningKey, SigningKey) { + let mut simulator = Simulator::new(0xD15E_A5E5, SimConfig::default()); + let alice_key = generate_signing_key(&mut simulator, 64).expect("alice signing key"); + let bob_key = generate_signing_key(&mut simulator, 64).expect("bob signing key"); + (alice_key, bob_key) +} - // Set up account codes +fn setup_genesis( + chain_id: u64, + alice_address: Address, + bob_address: Address, +) -> (TestNodeHandles, EthGenesisAccounts, AccountId, AccountId) { let mut codes = AccountStorageMock::new(); install_account_codes(&mut codes); - // Create initial storage let init_storage = AsyncMockStorage::new(); - - // Build STF for TxContext let gas_config = default_gas_config(); let stf = build_mempool_stf(gas_config.clone(), AccountId::new(0)); - // Run genesis to create token and scheduler let (genesis_state, genesis_accounts) = do_eth_genesis( &stf, &codes, @@ -288,107 +281,74 @@ async fn test_token_transfer_e2e() { ) .expect("genesis should succeed"); - // Apply genesis state changes to existing storage let genesis_changes = genesis_state.into_changes().expect("get changes"); init_storage.apply_changes(genesis_changes); - let storage = init_storage; - // Rebuild STF with correct scheduler ID let stf = build_mempool_stf(gas_config, genesis_accounts.scheduler); - let config = DevConfig { - block_interval: None, // Manual block production + block_interval: None, gas_limit: 30_000_000, initial_height: 1, chain_id, }; - let handles = build_dev_node_with_mempool(stf, storage, codes, config); - let dev = handles.dev; - let mempool = handles.mempool; + let handles = build_dev_node_with_mempool(stf, init_storage, codes, config); let alice_account_id = - evolve_tx_eth::lookup_account_id_in_storage(dev.storage(), alice_address) + evolve_tx_eth::lookup_account_id_in_storage(handles.dev.storage(), alice_address) .expect("lookup alice id") .expect("alice id exists"); - let bob_account_id = evolve_tx_eth::lookup_account_id_in_storage(dev.storage(), bob_address) - .expect("lookup bob id") - .expect("bob id exists"); + let bob_account_id = + evolve_tx_eth::lookup_account_id_in_storage(handles.dev.storage(), bob_address) + .expect("lookup bob id") + .expect("bob id exists"); - // Read initial state - let alice_nonce_before = read_nonce(dev.storage(), alice_account_id); - let alice_balance_before = - read_token_balance(dev.storage(), genesis_accounts.evolve, alice_account_id); - let bob_balance_before = - read_token_balance(dev.storage(), genesis_accounts.evolve, bob_account_id); - - println!("Initial state:"); - println!(" Alice nonce: {}", alice_nonce_before); - println!(" Alice token balance: {}", alice_balance_before); - println!(" Bob token balance: {}", bob_balance_before); - - assert_eq!(alice_nonce_before, 0); - assert_eq!(alice_balance_before, 1000); // From genesis - assert_eq!(bob_balance_before, 2000); // From genesis + (handles, genesis_accounts, alice_account_id, bob_account_id) +} - // Build the transfer call - // Function: transfer(to: AccountId, amount: u128) - // Selector: keccak256("transfer")[0..4] - let transfer_amount = 100u128; +fn build_transfer_tx( + alice_key: &SigningKey, + chain_id: u64, + token_address: Address, + bob_account_id: AccountId, + transfer_amount: u128, +) -> Vec { let selector = compute_selector("transfer"); - - // Borsh-encode the arguments: (AccountId, u128) - // AccountId is u128 (16 bytes), amount is u128 (16 bytes) let args = borsh::to_vec(&(bob_account_id, transfer_amount)).expect("encode args"); - - // Calldata = selector + args let mut calldata = Vec::with_capacity(4 + args.len()); calldata.extend_from_slice(&selector); calldata.extend_from_slice(&args); - // Get token's Ethereum address - let token_address = derive_runtime_contract_address(genesis_accounts.evolve); - - println!("\nTransaction details:"); - println!(" Token account: {:?}", genesis_accounts.evolve); - println!(" Token address: {:?}", token_address); - println!(" Selector: 0x{}", hex::encode(selector)); - println!(" Transfer amount: {}", transfer_amount); - - // Create and sign transaction from Alice to Token - let raw_tx = create_signed_tx( - &alice_key, + create_signed_tx( + alice_key, chain_id, - 0, // nonce + 0, token_address, U256::ZERO, Bytes::from(calldata), - ); + ) +} - // Submit transaction to mempool +async fn submit_and_produce_block(handles: &TestNodeHandles, chain_id: u64, raw_tx: &[u8]) -> B256 { let tx_hash = { let gateway = EthGateway::new(chain_id); - let tx_context = gateway.decode_and_verify(&raw_tx).expect("decode tx"); - let mut pool = mempool.write().await; + let tx_context = gateway.decode_and_verify(raw_tx).expect("decode tx"); + let mut pool = handles.mempool.write().await; let tx_id = pool.add(tx_context).expect("add tx to mempool"); - alloy_primitives::B256::from(tx_id) + B256::from(tx_id) }; - println!("\nSubmitted transaction: {:?}", tx_hash); - // Verify transaction is in mempool - assert_eq!(mempool.read().await.len(), 1, "mempool should have 1 tx"); + assert_eq!( + handles.mempool.read().await.len(), + 1, + "mempool should have 1 tx" + ); - // Produce a block from mempool - let block_result = dev + let block_result = handles + .dev .produce_block_from_mempool(10) .await .expect("produce block"); - println!("\nBlock produced at height {}", block_result.height); - println!(" Transactions: {}", block_result.tx_count); - println!(" Successful: {}", block_result.successful_txs); - println!(" Failed: {}", block_result.failed_txs); - - // Verify block was produced assert_eq!(block_result.height, 1, "should be block 1"); assert_eq!(block_result.tx_count, 1, "should have 1 tx"); assert_eq!( @@ -399,32 +359,39 @@ async fn test_token_transfer_e2e() { block_result.failed_txs, 0, "no transactions should have failed" ); - - // Mempool should be empty after block production assert!( - mempool.read().await.is_empty(), + handles.mempool.read().await.is_empty(), "mempool should be empty after block" ); - // Verify state changes - let alice_nonce_after = read_nonce(dev.storage(), alice_account_id); - let alice_balance_after = - read_token_balance(dev.storage(), genesis_accounts.evolve, alice_account_id); - let bob_balance_after = - read_token_balance(dev.storage(), genesis_accounts.evolve, bob_account_id); + tx_hash +} - println!("\nFinal state:"); - println!(" Alice nonce: {}", alice_nonce_after); - println!(" Alice token balance: {}", alice_balance_after); - println!(" Bob token balance: {}", bob_balance_after); +fn assert_post_block_state( + handles: &TestNodeHandles, + genesis_accounts: &EthGenesisAccounts, + alice_account_id: AccountId, + bob_account_id: AccountId, + transfer_amount: u128, + alice_balance_before: u128, + bob_balance_before: u128, +) { + let alice_nonce_after = read_nonce(handles.dev.storage(), alice_account_id); + let alice_balance_after = read_token_balance( + handles.dev.storage(), + genesis_accounts.evolve, + alice_account_id, + ); + let bob_balance_after = read_token_balance( + handles.dev.storage(), + genesis_accounts.evolve, + bob_account_id, + ); - // Verify nonce was incremented assert_eq!( alice_nonce_after, 1, "alice nonce should increment after tx" ); - - // Verify token balances changed correctly assert_eq!( alice_balance_after, alice_balance_before - transfer_amount, @@ -435,13 +402,62 @@ async fn test_token_transfer_e2e() { bob_balance_before + transfer_amount, "bob balance should increase by transfer amount" ); +} + +/// End-to-end test of token transfer via Ethereum transaction. +/// +/// This test verifies: +/// 1. Alice signs a tx calling token.transfer(bob, 100) +/// 2. Transaction is submitted to mempool +/// 3. DevConsensus produces a block +/// 4. Transaction is authenticated (signature verified, nonce incremented) +/// 5. Token transfer executes (Alice balance decreases, Bob balance increases) +#[tokio::test] +async fn test_token_transfer_e2e() { + let chain_id = 1337u64; + let transfer_amount = 100u128; + + let (alice_key, bob_key) = deterministic_signing_keys(); + let alice_address = get_address(&alice_key); + let bob_address = get_address(&bob_key); + + let (handles, genesis_accounts, alice_account_id, bob_account_id) = + setup_genesis(chain_id, alice_address, bob_address); + + // Read initial state + let alice_nonce_before = read_nonce(handles.dev.storage(), alice_account_id); + let alice_balance_before = read_token_balance( + handles.dev.storage(), + genesis_accounts.evolve, + alice_account_id, + ); + let bob_balance_before = read_token_balance( + handles.dev.storage(), + genesis_accounts.evolve, + bob_account_id, + ); + + assert_eq!(alice_nonce_before, 0); + assert_eq!(alice_balance_before, 1000); + assert_eq!(bob_balance_before, 2000); + + let token_address = derive_runtime_contract_address(genesis_accounts.evolve); + let raw_tx = build_transfer_tx( + &alice_key, + chain_id, + token_address, + bob_account_id, + transfer_amount, + ); - println!("\n✓ Token transfer e2e test passed!"); - println!(" - Transaction submitted to mempool"); - println!(" - DevConsensus produced block from mempool"); - println!(" - Authentication succeeded (nonce incremented)"); - println!( - " - Token transfer executed ({} tokens from Alice to Bob)", - transfer_amount + let _tx_hash = submit_and_produce_block(&handles, chain_id, &raw_tx).await; + assert_post_block_state( + &handles, + &genesis_accounts, + alice_account_id, + bob_account_id, + transfer_amount, + alice_balance_before, + bob_balance_before, ); } diff --git a/crates/app/node/src/lib.rs b/crates/app/node/src/lib.rs index 7875ab4..bd7d211 100644 --- a/crates/app/node/src/lib.rs +++ b/crates/app/node/src/lib.rs @@ -441,7 +441,7 @@ pub fn run_dev_node_with_rpc< state_provider_config.clone(), Arc::clone(&codes_for_rpc), ) - .with_state_querier(state_querier); + .with_state_querier(Arc::clone(&state_querier)); // Start JSON-RPC server let server_config = RpcServerConfig { @@ -463,7 +463,8 @@ pub fn run_dev_node_with_rpc< Arc::clone(&chain_index), state_provider_config, codes_for_rpc, - ); + ) + .with_state_querier(state_querier); let grpc_config = GrpcServerConfig { addr: grpc_addr, chain_id: rpc_config.chain_id, diff --git a/crates/app/stf/src/lib.rs b/crates/app/stf/src/lib.rs index 5872627..fbc3a7d 100644 --- a/crates/app/stf/src/lib.rs +++ b/crates/app/stf/src/lib.rs @@ -1250,6 +1250,78 @@ where state.pop_events() } + fn tx_error_result( + state: &mut ExecutionState<'_, S>, + gas_counter: &GasCounter, + err: evolve_core::ErrorCode, + ) -> TxResult { + TxResult { + events: state.pop_events(), + gas_used: gas_counter.gas_used(), + response: Err(err), + } + } + + fn resolve_sender_phase<'a, S: ReadonlyKV + 'a, A: AccountsCodeStorage + 'a>( + &self, + state: &mut ExecutionState<'a, S>, + codes: &'a A, + gas_counter: &mut GasCounter, + tx: &Tx, + block: BlockContext, + ) -> SdkResult { + let mut resolve_ctx = Invoker::new_for_begin_block(state, codes, gas_counter, block); + tx.resolve_sender_account(&mut resolve_ctx) + } + + fn bootstrap_sender_phase<'a, S: ReadonlyKV + 'a, A: AccountsCodeStorage + 'a>( + &self, + state: &mut ExecutionState<'a, S>, + codes: &'a A, + gas_counter: &mut GasCounter, + tx: &Tx, + resolved_sender: AccountId, + block: BlockContext, + ) -> SdkResult<()> { + let Some(bootstrap) = tx.sender_bootstrap() else { + return Ok(()); + }; + let mut reg_ctx = Invoker::new_for_begin_block(state, codes, gas_counter, block); + reg_ctx.register_account_at_id( + resolved_sender, + bootstrap.account_code_id, + bootstrap.init_message, + )?; + drop(reg_ctx); + state.pop_events(); + Ok(()) + } + + fn validate_tx_phase<'s, 'b, S: ReadonlyKV + 's, A: AccountsCodeStorage + 'b>( + &self, + tx: &Tx, + ctx: &mut Invoker<'s, 'b, S, A>, + ) -> SdkResult<()> { + self.tx_validator.validate_tx(tx, ctx) + } + + fn execute_tx_phase<'s, 'b, S: ReadonlyKV + 's, A: AccountsCodeStorage + 'b>( + &self, + tx: &Tx, + ctx: Invoker<'s, 'b, S, A>, + resolved_sender: AccountId, + ) -> SdkResult { + let mut exec_ctx = ctx.into_new_exec(resolved_sender); + let recipient = tx.resolve_recipient_account(&mut exec_ctx)?; + let response = exec_ctx.do_exec(recipient, tx.request(), tx.funds().to_vec()); + let post_tx_result = + PostTx::after_tx_executed(tx, exec_ctx.gas_used(), &response, &mut exec_ctx); + match post_tx_result { + Ok(()) => response, + Err(post_tx_err) => Err(post_tx_err), + } + } + fn apply_tx<'a, S: ReadonlyKV + 'a, A: AccountsCodeStorage + 'a>( &self, state: &mut ExecutionState<'a, S>, @@ -1258,117 +1330,37 @@ where gas_config: StorageGasConfig, block: BlockContext, ) -> TxResult { - // create a finite gas counter for the full tx lifecycle, - // including optional sender bootstrap registration. let mut gas_counter = GasCounter::Finite { gas_limit: tx.gas_limit(), gas_used: 0, storage_gas_config: gas_config, }; - let resolved_sender = { - let mut resolve_ctx = - Invoker::new_for_begin_block(state, codes, &mut gas_counter, block); - let sender = match tx.resolve_sender_account(&mut resolve_ctx) { + let resolved_sender = + match self.resolve_sender_phase(state, codes, &mut gas_counter, tx, block) { Ok(sender) => sender, - Err(err) => { - drop(resolve_ctx); - let gas_used = gas_counter.gas_used(); - let events = state.pop_events(); - return TxResult { - events, - gas_used, - response: Err(err), - }; - } + Err(err) => return Self::tx_error_result(state, &gas_counter, err), }; - drop(resolve_ctx); - sender - }; - // Auto-register sender when transaction provides a bootstrap primitive. - if let Some(bootstrap) = tx.sender_bootstrap() { - let mut reg_ctx = Invoker::new_for_begin_block(state, codes, &mut gas_counter, block); - if let Err(err) = reg_ctx.register_account_at_id( - resolved_sender, - bootstrap.account_code_id, - bootstrap.init_message, - ) { - drop(reg_ctx); - let gas_used = gas_counter.gas_used(); - let events = state.pop_events(); - return TxResult { - events, - gas_used, - response: Err(err), - }; - } - drop(reg_ctx); - state.pop_events(); + if let Err(err) = + self.bootstrap_sender_phase(state, codes, &mut gas_counter, tx, resolved_sender, block) + { + return Self::tx_error_result(state, &gas_counter, err); } - // NOTE: Transaction validation and execution are atomic - they share the same - // ExecutionState throughout the process. The state cannot change between - // validation and execution because: - // 1. The same ExecutionState instance is used for both phases - // 2. into_new_exec() preserves the storage, maintaining consistency - // 3. Transactions are processed sequentially, not concurrently - - // create validation context - let mut ctx = Invoker::new_for_validate_tx(state, codes, &mut gas_counter, tx, block); - // do tx validation; we do not swap invoker - match self.tx_validator.validate_tx(tx, &mut ctx) { - Ok(()) => (), - Err(err) => { - drop(ctx); - let gas_used = gas_counter.gas_used(); - let events = state.pop_events(); - return TxResult { - events, - gas_used, - response: Err(err), - }; - } + let mut validation_ctx = + Invoker::new_for_validate_tx(state, codes, &mut gas_counter, tx, block); + if let Err(err) = self.validate_tx_phase(tx, &mut validation_ctx) { + drop(validation_ctx); + return Self::tx_error_result(state, &gas_counter, err); } - // exec tx - transforms validation context to execution context - // while preserving the same underlying state - let mut ctx = ctx.into_new_exec(resolved_sender); - - let recipient = match tx.resolve_recipient_account(&mut ctx) { - Ok(recipient) => recipient, - Err(err) => { - drop(ctx); - let gas_used = gas_counter.gas_used(); - let events = state.pop_events(); - return TxResult { - events, - gas_used, - response: Err(err), - }; - } - }; - let response = ctx.do_exec(recipient, tx.request(), tx.funds().to_vec()); - - // Run post-tx handler (e.g., for fee collection, logging, etc.) - // The handler can observe the result and make additional state changes - let post_tx_result = PostTx::after_tx_executed(tx, ctx.gas_used(), &response, &mut ctx); - - drop(ctx); - let gas_used = gas_counter.gas_used(); - let events = state.pop_events(); - - // If post-tx handler fails, the tx response becomes the error - // This allows the handler to reject transactions after execution - let final_response = match post_tx_result { - Ok(()) => response, - Err(post_tx_err) => Err(post_tx_err), - }; + let response = self.execute_tx_phase(tx, validation_ctx, resolved_sender); TxResult { - events, - gas_used, - response: final_response, + events: state.pop_events(), + gas_used: gas_counter.gas_used(), + response, } } diff --git a/crates/app/tx/eth/src/eoa_registry.rs b/crates/app/tx/eth/src/eoa_registry.rs index 0f3578c..0115cad 100644 --- a/crates/app/tx/eth/src/eoa_registry.rs +++ b/crates/app/tx/eth/src/eoa_registry.rs @@ -1,3 +1,4 @@ +use crate::error::ERR_ADDRESS_ACCOUNT_CONFLICT; use crate::traits::{derive_eth_eoa_account_id, derive_runtime_contract_address}; use alloy_primitives::Address; use evolve_core::low_level::{exec_account, query_account, register_account_at_id}; @@ -151,12 +152,12 @@ fn set_mapping( ) -> SdkResult<()> { if let Some(existing) = lookup_account_id_in_env(address, env)? { if existing != account_id { - return Err(evolve_core::ErrorCode::new(0x5A)); + return Err(ERR_ADDRESS_ACCOUNT_CONFLICT); } } if let Some(existing_addr) = lookup_address_in_env(account_id, env)? { if existing_addr != address { - return Err(evolve_core::ErrorCode::new(0x5A)); + return Err(ERR_ADDRESS_ACCOUNT_CONFLICT); } } @@ -208,7 +209,7 @@ pub fn register_runtime_contract_account( let address = derive_runtime_contract_address(account_id); if let Some(existing) = lookup_contract_account_id_in_env(address, env)? { if existing != account_id { - return Err(evolve_core::ErrorCode::new(0x5A)); + return Err(ERR_ADDRESS_ACCOUNT_CONFLICT); } } @@ -223,7 +224,7 @@ pub fn register_runtime_contract_account( if let Some(raw) = response.value { let existing = raw.get::<[u8; 20]>()?; if existing != address.into_array() { - return Err(evolve_core::ErrorCode::new(0x5A)); + return Err(ERR_ADDRESS_ACCOUNT_CONFLICT); } } diff --git a/crates/app/tx/eth/src/error.rs b/crates/app/tx/eth/src/error.rs index 5259154..abcc561 100644 --- a/crates/app/tx/eth/src/error.rs +++ b/crates/app/tx/eth/src/error.rs @@ -30,3 +30,20 @@ define_error!( 0x1D, "gas limit exceeds block limit" ); + +// System/runtime lookup errors (0x50-0x5F range) +define_error!( + ERR_SYSTEM_LOOKUP_MISSING, + 0x50, + "system lookup missing for recipient" +); +define_error!( + ERR_SYSTEM_RECIPIENT_NOT_FOUND, + 0x52, + "recipient account not found in registry" +); +define_error!( + ERR_ADDRESS_ACCOUNT_CONFLICT, + 0x5A, + "address/account registry conflict" +); diff --git a/crates/app/tx/eth/src/mempool.rs b/crates/app/tx/eth/src/mempool.rs index 2e44801..49d0a22 100644 --- a/crates/app/tx/eth/src/mempool.rs +++ b/crates/app/tx/eth/src/mempool.rs @@ -21,6 +21,7 @@ use crate::envelope::TxEnvelope; use crate::eoa_registry::{ lookup_account_id_in_env, lookup_contract_account_id_in_env, resolve_or_create_eoa_account, }; +use crate::error::{ERR_SYSTEM_LOOKUP_MISSING, ERR_SYSTEM_RECIPIENT_NOT_FOUND}; use crate::traits::TypedTransaction; /// A verified transaction ready for mempool storage. @@ -121,17 +122,14 @@ impl Transaction for TxContext { } fn resolve_recipient_account(&self, env: &mut dyn Environment) -> SdkResult { - let to = self - .envelope - .to() - .ok_or_else(|| evolve_core::ErrorCode::new(0x50))?; + let to = self.envelope.to().ok_or(ERR_SYSTEM_LOOKUP_MISSING)?; if let Some(account_id) = lookup_account_id_in_env(to, env)? { return Ok(account_id); } if let Some(account_id) = lookup_contract_account_id_in_env(to, env)? { return Ok(account_id); } - Err(evolve_core::ErrorCode::new(0x52)) + Err(ERR_SYSTEM_RECIPIENT_NOT_FOUND) } fn request(&self) -> &InvokeRequest { @@ -178,9 +176,7 @@ impl Decodable for TxContext { let envelope = TxEnvelope::decode(bytes)?; // Use base_fee of 0 for decoding - the effective gas price will be // recalculated if needed when the transaction is added to a mempool - TxContext::new(envelope, 0).ok_or_else(|| { - evolve_core::ErrorCode::new(0x50) // Contract creation not supported - }) + TxContext::new(envelope, 0).ok_or(ERR_SYSTEM_LOOKUP_MISSING) } } diff --git a/crates/rpc/chain-index/src/integration.rs b/crates/rpc/chain-index/src/integration.rs index 3bb298c..35bab4c 100644 --- a/crates/rpc/chain-index/src/integration.rs +++ b/crates/rpc/chain-index/src/integration.rs @@ -192,23 +192,8 @@ fn build_stored_transaction( transaction_index: u32, chain_id: u64, ) -> StoredTransaction { - let from = tx - .sender_eth_address() - .map(Address::from) - .unwrap_or_else(|| account_id_to_address(tx.sender())); - let to = { - if let Some(recipient) = tx.recipient_eth_address() { - Some(Address::from(recipient)) - } else { - let recipient = tx.recipient(); - // Check if recipient is the invalid/zero account - if recipient == AccountId::invalid() { - None - } else { - Some(account_id_to_address(recipient)) - } - } - }; + let from = resolve_sender_address(tx); + let to = resolve_recipient_address(tx); // Extract value from funds (sum of all fungible assets as a simple approach) let value = tx @@ -249,22 +234,8 @@ fn build_stored_receipt( transaction_index: u32, cumulative_gas_used: u64, ) -> StoredReceipt { - let from = tx - .sender_eth_address() - .map(Address::from) - .unwrap_or_else(|| account_id_to_address(tx.sender())); - let to = { - if let Some(recipient) = tx.recipient_eth_address() { - Some(Address::from(recipient)) - } else { - let recipient = tx.recipient(); - if recipient == AccountId::invalid() { - None - } else { - Some(account_id_to_address(recipient)) - } - } - }; + let from = resolve_sender_address(tx); + let to = resolve_recipient_address(tx); // Convert events to logs let logs: Vec = tx_result.events.iter().map(event_to_stored_log).collect(); @@ -288,6 +259,25 @@ fn build_stored_receipt( } } +fn resolve_sender_address(tx: &Tx) -> Address { + tx.sender_eth_address() + .map(Address::from) + .unwrap_or_else(|| account_id_to_address(tx.sender())) +} + +fn resolve_recipient_address(tx: &Tx) -> Option
{ + if let Some(recipient) = tx.recipient_eth_address() { + Some(Address::from(recipient)) + } else { + let recipient = tx.recipient(); + if recipient == AccountId::invalid() { + None + } else { + Some(account_id_to_address(recipient)) + } + } +} + /// Compute a simple transactions root (hash of all tx hashes). fn compute_tx_root(txs: &[StoredTransaction]) -> B256 { if txs.is_empty() { diff --git a/crates/rpc/chain-index/src/provider.rs b/crates/rpc/chain-index/src/provider.rs index d82d5d4..2bde5c5 100644 --- a/crates/rpc/chain-index/src/provider.rs +++ b/crates/rpc/chain-index/src/provider.rs @@ -376,10 +376,11 @@ impl St } async fn get_balance(&self, address: Address, _block: Option) -> Result { - match &self.state_querier { - Some(q) => q.get_balance(address).await, - None => Ok(U256::ZERO), - } + let querier = self + .state_querier + .as_ref() + .ok_or_else(|| RpcError::NotImplemented("state_querier not configured".to_string()))?; + querier.get_balance(address).await } async fn get_transaction_count( @@ -387,17 +388,19 @@ impl St address: Address, _block: Option, ) -> Result { - match &self.state_querier { - Some(q) => q.get_transaction_count(address).await, - None => Ok(0), - } + let querier = self + .state_querier + .as_ref() + .ok_or_else(|| RpcError::NotImplemented("state_querier not configured".to_string()))?; + querier.get_transaction_count(address).await } async fn call(&self, request: &CallRequest, _block: Option) -> Result { - match &self.state_querier { - Some(q) => q.call(request).await, - None => Ok(Bytes::new()), - } + let querier = self + .state_querier + .as_ref() + .ok_or_else(|| RpcError::NotImplemented("state_querier not configured".to_string()))?; + querier.call(request).await } async fn estimate_gas( @@ -405,10 +408,11 @@ impl St request: &CallRequest, _block: Option, ) -> Result { - match &self.state_querier { - Some(q) => q.estimate_gas(request).await, - None => Ok(21000), - } + let querier = self + .state_querier + .as_ref() + .ok_or_else(|| RpcError::NotImplemented("state_querier not configured".to_string()))?; + querier.estimate_gas(request).await } async fn get_logs(&self, filter: &LogFilter) -> Result, RpcError> { diff --git a/crates/rpc/chain-index/src/querier.rs b/crates/rpc/chain-index/src/querier.rs index 41168fa..06afcdb 100644 --- a/crates/rpc/chain-index/src/querier.rs +++ b/crates/rpc/chain-index/src/querier.rs @@ -11,7 +11,7 @@ use evolve_core::encoding::Encodable; use evolve_core::{AccountId, Message, ReadonlyKV}; use evolve_eth_jsonrpc::error::RpcError; use evolve_rpc_types::CallRequest; -use evolve_tx_eth::lookup_account_id_in_storage; +use evolve_tx_eth::{lookup_account_id_in_storage, lookup_contract_account_id_in_storage}; /// Trait for querying on-chain state (balance, nonce). /// @@ -86,14 +86,23 @@ impl StorageStateQuerier { None => Ok(0), } } + + fn resolve_account_id(&self, address: Address) -> Result, RpcError> { + if let Some(account_id) = lookup_account_id_in_storage(&self.storage, address) + .map_err(|e| RpcError::InternalError(format!("lookup account id: {:?}", e)))? + { + return Ok(Some(account_id)); + } + + lookup_contract_account_id_in_storage(&self.storage, address) + .map_err(|e| RpcError::InternalError(format!("lookup contract account id: {:?}", e))) + } } #[async_trait] impl StateQuerier for StorageStateQuerier { async fn get_balance(&self, address: Address) -> Result { - let Some(account_id) = lookup_account_id_in_storage(&self.storage, address) - .map_err(|e| RpcError::InternalError(format!("lookup account id: {:?}", e)))? - else { + let Some(account_id) = self.resolve_account_id(address)? else { return Ok(U256::ZERO); }; let balance = self.read_balance(account_id)?; @@ -101,9 +110,7 @@ impl StateQuerier for StorageStateQuerier { } async fn get_transaction_count(&self, address: Address) -> Result { - let Some(account_id) = lookup_account_id_in_storage(&self.storage, address) - .map_err(|e| RpcError::InternalError(format!("lookup account id: {:?}", e)))? - else { + let Some(account_id) = self.resolve_account_id(address)? else { return Ok(0); }; self.read_nonce(account_id) diff --git a/crates/testing/simulator/src/eth_eoa.rs b/crates/testing/simulator/src/eth_eoa.rs index 60720d0..aa661e5 100644 --- a/crates/testing/simulator/src/eth_eoa.rs +++ b/crates/testing/simulator/src/eth_eoa.rs @@ -33,14 +33,16 @@ pub fn init_eth_eoa_storage( account_id: AccountId, eth_address: [u8; 20], ) -> Result<(), ErrorCode> { - let mut nonce_key = account_id.as_bytes().to_vec(); + let mut nonce_key = Vec::with_capacity(account_id.as_bytes().len() + 1); + nonce_key.extend_from_slice(&account_id.as_bytes()); nonce_key.push(0u8); let nonce_value = Message::new(&0u64) .expect("encode nonce") .into_bytes() .expect("nonce bytes"); - let mut addr_key = account_id.as_bytes().to_vec(); + let mut addr_key = Vec::with_capacity(account_id.as_bytes().len() + 1); + addr_key.extend_from_slice(&account_id.as_bytes()); addr_key.push(1u8); let addr_value = Message::new(ð_address) .expect("encode eth address") diff --git a/docker/testapp/Dockerfile b/docker/testapp/Dockerfile index f5980b8..575e83f 100644 --- a/docker/testapp/Dockerfile +++ b/docker/testapp/Dockerfile @@ -1,4 +1,16 @@ -FROM rust:bookworm AS builder +FROM rust:bookworm AS chef + +WORKDIR /app + +RUN cargo install cargo-chef + +FROM chef AS planner + +COPY . . + +RUN cargo chef prepare --recipe-path recipe.json + +FROM chef AS builder WORKDIR /app @@ -6,6 +18,10 @@ RUN apt-get update \ && apt-get install -y --no-install-recommends protobuf-compiler libprotobuf-dev \ && rm -rf /var/lib/apt/lists/* +COPY --from=planner /app/recipe.json recipe.json + +RUN cargo chef cook --recipe-path recipe.json --release + COPY . . RUN cargo build -p evolve_testapp --release @@ -14,12 +30,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/testapp /usr/local/bin/testapp +RUN chown evolve:evolve /usr/local/bin/testapp /app + +USER evolve:evolve + EXPOSE 8545 50051 ENTRYPOINT ["/usr/local/bin/testapp"] From 52a6f2e9239f0a10f20a6dcb237f039866c3de46 Mon Sep 17 00:00:00 2001 From: tac0turtle Date: Fri, 27 Feb 2026 15:46:48 +0100 Subject: [PATCH 08/10] Clarify recipient-required policy and cover auto-mapping --- crates/app/tx/eth/src/error.rs | 9 +- crates/app/tx/eth/src/mempool.rs | 189 ++++++++++++++++++++++++++++++- 2 files changed, 186 insertions(+), 12 deletions(-) diff --git a/crates/app/tx/eth/src/error.rs b/crates/app/tx/eth/src/error.rs index abcc561..0ceb3c4 100644 --- a/crates/app/tx/eth/src/error.rs +++ b/crates/app/tx/eth/src/error.rs @@ -33,14 +33,9 @@ define_error!( // System/runtime lookup errors (0x50-0x5F range) define_error!( - ERR_SYSTEM_LOOKUP_MISSING, + ERR_RECIPIENT_REQUIRED, 0x50, - "system lookup missing for recipient" -); -define_error!( - ERR_SYSTEM_RECIPIENT_NOT_FOUND, - 0x52, - "recipient account not found in registry" + "recipient required (contract creation not supported yet)" ); define_error!( ERR_ADDRESS_ACCOUNT_CONFLICT, diff --git a/crates/app/tx/eth/src/mempool.rs b/crates/app/tx/eth/src/mempool.rs index 49d0a22..d602fbc 100644 --- a/crates/app/tx/eth/src/mempool.rs +++ b/crates/app/tx/eth/src/mempool.rs @@ -21,7 +21,7 @@ use crate::envelope::TxEnvelope; use crate::eoa_registry::{ lookup_account_id_in_env, lookup_contract_account_id_in_env, resolve_or_create_eoa_account, }; -use crate::error::{ERR_SYSTEM_LOOKUP_MISSING, ERR_SYSTEM_RECIPIENT_NOT_FOUND}; +use crate::error::ERR_RECIPIENT_REQUIRED; use crate::traits::TypedTransaction; /// A verified transaction ready for mempool storage. @@ -43,6 +43,9 @@ impl TxContext { /// /// Returns `None` if the transaction has no recipient (contract creation). pub fn new(envelope: TxEnvelope, base_fee: u128) -> Option { + // TODO(vm): when EVM contract creation is supported, allow `to == None` + // and route create-transactions through deployment execution instead of + // rejecting them at mempool decode time. envelope.to()?; let invoke_request = envelope.to_invoke_requests().into_iter().next()?; @@ -122,14 +125,17 @@ impl Transaction for TxContext { } fn resolve_recipient_account(&self, env: &mut dyn Environment) -> SdkResult { - let to = self.envelope.to().ok_or(ERR_SYSTEM_LOOKUP_MISSING)?; + // TODO(vm): contract creation currently has no recipient and is rejected. + // Once VM deployment is supported, this branch should route to creation + // logic instead of returning recipient-required. + let to = self.envelope.to().ok_or(ERR_RECIPIENT_REQUIRED)?; if let Some(account_id) = lookup_account_id_in_env(to, env)? { return Ok(account_id); } if let Some(account_id) = lookup_contract_account_id_in_env(to, env)? { return Ok(account_id); } - Err(ERR_SYSTEM_RECIPIENT_NOT_FOUND) + resolve_or_create_eoa_account(to, env) } fn request(&self) -> &InvokeRequest { @@ -176,7 +182,7 @@ impl Decodable for TxContext { let envelope = TxEnvelope::decode(bytes)?; // Use base_fee of 0 for decoding - the effective gas price will be // recalculated if needed when the transaction is added to a mempool - TxContext::new(envelope, 0).ok_or(ERR_SYSTEM_LOOKUP_MISSING) + TxContext::new(envelope, 0).ok_or(ERR_RECIPIENT_REQUIRED) } } @@ -200,4 +206,177 @@ fn calculate_effective_gas_price(envelope: &TxEnvelope, base_fee: u128) -> u128 } #[cfg(test)] -mod tests {} +mod tests { + use super::*; + use crate::eoa_registry::{lookup_account_id_in_env, register_runtime_contract_account}; + use crate::traits::{derive_eth_eoa_account_id, derive_runtime_contract_account_id}; + use alloy_consensus::{SignableTransaction, TxLegacy}; + use alloy_primitives::{Bytes, PrimitiveSignature, TxKind, U256}; + use evolve_core::runtime_api::{ + RegisterAccountAtIdRequest, RegisterAccountAtIdResponse, ACCOUNT_IDENTIFIER_PREFIX, + RUNTIME_ACCOUNT_ID, + }; + use evolve_core::storage_api::{ + StorageGetRequest, StorageGetResponse, StorageSetRequest, StorageSetResponse, + STORAGE_ACCOUNT_ID, + }; + use evolve_core::{ + BlockContext, EnvironmentQuery, FungibleAsset, InvokableMessage, InvokeResponse, + ERR_UNKNOWN_FUNCTION, + }; + use k256::ecdsa::{signature::hazmat::PrehashSigner, SigningKey}; + use rand::rngs::OsRng; + use std::collections::BTreeMap; + + #[derive(Default)] + struct MockEnv { + funds: Vec, + storage: BTreeMap, Vec>, + } + + impl MockEnv { + fn account_scoped_key(account_id: AccountId, key: &[u8]) -> Vec { + let mut full = account_id.as_bytes().to_vec(); + full.extend_from_slice(key); + full + } + } + + impl EnvironmentQuery for MockEnv { + fn whoami(&self) -> AccountId { + RUNTIME_ACCOUNT_ID + } + + fn sender(&self) -> AccountId { + AccountId::invalid() + } + + fn funds(&self) -> &[FungibleAsset] { + &self.funds + } + + fn block(&self) -> BlockContext { + BlockContext::default() + } + + fn do_query( + &mut self, + to: AccountId, + data: &InvokeRequest, + ) -> evolve_core::SdkResult { + if to != STORAGE_ACCOUNT_ID { + return Err(ERR_UNKNOWN_FUNCTION); + } + match data.function() { + StorageGetRequest::FUNCTION_IDENTIFIER => { + let req: StorageGetRequest = data.get()?; + let key = Self::account_scoped_key(req.account_id, &req.key); + let value = self + .storage + .get(&key) + .cloned() + .map(evolve_core::Message::from_bytes); + InvokeResponse::new(&StorageGetResponse { value }) + } + _ => Err(ERR_UNKNOWN_FUNCTION), + } + } + } + + impl Environment for MockEnv { + fn do_exec( + &mut self, + to: AccountId, + data: &InvokeRequest, + _funds: Vec, + ) -> evolve_core::SdkResult { + if to == STORAGE_ACCOUNT_ID && data.function() == StorageSetRequest::FUNCTION_IDENTIFIER + { + let req: StorageSetRequest = data.get()?; + let key = Self::account_scoped_key(RUNTIME_ACCOUNT_ID, &req.key); + self.storage.insert(key, req.value.into_bytes()?); + return InvokeResponse::new(&StorageSetResponse {}); + } + + if to == RUNTIME_ACCOUNT_ID + && data.function() == RegisterAccountAtIdRequest::FUNCTION_IDENTIFIER + { + let req: RegisterAccountAtIdRequest = data.get()?; + let mut key = vec![ACCOUNT_IDENTIFIER_PREFIX]; + key.extend_from_slice(&req.account_id.as_bytes()); + self.storage + .insert(key, evolve_core::Message::new(&req.code_id)?.into_bytes()?); + return InvokeResponse::new(&RegisterAccountAtIdResponse {}); + } + + Err(ERR_UNKNOWN_FUNCTION) + } + + fn emit_event(&mut self, _name: &str, _data: &[u8]) -> evolve_core::SdkResult<()> { + Ok(()) + } + + fn unique_id(&mut self) -> evolve_core::SdkResult<[u8; 32]> { + Ok([0u8; 32]) + } + } + + fn sign_hash(signing_key: &SigningKey, hash: B256) -> PrimitiveSignature { + let (sig, recovery_id) = signing_key.sign_prehash(hash.as_ref()).unwrap(); + let r = U256::from_be_slice(&sig.r().to_bytes()); + let s = U256::from_be_slice(&sig.s().to_bytes()); + let v = recovery_id.is_y_odd(); + PrimitiveSignature::new(r, s, v) + } + + fn build_tx_context(to: Address) -> TxContext { + let signing_key = SigningKey::random(&mut OsRng); + let tx = TxLegacy { + chain_id: Some(1), + nonce: 0, + gas_price: 1_000_000_000, + gas_limit: 21_000, + to: TxKind::Call(to), + value: U256::ZERO, + input: Bytes::new(), + }; + let signature = sign_hash(&signing_key, tx.signature_hash()); + let signed = tx.into_signed(signature); + let mut encoded = Vec::new(); + signed.rlp_encode(&mut encoded); + + let envelope = TxEnvelope::decode(&encoded).expect("decode signed tx"); + TxContext::new(envelope, 0).expect("construct tx context") + } + + #[test] + fn resolve_recipient_account_creates_mapping_for_unseen_eoa() { + let recipient = Address::repeat_byte(0xAB); + let tx = build_tx_context(recipient); + let mut env = MockEnv::default(); + + let resolved = tx + .resolve_recipient_account(&mut env) + .expect("resolve recipient"); + assert_eq!(resolved, derive_eth_eoa_account_id(recipient)); + + let mapped = lookup_account_id_in_env(recipient, &mut env) + .expect("lookup recipient") + .expect("recipient mapping exists"); + assert_eq!(mapped, resolved); + } + + #[test] + fn resolve_recipient_account_prefers_existing_contract_mapping() { + let mut env = MockEnv::default(); + let contract_id = derive_runtime_contract_account_id(b"mempool-test-contract"); + let contract_address = + register_runtime_contract_account(contract_id, &mut env).expect("register contract"); + + let tx = build_tx_context(contract_address); + let resolved = tx + .resolve_recipient_account(&mut env) + .expect("resolve recipient"); + assert_eq!(resolved, contract_id); + } +} From 71a005f8b304430473bac60ebea516d9913d289c Mon Sep 17 00:00:00 2001 From: tac0turtle Date: Fri, 27 Feb 2026 15:55:20 +0100 Subject: [PATCH 09/10] Track AccountId numeric-compat cleanup --- crates/app/sdk/core/src/lib.rs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/crates/app/sdk/core/src/lib.rs b/crates/app/sdk/core/src/lib.rs index f8da4d3..0314223 100644 --- a/crates/app/sdk/core/src/lib.rs +++ b/crates/app/sdk/core/src/lib.rs @@ -71,6 +71,8 @@ impl AccountId { /// Backward-compatible constructor from `u128`. /// /// Encodes into the lower 16 bytes in big-endian order. + // TODO(account-id-cleanup): remove numeric AccountId compatibility (`new`/`inner`) + // after migrating genesis/tooling/docs to canonical 32-byte account IDs end-to-end. pub const fn new(u: u128) -> Self { let mut out = [0u8; 32]; let bytes = u.to_be_bytes(); From f8b66a70066c58a422d282bfea7b1c60282cd13e Mon Sep 17 00:00:00 2001 From: tac0turtle Date: Fri, 27 Feb 2026 16:14:41 +0100 Subject: [PATCH 10/10] Fix unresolved PR issues in evd and custom genesis --- bin/evd/src/main.rs | 104 +++++++++++++++++++++++++++++++++------- bin/testapp/src/main.rs | 29 +++++++++-- 2 files changed, 112 insertions(+), 21 deletions(-) diff --git a/bin/evd/src/main.rs b/bin/evd/src/main.rs index 15f6b81..a2d5b75 100644 --- a/bin/evd/src/main.rs +++ b/bin/evd/src/main.rs @@ -287,7 +287,8 @@ fn build_on_block_executed( callback_max_gas: u64, callback_indexing_enabled: bool, ) -> (OnBlockExecuted, Arc) { - let parent_hash = Arc::new(std::sync::RwLock::new(B256::ZERO)); + let initial_parent_hash = resolve_initial_parent_hash(chain_index.as_ref(), initial_height); + let parent_hash = Arc::new(std::sync::RwLock::new(initial_parent_hash)); let current_height = Arc::new(AtomicU64::new(initial_height)); let parent_hash_for_callback = Arc::clone(&parent_hash); let current_height_for_callback = Arc::clone(¤t_height); @@ -345,6 +346,72 @@ fn build_on_block_executed( (on_block_executed, current_height) } +fn resolve_initial_parent_hash( + chain_index: Option<&SharedChainIndex>, + initial_height: u64, +) -> B256 { + let Some(index) = chain_index else { + return B256::ZERO; + }; + + match index.get_block(initial_height) { + Ok(Some(block)) => { + tracing::info!("Seeding parent hash from indexed block {}", initial_height); + return block.hash; + } + Ok(None) => {} + Err(err) => { + tracing::warn!( + "Failed to read indexed block {} while seeding parent hash: {:?}", + initial_height, + err + ); + } + } + + let latest_indexed = match index.latest_block_number() { + Ok(latest) => latest, + Err(err) => { + tracing::warn!( + "Failed to read latest indexed block while seeding parent hash: {:?}", + err + ); + return B256::ZERO; + } + }; + + let Some(latest_height) = latest_indexed else { + return B256::ZERO; + }; + + if latest_height != initial_height { + tracing::warn!( + "Chain state height {} does not match indexed head {}; seeding parent hash from indexed head", + initial_height, + latest_height + ); + } + + match index.get_block(latest_height) { + Ok(Some(block)) => block.hash, + Ok(None) => { + tracing::warn!( + "Indexed head {} missing block payload while seeding parent hash", + latest_height + ); + B256::ZERO + } + Err(err) => { + tracing::warn!( + "Failed to read indexed head block {} while seeding parent hash: {:?}", + latest_height, + err + ); + B256::ZERO + } + } +} + fn log_server_configuration(config: &NodeConfig, initial_height: u64) { let grpc_addr = config.parsed_grpc_addr(); tracing::info!("Starting gRPC server on {}", grpc_addr); @@ -689,30 +756,34 @@ mod tests { static ENV_VAR_LOCK: Mutex<()> = Mutex::new(()); struct EnvVarGuard { - key: &'static str, - old: Option, + entries: Vec<(&'static str, Option)>, _guard: MutexGuard<'static, ()>, } impl EnvVarGuard { - fn set(key: &'static str, value: &str) -> Self { + fn acquire() -> Self { let guard = ENV_VAR_LOCK.lock().expect("env var lock poisoned"); - let old = std::env::var(key).ok(); - std::env::set_var(key, value); Self { - key, - old, + entries: Vec::new(), _guard: guard, } } + + fn set(&mut self, key: &'static str, value: &str) { + let old = std::env::var(key).ok(); + std::env::set_var(key, value); + self.entries.push((key, old)); + } } impl Drop for EnvVarGuard { fn drop(&mut self) { - if let Some(value) = &self.old { - std::env::set_var(self.key, value); - } else { - std::env::remove_var(self.key); + for (key, old) in self.entries.iter().rev() { + if let Some(value) = old { + std::env::set_var(key, value); + } else { + std::env::remove_var(key); + } } } } @@ -768,16 +839,17 @@ mod tests { #[test] fn default_genesis_funds_eth_mapped_sender_account() { - let _alice_addr = EnvVarGuard::set( + let mut env = EnvVarGuard::acquire(); + env.set( "GENESIS_ALICE_ETH_ADDRESS", "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266", ); - let _bob_addr = EnvVarGuard::set( + env.set( "GENESIS_BOB_ETH_ADDRESS", "0x70997970C51812dc3A010C7d01b50e0d17dc79C8", ); - let _alice_bal = EnvVarGuard::set("GENESIS_ALICE_TOKEN_BALANCE", "1234"); - let _bob_bal = EnvVarGuard::set("GENESIS_BOB_TOKEN_BALANCE", "5678"); + env.set("GENESIS_ALICE_TOKEN_BALANCE", "1234"); + env.set("GENESIS_BOB_TOKEN_BALANCE", "5678"); let storage = MockStorage::new(); let codes = build_codes(); diff --git a/bin/testapp/src/main.rs b/bin/testapp/src/main.rs index f10fef1..0bec09d 100644 --- a/bin/testapp/src/main.rs +++ b/bin/testapp/src/main.rs @@ -206,15 +206,17 @@ fn run_custom_genesis( use evolve_token::account::TokenRef; use evolve_tx_eth::{register_runtime_contract_account, resolve_or_create_eoa_account}; - let funded_accounts: Vec<([u8; 20], u128)> = config + let configured_accounts: Vec<([u8; 20], u128)> = config .accounts .iter() - .filter(|acc| acc.balance > 0) .map(|acc| { acc.parse_address() .map(|addr| (addr.into_array(), acc.balance)) }) .collect::, _>>()?; + if configured_accounts.is_empty() { + return Err("custom genesis requires at least one account".into()); + } let minter = AccountId::new(config.minter_id); let metadata = config.token.to_metadata(); @@ -223,7 +225,7 @@ fn run_custom_genesis( let (accounts, state) = stf .system_exec(storage, codes, genesis_block, |env| { - let balances: Vec<(AccountId, u128)> = funded_accounts + let resolved_accounts: Vec<(AccountId, u128)> = configured_accounts .iter() .map( |(eth_addr, balance)| -> evolve_core::SdkResult<(AccountId, u128)> { @@ -232,6 +234,19 @@ fn run_custom_genesis( }, ) .collect::>>()?; + let balances: Vec<(AccountId, u128)> = resolved_accounts + .iter() + .filter(|(_, balance)| *balance > 0) + .map(|(account_id, balance)| (*account_id, *balance)) + .collect(); + let alice_acc = resolved_accounts + .first() + .map(|(account_id, _)| *account_id) + .expect("configured_accounts validated as non-empty"); + let bob_acc = resolved_accounts + .get(1) + .map(|(account_id, _)| *account_id) + .unwrap_or(alice_acc); let token = TokenRef::initialize(metadata.clone(), balances, Some(minter), env)?.0; let _token_eth_addr = register_runtime_contract_account(token.0, env)?; @@ -241,8 +256,8 @@ fn run_custom_genesis( scheduler_acc.update_begin_blockers(vec![], env)?; Ok(GenesisAccounts { - alice: token.0, - bob: token.0, + alice: alice_acc, + bob: bob_acc, atom: token.0, scheduler: scheduler_acc.0, }) @@ -371,6 +386,10 @@ mod tests { read_token_balance(&state, output.genesis_result.atom, mapped_id), 777 ); + assert_eq!(output.genesis_result.alice, mapped_id); + assert_eq!(output.genesis_result.bob, mapped_id); + assert_ne!(output.genesis_result.alice, output.genesis_result.atom); + assert_ne!(output.genesis_result.bob, output.genesis_result.atom); assert_eq!(count_registered_code_id(&state, "EthEoaAccount"), 1); }