use anyhow::Result;
use serde::{Deserialize, Serialize};
use crate::block::Block;
use crate::evm_backend::{self, EvmExecutor};
use crate::precompiles::PrecompileRegistry;
use crate::state::StateDb;
pub trait VmEngine {
fn initialize(&mut self, genesis: &[u8]) -> Result<()>;
fn build_block(&mut self, txs: Vec<Vec<u8>>) -> Result<Block>;
fn parse_block(&self, bytes: &[u8]) -> Result<Block>;
fn set_preference(&mut self, block_id: [u8; 32]) -> Result<()>;
fn accept(&mut self, block_id: [u8; 32]) -> Result<()>;
fn reject(&mut self, block_id: [u8; 32]) -> Result<()>;
fn last_accepted(&self) -> Result<[u8; 32]>;
fn health_check(&self) -> Result<HealthStatus>;
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct VmConfig {
pub chain_id: u64,
pub data_dir: String,
pub rpc_port: u16,
pub block_gas_limit: u64,
pub target_block_time_ms: u64,
#[serde(default)]
pub evm_backend: Option<evm_backend::EvmBackend>,
}
impl Default for VmConfig {
fn default() -> Self {
Self {
chain_id: 1313161554,
data_dir: "/tmp/hanzo-vm".into(),
rpc_port: 9650,
block_gas_limit: 30_000_000,
target_block_time_ms: 2_000,
evm_backend: None,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct HealthStatus {
pub healthy: bool,
pub reason: Option<String>,
pub last_accepted_height: u64,
}
pub struct HanzoVm {
pub state: StateDb,
pub precompiles: PrecompileRegistry,
pub executor: Box<dyn EvmExecutor>,
pub config: VmConfig,
preferred: [u8; 32],
last_accepted_id: [u8; 32],
last_accepted_height: u64,
initialized: bool,
}
impl HanzoVm {
pub fn new(config: VmConfig) -> Self {
let state = StateDb::new(&config.data_dir);
let precompiles = PrecompileRegistry::default();
let executor: Box<dyn EvmExecutor> = match config.evm_backend {
None => evm_backend::auto_detect(),
Some(evm_backend::EvmBackend::Revm) => Box::new(evm_backend::RevmExecutor),
Some(evm_backend::EvmBackend::Cevm) => Box::new(evm_backend::CevmExecutor::auto()),
Some(evm_backend::EvmBackend::GoEvm) => {
let home = std::env::var("HOME").unwrap_or_default();
let binary = format!("{home}/work/luxcpp/evm/build/bin/cevm");
Box::new(evm_backend::GoEvmExecutor::new(binary))
}
};
log::info!(
"HanzoVm: using EVM backend '{}' (gpu={})",
executor.name(),
executor.gpu_capable(),
);
Self {
state,
precompiles,
executor,
config,
preferred: [0u8; 32],
last_accepted_id: [0u8; 32],
last_accepted_height: 0,
initialized: false,
}
}
pub fn with_executor(config: VmConfig, executor: Box<dyn EvmExecutor>) -> Self {
let state = StateDb::new(&config.data_dir);
let precompiles = PrecompileRegistry::default();
Self {
state,
precompiles,
executor,
config,
preferred: [0u8; 32],
last_accepted_id: [0u8; 32],
last_accepted_height: 0,
initialized: false,
}
}
}
impl VmEngine for HanzoVm {
fn initialize(&mut self, genesis: &[u8]) -> Result<()> {
if self.initialized {
anyhow::bail!("VM already initialized");
}
let genesis_state: GenesisState = serde_json::from_slice(genesis)
.map_err(|e| anyhow::anyhow!("invalid genesis payload: {e}"))?;
self.state.init()?;
for alloc in &genesis_state.alloc {
let account = crate::state::Account {
nonce: alloc.nonce,
balance: alloc.balance,
code_hash: [0u8; 32],
storage_root: [0u8; 32],
};
self.state.set_account(&alloc.address, &account)?;
}
let genesis_block = Block::genesis(genesis_state.chain_id, genesis_state.timestamp);
let block_id = genesis_block.id();
self.last_accepted_id = block_id;
self.last_accepted_height = 0;
self.preferred = block_id;
self.config.chain_id = genesis_state.chain_id;
self.initialized = true;
log::info!(
"VM initialized: chain_id={}, genesis_block={}",
genesis_state.chain_id,
hex::encode(block_id)
);
Ok(())
}
fn build_block(&mut self, txs: Vec<Vec<u8>>) -> Result<Block> {
if !self.initialized {
anyhow::bail!("VM not initialized");
}
let mut transactions: Vec<crate::block::Transaction> = txs
.into_iter()
.enumerate()
.map(|(i, raw)| crate::block::Transaction {
raw_bytes: raw,
tx_index: i as u32,
gas_used: 0,
})
.collect();
let result = self.executor.execute_block(&transactions, &mut self.state)?;
for (tx, &gas) in transactions.iter_mut().zip(result.gas_used.iter()) {
tx.gas_used = gas;
}
let height = self.last_accepted_height + 1;
let timestamp = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap_or_default()
.as_secs();
let block = Block::new(
self.last_accepted_id,
height,
timestamp,
transactions,
self.state.root(),
);
log::debug!(
"built block height={height} id={} backend={} gas={} time={:.2}ms",
hex::encode(block.id()),
self.executor.name(),
result.total_gas,
result.exec_time_ms,
);
Ok(block)
}
fn parse_block(&self, bytes: &[u8]) -> Result<Block> {
let block: Block = serde_json::from_slice(bytes)
.map_err(|e| anyhow::anyhow!("failed to parse block: {e}"))?;
Ok(block)
}
fn set_preference(&mut self, block_id: [u8; 32]) -> Result<()> {
self.preferred = block_id;
log::debug!("preference set to {}", hex::encode(block_id));
Ok(())
}
fn accept(&mut self, block_id: [u8; 32]) -> Result<()> {
if !self.initialized {
anyhow::bail!("VM not initialized");
}
self.last_accepted_id = block_id;
self.last_accepted_height += 1;
log::info!(
"accepted block height={} id={}",
self.last_accepted_height,
hex::encode(block_id)
);
Ok(())
}
fn reject(&mut self, block_id: [u8; 32]) -> Result<()> {
log::info!("rejected block id={}", hex::encode(block_id));
Ok(())
}
fn last_accepted(&self) -> Result<[u8; 32]> {
Ok(self.last_accepted_id)
}
fn health_check(&self) -> Result<HealthStatus> {
if !self.initialized {
return Ok(HealthStatus {
healthy: false,
reason: Some("VM not yet initialized".into()),
last_accepted_height: 0,
});
}
if let Err(e) = self.state.ping() {
return Ok(HealthStatus {
healthy: false,
reason: Some(format!("state DB unreachable: {e}")),
last_accepted_height: self.last_accepted_height,
});
}
Ok(HealthStatus {
healthy: true,
reason: None,
last_accepted_height: self.last_accepted_height,
})
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
struct GenesisAlloc {
address: String,
balance: u128,
#[serde(default)]
nonce: u64,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
struct GenesisState {
chain_id: u64,
timestamp: u64,
#[serde(default)]
alloc: Vec<GenesisAlloc>,
}
#[cfg(test)]
mod tests {
use super::*;
fn test_genesis() -> Vec<u8> {
serde_json::to_vec(&serde_json::json!({
"chain_id": 31337,
"timestamp": 1700000000,
"alloc": [
{
"address": "0x0000000000000000000000000000000000000001",
"balance": 1_000_000_000_000_000_000u128,
"nonce": 0
}
]
}))
.unwrap()
}
#[test]
fn initialize_and_health_check() {
let dir = tempfile::tempdir().unwrap();
let config = VmConfig {
data_dir: dir.path().to_string_lossy().into_owned(),
..VmConfig::default()
};
let mut vm = HanzoVm::new(config);
vm.initialize(&test_genesis()).unwrap();
let status = vm.health_check().unwrap();
assert!(status.healthy);
assert_eq!(status.last_accepted_height, 0);
}
#[test]
fn double_initialize_fails() {
let dir = tempfile::tempdir().unwrap();
let config = VmConfig {
data_dir: dir.path().to_string_lossy().into_owned(),
..VmConfig::default()
};
let mut vm = HanzoVm::new(config);
vm.initialize(&test_genesis()).unwrap();
assert!(vm.initialize(&test_genesis()).is_err());
}
#[test]
fn build_and_accept_block() {
let dir = tempfile::tempdir().unwrap();
let config = VmConfig {
data_dir: dir.path().to_string_lossy().into_owned(),
..VmConfig::default()
};
let mut vm = HanzoVm::new(config);
vm.initialize(&test_genesis()).unwrap();
let block = vm.build_block(vec![vec![0xde, 0xad]]).unwrap();
let block_id = block.id();
assert_eq!(block.header.height, 1);
vm.accept(block_id).unwrap();
assert_eq!(vm.last_accepted().unwrap(), block_id);
assert_eq!(vm.health_check().unwrap().last_accepted_height, 1);
}
#[test]
fn parse_round_trip() {
let dir = tempfile::tempdir().unwrap();
let config = VmConfig {
data_dir: dir.path().to_string_lossy().into_owned(),
..VmConfig::default()
};
let mut vm = HanzoVm::new(config);
vm.initialize(&test_genesis()).unwrap();
let block = vm.build_block(vec![]).unwrap();
let bytes = serde_json::to_vec(&block).unwrap();
let parsed = vm.parse_block(&bytes).unwrap();
assert_eq!(block.id(), parsed.id());
assert_eq!(block.header.height, parsed.header.height);
}
}