//! Gas estimation for smart contract execution. //! //! Provides tools to estimate gas usage before executing transactions, //! helping users set appropriate gas limits. use crate::context::{BlockInfo, CallContext, ExecutionContext, TransactionInfo}; use crate::engine::{ContractModule, VmEngine}; use crate::storage::MemoryStorage; use crate::VmError; use synor_types::Address; /// Gas estimation result. #[derive(Clone, Debug)] pub struct GasEstimate { /// Estimated gas usage. pub gas_used: u64, /// Gas used with safety margin (1.2x). pub gas_limit_recommended: u64, /// Whether the execution succeeded. pub success: bool, /// Error message if failed. pub error: Option, /// Return data from the call. pub return_data: Vec, } impl GasEstimate { /// Creates a successful estimate. pub fn success(gas_used: u64, return_data: Vec) -> Self { let recommended = (gas_used as f64 * 1.2).ceil() as u64; GasEstimate { gas_used, gas_limit_recommended: recommended, success: true, error: None, return_data, } } /// Creates a failed estimate. pub fn failure(gas_used: u64, error: String) -> Self { GasEstimate { gas_used, gas_limit_recommended: 0, success: false, error: Some(error), return_data: Vec::new(), } } } /// Gas estimator for contract calls. pub struct GasEstimator { /// VM engine. engine: VmEngine, /// Maximum gas for estimation. max_gas: u64, } impl GasEstimator { /// Creates a new gas estimator. pub fn new() -> Result { Ok(GasEstimator { engine: VmEngine::new()?, max_gas: 10_000_000, // 10M gas limit for estimation }) } /// Creates with custom max gas. pub fn with_max_gas(max_gas: u64) -> Result { Ok(GasEstimator { engine: VmEngine::new()?, max_gas, }) } /// Estimates gas for deploying a contract. pub fn estimate_deploy( &self, bytecode: &[u8], init_params: &[u8], deployer: Address, ) -> GasEstimate { // Compile the contract let module = match self.engine.compile(bytecode.to_vec()) { Ok(m) => m, Err(e) => return GasEstimate::failure(0, e.to_string()), }; // Create execution context let contract_id = module.id; let call = CallContext::new(contract_id, deployer.clone(), 0, init_params.to_vec()); let context = ExecutionContext::::new( BlockInfo::default(), TransactionInfo::default(), call, self.max_gas, MemoryStorage::new(), 1, // chain_id ); // Execute init match self .engine .execute(&module, "__synor_init", init_params, context, self.max_gas) { Ok(result) => GasEstimate::success(result.gas_used, result.return_data), Err(e) => { // Try to extract gas used from error let gas = match &e { VmError::OutOfGas { used, .. } => *used, _ => 0, }; GasEstimate::failure(gas, e.to_string()) } } } /// Estimates gas for calling a contract method. pub fn estimate_call( &self, module: &ContractModule, method: &str, params: &[u8], caller: Address, value: u64, storage: MemoryStorage, ) -> GasEstimate { // Build call data: 4-byte selector + params let selector = synor_sdk_method_selector(method); let mut call_data = Vec::with_capacity(4 + params.len()); call_data.extend_from_slice(&selector); call_data.extend_from_slice(params); // Create execution context let call = CallContext::new(module.id, caller.clone(), value, call_data.clone()); let context = ExecutionContext::::new( BlockInfo::default(), TransactionInfo::default(), call, self.max_gas, storage, 1, // chain_id ); // Execute call match self .engine .execute(module, "__synor_call", &call_data, context, self.max_gas) { Ok(result) => GasEstimate::success(result.gas_used, result.return_data), Err(e) => { let gas = match &e { VmError::OutOfGas { used, .. } => *used, _ => 0, }; GasEstimate::failure(gas, e.to_string()) } } } /// Estimates gas using binary search for more accuracy. /// /// This runs the call multiple times with different gas limits /// to find the minimum required gas. pub fn estimate_call_precise( &self, module: &ContractModule, method: &str, params: &[u8], caller: Address, value: u64, storage: MemoryStorage, ) -> GasEstimate { // First, try with max gas to see if it succeeds let initial = self.estimate_call( module, method, params, caller.clone(), value, storage.clone(), ); if !initial.success { return initial; } let gas_used = initial.gas_used; // Binary search between gas_used and gas_used * 2 to find minimum let mut low = gas_used; let mut high = gas_used.saturating_mul(2).min(self.max_gas); while low < high { let mid = (low + high) / 2; let selector = synor_sdk_method_selector(method); let mut call_data = Vec::with_capacity(4 + params.len()); call_data.extend_from_slice(&selector); call_data.extend_from_slice(params); let call = CallContext::new(module.id, caller.clone(), value, call_data.clone()); let context = ExecutionContext::::new( BlockInfo::default(), TransactionInfo::default(), call, mid, storage.clone(), 1, // chain_id ); match self .engine .execute(module, "__synor_call", &call_data, context, mid) { Ok(_) => high = mid, Err(_) => low = mid + 1, } } GasEstimate::success(low, initial.return_data) } /// Gets the underlying VM engine. pub fn engine(&self) -> &VmEngine { &self.engine } /// Compiles a contract module. pub fn compile(&self, bytecode: Vec) -> Result { self.engine.compile(bytecode) } } impl Default for GasEstimator { fn default() -> Self { Self::new().expect("Failed to create gas estimator") } } /// Computes method selector (first 4 bytes of blake3 hash). fn synor_sdk_method_selector(method: &str) -> [u8; 4] { let hash = blake3::hash(method.as_bytes()); let bytes = hash.as_bytes(); [bytes[0], bytes[1], bytes[2], bytes[3]] } /// Gas costs for common operations. pub mod costs { /// Base cost for any transaction. pub const BASE_TX: u64 = 21_000; /// Cost per byte of call data. pub const CALLDATA_BYTE: u64 = 16; /// Cost per zero byte of call data. pub const CALLDATA_ZERO_BYTE: u64 = 4; /// Cost for contract deployment base. pub const CREATE_BASE: u64 = 32_000; /// Cost per byte of deployed code. pub const CREATE_BYTE: u64 = 200; /// Cost for storage write (new value). pub const STORAGE_SET: u64 = 20_000; /// Cost for storage write (update). pub const STORAGE_UPDATE: u64 = 5_000; /// Cost for storage read. pub const STORAGE_GET: u64 = 200; /// Cost for storage delete (refund). pub const STORAGE_REFUND: u64 = 15_000; /// Cost for SHA3/Keccak256. pub const SHA3_BASE: u64 = 30; /// Cost per word for SHA3. pub const SHA3_WORD: u64 = 6; /// Cost for Blake3 hash. pub const BLAKE3_BASE: u64 = 20; /// Cost per word for Blake3. pub const BLAKE3_WORD: u64 = 4; /// Cost for Ed25519 signature verification. pub const ED25519_VERIFY: u64 = 3_000; /// Cost for Dilithium signature verification. pub const DILITHIUM_VERIFY: u64 = 5_000; /// Cost for memory expansion per page (64KB). pub const MEMORY_PAGE: u64 = 512; /// Cost for event emission base. pub const LOG_BASE: u64 = 375; /// Cost per topic for event emission. pub const LOG_TOPIC: u64 = 375; /// Cost per byte for event data. pub const LOG_DATA_BYTE: u64 = 8; /// Cost for cross-contract call base. pub const CALL_BASE: u64 = 700; /// Cost for delegatecall base. pub const DELEGATECALL_BASE: u64 = 700; /// Estimates cost for calldata. pub fn calldata_cost(data: &[u8]) -> u64 { let zero_bytes = data.iter().filter(|&&b| b == 0).count() as u64; let non_zero_bytes = data.len() as u64 - zero_bytes; zero_bytes * CALLDATA_ZERO_BYTE + non_zero_bytes * CALLDATA_BYTE } /// Estimates cost for contract deployment. pub fn deploy_cost(bytecode: &[u8]) -> u64 { CREATE_BASE + (bytecode.len() as u64 * CREATE_BYTE) + calldata_cost(bytecode) } } #[cfg(test)] mod tests { use super::*; #[test] fn test_gas_estimate_success() { let estimate = GasEstimate::success(50_000, vec![1, 2, 3]); assert!(estimate.success); assert_eq!(estimate.gas_used, 50_000); assert_eq!(estimate.gas_limit_recommended, 60_000); // 1.2x assert!(estimate.error.is_none()); } #[test] fn test_gas_estimate_failure() { let estimate = GasEstimate::failure(10_000, "out of gas".to_string()); assert!(!estimate.success); assert_eq!(estimate.gas_used, 10_000); assert_eq!(estimate.gas_limit_recommended, 0); assert!(estimate.error.is_some()); } #[test] fn test_calldata_cost() { let data = [0u8, 1, 0, 2, 0, 3]; // 3 zeros, 3 non-zeros let cost = costs::calldata_cost(&data); assert_eq!( cost, 3 * costs::CALLDATA_ZERO_BYTE + 3 * costs::CALLDATA_BYTE ); } #[test] fn test_method_selector() { let sel1 = synor_sdk_method_selector("transfer"); let sel2 = synor_sdk_method_selector("transfer"); let sel3 = synor_sdk_method_selector("mint"); assert_eq!(sel1, sel2); assert_ne!(sel1, sel3); } }