Skip to content
Merged
Show file tree
Hide file tree
Changes from 7 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
191 changes: 168 additions & 23 deletions bin/evd/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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};
Expand All @@ -83,19 +83,21 @@ use evolve_node::{
GenesisOutput, InitArgs, NodeConfig, RunArgs,
};
use evolve_rpc_types::SyncStatus;
use evolve_scheduler::scheduler_account::SchedulerRef;
use evolve_server::{
load_chain_state, save_chain_state, BlockBuilder, ChainState, CHAIN_STATE_KEY,
};
use evolve_stf_traits::{AccountsCodeStorage, StateChange};
use evolve_storage::{Operation, QmdbStorage, Storage, StorageConfig};
use evolve_testapp::genesis_config::{load_genesis_config, EvdGenesisConfig, EvdGenesisResult};
use evolve_testapp::{
build_mempool_stf, default_gas_config, do_genesis_inner, initialize_custom_genesis_resources,
install_account_codes, PLACEHOLDER_ACCOUNT,
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::TxContext;

use evolve_tx_eth::{register_runtime_contract_account, resolve_or_create_eoa_account};
#[derive(Parser)]
#[command(name = "evd")]
#[command(about = "Evolve node daemon with gRPC execution layer")]
Expand Down Expand Up @@ -242,12 +244,17 @@ fn run_node(config: NodeConfig, genesis_config: Option<EvdGenesisConfig>) {
sync_status: SyncStatus::NotSyncing(false),
};

let state_querier: Arc<dyn StateQuerier> = 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 {
Expand Down Expand Up @@ -467,27 +474,40 @@ fn run_genesis<S: ReadonlyKV + Storage>(
}
}

/// 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<S: ReadonlyKV + Storage>(
storage: &S,
codes: &AccountStorageMock,
) -> GenesisOutput<EvdGenesisResult> {
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,
};

Expand All @@ -499,19 +519,25 @@ fn run_default_genesis<S: ReadonlyKV + Storage>(

/// 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<S: ReadonlyKV + Storage>(
storage: &S,
codes: &AccountStorageMock,
genesis_config: &EvdGenesisConfig,
) -> GenesisOutput<EvdGenesisResult> {
use evolve_core::BlockContext;

let funded_accounts = genesis_config
.funded_accounts()
.expect("invalid address in genesis config");

let funded_accounts: Vec<([u8; 20], u128)> = genesis_config
.accounts
.iter()
.filter(|acc| acc.balance > 0)
.map(|acc| {
let addr = acc
.parse_address()
.expect("invalid address in genesis config");
(addr.into_array(), acc.balance)
})
.collect();
let minter = AccountId::new(genesis_config.minter_id);
let metadata = genesis_config.token.to_metadata();

Expand All @@ -521,16 +547,26 @@ fn run_custom_genesis<S: ReadonlyKV + Storage>(

let (genesis_result, state) = stf
.system_exec(storage, codes, genesis_block, |env| {
let resources = initialize_custom_genesis_resources(
&funded_accounts,
metadata.clone(),
minter,
env,
)?;
let balances: Vec<(AccountId, u128)> = funded_accounts
.iter()
.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::<evolve_core::SdkResult<Vec<_>>>()?;

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 {
token: resources.token,
scheduler: resources.scheduler,
token: token.0,
scheduler: scheduler_acc.0,
})
})
.expect("genesis failed");
Expand Down Expand Up @@ -589,3 +625,112 @@ fn state_changes_to_operations(changes: Vec<StateChange>) -> Vec<Operation> {
})
.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<String>,
}

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 }
}
Comment on lines +763 to +770
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

EnvVarGuard can deadlock when setting multiple vars in one test.

Each EnvVarGuard::set holds the global mutex for the guard lifetime. Creating _alice_addr, _bob_addr, _alice_bal, _bob_bal sequentially in the same test attempts recursive locking and can block indefinitely.

Also applies to: 771-780

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@bin/evd/src/main.rs` around lines 697 - 707, EnvVarGuard currently stores the
MutexGuard for ENV_VAR_LOCK for the whole guard lifetime, causing deadlocks when
multiple EnvVarGuard::set calls are created; change the design so set only locks
briefly to read the old value and set the new one, then drops the lock before
returning (i.e., remove the _guard field from the EnvVarGuard struct), and
implement Drop for EnvVarGuard that locks ENV_VAR_LOCK when restoring the old
value; update EnvVarGuard::set, the struct fields, and the Drop impl to
reference ENV_VAR_LOCK, EnvVarGuard, and set accordingly.

}

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);
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Outdated
}
}

fn apply_changes_to_map(changes: Vec<StateChange>) -> BTreeMap<Vec<u8>, Vec<u8>> {
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<u8>, Vec<u8>>,
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::<u128>()
.expect("decode balance"),
None => 0,
}
}

fn eoa_account_ids(state: &BTreeMap<Vec<u8>, Vec<u8>>) -> Vec<AccountId> {
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::<String>().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));
}
}
15 changes: 8 additions & 7 deletions bin/testapp/src/eth_eoa.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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::<AccountId>() {
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::<AccountId>() {
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::<TxContext>() {
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
}
Expand Down
7 changes: 7 additions & 0 deletions bin/testapp/src/genesis_config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
use std::collections::BTreeSet;

Expand Down Expand Up @@ -37,6 +38,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<Self, String> {
Expand Down
Loading
Loading