//! Gas metering for contract execution. //! //! Gas provides predictable execution costs and prevents infinite loops. //! Each operation has an associated gas cost. use std::sync::atomic::{AtomicU64, Ordering}; /// Gas unit type. pub type Gas = u64; /// Gas configuration for different operations. #[derive(Clone, Debug)] pub struct GasConfig { /// Base cost per instruction. pub instruction_base: Gas, /// Cost per byte of memory allocation. pub memory_byte: Gas, /// Cost per byte of storage read. pub storage_read_byte: Gas, /// Cost per byte of storage write. pub storage_write_byte: Gas, /// Cost per byte of log data. pub log_byte: Gas, /// Cost per log topic. pub log_topic: Gas, /// Cost for contract call. pub call_base: Gas, /// Cost per byte of call data. pub call_data_byte: Gas, /// Cost for contract creation. pub create_base: Gas, /// Cost per byte of contract code. pub create_byte: Gas, /// Cost for hash operations. pub hash_base: Gas, /// Cost per byte hashed. pub hash_byte: Gas, /// Cost for signature verification. pub signature_verify: Gas, /// Cost for address computation. pub address_compute: Gas, /// Refund for storage deletion. pub storage_delete_refund: Gas, } impl Default for GasConfig { fn default() -> Self { GasConfig { instruction_base: 1, memory_byte: 3, storage_read_byte: 200, storage_write_byte: 5000, log_byte: 8, log_topic: 375, call_base: 700, call_data_byte: 3, create_base: 32000, create_byte: 200, hash_base: 30, hash_byte: 6, signature_verify: 3000, address_compute: 50, storage_delete_refund: 15000, } } } impl GasConfig { /// Creates a config for testing (cheaper operations). pub fn testing() -> Self { GasConfig { instruction_base: 1, memory_byte: 1, storage_read_byte: 10, storage_write_byte: 100, log_byte: 1, log_topic: 10, call_base: 100, call_data_byte: 1, create_base: 1000, create_byte: 10, hash_base: 10, hash_byte: 1, signature_verify: 100, address_compute: 10, storage_delete_refund: 500, } } /// Calculates gas for memory allocation. pub fn memory_gas(&self, bytes: usize) -> Gas { self.memory_byte * bytes as Gas } /// Calculates gas for storage read. pub fn storage_read_gas(&self, bytes: usize) -> Gas { self.storage_read_byte * bytes as Gas } /// Calculates gas for storage write. pub fn storage_write_gas(&self, bytes: usize) -> Gas { self.storage_write_byte * bytes as Gas } /// Calculates gas for hashing. pub fn hash_gas(&self, bytes: usize) -> Gas { self.hash_base + self.hash_byte * bytes as Gas } /// Calculates gas for logging. pub fn log_gas(&self, data_bytes: usize, topics: usize) -> Gas { self.log_byte * data_bytes as Gas + self.log_topic * topics as Gas } /// Calculates gas for contract call. pub fn call_gas(&self, data_bytes: usize) -> Gas { self.call_base + self.call_data_byte * data_bytes as Gas } /// Calculates gas for contract creation. pub fn create_gas(&self, code_bytes: usize) -> Gas { self.create_base + self.create_byte * code_bytes as Gas } } /// Gas meter for tracking consumption. #[derive(Debug)] pub struct GasMeter { /// Gas limit for this execution. limit: Gas, /// Gas consumed so far. consumed: AtomicU64, /// Refund accumulated. refund: AtomicU64, /// Gas configuration. config: GasConfig, } impl GasMeter { /// Creates a new gas meter with the given limit. pub fn new(limit: Gas) -> Self { GasMeter { limit, consumed: AtomicU64::new(0), refund: AtomicU64::new(0), config: GasConfig::default(), } } /// Creates with custom configuration. pub fn with_config(limit: Gas, config: GasConfig) -> Self { GasMeter { limit, consumed: AtomicU64::new(0), refund: AtomicU64::new(0), config, } } /// Returns the gas limit. pub fn limit(&self) -> Gas { self.limit } /// Returns gas consumed so far. pub fn consumed(&self) -> Gas { self.consumed.load(Ordering::Relaxed) } /// Returns remaining gas. pub fn remaining(&self) -> Gas { self.limit.saturating_sub(self.consumed()) } /// Returns accumulated refund. pub fn refund(&self) -> Gas { self.refund.load(Ordering::Relaxed) } /// Consumes gas, returning error if out of gas. pub fn consume(&self, amount: Gas) -> Result<(), GasError> { let current = self.consumed.fetch_add(amount, Ordering::Relaxed); let new_total = current + amount; if new_total > self.limit { // Rollback self.consumed.fetch_sub(amount, Ordering::Relaxed); return Err(GasError::OutOfGas { required: amount, remaining: self.limit.saturating_sub(current), }); } Ok(()) } /// Consumes gas without checking limit (for internal use). pub fn consume_unchecked(&self, amount: Gas) { self.consumed.fetch_add(amount, Ordering::Relaxed); } /// Adds a refund. pub fn add_refund(&self, amount: Gas) { self.refund.fetch_add(amount, Ordering::Relaxed); } /// Checks if there's enough gas without consuming. pub fn has_gas(&self, amount: Gas) -> bool { self.remaining() >= amount } /// Charges for memory allocation. pub fn charge_memory(&self, bytes: usize) -> Result<(), GasError> { self.consume(self.config.memory_gas(bytes)) } /// Charges for storage read. pub fn charge_storage_read(&self, bytes: usize) -> Result<(), GasError> { self.consume(self.config.storage_read_gas(bytes)) } /// Charges for storage write. pub fn charge_storage_write(&self, bytes: usize) -> Result<(), GasError> { self.consume(self.config.storage_write_gas(bytes)) } /// Charges for hashing. pub fn charge_hash(&self, bytes: usize) -> Result<(), GasError> { self.consume(self.config.hash_gas(bytes)) } /// Charges for logging. pub fn charge_log(&self, data_bytes: usize, topics: usize) -> Result<(), GasError> { self.consume(self.config.log_gas(data_bytes, topics)) } /// Charges for contract call. pub fn charge_call(&self, data_bytes: usize) -> Result<(), GasError> { self.consume(self.config.call_gas(data_bytes)) } /// Charges for contract creation. pub fn charge_create(&self, code_bytes: usize) -> Result<(), GasError> { self.consume(self.config.create_gas(code_bytes)) } /// Charges for signature verification. pub fn charge_signature(&self) -> Result<(), GasError> { self.consume(self.config.signature_verify) } /// Applies refunds and returns final gas used. pub fn finalize(&self) -> Gas { let consumed = self.consumed(); let refund = self.refund(); // Cap refund at 50% of consumed let max_refund = consumed / 2; let actual_refund = std::cmp::min(refund, max_refund); consumed.saturating_sub(actual_refund) } /// Returns gas configuration. pub fn config(&self) -> &GasConfig { &self.config } } /// Gas-related errors. #[derive(Debug, Clone, thiserror::Error)] pub enum GasError { /// Out of gas. #[error("Out of gas: required {required}, remaining {remaining}")] OutOfGas { required: Gas, remaining: Gas }, } /// Gas estimation result. #[derive(Clone, Debug)] pub struct GasEstimate { /// Estimated gas for successful execution. pub gas_used: Gas, /// Estimated refund. pub refund: Gas, /// Whether estimation succeeded. pub success: bool, /// Error message if failed. pub error: Option, } impl GasEstimate { /// Creates a successful estimate. pub fn success(gas_used: Gas, refund: Gas) -> Self { GasEstimate { gas_used, refund, success: true, error: None, } } /// Creates a failed estimate. pub fn failure(error: String) -> Self { GasEstimate { gas_used: 0, refund: 0, success: false, error: Some(error), } } /// Net gas after refunds. pub fn net_gas(&self) -> Gas { self.gas_used.saturating_sub(self.refund / 2) } } /// Tracks gas across nested calls. #[derive(Debug)] pub struct GasStack { /// Stack of gas meters. meters: Vec, } impl GasStack { /// Creates a new gas stack with initial gas. pub fn new(initial_gas: Gas) -> Self { GasStack { meters: vec![GasMeter::new(initial_gas)], } } /// Pushes a new gas context for a nested call. pub fn push(&mut self, gas_limit: Gas) { self.meters.push(GasMeter::new(gas_limit)); } /// Pops a gas context after a nested call. pub fn pop(&mut self) -> Option { if self.meters.len() > 1 { self.meters.pop().map(|m| m.consumed()) } else { None } } /// Gets the current gas meter. pub fn current(&self) -> &GasMeter { self.meters.last().expect("Gas stack is empty") } /// Consumes gas from current context. pub fn consume(&self, amount: Gas) -> Result<(), GasError> { self.current().consume(amount) } /// Total gas consumed across all contexts. pub fn total_consumed(&self) -> Gas { self.meters.iter().map(|m| m.consumed()).sum() } /// Depth of the call stack. pub fn depth(&self) -> usize { self.meters.len() } } #[cfg(test)] mod tests { use super::*; #[test] fn test_gas_meter() { let meter = GasMeter::new(1000); assert_eq!(meter.limit(), 1000); assert_eq!(meter.consumed(), 0); assert_eq!(meter.remaining(), 1000); assert!(meter.consume(500).is_ok()); assert_eq!(meter.consumed(), 500); assert_eq!(meter.remaining(), 500); assert!(meter.consume(500).is_ok()); assert_eq!(meter.remaining(), 0); // Out of gas assert!(meter.consume(1).is_err()); } #[test] fn test_gas_refund() { let meter = GasMeter::new(1000); meter.consume(600).unwrap(); meter.add_refund(500); // Refund capped at 50% of consumed let final_gas = meter.finalize(); assert_eq!(final_gas, 300); // 600 - min(500, 300) } #[test] fn test_gas_config() { let config = GasConfig::default(); assert!(config.memory_gas(100) > 0); assert!(config.storage_write_gas(32) > config.storage_read_gas(32)); assert!(config.create_gas(1000) > config.call_gas(1000)); } #[test] fn test_gas_stack() { let mut stack = GasStack::new(10000); stack.current().consume(1000).unwrap(); assert_eq!(stack.depth(), 1); stack.push(5000); stack.current().consume(2000).unwrap(); assert_eq!(stack.depth(), 2); let consumed = stack.pop(); assert_eq!(consumed, Some(2000)); assert_eq!(stack.depth(), 1); } }