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
426 lines
12 KiB
Rust
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);
|
|
}
|
|
}
|