Skip to content

Commit 7192134

Browse files
lupin012claude
andauthored
[3.4] rpc: cherry-pick tracing logger memory encoding (#20478) (#20749)
Summary of changes for compliance (ethereum/execution-apis PR 762) see ethereum/execution-apis#762) Impacted APIs * debug_traceTransaction * debug_traceBlockByNumber * debug_traceBlockByHash * debug_traceCall * debug_traceCallMany --------- Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent 2bd2c39 commit 7192134

8 files changed

Lines changed: 277 additions & 11 deletions

File tree

.github/workflows/scripts/run_rpc_tests_ethereum.sh

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,4 +42,4 @@ DISABLED_TEST_LIST=(
4242
DISABLED_TESTS=$(IFS=,; echo "${DISABLED_TEST_LIST[*]}")
4343

4444
# Call the main test runner script with the required and optional parameters
45-
"$(dirname "$0")/run_rpc_tests.sh" mainnet v2.1.0 "$DISABLED_TESTS" "$WORKSPACE" "$RESULT_DIR"
45+
"$(dirname "$0")/run_rpc_tests.sh" mainnet v2.1.1 "$DISABLED_TESTS" "$WORKSPACE" "$RESULT_DIR"

.github/workflows/scripts/run_rpc_tests_ethereum_latest.sh

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,4 +34,4 @@ DISABLED_TEST_LIST=(
3434
DISABLED_TESTS=$(IFS=,; echo "${DISABLED_TEST_LIST[*]}")
3535

3636
# Call the main test runner script with the required and optional parameters
37-
"$(dirname "$0")/run_rpc_tests.sh" mainnet v2.1.0 "$DISABLED_TESTS" "$WORKSPACE" "$RESULT_DIR" "latest" "$REFERENCE_HOST" "do-not-compare-error-message" "$DUMP_RESPONSE"
37+
"$(dirname "$0")/run_rpc_tests.sh" mainnet v2.1.1 "$DISABLED_TESTS" "$WORKSPACE" "$RESULT_DIR" "latest" "$REFERENCE_HOST" "do-not-compare-error-message" "$DUMP_RESPONSE"

.github/workflows/scripts/run_rpc_tests_gnosis.sh

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,5 +22,5 @@ DISABLED_TEST_LIST=(
2222
DISABLED_TESTS=$(IFS=,; echo "${DISABLED_TEST_LIST[*]}")
2323

2424
# Call the main test runner script with the required and optional parameters
25-
"$(dirname "$0")/run_rpc_tests.sh" gnosis v2.1.0 "$DISABLED_TESTS" "$WORKSPACE" "$RESULT_DIR"
25+
"$(dirname "$0")/run_rpc_tests.sh" gnosis v2.1.1 "$DISABLED_TESTS" "$WORKSPACE" "$RESULT_DIR"
2626

.github/workflows/scripts/run_rpc_tests_remote_ethereum.sh

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,4 +29,4 @@ DISABLED_TEST_LIST=(
2929
DISABLED_TESTS=$(IFS=,; echo "${DISABLED_TEST_LIST[*]}")
3030

3131
# Call the main test runner script with the required and optional parameters
32-
"$(dirname "$0")/run_rpc_tests.sh" mainnet v2.1.0 "$DISABLED_TESTS" "$WORKSPACE" "$RESULT_DIR"
32+
"$(dirname "$0")/run_rpc_tests.sh" mainnet v2.1.1 "$DISABLED_TESTS" "$WORKSPACE" "$RESULT_DIR"

execution/tracing/tracers/logger/gen_structlog.go

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

execution/tracing/tracers/logger/json_stream.go

Lines changed: 27 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,25 @@ func (l *JsonStreamLogger) OnTxStart(env *tracing.VMContext, tx types.Transactio
7979
l.env = env
8080
}
8181

82+
// hexWithPrefix encodes b as a 0x-prefixed hex string using the internal buffer.
83+
func (l *JsonStreamLogger) hexWithPrefix(b []byte) string {
84+
l.hexEncodeBuf[0] = '0'
85+
l.hexEncodeBuf[1] = 'x'
86+
n := hex.Encode(l.hexEncodeBuf[2:], b)
87+
return string(l.hexEncodeBuf[:2+n])
88+
}
89+
90+
// formatMemoryWord encodes a memory chunk as a 0x-prefixed 64-char hex string,
91+
// padding the last word to 32 bytes if needed.
92+
func (l *JsonStreamLogger) formatMemoryWord(chunk []byte) string {
93+
if len(chunk) == 32 {
94+
return l.hexWithPrefix(chunk)
95+
}
96+
var word [32]byte
97+
copy(word[:], chunk)
98+
return l.hexWithPrefix(word[:])
99+
}
100+
82101
func (l *JsonStreamLogger) OnExit(depth int, output []byte, gasUsed uint64, err error, reverted bool) {
83102
// no log entry are producer
84103
if l.firstCapture {
@@ -185,11 +204,15 @@ func (l *JsonStreamLogger) OnOpcode(pc uint64, typ byte, gas, cost uint64, scope
185204
l.stream.WriteMore()
186205
l.stream.WriteObjectField("memory")
187206
l.stream.WriteArrayStart()
188-
for i := 0; i+32 <= len(memData); i += 32 {
207+
for i := 0; i < len(memData); i += 32 {
208+
end := i + 32
209+
if end > len(memData) {
210+
end = len(memData)
211+
}
189212
if i > 0 {
190213
l.stream.WriteMore()
191214
}
192-
l.stream.WriteString(string(l.hexEncodeBuf[0:hex.Encode(l.hexEncodeBuf[:], memData[i:i+32])]))
215+
l.stream.WriteString(l.formatMemoryWord(memData[i:end]))
193216
}
194217
l.stream.WriteArrayEnd()
195218
}
@@ -214,8 +237,8 @@ func (l *JsonStreamLogger) OnOpcode(pc uint64, typ byte, gas, cost uint64, scope
214237
} else {
215238
l.stream.WriteMore()
216239
}
217-
l.stream.WriteObjectField(string(l.hexEncodeBuf[0:hex.Encode(l.hexEncodeBuf[:], loc[:])]))
218-
l.stream.WriteString(string(l.hexEncodeBuf[0:hex.Encode(l.hexEncodeBuf[:], value[:])]))
240+
l.stream.WriteObjectField(l.hexWithPrefix(loc[:]))
241+
l.stream.WriteString(l.hexWithPrefix(value[:]))
219242
}
220243
l.stream.WriteObjectEnd()
221244
}
Lines changed: 243 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,243 @@
1+
// Copyright 2024 The Erigon Authors
2+
// This file is part of Erigon.
3+
//
4+
// Erigon is free software: you can redistribute it and/or modify
5+
// it under the terms of the GNU Lesser General Public License as published by
6+
// the Free Software Foundation, either version 3 of the License, or
7+
// (at your option) any later version.
8+
//
9+
// Erigon is distributed in the hope that it will be useful,
10+
// but WITHOUT ANY WARRANTY; without even the implied warranty of
11+
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12+
// GNU Lesser General Public License for more details.
13+
//
14+
// You should have received a copy of the GNU Lesser General Public License
15+
// along with Erigon. If not, see <http://www.gnu.org/licenses/>.
16+
17+
package logger
18+
19+
import (
20+
"bytes"
21+
"context"
22+
"encoding/json"
23+
"errors"
24+
"strings"
25+
"testing"
26+
27+
"github.com/holiman/uint256"
28+
29+
"github.com/erigontech/erigon/common"
30+
"github.com/erigontech/erigon/execution/tracing"
31+
"github.com/erigontech/erigon/execution/types/accounts"
32+
"github.com/erigontech/erigon/execution/vm"
33+
"github.com/erigontech/erigon/rpc/jsonstream"
34+
)
35+
36+
// mockOpContext implements tracing.OpContext for tests.
37+
type mockOpContext struct {
38+
memory []byte
39+
stack []uint256.Int
40+
address accounts.Address
41+
}
42+
43+
func (m *mockOpContext) MemoryData() []byte { return m.memory }
44+
func (m *mockOpContext) StackData() []uint256.Int { return m.stack }
45+
func (m *mockOpContext) Caller() accounts.Address { return m.address }
46+
func (m *mockOpContext) Address() accounts.Address { return m.address }
47+
func (m *mockOpContext) CallValue() uint256.Int { return uint256.Int{} }
48+
func (m *mockOpContext) CallInput() []byte { return nil }
49+
func (m *mockOpContext) Code() []byte { return nil }
50+
func (m *mockOpContext) CodeHash() accounts.CodeHash { return accounts.CodeHash{} }
51+
52+
// mockIBS implements tracing.IntraBlockState for tests.
53+
type mockIBS struct{}
54+
55+
func (m *mockIBS) GetBalance(accounts.Address) (uint256.Int, error) { return uint256.Int{}, nil }
56+
func (m *mockIBS) GetNonce(accounts.Address) (uint64, error) { return 0, nil }
57+
func (m *mockIBS) GetCode(accounts.Address) ([]byte, error) { return nil, nil }
58+
func (m *mockIBS) GetState(accounts.Address, accounts.StorageKey) (uint256.Int, error) {
59+
return uint256.Int{}, nil
60+
}
61+
func (m *mockIBS) Exist(accounts.Address) (bool, error) { return false, nil }
62+
func (m *mockIBS) GetRefund() uint64 { return 0 }
63+
64+
// captureOnOpcode runs a single OnOpcode call and returns the parsed structLog entry.
65+
// It closes the stream the same way ExecuteTraceTx does after execution.
66+
// storageKey/storageVal are pushed onto the stack for SSTORE (top=key, below=val).
67+
func captureOnOpcode(t *testing.T, cfg *LogConfig, memory []byte, storageKey, storageVal *common.Hash) map[string]json.RawMessage {
68+
t.Helper()
69+
var buf bytes.Buffer
70+
stream := jsonstream.New(&buf)
71+
l := NewJsonStreamLogger(cfg, context.Background(), stream)
72+
l.env = &tracing.VMContext{IntraBlockState: &mockIBS{}}
73+
74+
scope := &mockOpContext{memory: memory}
75+
76+
op := vm.MLOAD
77+
if storageKey != nil {
78+
op = vm.SSTORE
79+
// SSTORE reads stack[top-1]=address, stack[top-2]=value.
80+
var key, val uint256.Int
81+
key.SetBytes(storageKey[:])
82+
val.SetBytes(storageVal[:])
83+
scope.stack = []uint256.Int{val, key} // bottom=val, top=key
84+
}
85+
86+
l.OnOpcode(0, byte(op), 100, 3, scope, nil, 1, nil)
87+
88+
// Mirror what ExecuteTraceTx does to close the stream after execution.
89+
stream.WriteArrayEnd()
90+
stream.WriteObjectEnd()
91+
stream.Flush()
92+
93+
// Parse the outer object and extract the first structLog entry.
94+
var outer struct {
95+
StructLogs []map[string]json.RawMessage `json:"structLogs"`
96+
}
97+
if err := json.Unmarshal(buf.Bytes(), &outer); err != nil {
98+
t.Fatalf("invalid JSON output: %v\nraw: %s", err, buf.Bytes())
99+
}
100+
if len(outer.StructLogs) == 0 {
101+
t.Fatal("no structLog entry in output")
102+
}
103+
return outer.StructLogs[0]
104+
}
105+
106+
// TestJsonStreamLogger_MemoryEncoding verifies that memory words are emitted as
107+
// 0x-prefixed 64-char hex strings and that a partial last word is padded to 32 bytes.
108+
func TestJsonStreamLogger_MemoryEncoding(t *testing.T) {
109+
zeros64 := "0x" + strings.Repeat("00", 32)
110+
tests := []struct {
111+
name string
112+
memory []byte
113+
want []string
114+
}{
115+
{
116+
name: "full 32-byte word",
117+
memory: bytes.Repeat([]byte{0xab}, 32),
118+
want: []string{"0x" + strings.Repeat("ab", 32)},
119+
},
120+
{
121+
name: "partial last word padded to 32 bytes",
122+
memory: []byte{0xaa, 0xbb},
123+
want: []string{"0xaabb" + strings.Repeat("00", 30)},
124+
},
125+
{
126+
name: "two full words",
127+
memory: func() []byte {
128+
b := make([]byte, 64)
129+
b[0] = 0x01
130+
b[32] = 0x02
131+
return b
132+
}(),
133+
want: []string{
134+
"0x01" + strings.Repeat("00", 31),
135+
"0x02" + strings.Repeat("00", 31),
136+
},
137+
},
138+
{
139+
name: "two full words plus partial third",
140+
memory: func() []byte {
141+
b := make([]byte, 65)
142+
b[64] = 0xff
143+
return b
144+
}(),
145+
want: []string{
146+
zeros64,
147+
zeros64,
148+
"0xff" + strings.Repeat("00", 31),
149+
},
150+
},
151+
}
152+
153+
for _, tt := range tests {
154+
t.Run(tt.name, func(t *testing.T) {
155+
obj := captureOnOpcode(t, nil, tt.memory, nil, nil)
156+
raw, ok := obj["memory"]
157+
if !ok {
158+
t.Fatal("missing 'memory' field")
159+
}
160+
var got []string
161+
if err := json.Unmarshal(raw, &got); err != nil {
162+
t.Fatalf("cannot parse memory: %v", err)
163+
}
164+
if len(got) != len(tt.want) {
165+
t.Fatalf("memory word count: got %d, want %d\ngot: %v", len(got), len(tt.want), got)
166+
}
167+
for i := range tt.want {
168+
if got[i] != tt.want[i] {
169+
t.Errorf("word[%d]: got %s, want %s", i, got[i], tt.want[i])
170+
}
171+
}
172+
})
173+
}
174+
}
175+
176+
// TestJsonStreamLogger_StorageEncoding verifies that storage keys and values are
177+
// emitted with the 0x prefix.
178+
func TestJsonStreamLogger_StorageEncoding(t *testing.T) {
179+
key := common.BigToHash(common.Big1)
180+
val := common.BigToHash(common.Big2)
181+
182+
obj := captureOnOpcode(t, nil, nil, &key, &val)
183+
184+
raw, ok := obj["storage"]
185+
if !ok {
186+
t.Fatal("missing 'storage' field")
187+
}
188+
var storage map[string]string
189+
if err := json.Unmarshal(raw, &storage); err != nil {
190+
t.Fatalf("cannot parse storage: %v", err)
191+
}
192+
wantKey := "0x0000000000000000000000000000000000000000000000000000000000000001"
193+
wantVal := "0x0000000000000000000000000000000000000000000000000000000000000002"
194+
gotVal, found := storage[wantKey]
195+
if !found {
196+
t.Fatalf("storage key %s not found; got: %v", wantKey, storage)
197+
}
198+
if gotVal != wantVal {
199+
t.Errorf("storage value: got %s, want %s", gotVal, wantVal)
200+
}
201+
}
202+
203+
// TestStructLog_ErrorOmitempty verifies that the 'error' field is omitted from
204+
// MarshalJSON output when there is no error, and present when there is.
205+
func TestStructLog_ErrorOmitempty(t *testing.T) {
206+
t.Run("no error omitted", func(t *testing.T) {
207+
log := StructLog{Pc: 1, Op: vm.STOP, Gas: 10, GasCost: 1, Depth: 1}
208+
b, err := log.MarshalJSON()
209+
if err != nil {
210+
t.Fatal(err)
211+
}
212+
var obj map[string]json.RawMessage
213+
if err := json.Unmarshal(b, &obj); err != nil {
214+
t.Fatal(err)
215+
}
216+
if _, found := obj["error"]; found {
217+
t.Errorf("expected 'error' field to be absent, but it was present: %s", obj["error"])
218+
}
219+
})
220+
221+
t.Run("error included when present", func(t *testing.T) {
222+
log := StructLog{Pc: 1, Op: vm.STOP, Gas: 10, GasCost: 1, Depth: 1, Err: errors.New("out of gas")}
223+
b, err := log.MarshalJSON()
224+
if err != nil {
225+
t.Fatal(err)
226+
}
227+
var obj map[string]json.RawMessage
228+
if err := json.Unmarshal(b, &obj); err != nil {
229+
t.Fatal(err)
230+
}
231+
raw, found := obj["error"]
232+
if !found {
233+
t.Fatal("expected 'error' field but it was absent")
234+
}
235+
var msg string
236+
if err := json.Unmarshal(raw, &msg); err != nil {
237+
t.Fatalf("cannot parse error field: %v", err)
238+
}
239+
if msg != "out of gas" {
240+
t.Errorf("error message: got %q, want %q", msg, "out of gas")
241+
}
242+
})
243+
}

execution/tracing/tracers/logger/logger.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -88,8 +88,8 @@ type structLogMarshaling struct {
8888
GasCost math.HexOrDecimal64
8989
Memory hexutil.Bytes
9090
ReturnData hexutil.Bytes
91-
OpName string `json:"opName"` // adds call to OpName() in MarshalJSON
92-
ErrorString string `json:"error"` // adds call to ErrorString() in MarshalJSON
91+
OpName string `json:"opName"` // adds call to OpName() in MarshalJSON
92+
ErrorString string `json:"error,omitempty"` // adds call to ErrorString() in MarshalJSON
9393
}
9494

9595
// OpName formats the operand name in a human-readable format.

0 commit comments

Comments
 (0)