368 lines
11 KiB
Rust
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);
|
|
}
|
|
}
|