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
| Address | Function | Status |
|---|
0x01 | ecRecover | Stub |
0x02 | SHA-256 | Production |
0x03 | RIPEMD-160 | Stub |
0x04 | Identity | Production |
0x05 | ModExp | Stub |
0x06 | EC_ADD (bn256) | Stub |
0x07 | EC_MUL (bn256) | Stub |
0x08 | EC_PAIRING (bn256) | Stub |
0x09 | BLAKE2F | Stub |
Tenzro Precompiles
| Address | Function | Gas Cost |
|---|
0x0100 | TEE Attestation Verify | 50,000 |
0x0101 | ZK Proof Verify | 100,000 |
0x0102 | Model Inference Request | 200,000 |
0x0103 | Settlement Execute | 75,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
| Version | Notable Changes | Support |
|---|
| Homestead | DELEGATECALL opcode | Full |
| Byzantium | REVERT, STATICCALL, precompiles | Full |
| Constantinople | CREATE2, bitwise shifts | Full |
| Istanbul | Gas cost changes, BLAKE2 precompile | Full |
| Berlin | Access lists, gas cost changes | Full |
| London | EIP-1559 base fee, BASEFEE opcode | Full (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)