Skip to content

Commit 8d21208

Browse files
authored
Implement CODECOPY opcode (#22)
- Add CODECOPY (0x39) opcode metadata and gas configuration. - Implement CODECOPY execution with dynamic copy + memory expansion costs and zero-padding beyond code bounds. - Add focused opcode tests covering in-bounds copy, out-of-bounds zero-padding, zero-length behavior, and gas accounting.
1 parent d3d9502 commit 8d21208

4 files changed

Lines changed: 83 additions & 0 deletions

File tree

lib/eevm/gas.ex

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -153,6 +153,7 @@ defmodule EEVM.Gas do
153153
def static_cost(0x37), do: @gas_very_low
154154
# CODESIZE
155155
def static_cost(0x38), do: @gas_base
156+
def static_cost(0x39), do: @gas_very_low
156157
# GASPRICE
157158
def static_cost(0x3A), do: @gas_base
158159
# RETURNDATASIZE

lib/eevm/opcodes.ex

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,7 @@ defmodule EEVM.Opcodes do
6363
@calldatasize 0x36
6464
@calldatacopy 0x37
6565
@codesize 0x38
66+
@codecopy 0x39
6667
@gasprice 0x3A
6768
@returndatasize 0x3D
6869
@returndatacopy 0x3E
@@ -162,6 +163,7 @@ defmodule EEVM.Opcodes do
162163
def info(@calldataload), do: {:ok, %{name: "CALLDATALOAD", inputs: 1, outputs: 1}}
163164
def info(@calldatasize), do: {:ok, %{name: "CALLDATASIZE", inputs: 0, outputs: 1}}
164165
def info(@calldatacopy), do: {:ok, %{name: "CALLDATACOPY", inputs: 3, outputs: 0}}
166+
def info(@codecopy), do: {:ok, %{name: "CODECOPY", inputs: 3, outputs: 0}}
165167
def info(@returndatacopy), do: {:ok, %{name: "RETURNDATACOPY", inputs: 3, outputs: 0}}
166168
def info(@codesize), do: {:ok, %{name: "CODESIZE", inputs: 0, outputs: 1}}
167169
def info(@gasprice), do: {:ok, %{name: "GASPRICE", inputs: 0, outputs: 1}}

lib/eevm/opcodes/environment.ex

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -134,6 +134,41 @@ defmodule EEVM.Opcodes.Environment do
134134
end
135135

136136
def execute(0x38, state), do: Helpers.push_value(state, byte_size(state.code))
137+
138+
def execute(0x39, state) do
139+
with {:ok, dest_offset, s1} <- Stack.pop(state.stack),
140+
{:ok, code_offset, s2} <- Stack.pop(s1),
141+
{:ok, length, s3} <- Stack.pop(s2) do
142+
if length == 0 do
143+
{:ok, %{state | stack: s3} |> MachineState.advance_pc()}
144+
else
145+
dynamic_cost =
146+
Gas.copy_cost(length) +
147+
Gas.memory_expansion_cost(Memory.size(state.memory), dest_offset, length)
148+
149+
case MachineState.consume_gas(%{state | stack: s3}, dynamic_cost) do
150+
{:ok, state_after_gas} ->
151+
bytes = MachineState.read_code(state_after_gas, code_offset, length)
152+
153+
new_memory =
154+
bytes
155+
|> :binary.bin_to_list()
156+
|> Enum.with_index()
157+
|> Enum.reduce(state_after_gas.memory, fn {byte, i}, mem ->
158+
Memory.store_byte(mem, dest_offset + i, byte)
159+
end)
160+
161+
{:ok, %{state_after_gas | memory: new_memory} |> MachineState.advance_pc()}
162+
163+
{:error, :out_of_gas, halted_state} ->
164+
{:error, :out_of_gas, halted_state}
165+
end
166+
end
167+
else
168+
{:error, reason} -> {:error, reason, state}
169+
end
170+
end
171+
137172
def execute(0x3A, state), do: Helpers.push_value(state, state.tx.gasprice)
138173
def execute(0x3D, state), do: Helpers.push_value(state, byte_size(state.return_data))
139174

test/opcodes/environment_test.exs

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -236,6 +236,51 @@ defmodule EEVM.Opcodes.EnvironmentTest do
236236
end
237237
end
238238

239+
describe "CODECOPY (0x39)" do
240+
test "copies code bytes to memory" do
241+
code = <<0x60, 4, 0x60, 11, 0x60, 0, 0x39, 0x60, 0, 0x51, 0x00, 0xAA, 0xBB, 0xCC, 0xDD>>
242+
243+
result = EEVM.execute(code)
244+
245+
expected = 0xAABBCCDD00000000000000000000000000000000000000000000000000000000
246+
assert EEVM.stack_values(result) == [expected]
247+
end
248+
249+
test "zero-pads when reading beyond code length" do
250+
code = <<0x60, 4, 0x60, 13, 0x60, 0, 0x39, 0x60, 0, 0x51, 0x00, 0xAA, 0xBB, 0xCC, 0xDD>>
251+
252+
result = EEVM.execute(code)
253+
254+
expected = :binary.decode_unsigned(<<0xCC, 0xDD, 0x00, 0x00, 0::224>>)
255+
assert EEVM.stack_values(result) == [expected]
256+
end
257+
258+
test "zero-length copy is a no-op" do
259+
code = <<0x60, 0, 0x60, 200, 0x60, 10, 0x39, 0x59, 0x00>>
260+
261+
result = EEVM.execute(code)
262+
263+
assert result.status == :stopped
264+
assert EEVM.stack_values(result) == [0]
265+
end
266+
267+
test "gas calculation includes static, copy, and memory expansion" do
268+
code = <<0x60, 4, 0x60, 11, 0x60, 0, 0x39, 0x60, 0, 0x51, 0x00, 0xAA, 0xBB, 0xCC, 0xDD>>
269+
initial_gas = 1_000
270+
271+
result = EEVM.execute(code, gas: initial_gas)
272+
273+
expected_spent =
274+
Gas.static_cost(0x60) * 4 +
275+
Gas.static_cost(0x39) +
276+
Gas.copy_cost(4) +
277+
Gas.memory_expansion_cost(0, 0, 4) +
278+
Gas.static_cost(0x51)
279+
280+
assert result.gas == initial_gas - expected_spent
281+
end
282+
end
283+
239284
describe "RETURNDATACOPY (0x3E)" do
240285
test "copies return data to memory" do
241286
return_data = <<0xAA, 0xBB, 0xCC, 0xDD>>

0 commit comments

Comments
 (0)