Skip to content

Commit 3886b0d

Browse files
feat: EIP-8024 — DUPN, SWAPN, EXCHANGE opcodes (Amsterdam) (#11)
* feat: EIP-8024 — DUPN, SWAPN, EXCHANGE opcodes (Amsterdam) Signed-off-by: garyschulte <garyschulte@gmail.com>
1 parent 96b3306 commit 3886b0d

File tree

9 files changed

+389
-19
lines changed

9 files changed

+389
-19
lines changed

src/bytecode/main.zig

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -226,6 +226,9 @@ pub const BASEFEE: u8 = 0x48;
226226
pub const BLOBHASH: u8 = 0x49;
227227
pub const BLOBBASEFEE: u8 = 0x4A;
228228
pub const SLOTNUM: u8 = 0x4B;
229+
pub const DUPN: u8 = 0xE6;
230+
pub const SWAPN: u8 = 0xE7;
231+
pub const EXCHANGE: u8 = 0xE8;
229232
pub const POP: u8 = 0x50;
230233
pub const MLOAD: u8 = 0x51;
231234
pub const MSTORE: u8 = 0x52;
@@ -486,6 +489,9 @@ pub const OPCODE_INFO: [256]?OpCodeInfo = blk: {
486489
map[LOG4] = OpCodeInfo{ .name = "LOG4", .inputs = 6, .outputs = 0, .immediate_size = 0, .terminating = false };
487490

488491
// System operations
492+
map[DUPN] = OpCodeInfo{ .name = "DUPN", .inputs = 0, .outputs = 1, .immediate_size = 1, .terminating = false };
493+
map[SWAPN] = OpCodeInfo{ .name = "SWAPN", .inputs = 0, .outputs = 0, .immediate_size = 1, .terminating = false };
494+
map[EXCHANGE] = OpCodeInfo{ .name = "EXCHANGE", .inputs = 0, .outputs = 0, .immediate_size = 1, .terminating = false };
489495
map[CREATE] = OpCodeInfo{ .name = "CREATE", .inputs = 3, .outputs = 1, .immediate_size = 0, .terminating = false };
490496
map[CALL] = OpCodeInfo{ .name = "CALL", .inputs = 7, .outputs = 1, .immediate_size = 0, .terminating = false };
491497
map[CALLCODE] = OpCodeInfo{ .name = "CALLCODE", .inputs = 7, .outputs = 1, .immediate_size = 0, .terminating = false };

src/interpreter/gas.zig

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -199,15 +199,18 @@ pub const MemoryGas = struct {
199199
return 0;
200200
}
201201

202-
const new_words = (new_size + 31) / 32;
203-
const current_words = (self.size + 31) / 32;
202+
// std.math.divCeil(u64, n, 32) avoids (n + 31) overflow for large n.
203+
const new_words = std.math.divCeil(u64, new_size, 32) catch return std.math.maxInt(u64);
204+
const current_words = std.math.divCeil(u64, self.size, 32) catch return std.math.maxInt(u64);
204205

205206
if (new_words <= current_words) {
206207
return 0;
207208
}
208209

209-
const cost = (new_words * new_words) / 512 + (3 * new_words);
210-
const current_cost = (current_words * current_words) / 512 + (3 * current_words);
210+
const sq_new = std.math.mul(u64, new_words, new_words) catch return std.math.maxInt(u64);
211+
const cost = std.math.add(u64, sq_new / 512, 3 * new_words) catch return std.math.maxInt(u64);
212+
const sq_cur = std.math.mul(u64, current_words, current_words) catch return std.math.maxInt(u64);
213+
const current_cost = std.math.add(u64, sq_cur / 512, 3 * current_words) catch return std.math.maxInt(u64);
211214

212215
return cost - current_cost;
213216
}

src/interpreter/gas_costs.zig

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -87,9 +87,10 @@ pub fn memoryExpansionCost(current_words: usize, new_words: usize) u64 {
8787
}
8888

8989
fn memoryCost(num_words: usize) u64 {
90-
const linear = num_words * G_MEMORY;
91-
const quadratic = (num_words * num_words) / 512;
92-
return @intCast(linear + quadratic);
90+
const n: u64 = @intCast(num_words);
91+
const linear = std.math.mul(u64, n, G_MEMORY) catch return std.math.maxInt(u64);
92+
const quadratic = (std.math.mul(u64, n, n) catch return std.math.maxInt(u64)) / 512;
93+
return std.math.add(u64, linear, quadratic) catch std.math.maxInt(u64);
9394
}
9495

9596
// Calculate memory size in words (rounded up)

src/interpreter/memory.zig

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -284,15 +284,20 @@ pub const Memory = struct {
284284
return 0;
285285
}
286286

287-
const new_words = (new_size + 31) / 32;
288-
const current_words = (self.buffer.items.len + 31) / 32;
287+
// std.math.divCeil avoids (n + 31) overflow when n is near maxInt(usize).
288+
const new_words = std.math.divCeil(usize, new_size, 32) catch return std.math.maxInt(u64);
289+
const current_words = std.math.divCeil(usize, self.buffer.items.len, 32) catch return std.math.maxInt(u64);
289290

290291
if (new_words <= current_words) {
291292
return 0;
292293
}
293294

294-
const cost = (new_words * new_words) / 512 + (3 * new_words);
295-
const current_cost = (current_words * current_words) / 512 + (3 * current_words);
295+
const n: u64 = @intCast(new_words);
296+
const c: u64 = @intCast(current_words);
297+
const sq_n = std.math.mul(u64, n, n) catch return std.math.maxInt(u64);
298+
const cost = std.math.add(u64, sq_n / 512, 3 * n) catch return std.math.maxInt(u64);
299+
const sq_c = std.math.mul(u64, c, c) catch return std.math.maxInt(u64);
300+
const current_cost = std.math.add(u64, sq_c / 512, 3 * c) catch return std.math.maxInt(u64);
296301

297302
return cost - current_cost;
298303
}

src/interpreter/opcodes/keccak.zig

Lines changed: 15 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -36,30 +36,37 @@ pub fn opKeccak256(ctx: *InstructionContext) void {
3636
}
3737
const offset_usize: usize = if (length_usize == 0) 0 else @intCast(offset);
3838

39-
// Dynamic: word cost
40-
const num_words: u64 = (length_usize + 31) / 32;
39+
// Compute end = offset + length before word-cost so a huge length is caught early.
40+
const end = if (length_usize > 0)
41+
std.math.add(usize, offset_usize, length_usize) catch {
42+
ctx.interpreter.halt(.memory_limit_oog);
43+
return;
44+
}
45+
else
46+
offset_usize;
47+
48+
// Dynamic: word cost — std.math.divCeil avoids (length + 31) overflow when
49+
// length_usize is near maxInt(usize) (e.g. from GASLIMIT with maxInt gas limit).
50+
const num_words: u64 = @intCast(std.math.divCeil(usize, length_usize, 32) catch unreachable);
4151
const word_cost = gas_costs.G_KECCAK256WORD * num_words;
4252
if (!ctx.interpreter.gas.spend(word_cost)) {
4353
ctx.interpreter.halt(.out_of_gas);
4454
return;
4555
}
4656

4757
// Dynamic: memory expansion
48-
const end = std.math.add(usize, offset_usize, length_usize) catch {
49-
ctx.interpreter.halt(.memory_limit_oog);
50-
return;
51-
};
5258
if (length_usize > 0) {
5359
const current_words = (ctx.interpreter.memory.size() + 31) / 32;
54-
const new_words = (end + 31) / 32;
60+
// std.math.divCeil avoids (end + 31) overflow when end is near maxInt(usize).
61+
const new_words = std.math.divCeil(usize, end, 32) catch unreachable;
5562
if (new_words > current_words) {
5663
const expansion_cost = memoryCostWords(new_words) - memoryCostWords(current_words);
5764
if (!ctx.interpreter.gas.spend(expansion_cost)) {
5865
ctx.interpreter.halt(.out_of_gas);
5966
return;
6067
}
6168
}
62-
const aligned_end = ((end + 31) / 32) * 32;
69+
const aligned_end = new_words * 32;
6370
if (aligned_end > ctx.interpreter.memory.size()) {
6471
const old_size = ctx.interpreter.memory.size();
6572
ctx.interpreter.memory.buffer.resize(alloc_mod.get(), aligned_end) catch {

src/interpreter/opcodes/main.zig

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,9 @@ pub const opPush0 = stack.opPush0;
4040
pub const makePushFn = stack.makePushFn;
4141
pub const makeDupFn = stack.makeDupFn;
4242
pub const makeSwapFn = stack.makeSwapFn;
43+
pub const opDupN = stack.opDupN;
44+
pub const opSwapN = stack.opSwapN;
45+
pub const opExchange = stack.opExchange;
4346

4447
// Control flow operations
4548
pub const control = @import("control.zig");

src/interpreter/opcodes/stack.zig

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,86 @@ pub fn makeSwapFn(comptime n: u8) InstructionFn {
9292
}.op;
9393
}
9494

95+
/// DUPN (0xE6): Duplicate item at depth n from top (EIP-8024, Amsterdam+).
96+
/// Reads 1 immediate byte `imm`. Valid range: 0..=90 or 128..=255 (91..=127 → exceptional halt).
97+
/// Depth n = decode_single(imm) = (imm + 145) % 256, with 17 <= n <= 235.
98+
/// Gas: 3 (G_VERYLOW, charged by dispatch).
99+
pub fn opDupN(ctx: *InstructionContext) void {
100+
const stack = &ctx.interpreter.stack;
101+
const imm = ctx.interpreter.bytecode.readImmediates(1)[0];
102+
ctx.interpreter.bytecode.relativeJump(1);
103+
// EIP-8024: immediates 91..=127 (0x5B..=0x7F) are invalid per decode_single.
104+
if (imm > 90 and imm < 128) {
105+
ctx.interpreter.halt(.invalid_opcode);
106+
return;
107+
}
108+
const n: usize = (@as(usize, imm) + 145) % 256;
109+
if (!stack.hasItems(n)) {
110+
ctx.interpreter.halt(.stack_underflow);
111+
return;
112+
}
113+
if (!stack.hasSpace(1)) {
114+
ctx.interpreter.halt(.stack_overflow);
115+
return;
116+
}
117+
stack.dupUnsafe(n);
118+
}
119+
120+
/// SWAPN (0xE7): Swap top with item at 0-indexed depth n (EIP-8024, Amsterdam+).
121+
/// Reads 1 immediate byte `imm`. Valid range: 0..=90 or 128..=255 (91..=127 → exceptional halt).
122+
/// Depth n = decode_single(imm) = (imm + 145) % 256, with 17 <= n <= 235.
123+
/// Gas: 3 (G_VERYLOW, charged by dispatch).
124+
pub fn opSwapN(ctx: *InstructionContext) void {
125+
const stack = &ctx.interpreter.stack;
126+
const imm = ctx.interpreter.bytecode.readImmediates(1)[0];
127+
ctx.interpreter.bytecode.relativeJump(1);
128+
// EIP-8024: immediates 91..=127 (0x5B..=0x7F) are invalid per decode_single.
129+
if (imm > 90 and imm < 128) {
130+
ctx.interpreter.halt(.invalid_opcode);
131+
return;
132+
}
133+
const n: usize = (@as(usize, imm) + 145) % 256;
134+
if (!stack.hasItems(n + 1)) {
135+
ctx.interpreter.halt(.stack_underflow);
136+
return;
137+
}
138+
stack.swapUnsafe(n);
139+
}
140+
141+
/// EXCHANGE (0xE8): Swap two non-top stack items (EIP-8024, Amsterdam+).
142+
/// Immediate byte `x` decoded via EIP-8024 decode_pair:
143+
/// k = x ^ 143; q = k >> 4; r = k & 0xF
144+
/// if q < r: n = q+1, m = r+1
145+
/// else: n = r+1, m = 29-q
146+
/// Valid range: 0..=81 or 128..=255 (82..=127 → exceptional failure).
147+
/// Swaps stack[top - n] and stack[top - m], needs m+1 items (n < m always).
148+
/// Gas: 3 (G_VERYLOW, charged by dispatch).
149+
pub fn opExchange(ctx: *InstructionContext) void {
150+
const stack = &ctx.interpreter.stack;
151+
const imm = ctx.interpreter.bytecode.readImmediates(1)[0];
152+
ctx.interpreter.bytecode.relativeJump(1);
153+
// Immediates 82..=127 (0x52..=0x7F) are invalid per EIP-8024.
154+
if (imm >= 82 and imm <= 127) {
155+
ctx.interpreter.halt(.invalid_opcode);
156+
return;
157+
}
158+
// decode_pair: k = imm XOR 143, q = k >> 4, r = k & 0xF
159+
const k: usize = @as(usize, imm) ^ 143;
160+
const q: usize = k >> 4;
161+
const r: usize = k & 0xF;
162+
const n: usize = if (q < r) q + 1 else r + 1; // smaller depth (1..14)
163+
const m: usize = if (q < r) r + 1 else 29 - q; // larger depth (n < m)
164+
// Need m+1 items on stack.
165+
if (!stack.hasItems(m + 1)) {
166+
ctx.interpreter.halt(.stack_underflow);
167+
return;
168+
}
169+
const top_idx = stack.length - 1;
170+
const tmp = stack.data[top_idx - n];
171+
stack.data[top_idx - n] = stack.data[top_idx - m];
172+
stack.data[top_idx - m] = tmp;
173+
}
174+
95175
test {
96176
_ = @import("stack_tests.zig");
97177
}

0 commit comments

Comments
 (0)