Minichain uses a gas system similar to Ethereum to prevent infinite loops and abuse. Every operation consumes gas, and transactions must specify a gas limit and gas price.
What is Gas?
Gas is a measure of computational work required to execute operations. It serves several purposes:
Prevent Infinite Loops Programs must halt when gas runs out, preventing endless execution.
Fair Resource Allocation Users pay for the resources they consume, preventing spam.
Incentivize Efficiency Developers optimize code to minimize gas costs.
Prioritize Transactions Higher gas prices get faster inclusion in blocks.
Gas Costs
Cost Tiers
Minichain defines five gas cost tiers:
pub struct GasCosts ;
impl GasCosts {
// Tier 1: Very cheap (simple register operations)
pub const ZERO : u64 = 0 ; // HALT, NOP
pub const BASE : u64 = 2 ; // ADD, SUB, AND, OR, MOV
// Tier 2: Cheap (more complex ALU operations)
pub const LOW : u64 = 3 ; // MUL, comparison ops
// Tier 3: Medium (division, shifts)
pub const MID : u64 = 5 ; // DIV, MOD, SHL, SHR
// Tier 4: Memory operations
pub const MEMORY_READ : u64 = 3 ;
pub const MEMORY_WRITE : u64 = 3 ;
pub const MEMORY_GROW_PER_BYTE : u64 = 1 ;
// Tier 5: Storage (expensive!)
pub const SLOAD : u64 = 100 ; // Read from storage
pub const SSTORE_SET : u64 = 20000 ; // Write to empty slot
pub const SSTORE_RESET : u64 = 5000 ; // Overwrite existing slot
// Control flow
pub const JUMP : u64 = 8 ;
pub const CALL : u64 = 700 ;
}
Complete Gas Table
Operation Category Opcode Gas Cost Notes Control Flow Halt HALT0 Stops execution No-op NOP0 Does nothing Jump JUMP8 Unconditional jump Conditional Jump JUMPI8 Jump if condition Call CALL700 Call another contract Return RET8 Return from call Revert REVERT0 Revert transaction Arithmetic Add/Sub ADD, SUB2 Basic arithmetic Multiply MUL3 Multiplication Divide DIV5 Division (more expensive) Modulo MOD5 Remainder operation Add Immediate ADDI2 Add constant Bitwise And/Or/Xor AND, OR, XOR2 Bitwise operations Not NOT2 Bitwise negation Shift Left/Right SHL, SHR5 Bit shifting Comparison Equal/Not Equal EQ, NE3 Equality checks Less/Greater Than LT, GT3 Comparisons Less/Greater Equal LE, GE3 Inclusive comparisons Is Zero ISZERO3 Check if zero Memory (RAM) Load 8-bit LOAD83 Read byte from memory Load 64-bit LOAD643 Read word from memory Store 8-bit STORE83 Write byte to memory Store 64-bit STORE643 Write word to memory Memory Size MSIZE2 Get memory size Memory Copy MCOPY3 + size Copy memory region Storage (Disk) Storage Load SLOAD100 Read from persistent storage Storage Store (new) SSTORE20,000 Write to empty slot Storage Store (update) SSTORE5,000 Overwrite existing slot Immediate/Move Load Immediate LOADI2 Load constant into register Move Register MOV2 Copy between registers Context Get Caller CALLER2 Get caller address Get Call Value CALLVALUE2 Get sent value Get Address ADDRESS2 Get contract address Get Block Number BLOCKNUMBER2 Get current block Get Timestamp TIMESTAMP2 Get block timestamp Get Gas GAS2 Get remaining gas Debug Log LOG8 Emit log event
Storage operations (SLOAD/SSTORE) are 100x more expensive than memory operations! Use storage sparingly and prefer keeping data in memory during execution.
Gas Metering
Gas Meter
The GasMeter tracks gas consumption during execution:
pub struct GasMeter {
remaining : u64 ,
used : u64 ,
}
impl GasMeter {
pub fn new ( limit : u64 ) -> Self {
Self {
remaining : limit ,
used : 0 ,
}
}
/// Consume gas, returning error if insufficient.
pub fn consume ( & mut self , amount : u64 ) -> Result <(), VmError > {
if self . remaining < amount {
return Err ( VmError :: OutOfGas {
required : amount ,
remaining : self . remaining,
});
}
self . remaining -= amount ;
self . used += amount ;
Ok (())
}
pub fn remaining ( & self ) -> u64 {
self . remaining
}
pub fn used ( & self ) -> u64 {
self . used
}
}
Gas Consumption Flow
Every opcode execution follows this pattern:
// 1. Decode opcode
let opcode = Opcode :: from_byte ( bytecode [ pc ]) ? ;
// 2. Consume gas BEFORE execution
let cost = match opcode {
Opcode :: ADD => GasCosts :: BASE ,
Opcode :: MUL => GasCosts :: LOW ,
Opcode :: DIV => GasCosts :: MID ,
Opcode :: SLOAD => GasCosts :: SLOAD ,
Opcode :: SSTORE => {
// Dynamic cost based on storage state
if storage_slot_is_empty {
GasCosts :: SSTORE_SET
} else {
GasCosts :: SSTORE_RESET
}
}
_ => GasCosts :: BASE ,
};
gas_meter . consume ( cost ) ? ;
// 3. Execute operation
execute_opcode ( opcode ) ? ;
// 4. Move to next instruction
pc += opcode . instruction_size ();
Gas is consumed before execution, not after. This prevents operations from executing if they would run out of gas mid-execution.
Out of Gas Error
When gas runs out, execution halts immediately:
#[derive( Error , Debug , Clone , PartialEq , Eq )]
pub enum VmError {
#[error( "Out of gas: required {required}, remaining {remaining}" )]
OutOfGas { required : u64 , remaining : u64 },
// ... other errors
}
// Example:
// Trying to execute SLOAD (cost: 100) with only 50 gas remaining
// Error: Out of gas: required 100, remaining 50
Transaction Gas
Gas Limit and Gas Price
Transactions specify two gas-related fields:
pub struct Transaction {
// ... other fields
/// Maximum gas to use.
pub gas_limit : u64 ,
/// Price per unit of gas.
pub gas_price : u64 ,
// ...
}
Gas Limit:
Maximum gas the transaction can consume
Set by sender based on estimated execution cost
Unused gas is refunded
Minimum: 21,000 for simple transfers
Gas Price:
Amount sender pays per unit of gas
Higher gas price = faster inclusion in blocks
Total cost = gas_used * gas_price
Transaction Cost Calculation
impl Transaction {
/// Calculate the maximum cost of this transaction.
pub fn max_cost ( & self ) -> u64 {
self . value
. saturating_add ( self . gas_limit . saturating_mul ( self . gas_price))
}
}
// Example:
// value = 1000
// gas_limit = 50000
// gas_price = 2
// max_cost = 1000 + (50000 * 2) = 101,000
Pre-Execution Balance Check:
// Sender must have enough for value + max gas cost
let max_cost = tx . max_cost ();
if account . balance < max_cost {
return Err ( ValidationError :: InsufficientBalance {
required : max_cost ,
available : account . balance,
});
}
Gas Refund Mechanism
// 1. Deduct max gas cost upfront
let max_gas_cost = tx . gas_limit * tx . gas_price;
account . debit ( tx . value + max_gas_cost ) ? ;
// 2. Execute transaction
let result = vm . execute ( bytecode , tx . gas_limit) ? ;
// 3. Refund unused gas
let gas_used = result . gas_used;
let gas_refund = ( tx . gas_limit - gas_used ) * tx . gas_price;
account . credit ( gas_refund );
// 4. Charge only for gas actually used
let actual_cost = tx . value + ( gas_used * tx . gas_price);
Unused gas is always refunded! If you set gas_limit = 100,000 but only use 50,000 gas, you only pay for 50,000.
Base Transaction Costs
Transfer Cost
Simple value transfers have a fixed cost:
// Base cost for a transfer transaction
const BASE_TX_GAS : u64 = 21_000 ;
let tx = Transaction :: transfer (
from ,
to ,
amount ,
nonce ,
gas_price ,
);
assert_eq! ( tx . gas_limit, 21_000 );
Deployment Cost
Contract deployments pay per byte of bytecode:
const DEPLOYMENT_GAS_PER_BYTE : u64 = 200 ;
// Calculate deployment cost
let bytecode_size = bytecode . len () as u64 ;
let deployment_gas = bytecode_size * DEPLOYMENT_GAS_PER_BYTE ;
let total_gas = BASE_TX_GAS + deployment_gas ;
// Example: 100-byte contract
// total_gas = 21,000 + (100 * 200) = 41,000
Contract Call Cost
Contract calls include base cost plus execution:
const BASE_CALL_GAS : u64 = 21_000 ;
// Total cost = base + execution
let total_gas = BASE_CALL_GAS + execution_gas ;
Memory Expansion Cost
Growing memory incurs additional gas:
impl GasMeter {
/// Calculate gas for memory expansion.
pub fn memory_expansion_cost ( current_size : usize , new_size : usize ) -> u64 {
if new_size <= current_size {
return 0 ;
}
let expansion = ( new_size - current_size ) as u64 ;
expansion * GasCosts :: MEMORY_GROW_PER_BYTE
}
}
// Example: Growing memory from 1024 to 2048 bytes
let expansion_cost = GasMeter :: memory_expansion_cost ( 1024 , 2048 );
// expansion_cost = (2048 - 1024) * 1 = 1024 gas
Storage vs Memory Cost Comparison
Let’s compare the costs of storage vs memory operations:
Storage Example
Memory Example
; Write to storage (expensive)
LOADI R0, 42
LOADI R1, 0
SSTORE R1, R0 ; Cost: 20,000 gas (new slot)
; Read from storage
LOADI R1, 0
SLOAD R2, R1 ; Cost: 100 gas
; Total: 20,100 gas
Cost Comparison:
Operation Storage Memory Ratio Write (new) 20,000 3 6,667x Write (update) 5,000 3 1,667x Read 100 3 33x
Storage is 1,667x to 6,667x more expensive than memory! Only use storage for data that must persist between transactions.
Real-World Examples
Example 1: Counter Contract
Let’s analyze the gas cost of a simple counter:
.entry main
main:
LOADI R0, 0 ; Load slot 0 - 2 gas
SLOAD R1, R0 ; Read counter - 100 gas
LOADI R2, 1 ; Load constant 1 - 2 gas
ADD R1, R1, R2 ; Increment - 2 gas
SSTORE R0, R1 ; Save counter - 5,000 gas (update)
HALT ; Stop - 0 gas
; Total execution: ~5,106 gas
; Plus base transaction cost: 21,000 gas
; Total: ~26,106 gas
Example 2: Complex Calculation
Compare computing in memory vs storage:
In Memory (Efficient)
In Storage (Inefficient)
; Calculate: (a + b) * (c - d)
; All data in registers/memory
LOADI R0, 10 ; a = 10 - 2 gas
LOADI R1, 20 ; b = 20 - 2 gas
LOADI R2, 5 ; c = 5 - 2 gas
LOADI R3, 3 ; d = 3 - 2 gas
ADD R4, R0, R1 ; R4 = a + b - 2 gas
SUB R5, R2, R3 ; R5 = c - d - 2 gas
MUL R6, R4, R5 ; R6 = result - 3 gas
HALT ; Stop - 0 gas
; Total: 15 gas
; Same calculation but using storage
LOADI R0, 0 ; slot 0 - 2 gas
LOADI R1, 10
SSTORE R0, R1 ; store a - 20,000 gas
LOADI R0, 1
LOADI R1, 20
SSTORE R0, R1 ; store b - 20,000 gas
LOADI R0, 0
SLOAD R1, R0 ; load a - 100 gas
LOADI R0, 1
SLOAD R2, R0 ; load b - 100 gas
ADD R3, R1, R2 ; add - 2 gas
; ... continue pattern
; Total: ~60,000+ gas (4000x more expensive!)
Example 3: Gas Limit Selection
// Estimate gas for contract call
let estimated_gas = 30_000 ;
// Add 20% buffer for safety
let gas_limit = ( estimated_gas as f64 * 1.2 ) as u64 ;
// Create transaction
let tx = Transaction :: call (
from ,
contract ,
calldata ,
0 ,
nonce ,
gas_limit , // 36,000
gas_price ,
);
// If actual usage is 28,000:
// - Gas used: 28,000
// - Gas refunded: 36,000 - 28,000 = 8,000
// - Final cost: 28,000 * gas_price
Gas Optimization Strategies
1. Minimize Storage Operations
Bad: ; Reading storage multiple times
LOADI R0, 0
SLOAD R1, R0 ; 100 gas
; ... use R1
SLOAD R1, R0 ; 100 gas (redundant!)
Good: ; Read once, reuse register
LOADI R0, 0
SLOAD R1, R0 ; 100 gas
; ... use R1 multiple times
2. Use Registers for Temporary Data
Bad: ; Using storage as scratch space
LOADI R0, 0
LOADI R1, 42
SSTORE R0, R1 ; 20,000 gas (wasteful!)
SLOAD R2, R0 ; 100 gas
Good: ; Use registers for temporary values
LOADI R1, 42 ; 2 gas
MOV R2, R1 ; 2 gas
Bad: ; Multiple small storage writes
SSTORE R0, R1 ; 5,000 gas
; ... some computation
SSTORE R0, R2 ; 5,000 gas
; ... more computation
SSTORE R0, R3 ; 5,000 gas
; Total: 15,000 gas
Good: ; Compute final value, write once
; ... all computation in registers
SSTORE R0, R3 ; 5,000 gas
; Total: 5,000 gas
Bad: ; Inefficient: x / 2 using DIV
LOADI R1, 2
DIV R0, R0, R1 ; 5 gas
Good: ; Efficient: x / 2 using shift
LOADI R1, 1
SHR R0, R0, R1 ; 5 gas (same cost, but more idiomatic)
5. Exit Early on Conditions
Bad: ; Always do all work
; ... expensive computation (1000 gas)
; ... check condition
JUMPI end ; Waste if condition false
Good: ; Check condition first
; ... check condition
JUMPI end ; Exit early
; ... expensive computation (only if needed)
end:
Gas Profiling
Minichain provides execution traces to analyze gas usage:
// Enable tracing
let mut vm = Vm :: new_with_context (
bytecode ,
gas_limit ,
caller ,
address ,
value ,
block_number ,
timestamp ,
);
let tracer = Tracer :: new ();
vm . set_tracer ( Some ( tracer ));
// Execute
let result = vm . execute () ? ;
// Analyze trace
let trace = vm . get_tracer () . unwrap ();
for step in trace . steps () {
println! (
"PC: {:4} | Opcode: {:?} | Gas: {} | Remaining: {}" ,
step . pc,
step . opcode,
step . gas_cost,
step . gas_remaining,
);
}
println! ( "Total gas used: {}" , result . gas_used);
Best Practices
Set appropriate gas limits
Add 20-30% buffer to estimated gas for safety.
Optimize storage access
Minimize SLOAD/SSTORE operations. Use registers and memory.
Profile before deploying
Use tracing to identify gas hotspots.
Consider gas price
Higher gas price = faster inclusion, but higher cost.
Handle out-of-gas gracefully
Check gas remaining before expensive operations.
Next Steps
Virtual Machine Learn how the VM executes opcodes
Writing Assembly Write gas-efficient smart contracts
Opcode Reference Complete opcode and gas cost reference
Transaction Types Understand transaction gas requirements