synor/crates/synor-vm/src/gas_estimator.rs
2026-01-08 05:22:24 +05:30

368 lines
11 KiB
Rust

//! 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<String>,
/// Return data from the call.
pub return_data: Vec<u8>,
}
impl GasEstimate {
/// Creates a successful estimate.
pub fn success(gas_used: u64, return_data: Vec<u8>) -> 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<Self, VmError> {
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<Self, VmError> {
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::<MemoryStorage>::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::<MemoryStorage>::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::<MemoryStorage>::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<u8>) -> Result<ContractModule, VmError> {
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);
}
}