Skip to content

Commit 581554c

Browse files
authored
Add MCOPY memory copy opcode (EIP-5656) (#21)
Implement MCOPY opcode (0x5E) from the Cancun hard fork (EIP-5656)
1 parent f7c1af9 commit 581554c

6 files changed

Lines changed: 178 additions & 0 deletions

File tree

lib/eevm/executor.ex

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -126,6 +126,7 @@ defmodule EEVM.Executor do
126126
defp execute_opcode(0x50, state), do: StackMemoryStorage.execute(0x50, state)
127127
defp execute_opcode(op, state) when op in 0x51..0x55, do: StackMemoryStorage.execute(op, state)
128128
defp execute_opcode(0x59, state), do: StackMemoryStorage.execute(0x59, state)
129+
defp execute_opcode(0x5E, state), do: StackMemoryStorage.execute(0x5E, state)
129130
defp execute_opcode(0x5A, state), do: Environment.execute(0x5A, state)
130131
defp execute_opcode(op, state) when op in 0x56..0x5B, do: ControlFlow.execute(op, state)
131132
defp execute_opcode(0x5F, state), do: ControlFlow.execute(0x5F, state)

lib/eevm/gas.ex

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@ defmodule EEVM.Gas do
4444
@gas_exp_byte 50
4545
@gas_keccak256 30
4646
@gas_keccak256_word 6
47+
@gas_copy 3
4748
@gas_memory 3
4849
@gas_sload 200
4950
@gas_sstore 20_000
@@ -191,6 +192,7 @@ defmodule EEVM.Gas do
191192
def static_cost(0x58), do: @gas_base
192193
# MSIZE
193194
def static_cost(0x59), do: @gas_base
195+
def static_cost(0x5E), do: @gas_very_low
194196
# JUMPDEST
195197
def static_cost(0x5B), do: @gas_jumpdest
196198

@@ -251,6 +253,10 @@ defmodule EEVM.Gas do
251253
@gas_keccak256_word * words
252254
end
253255

256+
@doc "Dynamic gas for copy operations: 3 gas per 32-byte word (ceiling)."
257+
@spec copy_cost(non_neg_integer()) :: non_neg_integer()
258+
def copy_cost(size), do: div(size + 31, 32) * @gas_copy
259+
254260
@doc """
255261
Calculates the gas cost of memory expansion.
256262

lib/eevm/memory.ex

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -105,6 +105,26 @@ defmodule EEVM.Memory do
105105

106106
def read_bytes(memory, _offset, 0), do: {<<>>, memory}
107107

108+
@doc """
109+
Copies `length` bytes from `src` to `dst` within memory (memmove semantics).
110+
111+
This is the MCOPY operation (EIP-5656). Handles overlapping regions correctly
112+
by reading all source bytes before writing — similar to C's `memmove`.
113+
"""
114+
@spec copy(t(), non_neg_integer(), non_neg_integer(), non_neg_integer()) :: t()
115+
def copy(memory, _dst, _src, 0), do: memory
116+
117+
def copy(%__MODULE__{} = memory, dst, src, length) do
118+
{bytes, memory_after_read} = read_bytes(memory, src, length)
119+
120+
bytes
121+
|> :binary.bin_to_list()
122+
|> Enum.with_index()
123+
|> Enum.reduce(memory_after_read, fn {byte, i}, acc ->
124+
store_byte(acc, dst + i, byte)
125+
end)
126+
end
127+
108128
@doc "Returns the current memory size in bytes (always a multiple of 32)."
109129
@spec size(t()) :: non_neg_integer()
110130
def size(%__MODULE__{size: size}), do: size

lib/eevm/opcodes.ex

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,7 @@ defmodule EEVM.Opcodes do
8484
@sload 0x54
8585
@sstore 0x55
8686
@msize 0x59
87+
@mcopy 0x5E
8788
@jump 0x56
8889
@jumpi 0x57
8990
@pc 0x58
@@ -177,6 +178,7 @@ defmodule EEVM.Opcodes do
177178
def info(@sload), do: {:ok, %{name: "SLOAD", inputs: 1, outputs: 1}}
178179
def info(@sstore), do: {:ok, %{name: "SSTORE", inputs: 2, outputs: 0}}
179180
def info(@msize), do: {:ok, %{name: "MSIZE", inputs: 0, outputs: 1}}
181+
def info(@mcopy), do: {:ok, %{name: "MCOPY", inputs: 3, outputs: 0}}
180182
def info(@jump), do: {:ok, %{name: "JUMP", inputs: 1, outputs: 0}}
181183
def info(@jumpi), do: {:ok, %{name: "JUMPI", inputs: 2, outputs: 0}}
182184
def info(@pc), do: {:ok, %{name: "PC", inputs: 0, outputs: 1}}

lib/eevm/opcodes/stack_memory_storage.ex

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -151,5 +151,30 @@ defmodule EEVM.Opcodes.StackMemoryStorage do
151151
Helpers.push_value(state, size)
152152
end
153153

154+
def execute(0x5E, state) do
155+
with {:ok, dst, s1} <- Stack.pop(state.stack),
156+
{:ok, src, s2} <- Stack.pop(s1),
157+
{:ok, length, s3} <- Stack.pop(s2) do
158+
if length == 0 do
159+
{:ok, MachineState.advance_pc(%{state | stack: s3})}
160+
else
161+
max_offset = max(src + length, dst + length)
162+
expansion_cost = Gas.memory_expansion_cost(Memory.size(state.memory), 0, max_offset)
163+
dynamic_cost = Gas.copy_cost(length) + expansion_cost
164+
165+
case MachineState.consume_gas(%{state | stack: s3}, dynamic_cost) do
166+
{:ok, s4} ->
167+
new_memory = Memory.copy(s4.memory, dst, src, length)
168+
{:ok, MachineState.advance_pc(%{s4 | memory: new_memory})}
169+
170+
{:error, :out_of_gas, halted_state} ->
171+
{:error, :out_of_gas, halted_state}
172+
end
173+
end
174+
else
175+
{:error, reason} -> {:error, reason, state}
176+
end
177+
end
178+
154179
def execute(_opcode, state), do: {:ok, MachineState.halt(state, :invalid)}
155180
end

test/opcodes/stack_memory_test.exs

Lines changed: 124 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
defmodule EEVM.Opcodes.StackMemoryTest do
22
use ExUnit.Case, async: true
33

4+
alias EEVM.Gas
5+
46
describe "Executor - Memory" do
57
test "MSTORE and MLOAD" do
68
# PUSH1 0xFF, PUSH1 0, MSTORE, PUSH1 0, MLOAD, STOP
@@ -16,4 +18,126 @@ defmodule EEVM.Opcodes.StackMemoryTest do
1618
assert EEVM.stack_values(result) == [32]
1719
end
1820
end
21+
22+
# ── MCOPY Tests (EIP-5656) ──────
23+
describe "MCOPY (EIP-5656)" do
24+
test "non-overlapping copy" do
25+
code =
26+
IO.iodata_to_binary([
27+
mstore8(0, 0xAA),
28+
mstore8(1, 0xBB),
29+
mstore8(2, 0xCC),
30+
mstore8(3, 0xDD),
31+
mcopy(32, 0, 4),
32+
mload(32),
33+
<<0x00>>
34+
])
35+
36+
result = EEVM.execute(code)
37+
[word] = EEVM.stack_values(result)
38+
39+
assert bytes32(word) |> Enum.take(4) == [0xAA, 0xBB, 0xCC, 0xDD]
40+
end
41+
42+
test "overlapping forward copy uses memmove semantics" do
43+
code =
44+
IO.iodata_to_binary([
45+
mstore8(0, 0x11),
46+
mstore8(1, 0x22),
47+
mstore8(2, 0x33),
48+
mstore8(3, 0x44),
49+
mcopy(1, 0, 3),
50+
mload(0),
51+
<<0x00>>
52+
])
53+
54+
result = EEVM.execute(code)
55+
[word] = EEVM.stack_values(result)
56+
57+
assert bytes32(word) |> Enum.take(4) == [0x11, 0x11, 0x22, 0x33]
58+
end
59+
60+
test "overlapping backward copy uses memmove semantics" do
61+
code =
62+
IO.iodata_to_binary([
63+
mstore8(0, 0x11),
64+
mstore8(1, 0x22),
65+
mstore8(2, 0x33),
66+
mstore8(3, 0x44),
67+
mcopy(0, 1, 3),
68+
mload(0),
69+
<<0x00>>
70+
])
71+
72+
result = EEVM.execute(code)
73+
[word] = EEVM.stack_values(result)
74+
75+
assert bytes32(word) |> Enum.take(4) == [0x22, 0x33, 0x44, 0x44]
76+
end
77+
78+
test "zero-length copy is a no-op and keeps msize unchanged" do
79+
code =
80+
IO.iodata_to_binary([
81+
mstore8(0, 0xAA),
82+
<<0x59>>,
83+
mcopy(0, 0, 0),
84+
<<0x59, 0x00>>
85+
])
86+
87+
result = EEVM.execute(code)
88+
assert EEVM.stack_values(result) == [32, 32]
89+
end
90+
91+
test "gas calculation includes static, copy words, and memory expansion" do
92+
code = IO.iodata_to_binary([mcopy(64, 0, 33), <<0x00>>])
93+
result = EEVM.execute(code, gas: 100_000)
94+
95+
expected =
96+
3 + 3 + 3 + Gas.static_cost(0x5E) + Gas.copy_cost(33) +
97+
Gas.memory_expansion_cost(0, 0, 97)
98+
99+
assert result.status == :stopped
100+
assert result.gas == 100_000 - expected
101+
end
102+
103+
test "memory expansion covers both src and dst ranges" do
104+
code = IO.iodata_to_binary([mcopy(0, 96, 32), <<0x59, 0x00>>])
105+
result = EEVM.execute(code)
106+
assert EEVM.stack_values(result) == [128]
107+
end
108+
109+
test "large copy across word boundaries" do
110+
writes =
111+
for i <- 0..39 do
112+
mstore8(i, i + 1)
113+
end
114+
115+
code =
116+
IO.iodata_to_binary([
117+
writes,
118+
mcopy(64, 0, 40),
119+
mload(96),
120+
mload(64),
121+
<<0x00>>
122+
])
123+
124+
result = EEVM.execute(code)
125+
[word1, word2] = EEVM.stack_values(result)
126+
127+
assert bytes32(word1) == Enum.to_list(1..32)
128+
assert bytes32(word2) |> Enum.take(8) == Enum.to_list(33..40)
129+
assert bytes32(word2) |> Enum.drop(8) == List.duplicate(0, 24)
130+
end
131+
end
132+
133+
defp push1(value), do: <<0x60, value>>
134+
135+
defp mstore8(offset, byte), do: IO.iodata_to_binary([push1(byte), push1(offset), <<0x53>>])
136+
137+
defp mcopy(dst, src, length),
138+
do: IO.iodata_to_binary([push1(length), push1(src), push1(dst), <<0x5E>>])
139+
140+
defp mload(offset), do: IO.iodata_to_binary([push1(offset), <<0x51>>])
141+
142+
defp bytes32(word), do: :binary.bin_to_list(<<word::unsigned-big-integer-size(256)>>)
19143
end

0 commit comments

Comments
 (0)