synor/crates/synor-vm/src/gas.rs
Gulshan Yadav 48949ebb3f Initial commit: Synor blockchain monorepo
A complete blockchain implementation featuring:
- synord: Full node with GHOSTDAG consensus
- explorer-web: Modern React blockchain explorer with 3D DAG visualization
- CLI wallet and tools
- Smart contract SDK and example contracts (DEX, NFT, token)
- WASM crypto library for browser/mobile
2026-01-08 05:22:17 +05:30

426 lines
12 KiB
Rust

//! 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<String>,
}
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<GasMeter>,
}
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<Gas> {
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);
}
}