Skip to content

Commit d3d9502

Browse files
authored
Add LOG0–LOG4 event logging opcodes (#19)
Add LOG0-LOG4 opcodes Implements the five LOG opcodes (0xA0-0xA4) for emitting events. Each pops a memory region and N topic words off the stack, charges 375 + 375*topics + 8*data_bytes gas, reads the data from memory, and appends a log entry to the machine state. Logs are accessible via EEVM.logs/1 after execution.
1 parent da0f4df commit d3d9502

7 files changed

Lines changed: 247 additions & 1 deletion

File tree

lib/eevm.ex

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ defmodule EEVM do
3434
- **Typespecs** — `@spec` annotations for documentation and Dialyzer
3535
"""
3636

37-
alias EEVM.{Executor, Stack}
37+
alias EEVM.{Executor, MachineState, Stack}
3838

3939
@doc """
4040
Executes EVM bytecode and returns the final machine state.
@@ -63,6 +63,10 @@ defmodule EEVM do
6363
Stack.to_list(state.stack)
6464
end
6565

66+
@doc "Returns the list of logs emitted during execution."
67+
@spec logs(MachineState.t()) :: [map()]
68+
def logs(%MachineState{} = state), do: state.logs
69+
6670
@doc """
6771
Disassembles bytecode into a human-readable list of instructions.
6872

lib/eevm/executor.ex

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ defmodule EEVM.Executor do
4141
ControlFlow,
4242
Crypto,
4343
Environment,
44+
Logging,
4445
StackMemoryStorage,
4546
System
4647
}
@@ -131,6 +132,7 @@ defmodule EEVM.Executor do
131132
defp execute_opcode(op, state) when op in 0x56..0x5B, do: ControlFlow.execute(op, state)
132133
defp execute_opcode(0x5F, state), do: ControlFlow.execute(0x5F, state)
133134
defp execute_opcode(op, state) when op in 0x60..0x9F, do: ControlFlow.execute(op, state)
135+
defp execute_opcode(op, state) when op in 0xA0..0xA4, do: Logging.execute(op, state)
134136
defp execute_opcode(0xF3, state), do: System.execute(0xF3, state)
135137
defp execute_opcode(0xFD, state), do: System.execute(0xFD, state)
136138
defp execute_opcode(0xFE, state), do: System.execute(0xFE, state)

lib/eevm/gas.ex

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,9 @@ defmodule EEVM.Gas do
5151
@gas_blockhash 20
5252
@gas_balance 2600
5353
@gas_selfbalance 5
54+
@gas_log 375
55+
@gas_log_topic 375
56+
@gas_log_data 8
5457

5558
@doc """
5659
Returns the static gas cost for a given opcode byte.
@@ -215,6 +218,8 @@ defmodule EEVM.Gas do
215218
# SWAP1–SWAP16 (0x90–0x9F)
216219
def static_cost(op) when op >= 0x90 and op <= 0x9F, do: @gas_very_low
217220

221+
def static_cost(op) when op in 0xA0..0xA4, do: @gas_log
222+
218223
# System (0xF3, 0xFD, 0xFE)
219224
# RETURN (+ memory expansion)
220225
def static_cost(0xF3), do: @gas_zero
@@ -258,6 +263,11 @@ defmodule EEVM.Gas do
258263
@spec copy_cost(non_neg_integer()) :: non_neg_integer()
259264
def copy_cost(size), do: div(size + 31, 32) * @gas_copy
260265

266+
@doc "Dynamic gas for LOGn: base + per-topic + per-byte-of-data."
267+
@spec log_cost(non_neg_integer(), non_neg_integer()) :: non_neg_integer()
268+
def log_cost(topic_count, data_size),
269+
do: @gas_log + @gas_log_topic * topic_count + @gas_log_data * data_size
270+
261271
@doc """
262272
Calculates the gas cost of memory expansion.
263273

lib/eevm/machine_state.ex

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ defmodule EEVM.MachineState do
3737
gas: non_neg_integer(),
3838
status: status(),
3939
return_data: binary(),
40+
logs: [%{address: non_neg_integer(), data: binary(), topics: [non_neg_integer()]}],
4041
code: binary()
4142
}
4243

@@ -51,6 +52,7 @@ defmodule EEVM.MachineState do
5152
gas: 1_000_000,
5253
status: :running,
5354
return_data: <<>>,
55+
logs: [],
5456
code: <<>>
5557

5658
@doc """

lib/eevm/opcodes.ex

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,12 @@ defmodule EEVM.Opcodes do
103103
@swap1 0x90
104104
@swap16 0x9F
105105

106+
@log0 0xA0
107+
@log1 0xA1
108+
@log2 0xA2
109+
@log3 0xA3
110+
@log4 0xA4
111+
106112
# System operations
107113
@return_ 0xF3
108114
@revert 0xFD
@@ -186,6 +192,12 @@ defmodule EEVM.Opcodes do
186192
def info(@pc), do: {:ok, %{name: "PC", inputs: 0, outputs: 1}}
187193
def info(@jumpdest), do: {:ok, %{name: "JUMPDEST", inputs: 0, outputs: 0}}
188194

195+
def info(@log0), do: {:ok, %{name: "LOG0", inputs: 2, outputs: 0}}
196+
def info(@log1), do: {:ok, %{name: "LOG1", inputs: 3, outputs: 0}}
197+
def info(@log2), do: {:ok, %{name: "LOG2", inputs: 4, outputs: 0}}
198+
def info(@log3), do: {:ok, %{name: "LOG3", inputs: 5, outputs: 0}}
199+
def info(@log4), do: {:ok, %{name: "LOG4", inputs: 6, outputs: 0}}
200+
189201
def info(@return_), do: {:ok, %{name: "RETURN", inputs: 2, outputs: 0}}
190202
def info(@revert), do: {:ok, %{name: "REVERT", inputs: 2, outputs: 0}}
191203
def info(@invalid), do: {:ok, %{name: "INVALID", inputs: 0, outputs: 0}}

lib/eevm/opcodes/logging.ex

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
defmodule EEVM.Opcodes.Logging do
2+
@moduledoc """
3+
EVM logging opcodes — LOG0 through LOG4.
4+
5+
## EVM Concepts
6+
7+
- LOG instructions emit event logs that are stored in transaction receipts.
8+
- Each log contains the emitting contract address, a data payload from memory,
9+
and 0–4 indexed topic values from the stack.
10+
- Topics are 256-bit values used for efficient off-chain filtering (e.g. event signatures).
11+
- Gas cost: 375 base + 375 per topic + 8 per byte of data + memory expansion.
12+
13+
## Elixir Learning Notes
14+
15+
- We use pattern matching on the topic count (0–4) to dispatch LOG variants.
16+
- Logs accumulate in `state.logs` as a list of maps, preserving insertion order.
17+
- `binary_part/3` extracts a slice from a binary — used to read log data from memory bytes.
18+
"""
19+
20+
alias EEVM.{Gas, MachineState, Memory, Stack}
21+
22+
@doc """
23+
Dispatches a LOG opcode (LOG0–LOG4) to the matching topic-count handler.
24+
"""
25+
@spec execute(non_neg_integer(), MachineState.t()) ::
26+
{:ok, MachineState.t()} | {:error, atom(), MachineState.t()}
27+
def execute(0xA0, state), do: execute_log(state, 0)
28+
def execute(0xA1, state), do: execute_log(state, 1)
29+
def execute(0xA2, state), do: execute_log(state, 2)
30+
def execute(0xA3, state), do: execute_log(state, 3)
31+
def execute(0xA4, state), do: execute_log(state, 4)
32+
33+
def execute(_opcode, state), do: {:ok, MachineState.halt(state, :invalid)}
34+
35+
defp execute_log(state, topic_count) do
36+
with {:ok, offset, s1} <- Stack.pop(state.stack),
37+
{:ok, size, s2} <- Stack.pop(s1),
38+
{:ok, topics, s3} <- pop_topics(s2, topic_count, []) do
39+
dynamic_cost =
40+
Gas.log_cost(topic_count, size) +
41+
Gas.memory_expansion_cost(Memory.size(state.memory), offset, size)
42+
43+
case MachineState.consume_gas(%{state | stack: s3}, dynamic_cost) do
44+
{:ok, s4} ->
45+
{data, new_memory} = Memory.read_bytes(s4.memory, offset, size)
46+
47+
log_entry = %{
48+
address: s4.contract.address,
49+
data: data,
50+
topics: topics
51+
}
52+
53+
s5 = %{s4 | memory: new_memory, logs: s4.logs ++ [log_entry]}
54+
{:ok, MachineState.advance_pc(s5)}
55+
56+
{:error, :out_of_gas, halted_state} ->
57+
{:error, :out_of_gas, halted_state}
58+
end
59+
else
60+
{:error, reason} -> {:error, reason, state}
61+
end
62+
end
63+
64+
defp pop_topics(stack, 0, acc), do: {:ok, Enum.reverse(acc), stack}
65+
66+
defp pop_topics(stack, n, acc) do
67+
case Stack.pop(stack) do
68+
{:ok, value, new_stack} -> pop_topics(new_stack, n - 1, [value | acc])
69+
error -> error
70+
end
71+
end
72+
end

test/opcodes/logging_test.exs

Lines changed: 144 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,144 @@
1+
defmodule EEVM.Opcodes.LoggingTest do
2+
use ExUnit.Case, async: true
3+
4+
alias EEVM.Context.Contract
5+
alias EEVM.Gas
6+
7+
describe "LOG opcodes" do
8+
# ── LOG0–LOG4 Tests ──────
9+
10+
test "LOG0 empty data (0-byte log, no topics)" do
11+
code = <<0x60, 0x00, 0x60, 0x00, 0xA0, 0x00>>
12+
result = EEVM.execute(code)
13+
14+
assert EEVM.logs(result) == [%{address: 0, data: <<>>, topics: []}]
15+
end
16+
17+
test "LOG0 with data reads memory bytes" do
18+
code =
19+
<<
20+
0x60,
21+
0xAA,
22+
0x60,
23+
0x00,
24+
0x53,
25+
0x60,
26+
0xBB,
27+
0x60,
28+
0x01,
29+
0x53,
30+
0x60,
31+
0xCC,
32+
0x60,
33+
0x02,
34+
0x53,
35+
0x60,
36+
0x03,
37+
0x60,
38+
0x00,
39+
0xA0,
40+
0x00
41+
>>
42+
43+
result = EEVM.execute(code)
44+
assert EEVM.logs(result) == [%{address: 0, data: <<0xAA, 0xBB, 0xCC>>, topics: []}]
45+
end
46+
47+
test "LOG1 single topic" do
48+
code = <<0x60, 0x2A, 0x60, 0x00, 0x60, 0x00, 0xA1, 0x00>>
49+
result = EEVM.execute(code)
50+
51+
assert EEVM.logs(result) == [%{address: 0, data: <<>>, topics: [0x2A]}]
52+
end
53+
54+
test "LOG2 two topics" do
55+
code = <<0x60, 0x22, 0x60, 0x11, 0x60, 0x00, 0x60, 0x00, 0xA2, 0x00>>
56+
result = EEVM.execute(code)
57+
58+
assert EEVM.logs(result) == [%{address: 0, data: <<>>, topics: [0x11, 0x22]}]
59+
end
60+
61+
test "LOG3 three topics" do
62+
code = <<0x60, 0x03, 0x60, 0x02, 0x60, 0x01, 0x60, 0x00, 0x60, 0x00, 0xA3, 0x00>>
63+
result = EEVM.execute(code)
64+
65+
assert EEVM.logs(result) == [%{address: 0, data: <<>>, topics: [0x01, 0x02, 0x03]}]
66+
end
67+
68+
test "LOG4 four topics" do
69+
code =
70+
<<
71+
0x60,
72+
0x04,
73+
0x60,
74+
0x03,
75+
0x60,
76+
0x02,
77+
0x60,
78+
0x01,
79+
0x60,
80+
0x00,
81+
0x60,
82+
0x00,
83+
0xA4,
84+
0x00
85+
>>
86+
87+
result = EEVM.execute(code)
88+
assert EEVM.logs(result) == [%{address: 0, data: <<>>, topics: [0x01, 0x02, 0x03, 0x04]}]
89+
end
90+
91+
test "multiple LOGs accumulate in state.logs list" do
92+
code =
93+
<<
94+
0x60,
95+
0xAA,
96+
0x60,
97+
0x00,
98+
0x53,
99+
0x60,
100+
0x01,
101+
0x60,
102+
0x00,
103+
0xA0,
104+
0x60,
105+
0x99,
106+
0x60,
107+
0x00,
108+
0x60,
109+
0x00,
110+
0xA1,
111+
0x00
112+
>>
113+
114+
result = EEVM.execute(code)
115+
116+
assert EEVM.logs(result) == [
117+
%{address: 0, data: <<0xAA>>, topics: []},
118+
%{address: 0, data: <<>>, topics: [0x99]}
119+
]
120+
end
121+
122+
test "gas calculation for LOGn is 375 + 375*N + 8*size" do
123+
assert Gas.log_cost(0, 0) == 375
124+
assert Gas.log_cost(2, 10) == 375 + 375 * 2 + 8 * 10
125+
assert Gas.log_cost(4, 64) == 375 + 375 * 4 + 8 * 64
126+
end
127+
128+
test "memory expansion is triggered by LOG data reads beyond current memory" do
129+
code = <<0x60, 0x40, 0x60, 0x00, 0xA0, 0x00>>
130+
result = EEVM.execute(code)
131+
132+
assert result.memory.size == 64
133+
assert [%{data: data}] = EEVM.logs(result)
134+
assert byte_size(data) == 64
135+
end
136+
137+
test "contract address is attached to emitted logs" do
138+
code = <<0x60, 0x00, 0x60, 0x00, 0xA0, 0x00>>
139+
result = EEVM.execute(code, contract: Contract.new(address: 0xCAFE))
140+
141+
assert EEVM.logs(result) == [%{address: 0xCAFE, data: <<>>, topics: []}]
142+
end
143+
end
144+
end

0 commit comments

Comments
 (0)