Type-first EVM architecture. Every execution primitive is a strongly-typed branded Uint8Array or interface.
These are low-level primitives for EVM operations, not a full EVM implementation. For complete EVM implementations, see evmts/guillotine-mini (lightweight) or evmts/guillotine (full-featured).
Core Types
Opcode
Branded number type for EVM instructions (0x00-0xFF).
import { Opcode } from "@tevm/voltaire";
// Opcode type is a branded number
type OpcodeType = number & { readonly __tag: "Opcode" };
// Create opcodes
const add: Opcode = Opcode.ADD; // 0x01
const push1: Opcode = Opcode.PUSH1; // 0x60
const sload: Opcode = Opcode.SLOAD; // 0x54
// Type safety prevents passing arbitrary numbers
// ❌ const invalid: Opcode = 0x01; // Type error!
See Opcode documentation for complete reference.
Instruction
Opcode with program counter offset and optional immediate data.
import { Instruction, Opcode } from "@tevm/voltaire";
type Instruction = {
/** Program counter offset */
offset: number;
/** The opcode */
opcode: OpcodeType;
/** Immediate data for PUSH operations */
immediate?: Uint8Array;
};
// PUSH1 0x42 at offset 0
const push: Instruction = {
offset: 0,
opcode: Opcode.PUSH1,
immediate: new Uint8Array([0x42]),
};
// ADD at offset 2
const add: Instruction = {
offset: 2,
opcode: Opcode.ADD,
};
Instructions are returned by bytecode analysis:
import { Bytecode } from "@tevm/voltaire";
const code = Bytecode.from("0x6001600201");
const analysis = code.analyze();
// analysis.instructions: Instruction[]
// [
// { offset: 0, opcode: 0x60, immediate: Uint8Array([0x01]) }, // PUSH1 1
// { offset: 2, opcode: 0x60, immediate: Uint8Array([0x02]) }, // PUSH1 2
// { offset: 4, opcode: 0x01 } // ADD
// ]
Execution Types
Frame
EVM execution frame with stack, memory, and execution state.
import type { BrandedFrame } from "@tevm/voltaire/evm";
import type { Address } from "@tevm/voltaire";
type BrandedFrame = {
readonly __tag: "Frame";
// Stack (max 1024 items, 256-bit words)
stack: bigint[];
// Memory (sparse map, byte-addressable)
memory: Map<number, number>;
memorySize: number; // Word-aligned size
// Execution state
pc: number; // Program counter
gasRemaining: bigint;
bytecode: Uint8Array;
// Call context
caller: Address;
address: Address;
value: bigint;
calldata: Uint8Array;
output: Uint8Array;
returnData: Uint8Array;
// Flags
stopped: boolean;
reverted: boolean;
isStatic: boolean;
// Block context (for block opcodes)
blockNumber?: bigint;
blockTimestamp?: bigint;
blockGasLimit?: bigint;
chainId?: bigint;
blockBaseFee?: bigint;
blobBaseFee?: bigint;
// Logs (LOG0-LOG4 opcodes)
logs?: Array<{
address: Address;
topics: bigint[];
data: Uint8Array;
}>;
};
Frame represents the complete execution state at any point in EVM execution.
Host
Interface for external state access (accounts, storage, code) and nested execution.
import type { BrandedHost, CallParams, CallResult, CreateParams, CreateResult } from "@tevm/voltaire/evm";
import type { Address } from "@tevm/voltaire";
type BrandedHost = {
readonly __tag: "Host";
// Account balance
getBalance: (address: Address) => bigint;
setBalance: (address: Address, balance: bigint) => void;
// Contract code
getCode: (address: Address) => Uint8Array;
setCode: (address: Address, code: Uint8Array) => void;
// Persistent storage
getStorage: (address: Address, slot: bigint) => bigint;
setStorage: (address: Address, slot: bigint, value: bigint) => void;
// Account nonce
getNonce: (address: Address) => bigint;
setNonce: (address: Address, nonce: bigint) => void;
// Transient storage (EIP-1153, transaction-scoped)
getTransientStorage: (address: Address, slot: bigint) => bigint;
setTransientStorage: (address: Address, slot: bigint, value: bigint) => void;
// Nested execution (optional - for CALL/CREATE opcodes)
call?: (params: CallParams) => CallResult;
create?: (params: CreateParams) => CreateResult;
};
The call and create methods are optional. When not provided, system opcodes (CALL, CREATE, etc.) return a NotImplemented error. For full EVM execution with nested calls, use:
- guillotine: Production EVM with async state, tracing, full EIP support
- guillotine-mini: Lightweight synchronous EVM for testing
Host provides pluggable state backend - implement for custom chains or test environments.
InstructionHandler
Function signature for opcode implementations.
import type { InstructionHandler, BrandedFrame, BrandedHost, EvmError } from "@tevm/voltaire/evm";
type InstructionHandler = (
frame: BrandedFrame,
host: BrandedHost,
) => EvmError | { type: "Success" };
Example handler implementation:
// ADD opcode (0x01): Pop two values, push sum
const addHandler: InstructionHandler = (frame, host) => {
// Check stack depth
if (frame.stack.length < 2) {
return { type: "StackUnderflow" };
}
// Check gas
if (frame.gasRemaining < 3n) {
return { type: "OutOfGas" };
}
// Execute
const b = frame.stack.pop()!;
const a = frame.stack.pop()!;
const result = (a + b) % 2n**256n; // Mod 2^256 for overflow
frame.stack.push(result);
frame.gasRemaining -= 3n;
frame.pc += 1;
return { type: "Success" };
};
All 166 EVM opcodes follow this pattern. See Instructions.
Call Types
CallParams
Parameters for cross-contract calls (CALL, STATICCALL, DELEGATECALL).
import type { CallParams, CallType, Address } from "@tevm/voltaire";
type CallType = "CALL" | "STATICCALL" | "DELEGATECALL" | "CALLCODE";
type CallParams = {
callType: CallType;
target: Address;
value: bigint;
gasLimit: bigint;
input: Uint8Array;
caller: Address;
isStatic: boolean;
depth: number;
};
Example usage:
// STATICCALL to view function
const params: CallParams = {
callType: "STATICCALL",
target: contractAddress,
value: 0n,
gasLimit: 100000n,
input: encodedCalldata,
caller: myAddress,
isStatic: true,
depth: 1,
};
CallResult
Result of call operation.
import type { CallResult, Address } from "@tevm/voltaire";
type CallResult = {
success: boolean; // False if reverted
gasUsed: bigint;
output: Uint8Array; // Return data or revert reason
logs: Array<{
address: Address;
topics: bigint[];
data: Uint8Array;
}>;
gasRefund: bigint;
};
Example:
// Successful call
const result: CallResult = {
success: true,
gasUsed: 21000n,
output: returnData,
logs: [],
gasRefund: 0n,
};
// Reverted call
const revertResult: CallResult = {
success: false,
gasUsed: 50000n,
output: revertReason, // Revert message
logs: [],
gasRefund: 0n,
};
Creation Types
CreateParams
Parameters for contract deployment (CREATE, CREATE2).
import type { CreateParams, Address } from "@tevm/voltaire";
type CreateParams = {
caller: Address;
value: bigint;
initCode: Uint8Array;
gasLimit: bigint;
salt?: bigint; // For CREATE2
depth: number;
};
Example:
// CREATE2 deployment with deterministic address
const params: CreateParams = {
caller: deployerAddress,
value: 0n,
initCode: contractBytecode,
gasLimit: 1000000n,
salt: 0x1234n, // Determines address
depth: 1,
};
CreateResult
Result of contract creation.
import type { CreateResult, Address } from "@tevm/voltaire";
type CreateResult = {
success: boolean;
address: Address | null; // Null if failed
gasUsed: bigint;
output: Uint8Array; // Runtime code or revert reason
gasRefund: bigint;
};
Example:
// Successful deployment
const result: CreateResult = {
success: true,
address: newContractAddress,
gasUsed: 200000n,
output: runtimeCode,
gasRefund: 0n,
};
Error Types
EvmError
Execution errors that halt the EVM.
type EvmError =
| { type: "StackOverflow" }
| { type: "StackUnderflow" }
| { type: "OutOfGas" }
| { type: "OutOfBounds" }
| { type: "InvalidJump" }
| { type: "InvalidOpcode" }
| { type: "RevertExecuted" }
| { type: "CallDepthExceeded" }
| { type: "WriteProtection" }
| { type: "InsufficientBalance" }
| { type: "NotImplemented"; message: string };
The NotImplemented error is returned by system opcodes (CALL, CREATE, etc.) when the host doesn’t provide call or create methods. This is intentional - these low-level primitives don’t include an execution engine. Use guillotine or guillotine-mini for full EVM execution.
Error handling:
const result = instructionHandler(frame, host);
if (result.type !== "Success") {
// Handle error
switch (result.type) {
case "StackUnderflow":
console.error("Stack underflow - not enough items");
break;
case "OutOfGas":
console.error("Insufficient gas");
break;
case "RevertExecuted":
console.error("Execution reverted");
break;
case "NotImplemented":
console.error("Feature not implemented:", result.message);
break;
// ... handle other errors
}
}
Info
Opcode metadata (gas cost, stack effect).
import { Opcode } from "@tevm/voltaire";
type Info = {
gasCost: number; // Base gas cost (may be dynamic)
stackInputs: number; // Items consumed from stack
stackOutputs: number; // Items pushed to stack
name: string; // Opcode name
};
// Get opcode info
const info = Opcode.info(Opcode.ADD);
// {
// gasCost: 3,
// stackInputs: 2,
// stackOutputs: 1,
// name: "ADD"
// }
Type Safety Benefits
Prevents Type Confusion
import { Opcode, Address, Bytecode } from "@tevm/voltaire";
// ❌ Cannot pass wrong type
const opcode: Opcode = 0x01; // Type error!
const addr: Address = "0x123..."; // Type error!
// ✅ Must use constructors
const opcode = Opcode.ADD; // Correct
const addr = Address("0x123..."); // Correct
Compile-Time Validation
// ❌ Type mismatch caught at compile time
function executeOpcode(op: Opcode) { /*...*/ }
executeOpcode(0x60); // Type error!
// ✅ Type-safe
executeOpcode(Opcode.PUSH1); // Correct
IDE Autocomplete
TypeScript provides full IntelliSense:
import { Opcode } from "@tevm/voltaire";
const op = Opcode.
// ^ Shows all opcode names with documentation
Zero Runtime Overhead
Branded types are compile-time only:
// TypeScript
const op: OpcodeType = Opcode.ADD;
// Compiles to JavaScript
const op = 0x01; // No wrapper, just the number
Architecture
Execution Flow
import type { BrandedFrame, BrandedHost, InstructionHandler } from "@tevm/voltaire/evm";
// 1. Initialize frame
const frame: BrandedFrame = {
stack: [],
memory: new Map(),
memorySize: 0,
pc: 0,
gasRemaining: 1000000n,
bytecode: code,
// ... other fields
};
// 2. Initialize host
const host: BrandedHost = {
getBalance: (addr) => balances.get(addr) || 0n,
setBalance: (addr, bal) => balances.set(addr, bal),
// ... other methods
};
// 3. Execute instructions
while (!frame.stopped && !frame.reverted) {
const opcode = frame.bytecode[frame.pc];
const handler = getHandler(opcode);
const result = handler(frame, host);
if (result.type !== "Success") {
// Handle error
break;
}
}
Pluggable Backend
Host interface allows custom state implementations:
// In-memory state for testing
class MemoryHost implements BrandedHost {
private balances = new Map<Address, bigint>();
private storage = new Map<string, bigint>();
getBalance(addr: Address) { return this.balances.get(addr) || 0n; }
setBalance(addr: Address, bal: bigint) { this.balances.set(addr, bal); }
// ... implement other methods
}
// Database-backed state for production
class DatabaseHost implements BrandedHost {
async getBalance(addr: Address) { return await db.getBalance(addr); }
// ... implement with DB queries
}
References