Tenzro Testnet is live. Get testnet TNZO

EVM Executor

The Tenzro EVM executor provides full Ethereum Virtual Machine compatibility using the revm library. It supports complete EVM bytecode execution, EIP-1559 transactions, contract deployment, log extraction, and Ethereum-compatible address derivation using keccak256 hashing.

Architecture

The EvmExecutor wraps the revm library to provide Ethereum-compatible smart contract execution. It manages execution context, gas accounting, state access, and precompile integration through a unified interface.

┌────────────────────────────────┐ │ EvmExecutor │ │ (Ethereum Compatibility) │ └───────────┬────────────────────┘ │ │ wraps ▼ ┌────────────────────────────────┐ │ revm │ │ (Rust EVM Implementation) │ ├────────────────────────────────┤ │ • Bytecode interpreter │ │ • Gas metering │ │ • EVM opcodes (300+) │ │ • EIP-1559 support │ └───────────┬────────────────────┘ │ │ state access ▼ ┌────────────────────────────────┐ │ StateAdapter │ │ (RocksDB Backend) │ └────────────────────────────────┘

Transaction Execution

The executor supports three primary execution paths: standard transactions, contract calls, and contract deployments. All execution paths use revm for full bytecode interpretation with proper gas metering.

Standard Transaction

use tenzro_vm::evm::EvmExecutor; use tenzro_types::{Transaction, Address}; let executor = EvmExecutor::new(state_adapter); // Create EVM transaction let tx = Transaction { from: sender_address, to: Some(recipient_address), value: parse_ether("1.5")?, // 1.5 TNZO data: vec![], gas_limit: 21_000, gas_price: 20_000_000_000, // 20 Gwei nonce: 0, chain_id: 1337, ..Default::default() }; // Execute transaction let result = executor.execute_transaction(&tx).await?; println!("Gas used: {}", result.gas_used); println!("Status: {:?}", result.status); println!("Output: {}", hex::encode(&result.output)); // Check for logs (events) for log in result.logs { println!("Event emitted:"); println!(" Address: {}", log.address); println!(" Topics: {:?}", log.topics); println!(" Data: {}", hex::encode(&log.data)); }

Contract Call

Contract calls execute bytecode at a specific address with provided calldata. The EVM interprets the bytecode, performs state reads/writes, and returns output data or reverts with an error message.

use tiny_keccak::{Hasher, Keccak}; // Encode ERC-20 transfer function call fn encode_transfer(to: Address, amount: u64) -> Vec<u8> { let mut result = Vec::new(); // Function selector: keccak256("transfer(address,uint256)")[:4] let mut hasher = Keccak::v256(); hasher.update(b"transfer(address,uint256)"); let mut selector = [0u8; 32]; hasher.finalize(&mut selector); result.extend_from_slice(&selector[0..4]); // Encode address (32 bytes, left-padded) let mut address_bytes = [0u8; 32]; address_bytes[12..].copy_from_slice(to.as_bytes()); result.extend_from_slice(&address_bytes); // Encode amount (32 bytes, big-endian) let mut amount_bytes = [0u8; 32]; amount_bytes[24..].copy_from_slice(&amount.to_be_bytes()); result.extend_from_slice(&amount_bytes); result } let erc20_contract = Address::from_hex("0x742d35Cc6...")?; let recipient = Address::from_hex("0x8626f6940E...")?; let amount = parse_ether("100")?; // 100 tokens let tx = Transaction { from: sender_address, to: Some(erc20_contract), data: encode_transfer(recipient, amount), gas_limit: 100_000, gas_price: 20_000_000_000, ..Default::default() }; let result = executor.execute_transaction(&tx).await?; if result.status == ExecutionStatus::Success { println!("Transfer successful!"); println!("Gas used: {}", result.gas_used); } else { println!("Transfer failed: {}", String::from_utf8_lossy(&result.output)); }

Contract Deployment

Contract deployments execute the constructor bytecode and store the runtime bytecode at a deterministic address. The address is derived using keccak256(rlp([sender, nonce])) for Ethereum compatibility.

use tenzro_vm::evm::utils::compute_create_address; // Compile contract bytecode (example: simple storage contract) let bytecode = hex::decode( "608060405234801561001057600080fd5b5060c78061001f6000396000f3fe..." )?; // Deploy contract (to = None for deployment) let tx = Transaction { from: deployer_address, to: None, // Signals deployment data: bytecode, gas_limit: 500_000, gas_price: 20_000_000_000, nonce: 5, ..Default::default() }; let result = executor.execute_transaction(&tx).await?; if result.status == ExecutionStatus::Success { // Compute deployed contract address let contract_address = compute_create_address(&deployer_address, 5); println!("Contract deployed at: {}", contract_address); println!("Gas used: {}", result.gas_used); println!("Runtime bytecode length: {} bytes", result.output.len()); // Verify contract exists in state let code = state_adapter.get_code(&contract_address)?; assert_eq!(code, result.output); }

Address Derivation

Tenzro uses Ethereum-compatible address derivation for CREATE and CREATE2 operations. Addresses are computed using keccak256 hashing over RLP-encoded sender and nonce (CREATE) or deterministic salt (CREATE2).

CREATE Address

use tenzro_vm::evm::utils::compute_create_address; use tiny_keccak::{Hasher, Keccak}; // Standard CREATE: keccak256(rlp([sender, nonce]))[12:] fn compute_create_address(sender: &Address, nonce: u64) -> Address { let mut stream = rlp::RlpStream::new_list(2); stream.append(&sender.as_bytes()); stream.append(&nonce); let encoded = stream.out(); let mut hasher = Keccak::v256(); hasher.update(&encoded); let mut hash = [0u8; 32]; hasher.finalize(&mut hash); Address::from_bytes(&hash[12..]) } // Example let deployer = Address::from_hex("0x742d35Cc6...")?; let nonce = 42; let contract_address = compute_create_address(&deployer, nonce); println!("Contract will deploy at: {}", contract_address);

CREATE2 Address

use tenzro_vm::evm::utils::compute_create2_address; // CREATE2: keccak256(0xff ++ sender ++ salt ++ keccak256(init_code))[12:] fn compute_create2_address( sender: &Address, salt: [u8; 32], init_code: &[u8], ) -> Address { let mut hasher = Keccak::v256(); hasher.update(init_code); let mut code_hash = [0u8; 32]; hasher.finalize(&mut code_hash); let mut input = Vec::with_capacity(85); input.push(0xff); input.extend_from_slice(sender.as_bytes()); input.extend_from_slice(&salt); input.extend_from_slice(&code_hash); let mut hasher = Keccak::v256(); hasher.update(&input); let mut hash = [0u8; 32]; hasher.finalize(&mut hash); Address::from_bytes(&hash[12..]) } // Deterministic deployment let factory = Address::from_hex("0x8626f6940E...")?; let salt = [42u8; 32]; let init_code = hex::decode("6080604052...")?; let contract_address = compute_create2_address(&factory, salt, &init_code); println!("Deterministic address: {}", contract_address);

Gas Metering

Revm provides accurate gas metering for all EVM operations. Gas is consumed for opcode execution, memory expansion, storage access (SLOAD/SSTORE), log emission, and contract creation. The executor enforces gas limits and refunds unused gas.

use tenzro_vm::evm::gas::GasTracker; // Gas costs (post-London fork) const GAS_ADD: u64 = 3; const GAS_MUL: u64 = 5; const GAS_SLOAD: u64 = 2_100; const GAS_SSTORE_SET: u64 = 20_000; const GAS_SSTORE_RESET: u64 = 2_900; const GAS_LOG: u64 = 375; const GAS_LOG_DATA: u64 = 8; // per byte const GAS_LOG_TOPIC: u64 = 375; // per topic const GAS_CALL: u64 = 2_600; // base call cost const GAS_CREATE: u64 = 32_000; // Execution gas tracking let result = executor.execute_transaction(&tx).await?; println!("Gas limit: {}", tx.gas_limit); println!("Gas used: {}", result.gas_used); println!("Gas refund: {}", result.gas_refund); println!("Gas remaining: {}", tx.gas_limit - result.gas_used); // Calculate transaction cost let base_fee = 10_000_000_000; // 10 Gwei let priority_fee = 2_000_000_000; // 2 Gwei let effective_gas_price = base_fee + priority_fee; let total_cost = result.gas_used * effective_gas_price; println!("Total cost: {} TNZO", format_ether(total_cost));

Log Extraction

The executor extracts logs (events) emitted during execution. Each log contains the emitting contract address, up to 4 indexed topics, and arbitrary data bytes. Logs are used for event indexing and off-chain monitoring.

use tenzro_types::Log; // Example: ERC-20 Transfer event // event Transfer(address indexed from, address indexed to, uint256 value); let result = executor.execute_transaction(&tx).await?; for log in result.logs { if log.topics.len() == 3 { // topic[0] = keccak256("Transfer(address,address,uint256)") let event_sig = hex::encode(&log.topics[0]); if event_sig == "ddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef" { // Decode indexed parameters let from = Address::from_bytes(&log.topics[1][12..]); let to = Address::from_bytes(&log.topics[2][12..]); // Decode non-indexed parameter (value) let value = u64::from_be_bytes( log.data[24..32].try_into().unwrap() ); println!("Transfer Event:"); println!(" From: {}", from); println!(" To: {}", to); println!(" Value: {} tokens", format_ether(value)); } } } // Filter logs by address let contract_logs: Vec<Log> = result.logs .into_iter() .filter(|log| log.address == erc20_contract) .collect();

Precompiled Contracts

Tenzro supports standard Ethereum precompiles at addresses 0x01-0x09 and custom Tenzro precompiles at 0x0100+. Precompiles provide efficient native implementations of common operations like hashing, signature verification, and elliptic curve math.

Standard EVM Precompiles

AddressFunctionStatus
0x01ecRecoverStub
0x02SHA-256Production
0x03RIPEMD-160Stub
0x04IdentityProduction
0x05ModExpStub
0x06EC_ADD (bn256)Stub
0x07EC_MUL (bn256)Stub
0x08EC_PAIRING (bn256)Stub
0x09BLAKE2FStub

Tenzro Precompiles

AddressFunctionGas Cost
0x0100TEE Attestation Verify50,000
0x0101ZK Proof Verify100,000
0x0102Model Inference Request200,000
0x0103Settlement Execute75,000

State Access

The executor provides state access operations through the state adapter interface. State operations include account balance queries, nonce management, code storage, and arbitrary storage slot access (SLOAD/SSTORE).

use tenzro_vm::evm::state::StateAdapter; // Account operations let balance = state_adapter.get_balance(&address)?; state_adapter.set_balance(&address, new_balance)?; let nonce = state_adapter.get_nonce(&address)?; state_adapter.set_nonce(&address, nonce + 1)?; // Code operations let code = state_adapter.get_code(&contract_address)?; state_adapter.set_code(&contract_address, bytecode)?; // Storage operations (SLOAD/SSTORE) let storage_key = [0u8; 32]; let value = state_adapter.get_storage(&contract_address, &storage_key)?; state_adapter.set_storage(&contract_address, &storage_key, new_value)?; // Batch state changes state_adapter.begin_transaction(); // ... multiple state operations ... state_adapter.commit(); // Atomically apply all changes // Rollback on error if execution_failed { state_adapter.rollback(); }

Error Handling

The executor distinguishes between different failure modes: out-of-gas, revert (controlled failure), invalid opcode, stack overflow, and state access errors. Each error type is captured in the execution result for proper handling.

use tenzro_vm::evm::{ExecutionStatus, ExecutionResult}; match executor.execute_transaction(&tx).await { Ok(result) => { match result.status { ExecutionStatus::Success => { println!("Transaction succeeded"); println!("Output: {}", hex::encode(&result.output)); } ExecutionStatus::Revert => { println!("Transaction reverted"); // Decode revert reason if result.output.len() >= 68 { let reason = String::from_utf8_lossy(&result.output[68..]); println!("Reason: {}", reason); } } ExecutionStatus::OutOfGas => { println!("Transaction ran out of gas"); println!("Gas limit: {}", tx.gas_limit); } ExecutionStatus::InvalidOpcode => { println!("Invalid opcode encountered"); } ExecutionStatus::StackOverflow => { println!("Stack overflow (depth > 1024)"); } } // Check logs even on revert for log in result.logs { println!("Event: {:?}", log); } } Err(e) => { eprintln!("Execution error: {}", e); } }

Configuration

use tenzro_vm::evm::{EvmExecutor, EvmConfig}; use revm::EvmVersion; let config = EvmConfig { evm_version: EvmVersion::London, // EIP-1559 support enable_precompiles: true, max_code_size: 24_576, // 24 KB max_call_depth: 1_024, enable_gas_refunds: true, allow_contract_creation: true, }; let executor = EvmExecutor::with_config(config, state_adapter); // Configure gas settings executor.set_max_gas_limit(30_000_000); executor.set_default_gas_limit(10_000_000); executor.set_min_gas_price(1_000_000_000); // 1 Gwei

EVM Version Support

VersionNotable ChangesSupport
HomesteadDELEGATECALL opcodeFull
ByzantiumREVERT, STATICCALL, precompilesFull
ConstantinopleCREATE2, bitwise shiftsFull
IstanbulGas cost changes, BLAKE2 precompileFull
BerlinAccess lists, gas cost changesFull
LondonEIP-1559 base fee, BASEFEE opcodeFull (default)

Testing

#[cfg(test)] mod tests { use super::*; #[tokio::test] async fn test_simple_transfer() { let state = InMemoryStateAdapter::new(); let executor = EvmExecutor::new(state.clone()); // Fund sender state.set_balance(&sender, parse_ether("10")?)?; let tx = Transaction { from: sender, to: Some(recipient), value: parse_ether("1")?, gas_limit: 21_000, ..Default::default() }; let result = executor.execute_transaction(&tx).await?; assert_eq!(result.status, ExecutionStatus::Success); assert_eq!(result.gas_used, 21_000); let sender_balance = state.get_balance(&sender)?; let recipient_balance = state.get_balance(&recipient)?; assert_eq!(sender_balance, parse_ether("9")?); assert_eq!(recipient_balance, parse_ether("1")?); } #[tokio::test] async fn test_contract_deployment() { // Test contract deployment and initialization } #[tokio::test] async fn test_revert_handling() { // Test transaction revert with reason string } }

Production Readiness

Production-Ready:

  • Full revm integration for bytecode execution
  • Accurate gas metering and consumption tracking
  • Log extraction and event parsing
  • Keccak256-based CREATE address derivation
  • Support for EVM versions up to London fork
  • Error handling for all failure modes

Outstanding Issues:

  • Most standard precompiles return empty results (issue #23)
  • No gas refunds for SSTORE clearing (issue #81)
  • No reentrancy protection (issue #83)
  • State adapter does not persist to storage (issue #25)