Last active
June 8, 2025 10:09
-
-
Save roninjin10/d25788afa1a0b1c910026e1d7625aeb9 to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// evm.ts | |
// —— Helpers ——————————————————————————————————————————————— | |
type U256 = bigint; | |
// bytes → bigint via hex string | |
function fromBytes(bytes: Uint8Array): U256 { | |
const hex = [...bytes].map(b => b.toString(16).padStart(2, '0')).join(''); | |
return BigInt('0x' + hex); | |
} | |
// bigint → 32-byte Uint8Array via hex string | |
function toBytes(x: U256): Uint8Array { | |
const hex = x.toString(16).padStart(64, '0'); | |
const pairs = hex.match(/.{2}/g)!; | |
return Uint8Array.from(pairs.map(h => parseInt(h, 16))); | |
} | |
// —— Opcode enum ——————————————————————————————————————————— | |
enum Opcode { | |
STOP = 0x00, | |
ADD = 0x01, | |
MUL = 0x02, | |
SUB = 0x03, | |
DIV = 0x04, | |
POP = 0x50, | |
MLOAD = 0x51, | |
MSTORE = 0x52, | |
SLOAD = 0x54, | |
SSTORE = 0x55, | |
JUMP = 0x56, | |
JUMPI = 0x57, | |
PC = 0x58, | |
MSIZE = 0x59, | |
JUMPDEST = 0x5b, | |
PUSH1 = 0x60, // … up to PUSH32 = 0x7f | |
} | |
// —— Stack ————————————————————————————————————————————————— | |
class Stack { | |
items: U256[] = []; | |
limit: number; | |
constructor(limit = 1024) { this.limit = limit; } | |
push(x: U256) { | |
if (this.items.length >= this.limit) throw new Error('StackOverflow'); | |
this.items.push(x); | |
} | |
pop(): U256 { | |
const v = this.items.pop(); | |
if (v === undefined) throw new Error('StackUnderflow'); | |
return v; | |
} | |
} | |
// —— VM ——————————————————————————————————————————————————— | |
class EVM { | |
pc = 0; | |
stack = new Stack(); | |
// word-aligned memory & storage | |
memory = new Map<number, U256>(); | |
storage = new Map<bigint, U256>(); | |
stopped = false; | |
gasUsed = 0; | |
gasLimit: number; | |
constructor( | |
public bytecode: Uint8Array, | |
gasLimit = 1_000_000 | |
) { | |
this.gasLimit = gasLimit; | |
} | |
private charge(amount = 1) { | |
if (this.gasUsed + amount > this.gasLimit) { | |
throw new Error('OutOfGas'); | |
} | |
this.gasUsed += amount; | |
} | |
step() { | |
const op = this.bytecode[this.pc++]; | |
this.charge(); | |
// PUSH1..PUSH32 | |
if (op >= Opcode.PUSH1 && op <= Opcode.PUSH1 + 31) { | |
const len = op - Opcode.PUSH1 + 1; | |
const data = this.bytecode.slice(this.pc, this.pc + len); | |
this.pc += len; | |
// pad to 32 bytes | |
const padded = new Uint8Array(32); | |
padded.set(data, 32 - data.length); | |
this.stack.push(fromBytes(padded)); | |
return; | |
} | |
switch (op) { | |
case Opcode.STOP: | |
this.stopped = true; | |
return; | |
case Opcode.ADD: { | |
const b = this.stack.pop(), a = this.stack.pop(); | |
this.stack.push(a + b); | |
return; | |
} | |
case Opcode.MUL: { | |
const b = this.stack.pop(), a = this.stack.pop(); | |
this.stack.push(a * b); | |
return; | |
} | |
case Opcode.SUB: { | |
const b = this.stack.pop(), a = this.stack.pop(); | |
this.stack.push(a - b); | |
return; | |
} | |
case Opcode.DIV: { | |
const b = this.stack.pop(), a = this.stack.pop(); | |
this.stack.push(b === 0n ? 0n : a / b); | |
return; | |
} | |
case Opcode.POP: | |
this.stack.pop(); | |
return; | |
case Opcode.MLOAD: { | |
const offset = Number(this.stack.pop()); | |
const val = this.memory.get(offset) ?? 0n; | |
this.stack.push(val); | |
return; | |
} | |
case Opcode.MSTORE: { | |
const offset = Number(this.stack.pop()); | |
const val = this.stack.pop(); | |
this.memory.set(offset, val); | |
return; | |
} | |
case Opcode.SLOAD: { | |
const slot = this.stack.pop(); | |
const val = this.storage.get(slot) ?? 0n; | |
this.stack.push(val); | |
return; | |
} | |
case Opcode.SSTORE: { | |
const slot = this.stack.pop(); | |
const val = this.stack.pop(); | |
this.storage.set(slot, val); | |
return; | |
} | |
case Opcode.PC: | |
this.stack.push(BigInt(this.pc - 1)); | |
return; | |
case Opcode.MSIZE: | |
// memory size in bytes = number of words * 32 | |
this.stack.push(BigInt(this.memory.size * 32)); | |
return; | |
case Opcode.JUMP: { | |
const dest = Number(this.stack.pop()); | |
if (this.bytecode[dest] !== Opcode.JUMPDEST) { | |
throw new Error('InvalidJump'); | |
} | |
this.pc = dest; | |
return; | |
} | |
case Opcode.JUMPI: { | |
const dest = Number(this.stack.pop()); | |
const cond = this.stack.pop(); | |
if (cond !== 0n) { | |
if (this.bytecode[dest] !== Opcode.JUMPDEST) { | |
throw new Error('InvalidJump'); | |
} | |
this.pc = dest; | |
} | |
return; | |
} | |
case Opcode.JUMPDEST: | |
return; // no-op | |
default: | |
throw new Error(`Unsupported opcode 0x${op.toString(16)}`); | |
} | |
} | |
run() { | |
while (!this.stopped) this.step(); | |
} | |
} | |
// —— Example ————————————————————————————————————————————— | |
const code = Uint8Array.from([ | |
Opcode.PUSH1, 5, | |
Opcode.PUSH1, 3, | |
Opcode.ADD, | |
Opcode.PUSH1, 0, | |
Opcode.MSTORE, | |
Opcode.PUSH1, 0, | |
Opcode.MLOAD, | |
Opcode.PUSH1, 0, | |
Opcode.PUSH1, 32, | |
Opcode.SSTORE, | |
Opcode.STOP, | |
]); | |
const evm = new EVM(code); | |
evm.run(); | |
console.log('Gas used:', evm.gasUsed); | |
console.log('Stack:', evm.stack.items); | |
console.log('Memory words:', evm.memory); | |
console.log('Storage:', evm.storage); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment