https://github.com/evmts/primitives
Unopinionated low level utilities for Ethereum
https://github.com/evmts/primitives
Last synced: 8 months ago
JSON representation
Unopinionated low level utilities for Ethereum
- Host: GitHub
- URL: https://github.com/evmts/primitives
- Owner: evmts
- Created: 2025-10-01T23:21:57.000Z (8 months ago)
- Default Branch: main
- Last Pushed: 2025-10-02T01:02:51.000Z (8 months ago)
- Last Synced: 2025-10-02T01:18:50.260Z (8 months ago)
- Language: Zig
- Size: 94.7 KB
- Stars: 1
- Watchers: 0
- Forks: 0
- Open Issues: 0
-
Metadata Files:
- Readme: README.md
Awesome Lists containing this project
README
# Ethereum Primitives (Zig)
Modern, type-safe Zig library providing fundamental Ethereum primitives and utilities. Zero external dependencies, comptime-optimized, and designed for high-performance EVM implementations.
## Design Philosophy
1. **Type Safety** - Distinct types prevent mixing addresses, hashes, and raw bytes
2. **Comptime Everything** - Maximum use of compile-time computation and validation
3. **Zero Allocations** - Stack-first design; heap only when necessary
4. **Explicit Errors** - Comprehensive error types with clear semantics
5. **EIP Compliance** - Strict adherence to Ethereum specifications
6. **Performance** - Optimized for EVM hot paths (addressing, hashing, RLP)
---
## Core Types
### `Address`
20-byte Ethereum addresses with checksum validation and contract address computation.
```zig
pub const Address = struct {
bytes: [20]u8,
pub const ZERO: Address = .{ .bytes = [_]u8{0} ** 20 };
pub fn fromHex(hex: []const u8) !Address;
pub fn fromBytes(bytes: []const u8) !Address;
pub fn fromPublicKey(x: u256, y: u256) Address;
pub fn fromU256(value: u256) Address;
pub fn isValid(str: []const u8) bool;
pub fn isValidChecksum(str: []const u8) bool;
pub fn toHex(self: Address) [42]u8;
pub fn toChecksum(self: Address) [42]u8;
pub fn toU256(self: Address) u256;
pub fn create(deployer: Address, nonce: u64) Address;
pub fn create2(deployer: Address, salt: [32]u8, init_code_hash: [32]u8) Address;
pub fn isZero(self: Address) bool;
pub fn eql(self: Address, other: Address) bool;
pub fn format(
self: Address,
comptime fmt: []const u8,
options: std.fmt.FormatOptions,
writer: anytype,
) !void;
};
```
#### Usage
```zig
const addr = try Address.fromHex("0x742d35Cc6641C91B6E4bb6ac9e3ff2958c94E676");
const valid = Address.isValid("0x...");
const valid_checksum = Address.isValidChecksum("0x...");
const hex = addr.toChecksum();
const hex_lower = addr.toHex();
const contract = Address.create(deployer, nonce);
const create2_addr = Address.create2(deployer, salt, init_code_hash);
if (addr.isZero()) { ... }
if (addr.eql(other)) { ... }
const value = addr.toU256();
const addr_from_int = Address.fromU256(value);
```
---
### `Hash`
32-byte cryptographic hashes (Keccak256, transaction hashes, storage keys, etc).
```zig
pub const Hash = struct {
bytes: [32]u8,
pub const ZERO: Hash = .{ .bytes = [_]u8{0} ** 32 };
pub const EMPTY_CODE_HASH: Hash = .{ .bytes = .{
0xc5, 0xd2, 0x46, 0x01, 0x86, 0xf7, 0x23, 0x3c,
0x92, 0x7e, 0x7d, 0xb2, 0xdc, 0xc7, 0x03, 0xc0,
0xe5, 0x00, 0xb6, 0x53, 0xca, 0x82, 0x27, 0x3b,
0x7b, 0xfa, 0xd8, 0x04, 0x5d, 0x85, 0xa4, 0x70,
} };
pub const EMPTY_TRIE_ROOT: Hash = .{ .bytes = .{
0x56, 0xe8, 0x1f, 0x17, 0x1b, 0xcc, 0x55, 0xa6,
0xff, 0x83, 0x45, 0xe6, 0x92, 0xc0, 0xf8, 0x6e,
0x5b, 0x48, 0xe0, 0x1b, 0x99, 0x6c, 0xad, 0xc0,
0x01, 0x62, 0x2f, 0xb5, 0xe3, 0x63, 0xb4, 0x21,
} };
pub fn fromHex(hex: []const u8) !Hash;
pub fn fromBytes(bytes: []const u8) !Hash;
pub fn keccak256(data: []const u8) Hash;
pub fn toHex(self: Hash) [66]u8;
pub fn toU256(self: Hash) u256;
pub fn eql(self: Hash, other: Hash) bool;
pub fn format(
self: Hash,
comptime fmt: []const u8,
options: std.fmt.FormatOptions,
writer: anytype,
) !void;
};
```
#### Usage
```zig
const hash = try Hash.fromHex("0x1234...");
const hash_from_data = Hash.keccak256(data);
const hash_from_bytes = Hash.fromBytes(&bytes);
const hex = hash.toHex();
if (hash.eql(other)) { ... }
const value = hash.toU256();
```
---
### `Hex`
Hexadecimal encoding/decoding utilities with validation.
```zig
pub const Hex = struct {
pub fn encode(allocator: Allocator, bytes: []const u8) ![]u8;
pub fn encodeUpper(allocator: Allocator, bytes: []const u8) ![]u8;
pub fn encodeFixed(comptime N: usize, bytes: [N]u8) [2 + N * 2]u8;
pub fn decode(allocator: Allocator, hex: []const u8) ![]u8;
pub fn decodeFixed(comptime N: usize, hex: []const u8) ![N]u8;
pub fn toU256(hex: []const u8) !u256;
pub fn toU64(hex: []const u8) !u64;
pub fn fromU256(allocator: Allocator, value: u256) ![]u8;
pub fn fromU64(allocator: Allocator, value: u64) ![]u8;
pub fn isValid(str: []const u8) bool;
pub fn byteLength(hex: []const u8) usize;
pub fn padLeft(allocator: Allocator, bytes: []const u8, target_length: usize) ![]u8;
pub fn padRight(allocator: Allocator, bytes: []const u8, target_length: usize) ![]u8;
pub fn trimLeft(bytes: []const u8) []const u8;
pub fn trimRight(bytes: []const u8) []const u8;
pub fn concat(allocator: Allocator, arrays: []const []const u8) ![]u8;
pub fn slice(bytes: []const u8, start: usize, end: usize) []const u8;
};
```
#### Usage
```zig
const hex = try Hex.encode(allocator, bytes);
defer allocator.free(hex);
const hex_fixed = Hex.encodeFixed(20, bytes);
const bytes = try Hex.decode(allocator, "0x1234");
defer allocator.free(bytes);
const bytes_fixed = try Hex.decodeFixed(20, "0x1234...");
const value = try Hex.toU256("0x1234");
if (Hex.isValid("0x1234")) { ... }
const len = Hex.byteLength("0x1234");
const padded = try Hex.padLeft(allocator, bytes, 32);
const trimmed = Hex.trimLeft(bytes);
const result = try Hex.concat(allocator, &[_][]const u8{ bytes1, bytes2 });
```
---
### `Numeric`
Ethereum unit conversions and number utilities.
```zig
pub const Numeric = struct {
pub const WEI: u256 = 1;
pub const KWEI: u256 = 1_000;
pub const MWEI: u256 = 1_000_000;
pub const GWEI: u256 = 1_000_000_000;
pub const SZABO: u256 = 1_000_000_000_000;
pub const FINNEY: u256 = 1_000_000_000_000_000;
pub const ETHER: u256 = 1_000_000_000_000_000_000;
pub const Unit = enum {
wei,
kwei,
mwei,
gwei,
szabo,
finney,
ether,
pub fn toMultiplier(self: Unit) u256;
pub fn fromString(str: []const u8) ?Unit;
pub fn toString(self: Unit) []const u8;
};
pub fn parseEther(ether_str: []const u8) !u256;
pub fn parseGwei(gwei_str: []const u8) !u256;
pub fn parseUnits(value_str: []const u8, unit: Unit) !u256;
pub fn formatEther(allocator: Allocator, wei_value: u256) ![]u8;
pub fn formatGwei(allocator: Allocator, wei_value: u256) ![]u8;
pub fn formatUnits(allocator: Allocator, wei_value: u256, unit: Unit, decimals: ?u8) ![]u8;
pub fn convertUnits(value: u256, from_unit: Unit, to_unit: Unit) !u256;
pub fn calculateGasCost(gas_used: u64, gas_price_gwei: u256) u256;
pub fn formatGasCost(allocator: Allocator, gas_used: u64, gas_price_gwei: u256) ![]u8;
};
```
#### Usage
```zig
const wei = try Numeric.parseEther("1.5");
const wei_gwei = try Numeric.parseGwei("20");
const wei_custom = try Numeric.parseUnits("1.5", .ether);
const str = try Numeric.formatEther(allocator, wei_value);
defer allocator.free(str);
const str_gwei = try Numeric.formatGwei(allocator, wei_value);
defer allocator.free(str_gwei);
const converted = try Numeric.convertUnits(1, .ether, .gwei);
const cost = Numeric.calculateGasCost(gas_used, gas_price_gwei);
```
---
## Encoding & Serialization
### `RLP`
Recursive Length Prefix encoding/decoding.
```zig
pub const RLP = struct {
pub const Data = union(enum) {
String: []const u8,
List: []Data,
pub fn deinit(self: Data, allocator: Allocator) void;
};
pub const Decoded = struct {
data: Data,
remainder: []const u8,
};
pub fn encode(allocator: Allocator, input: anytype) ![]u8;
pub fn encodeBytes(allocator: Allocator, bytes: []const u8) ![]u8;
pub fn encodeList(allocator: Allocator, items: []const []const u8) ![]u8;
pub fn decode(allocator: Allocator, input: []const u8, stream: bool) !Decoded;
pub fn encodedLength(input: anytype) usize;
pub fn isList(data: []const u8) bool;
};
```
#### Usage
```zig
const encoded = try RLP.encode(allocator, "hello");
defer allocator.free(encoded);
const list = [_][]const u8{ "cat", "dog" };
const encoded_list = try RLP.encode(allocator, list);
defer allocator.free(encoded_list);
const decoded = try RLP.decode(allocator, data, false);
defer decoded.data.deinit(allocator);
switch (decoded.data) {
.String => |s| std.debug.print("String: {s}\n", .{s}),
.List => |l| std.debug.print("List with {} items\n", .{l.len}),
}
const decoded_stream = try RLP.decode(allocator, data, true);
defer decoded_stream.data.deinit(allocator);
```
---
### `ABI`
Application Binary Interface encoding/decoding for smart contracts.
```zig
pub const ABI = struct {
pub const Type = enum {
uint8, uint16, uint32, uint64, uint128, uint256,
int8, int16, int32, int64, int128, int256,
address,
bool,
bytes1, bytes2, bytes3, bytes4, bytes8, bytes16, bytes32,
bytes,
string,
uint256_array,
bytes32_array,
address_array,
string_array,
pub fn isDynamic(self: Type) bool;
pub fn size(self: Type) ?usize;
pub fn getType(self: Type) []const u8;
};
pub const Value = union(Type) {
uint8: u8,
uint16: u16,
uint32: u32,
uint64: u64,
uint128: u128,
uint256: u256,
int8: i8,
int16: i16,
int32: i32,
int64: i64,
int128: i128,
int256: i256,
address: Address,
bool: bool,
bytes1: [1]u8,
bytes2: [2]u8,
bytes3: [3]u8,
bytes4: [4]u8,
bytes8: [8]u8,
bytes16: [16]u8,
bytes32: [32]u8,
bytes: []const u8,
string: []const u8,
uint256_array: []const u256,
bytes32_array: []const [32]u8,
address_array: []const Address,
string_array: []const []const u8,
pub fn getType(self: Value) Type;
};
pub const Selector = [4]u8;
pub fn computeSelector(signature: []const u8) Selector;
pub fn createSignature(allocator: Allocator, name: []const u8, types: []const Type) ![]u8;
pub fn encodeParameters(allocator: Allocator, values: []const Value) ![]u8;
pub fn decodeParameters(allocator: Allocator, data: []const u8, types: []const Type) ![]Value;
pub fn encodeFunctionCall(allocator: Allocator, signature: []const u8, values: []const Value) ![]u8;
pub fn encodeEventTopic(signature: []const u8) Hash;
};
```
#### Usage
```zig
const selector = ABI.computeSelector("transfer(address,uint256)");
const sig = try ABI.createSignature(
allocator,
"transfer",
&[_]ABI.Type{ .address, .uint256 }
);
defer allocator.free(sig);
const values = [_]ABI.Value{
.{ .address = recipient },
.{ .uint256 = amount },
};
const encoded = try ABI.encodeParameters(allocator, &values);
defer allocator.free(encoded);
const calldata = try ABI.encodeFunctionCall(
allocator,
"transfer(address,uint256)",
&values
);
defer allocator.free(calldata);
const types = [_]ABI.Type{ .address, .uint256 };
const decoded = try ABI.decodeParameters(allocator, data, &types);
defer allocator.free(decoded);
const topic0 = ABI.encodeEventTopic("Transfer(address,address,uint256)");
```
---
## Transactions
### Transaction Types
```zig
pub const TransactionType = enum(u8) {
legacy = 0x00,
eip2930 = 0x01,
eip1559 = 0x02,
eip4844 = 0x03,
eip7702 = 0x04,
};
```
### `LegacyTransaction`
```zig
pub const LegacyTransaction = struct {
nonce: u64,
gas_price: u256,
gas_limit: u64,
to: ?Address,
value: u256,
data: []const u8,
v: u64,
r: [32]u8,
s: [32]u8,
pub fn sign(self: *LegacyTransaction, private_key: [32]u8, chain_id: u64) !void;
pub fn serialize(self: LegacyTransaction, allocator: Allocator) ![]u8;
pub fn deserialize(allocator: Allocator, data: []const u8) !LegacyTransaction;
pub fn hash(self: LegacyTransaction, allocator: Allocator) !Hash;
pub fn recoverSender(self: LegacyTransaction) !Address;
};
```
### `EIP1559Transaction`
```zig
pub const EIP1559Transaction = struct {
chain_id: u64,
nonce: u64,
max_priority_fee_per_gas: u256,
max_fee_per_gas: u256,
gas_limit: u64,
to: ?Address,
value: u256,
data: []const u8,
access_list: []const AccessListEntry,
v: u64,
r: [32]u8,
s: [32]u8,
pub fn effectiveGasPrice(self: EIP1559Transaction, base_fee: u256) u256;
pub fn validate(self: EIP1559Transaction) !void;
pub fn sign(self: *EIP1559Transaction, private_key: [32]u8) !void;
pub fn serialize(self: EIP1559Transaction, allocator: Allocator) ![]u8;
pub fn deserialize(allocator: Allocator, data: []const u8) !EIP1559Transaction;
pub fn hash(self: EIP1559Transaction, allocator: Allocator) !Hash;
pub fn recoverSender(self: EIP1559Transaction) !Address;
};
```
### `BlobTransaction` (EIP-4844)
```zig
pub const BlobTransaction = struct {
chain_id: u64,
nonce: u64,
max_priority_fee_per_gas: u256,
max_fee_per_gas: u256,
gas_limit: u64,
to: Address,
value: u256,
data: []const u8,
access_list: []const AccessListEntry,
max_fee_per_blob_gas: u64,
blob_versioned_hashes: []const Hash,
v: u64,
r: [32]u8,
s: [32]u8,
pub const BYTES_PER_BLOB = 131_072;
pub const MAX_BLOBS_PER_TX = 6;
pub const BLOB_GAS_PER_BLOB = 131_072;
pub fn blobGas(self: BlobTransaction) u64;
pub fn blobFee(self: BlobTransaction, blob_base_fee: u64) u64;
pub fn validate(self: BlobTransaction) !void;
pub fn sign(self: *BlobTransaction, private_key: [32]u8) !void;
pub fn serialize(self: BlobTransaction, allocator: Allocator) ![]u8;
pub fn deserialize(allocator: Allocator, data: []const u8) !BlobTransaction;
};
pub const Blob = [131_072]u8;
pub const BlobCommitment = [48]u8;
pub const BlobProof = [48]u8;
pub fn commitmentToVersionedHash(commitment: BlobCommitment) Hash;
pub fn calculateBlobBaseFee(excess_blob_gas: u64) u64;
```
### `SetCodeTransaction` (EIP-7702)
```zig
pub const SetCodeTransaction = struct {
chain_id: u64,
nonce: u64,
max_priority_fee_per_gas: u256,
max_fee_per_gas: u256,
gas_limit: u64,
to: ?Address,
value: u256,
data: []const u8,
access_list: []const AccessListEntry,
authorization_list: []const Authorization,
v: u64,
r: [32]u8,
s: [32]u8,
pub fn validate(self: SetCodeTransaction) !void;
pub fn sign(self: *SetCodeTransaction, private_key: [32]u8) !void;
pub fn serialize(self: SetCodeTransaction, allocator: Allocator) ![]u8;
pub fn deserialize(allocator: Allocator, data: []const u8) !SetCodeTransaction;
};
pub const Authorization = struct {
chain_id: u64,
address: Address,
nonce: u64,
v: u64,
r: [32]u8,
s: [32]u8,
pub fn create(chain_id: u64, address: Address, nonce: u64, private_key: [32]u8) !Authorization;
pub fn authority(self: Authorization) !Address;
pub fn validate(self: Authorization) !void;
pub fn signingHash(self: Authorization) !Hash;
};
```
---
### `AccessList` (EIP-2930)
```zig
pub const AccessListEntry = struct {
address: Address,
storage_keys: []const Hash,
};
pub const AccessList = []const AccessListEntry;
pub const ACCESS_LIST_ADDRESS_COST = 2400;
pub const ACCESS_LIST_STORAGE_KEY_COST = 1900;
pub fn calculateGas(list: AccessList) u64;
pub fn hasAddress(list: AccessList, address: Address) bool;
pub fn hasStorageKey(list: AccessList, address: Address, key: Hash) bool;
pub fn deduplicate(allocator: Allocator, list: AccessList) !AccessList;
pub fn serialize(allocator: Allocator, list: AccessList) ![]u8;
```
#### Usage
```zig
const entry = AccessListEntry{
.address = addr,
.storage_keys = &[_]Hash{ key1, key2 },
};
const list = [_]AccessListEntry{ entry };
const gas_cost = calculateGas(&list);
const has_addr = hasAddress(&list, addr);
const has_key = hasStorageKey(&list, addr, key);
const deduped = try deduplicate(allocator, &list);
defer allocator.free(deduped);
const serialized = try serialize(allocator, &list);
defer allocator.free(serialized);
```
---
## System Contracts
### `BeaconRoots` (EIP-4788)
Trust-minimized access to consensus layer (beacon chain) block roots from within the EVM. Beacon roots are stored in a ring buffer for recent block access without unbounded storage growth.
```zig
pub const BEACON_ROOTS_ADDRESS = Address{
.bytes = [_]u8{
0x00, 0x0F, 0x3d, 0xf6, 0xD7, 0x32, 0x80, 0x7E,
0xf1, 0x31, 0x9f, 0xB7, 0xB8, 0xbB, 0x85, 0x22,
0xd0, 0xBe, 0xac, 0x02,
},
};
pub const SYSTEM_ADDRESS = Address{
.bytes = [_]u8{0xff} ** 18 ++ [_]u8{0xff, 0xfe},
};
pub const HISTORY_BUFFER_LENGTH: u64 = 8191;
pub const BEACON_ROOT_READ_GAS: u64 = 4200;
pub const BEACON_ROOT_WRITE_GAS: u64 = 20000;
pub const BeaconRootsContract = struct {
database: *Database,
allocator: Allocator,
pub fn execute(
self: *Self,
caller: Address,
input: []const u8,
gas_limit: u64,
) !struct { output: []const u8, gas_used: u64 };
pub fn processBeaconRootUpdate(
database: *Database,
block_info: *const BlockInfo,
) !void;
};
pub fn computeSlots(timestamp: u64) struct {
timestamp_slot: u64,
root_slot: u64
};
```
#### Usage
```zig
var contract = BeaconRootsContract{
.database = &database,
.allocator = allocator,
};
// System call to store beacon root (called at block start)
const timestamp: u64 = 1710338135;
const beacon_root = [_]u8{0xAB} ** 32;
var input: [64]u8 = undefined;
std.mem.writeInt(u256, input[0..32], timestamp, .big);
@memcpy(input[32..64], &beacon_root);
const write_result = try contract.execute(
SYSTEM_ADDRESS,
&input,
100000,
);
defer allocator.free(write_result.output);
// Read beacon root for a timestamp
var read_input: [32]u8 = undefined;
std.mem.writeInt(u256, &read_input, timestamp, .big);
const read_result = try contract.execute(
caller_address,
&read_input,
10000,
);
defer allocator.free(read_result.output);
if (read_result.output.len == 32) {
// Root found
const beacon_root_bytes = read_result.output[0..32];
} else {
// Root not available (timestamp too old or not found)
}
// Process beacon root update at block start
try BeaconRootsContract.processBeaconRootUpdate(&database, &block_info);
// Compute ring buffer slots for a timestamp
const slots = computeSlots(timestamp);
// slots.timestamp_slot -> location for beacon root
// slots.root_slot -> location for timestamp verification
```
---
## Logs & Events
### `EventLog`
```zig
pub const EventLog = struct {
address: Address,
topics: []const Hash,
data: []const u8,
block_number: ?u64,
transaction_hash: ?Hash,
transaction_index: ?u32,
log_index: ?u32,
removed: bool,
pub fn eventSignature(self: EventLog) ?Hash;
};
pub fn filterLogs(logs: []const EventLog, topics: []const ?Hash) []const EventLog;
```
#### Usage
```zig
const log = EventLog{
.address = contract_addr,
.topics = &[_]Hash{ topic0, topic1, topic2 },
.data = event_data,
.block_number = 12345,
.transaction_hash = tx_hash,
.transaction_index = 0,
.log_index = 0,
.removed = false,
};
const sig = log.eventSignature();
const filtered = filterLogs(logs, &[_]?Hash{ topic0, null, null });
```
---
## Gas Constants
```zig
pub const Gas = struct {
pub const TX = 21_000;
pub const TX_CREATE = 32_000;
pub const TX_DATA_ZERO = 4;
pub const TX_DATA_NONZERO = 16;
pub const COLD_ACCOUNT_ACCESS = 2_600;
pub const COLD_SLOAD = 2_100;
pub const WARM_STORAGE_READ = 100;
pub const SSTORE_SET = 20_000;
pub const SSTORE_RESET = 5_000;
pub const SSTORE_CLEAR_REFUND = 15_000;
pub const CALL = 700;
pub const CALL_VALUE = 9_000;
pub const CALL_STIPEND = 2_300;
pub const NEW_ACCOUNT = 25_000;
pub const ECRECOVER = 3_000;
pub const SHA256_BASE = 60;
pub const SHA256_WORD = 12;
pub const RIPEMD160_BASE = 600;
pub const RIPEMD160_WORD = 120;
pub const IDENTITY_BASE = 15;
pub const IDENTITY_WORD = 3;
pub fn memoryExpansion(byte_size: u64) u64;
pub fn intrinsic(params: struct {
data: []const u8,
is_creation: bool,
access_list: ?AccessList,
}) u64;
};
```
#### Usage
```zig
const base_cost = Gas.TX;
const create_cost = Gas.TX_CREATE;
const memory_cost = Gas.memoryExpansion(1024);
const intrinsic_gas = Gas.intrinsic(.{
.data = calldata,
.is_creation = false,
.access_list = null,
});
```
---
## Opcodes
EVM opcode enumeration with categorization and utility methods.
```zig
pub const Opcode = enum(u8) {
STOP = 0x00,
ADD = 0x01,
MUL = 0x02,
SUB = 0x03,
DIV = 0x04,
SDIV = 0x05,
MOD = 0x06,
SMOD = 0x07,
ADDMOD = 0x08,
MULMOD = 0x09,
EXP = 0x0a,
SIGNEXTEND = 0x0b,
LT = 0x10,
GT = 0x11,
SLT = 0x12,
SGT = 0x13,
EQ = 0x14,
ISZERO = 0x15,
AND = 0x16,
OR = 0x17,
XOR = 0x18,
NOT = 0x19,
BYTE = 0x1a,
SHL = 0x1b,
SHR = 0x1c,
SAR = 0x1d,
KECCAK256 = 0x20,
ADDRESS = 0x30,
BALANCE = 0x31,
ORIGIN = 0x32,
CALLER = 0x33,
CALLVALUE = 0x34,
CALLDATALOAD = 0x35,
CALLDATASIZE = 0x36,
CALLDATACOPY = 0x37,
CODESIZE = 0x38,
CODECOPY = 0x39,
GASPRICE = 0x3a,
EXTCODESIZE = 0x3b,
EXTCODECOPY = 0x3c,
RETURNDATASIZE = 0x3d,
RETURNDATACOPY = 0x3e,
EXTCODEHASH = 0x3f,
BLOCKHASH = 0x40,
COINBASE = 0x41,
TIMESTAMP = 0x42,
NUMBER = 0x43,
PREVRANDAO = 0x44,
GASLIMIT = 0x45,
CHAINID = 0x46,
SELFBALANCE = 0x47,
BASEFEE = 0x48,
BLOBHASH = 0x49,
BLOBBASEFEE = 0x4a,
POP = 0x50,
MLOAD = 0x51,
MSTORE = 0x52,
MSTORE8 = 0x53,
SLOAD = 0x54,
SSTORE = 0x55,
JUMP = 0x56,
JUMPI = 0x57,
PC = 0x58,
MSIZE = 0x59,
GAS = 0x5a,
JUMPDEST = 0x5b,
TLOAD = 0x5c,
TSTORE = 0x5d,
MCOPY = 0x5e,
PUSH0 = 0x5f,
PUSH1 = 0x60,
PUSH2 = 0x61,
// ... PUSH3-PUSH31
PUSH32 = 0x7f,
DUP1 = 0x80,
DUP2 = 0x81,
// ... DUP3-DUP15
DUP16 = 0x8f,
SWAP1 = 0x90,
SWAP2 = 0x91,
// ... SWAP3-SWAP15
SWAP16 = 0x9f,
LOG0 = 0xa0,
LOG1 = 0xa1,
LOG2 = 0xa2,
LOG3 = 0xa3,
LOG4 = 0xa4,
CREATE = 0xf0,
CALL = 0xf1,
CALLCODE = 0xf2,
RETURN = 0xf3,
DELEGATECALL = 0xf4,
CREATE2 = 0xf5,
AUTH = 0xf6,
AUTHCALL = 0xf7,
STATICCALL = 0xfa,
REVERT = 0xfd,
INVALID = 0xfe,
SELFDESTRUCT = 0xff,
pub fn isPush(self: Opcode) bool;
pub fn pushSize(self: Opcode) u8;
pub fn isDup(self: Opcode) bool;
pub fn dupPosition(self: Opcode) u8;
pub fn isSwap(self: Opcode) bool;
pub fn swapPosition(self: Opcode) u8;
pub fn isLog(self: Opcode) bool;
pub fn logTopics(self: Opcode) u8;
pub fn isTerminating(self: Opcode) bool;
pub fn isStateModifying(self: Opcode) bool;
pub fn isArithmetic(self: Opcode) bool;
pub fn isComparison(self: Opcode) bool;
pub fn isBitwise(self: Opcode) bool;
pub fn name(self: Opcode) []const u8;
};
```
### Opcode Categories
#### PUSH Operations (0x5f-0x7f)
```zig
if (opcode.isPush()) {
const size = opcode.pushSize(); // 0-32 bytes
}
```
#### DUP Operations (0x80-0x8f)
```zig
if (opcode.isDup()) {
const position = opcode.dupPosition(); // 1-16
}
```
#### SWAP Operations (0x90-0x9f)
```zig
if (opcode.isSwap()) {
const position = opcode.swapPosition(); // 1-16
}
```
#### LOG Operations (0xa0-0xa4)
```zig
if (opcode.isLog()) {
const topics = opcode.logTopics(); // 0-4
}
```
### Opcode Classification
```zig
// Terminating opcodes
if (opcode.isTerminating()) {
// STOP, RETURN, REVERT, INVALID, SELFDESTRUCT
}
// State-modifying opcodes
if (opcode.isStateModifying()) {
// SSTORE, TSTORE, LOG*, CREATE*, CALL*, SELFDESTRUCT
}
// Arithmetic opcodes
if (opcode.isArithmetic()) {
// ADD, MUL, SUB, DIV, SDIV, MOD, SMOD, ADDMOD, MULMOD, EXP, SIGNEXTEND
}
// Comparison opcodes
if (opcode.isComparison()) {
// LT, GT, SLT, SGT, EQ, ISZERO
}
// Bitwise opcodes
if (opcode.isBitwise()) {
// AND, OR, XOR, NOT, BYTE, SHL, SHR, SAR
}
```
#### Usage
```zig
const opcode = Opcode.PUSH1;
if (opcode.isPush()) {
const bytes_to_read = opcode.pushSize();
// Read bytes_to_read bytes following the opcode
}
const name = opcode.name(); // "PUSH1"
// Check opcode properties
if (Opcode.SSTORE.isStateModifying()) {
// Handle state modification
}
if (Opcode.RETURN.isTerminating()) {
// End execution
}
// Categorize opcodes
switch (opcode) {
.ADD, .MUL, .SUB => {
// Arithmetic operations
},
.PUSH1...PUSH32 => {
const size = opcode.pushSize();
// Handle PUSH operations
},
.DUP1...DUP16 => {
const pos = opcode.dupPosition();
// Handle DUP operations
},
else => {},
}
```
---
## EIP Configuration
### `Eips`
Ethereum Improvement Proposal (EIP) configuration system that consolidates all EIP-specific logic for the EVM. Provides hardfork-based feature detection and gas cost calculations with support for custom EIP overrides.
```zig
pub const Hardfork = enum {
FRONTIER,
HOMESTEAD,
DAO,
TANGERINE_WHISTLE,
SPURIOUS_DRAGON,
BYZANTIUM,
CONSTANTINOPLE,
PETERSBURG,
ISTANBUL,
MUIR_GLACIER,
BERLIN,
LONDON,
ARROW_GLACIER,
GRAY_GLACIER,
MERGE,
SHANGHAI,
CANCUN,
PRAGUE,
};
pub const EipOverride = struct {
eip: u16,
enabled: bool,
};
pub const Eips = struct {
hardfork: Hardfork,
overrides: []const EipOverride = &.{},
// Feature detection
pub fn is_eip_active(self: Self, eip: u16) bool;
pub fn get_active_eips(self: Self) []const u16;
// Opcode availability
pub fn eip_3855_push0_enabled(self: Self) bool;
pub fn eip_3198_basefee_opcode_enabled(self: Self) bool;
pub fn eip_1153_transient_storage_enabled(self: Self) bool;
pub fn eip_5656_has_mcopy(self: Self) bool;
// Transaction types
pub fn eip_1559_is_enabled(self: Self) bool;
pub fn eip_4844_blob_transactions_enabled(self: Self) bool;
pub fn eip_7702_eoa_code_enabled(self: Self) bool;
// Gas costs
pub fn eip_2929_cold_sload_cost(self: Self) u64;
pub fn eip_2929_warm_storage_read_cost(self: Self) u64;
pub fn eip_2929_cold_account_access_cost(self: Self) u64;
pub fn eip_2929_warm_account_access_cost(self: Self) u64;
pub fn eip_3529_gas_refund_cap(self: Self, gas_used: u64, refund_counter: u64) u64;
pub fn eip_2028_calldata_gas_cost(self: Self, is_zero: bool) u64;
pub fn eip_160_exp_byte_gas_cost(self: Self) u64;
pub fn sstore_gas_cost(self: Self, current: u256, new: u256, original: u256) SstoreGasCost;
// Code limits
pub fn eip_170_max_code_size(self: Self) u32;
pub fn eip_3860_size_limit(self: Self) u64;
pub fn eip_3860_word_cost(self: Self) u64;
// Behavior changes
pub fn eip_6780_selfdestruct_same_transaction_only(self: Self) bool;
pub fn eip_3541_should_reject_ef_bytecode(self: Self) bool;
pub fn eip_4399_use_prevrandao(self: Self) bool;
// Warming & access lists
pub fn pre_warm_transaction_addresses(
self: Self,
access_list: *AccessList,
origin: Address,
target: ?Address,
coinbase: Address,
) !void;
};
```
### Major EIP Groups by Hardfork
#### Berlin (EIP-2929, EIP-2930)
- **EIP-2929**: Gas cost increases for state access opcodes
- **EIP-2930**: Optional access lists in transactions
#### London (EIP-1559, EIP-3198, EIP-3529, EIP-3541)
- **EIP-1559**: Fee market change with base fee per gas
- **EIP-3198**: BASEFEE opcode
- **EIP-3529**: Reduction in gas refunds (1/5 instead of 1/2)
- **EIP-3541**: Reject contracts starting with 0xEF
#### Shanghai (EIP-3651, EIP-3855, EIP-3860)
- **EIP-3651**: Warm COINBASE address
- **EIP-3855**: PUSH0 instruction
- **EIP-3860**: Limit and meter initcode (48KB limit, 2 gas/word)
#### Cancun (EIP-1153, EIP-4788, EIP-4844, EIP-5656, EIP-6780)
- **EIP-1153**: Transient storage opcodes (TLOAD/TSTORE)
- **EIP-4788**: Beacon block root in the EVM
- **EIP-4844**: Shard blob transactions
- **EIP-5656**: MCOPY instruction
- **EIP-6780**: SELFDESTRUCT only in same transaction
#### Prague (EIP-2537, EIP-2935, EIP-6110, EIP-7002, EIP-7702)
- **EIP-2537**: BLS12-381 precompile operations
- **EIP-2935**: Historical block hashes from state
- **EIP-6110**: Validator deposits on chain
- **EIP-7002**: Execution layer triggerable exits
- **EIP-7702**: Set EOA account code
### Usage
#### Basic Configuration
```zig
const eips = Eips{ .hardfork = .CANCUN };
// Check if specific EIPs are active
if (eips.is_eip_active(1559)) {
// EIP-1559 fee market is active
}
if (eips.eip_4844_blob_transactions_enabled()) {
// Handle blob transactions
}
// Get all active EIPs for current hardfork
const active_eips = eips.get_active_eips();
for (active_eips) |eip_num| {
std.debug.print("EIP-{}: active\n", .{eip_num});
}
```
#### Gas Cost Calculations
```zig
const berlin = Eips{ .hardfork = .BERLIN };
const istanbul = Eips{ .hardfork = .ISTANBUL };
// EIP-2929: Cold storage costs
const cold_sload = berlin.eip_2929_cold_sload_cost(); // 2100
const warm_sload = berlin.eip_2929_warm_storage_read_cost(); // 100
// Pre-Berlin costs
const old_sload = istanbul.eip_2929_cold_sload_cost(); // 200
// EIP-3529: Gas refund cap
const london = Eips{ .hardfork = .LONDON };
const gas_used: u64 = 100_000;
const refund_counter: u64 = 50_000;
// London: refund capped at 1/5 of gas used
const refund = london.eip_3529_gas_refund_cap(gas_used, refund_counter); // 20,000
// Pre-London: capped at 1/2
const old_refund = istanbul.eip_3529_gas_refund_cap(gas_used, refund_counter); // 50,000
// EIP-2028: Calldata gas costs
const zero_byte_cost = london.eip_2028_calldata_gas_cost(true); // 4
const nonzero_byte_cost = london.eip_2028_calldata_gas_cost(false); // 16
```
#### SSTORE Gas Costs
```zig
const eips = Eips{ .hardfork = .LONDON };
const current: u256 = 0;
const new: u256 = 1;
const original: u256 = 0;
const cost = eips.sstore_gas_cost(current, new, original);
// cost.gas = 20000 (setting from zero)
// cost.refund = 0
// Clearing storage
const clear_cost = eips.sstore_gas_cost(1, 0, 1);
// clear_cost.gas = 5000
// clear_cost.refund = 4800 (reduced by EIP-3529 in London)
```
#### Code Size Limits
```zig
const spurious = Eips{ .hardfork = .SPURIOUS_DRAGON };
const shanghai = Eips{ .hardfork = .SHANGHAI };
// EIP-170: Contract code size limit
const max_code = spurious.eip_170_max_code_size(); // 24,576 (0x6000)
// EIP-3860: Initcode size limits
const init_limit_pre = spurious.size_limit(); // 24,576 bytes
const init_limit_post = shanghai.size_limit(); // 49,152 bytes (48KB)
const word_cost = shanghai.word_cost(); // 2 gas per word
```
#### Transaction Address Warming
```zig
const shanghai = Eips{ .hardfork = .SHANGHAI };
var access_list = AccessList.init(allocator);
defer access_list.deinit();
// Pre-warm addresses for transaction execution
// Includes: origin, target, and coinbase (EIP-3651)
try shanghai.pre_warm_transaction_addresses(
&access_list,
tx_origin,
tx_target,
block_coinbase,
);
```
#### Custom EIP Overrides
```zig
// Enable future EIPs on older hardfork for testing
const custom = Eips{
.hardfork = .LONDON,
.overrides = &[_]EipOverride{
.{ .eip = 3855, .enabled = true }, // Enable PUSH0
.{ .eip = 3860, .enabled = true }, // Enable initcode metering
},
};
if (custom.eip_3855_push0_enabled()) {
// PUSH0 is now available on London
}
// Disable specific EIPs for testing
const restricted = Eips{
.hardfork = .CANCUN,
.overrides = &[_]EipOverride{
.{ .eip = 4844, .enabled = false }, // Disable blob transactions
.{ .eip = 1153, .enabled = false }, // Disable transient storage
},
};
if (!restricted.eip_4844_blob_transactions_enabled()) {
// Blob transactions disabled despite Cancun hardfork
}
```
#### Behavior Checks
```zig
const cancun = Eips{ .hardfork = .CANCUN };
const london = Eips{ .hardfork = .LONDON };
const merge = Eips{ .hardfork = .MERGE };
// EIP-6780: Restrict SELFDESTRUCT behavior
if (cancun.eip_6780_selfdestruct_same_transaction_only()) {
// Only destroy contracts created in same transaction
}
// EIP-3541: Reject 0xEF bytecode
const bytecode = [_]u8{ 0xEF, 0x00, 0x01 };
if (london.eip_3541_should_reject_ef_bytecode()) {
if (london.eip_3541_should_reject_create_with_ef_bytecode(&bytecode)) {
return error.InvalidBytecode; // Reject EIP-3540 magic
}
}
// EIP-4399: PREVRANDAO vs DIFFICULTY
if (merge.eip_4399_use_prevrandao()) {
// Use PREVRANDAO opcode instead of DIFFICULTY
}
```
---
## Error Handling
All fallible operations return Zig error unions:
```zig
pub const AddressError = error{
InvalidFormat,
InvalidLength,
InvalidChecksum,
};
pub const HexError = error{
InvalidFormat,
InvalidLength,
InvalidCharacter,
OddLength,
ValueTooLarge,
};
pub const NumericError = error{
InvalidInput,
InvalidUnit,
InvalidFormat,
ValueTooLarge,
};
pub const RLPError = error{
InputTooShort,
InputTooLong,
InvalidLength,
NonCanonical,
InvalidRemainder,
LeadingZeros,
};
pub const ABIError = error{
InvalidSelector,
InvalidType,
InvalidData,
DataTooSmall,
OutOfBounds,
InvalidAddress,
};
pub const TransactionError = error{
InvalidSignature,
InvalidChainId,
InvalidType,
};
```
#### Usage
```zig
const addr = Address.fromHex(hex_str) catch |err| switch (err) {
error.InvalidFormat => return error.BadInput,
error.InvalidChecksum => {
std.log.warn("Checksum failed\n", .{});
return error.InvalidChecksum;
},
else => return err,
};
```
---
## State Management
### `StorageKey`
Composite key for EVM storage (address + slot).
```zig
pub const StorageKey = struct {
address: Address,
slot: u256,
pub fn hash(self: StorageKey, hasher: anytype) void;
pub fn eql(a: StorageKey, b: StorageKey) bool;
};
```
#### Usage
```zig
var storage = std.AutoHashMap(StorageKey, u256).init(allocator);
defer storage.deinit();
const key = StorageKey{ .address = addr, .slot = 0 };
try storage.put(key, value);
const stored_value = storage.get(key);
```
### State Constants
```zig
pub const EMPTY_CODE_HASH: [32]u8 = .{
0xc5, 0xd2, 0x46, 0x01, 0x86, 0xf7, 0x23, 0x3c,
0x92, 0x7e, 0x7d, 0xb2, 0xdc, 0xc7, 0x03, 0xc0,
0xe5, 0x00, 0xb6, 0x53, 0xca, 0x82, 0x27, 0x3b,
0x7b, 0xfa, 0xd8, 0x04, 0x5d, 0x85, 0xa4, 0x70,
};
pub const EMPTY_TRIE_ROOT: [32]u8 = .{
0x56, 0xe8, 0x1f, 0x17, 0x1b, 0xcc, 0x55, 0xa6,
0xff, 0x83, 0x45, 0xe6, 0x92, 0xc0, 0xf8, 0x6e,
0x5b, 0x48, 0xe0, 0x1b, 0x99, 0x6c, 0xad, 0xc0,
0x01, 0x62, 0x2f, 0xb5, 0xe3, 0x63, 0xb4, 0x21,
};
```
---
## Module Structure
```zig
const primitives = @import("primitives");
pub const Address = primitives.Address;
pub const Hash = primitives.Hash;
pub const Hex = primitives.Hex;
pub const RLP = primitives.RLP;
pub const ABI = primitives.ABI;
pub const Numeric = primitives.Numeric;
pub const Gas = primitives.Gas;
pub const LegacyTransaction = primitives.LegacyTransaction;
pub const EIP1559Transaction = primitives.EIP1559Transaction;
pub const BlobTransaction = primitives.BlobTransaction;
pub const SetCodeTransaction = primitives.SetCodeTransaction;
pub const AccessList = primitives.AccessList;
pub const AccessListEntry = primitives.AccessListEntry;
pub const EventLog = primitives.EventLog;
pub const StorageKey = primitives.StorageKey;
pub const EMPTY_CODE_HASH = primitives.EMPTY_CODE_HASH;
pub const EMPTY_TRIE_ROOT = primitives.EMPTY_TRIE_ROOT;
```
---
## Examples
### Creating and Signing a Transaction
```zig
const std = @import("std");
const primitives = @import("primitives");
pub fn main() !void {
var gpa = std.heap.GeneralPurposeAllocator(.{}){};
defer _ = gpa.deinit();
const allocator = gpa.allocator();
var tx = primitives.EIP1559Transaction{
.chain_id = 1,
.nonce = 42,
.max_priority_fee_per_gas = try primitives.Numeric.parseGwei("2"),
.max_fee_per_gas = try primitives.Numeric.parseGwei("20"),
.gas_limit = 21_000,
.to = try primitives.Address.fromHex("0x742d35Cc6641C91B6E4bb6ac9e3ff2958c94E676"),
.value = try primitives.Numeric.parseEther("1.5"),
.data = &[_]u8{},
.access_list = &[_]primitives.AccessListEntry{},
.v = 0,
.r = [_]u8{0} ** 32,
.s = [_]u8{0} ** 32,
};
const private_key = try primitives.Hex.decodeFixed(32, "0x...");
try tx.sign(private_key);
const serialized = try tx.serialize(allocator);
defer allocator.free(serialized);
const tx_hash = try tx.hash(allocator);
std.debug.print("Transaction hash: {}\n", .{tx_hash});
}
```
### Encoding a Contract Call
```zig
const primitives = @import("primitives");
const recipient = try primitives.Address.fromHex("0x...");
const amount = try primitives.Numeric.parseEther("100");
const values = [_]primitives.ABI.Value{
.{ .address = recipient },
.{ .uint256 = amount },
};
const calldata = try primitives.ABI.encodeFunctionCall(
allocator,
"transfer(address,uint256)",
&values
);
defer allocator.free(calldata);
```
### Decoding Event Logs
```zig
const event_signature = primitives.ABI.encodeEventTopic(
"Transfer(address,address,uint256)"
);
for (logs) |log| {
if (log.topics.len > 0 and log.topics[0].eql(event_signature)) {
const from = primitives.Address.fromBytes(log.topics[1].bytes[12..32]);
const to = primitives.Address.fromBytes(log.topics[2].bytes[12..32]);
const types = [_]primitives.ABI.Type{.uint256};
const values = try primitives.ABI.decodeParameters(allocator, log.data, &types);
defer allocator.free(values);
const amount = values[0].uint256;
std.debug.print("Transfer from {} to {}: {}\n", .{ from, to, amount });
}
}
```
---
## License
MIT