diff --git a/.github/workflows/qa-rpc-integration-tests-gnosis.yml b/.github/workflows/qa-rpc-integration-tests-gnosis.yml index 9e4c2f9a1c8..73a5f39a3cb 100644 --- a/.github/workflows/qa-rpc-integration-tests-gnosis.yml +++ b/.github/workflows/qa-rpc-integration-tests-gnosis.yml @@ -73,7 +73,7 @@ jobs: uses: actions/cache@v4 with: path: ${{ runner.workspace }}/rpc-tests - key: rpc-tests-${{ runner.os }}-${{ runner.arch }}-v2.4.0 + key: rpc-tests-${{ runner.os }}-${{ runner.arch }}-v2.6.0 - name: Run RPC Integration Tests id: test_step diff --git a/.github/workflows/qa-rpc-integration-tests.yml b/.github/workflows/qa-rpc-integration-tests.yml index 14d4e0da349..89b0f17d027 100644 --- a/.github/workflows/qa-rpc-integration-tests.yml +++ b/.github/workflows/qa-rpc-integration-tests.yml @@ -73,7 +73,7 @@ jobs: uses: actions/cache@v4 with: path: ${{ runner.workspace }}/rpc-tests - key: rpc-tests-${{ runner.os }}-${{ runner.arch }}-v2.4.0 + key: rpc-tests-${{ runner.os }}-${{ runner.arch }}-v2.6.0 - name: Run RPC Integration Tests id: test_step diff --git a/.github/workflows/scripts/run_rpc_tests_ethereum.sh b/.github/workflows/scripts/run_rpc_tests_ethereum.sh index 28f76d5b72a..2c36dacfe86 100755 --- a/.github/workflows/scripts/run_rpc_tests_ethereum.sh +++ b/.github/workflows/scripts/run_rpc_tests_ethereum.sh @@ -47,4 +47,4 @@ DISABLED_TEST_LIST=( DISABLED_TESTS=$(IFS=,; echo "${DISABLED_TEST_LIST[*]}") # Call the main test runner script with the required and optional parameters -"$(dirname "$0")/run_rpc_tests.sh" mainnet v2.5.0 "$DISABLED_TESTS" "$WORKSPACE" "$RESULT_DIR" +"$(dirname "$0")/run_rpc_tests.sh" mainnet v2.6.0 "$DISABLED_TESTS" "$WORKSPACE" "$RESULT_DIR" diff --git a/.github/workflows/scripts/run_rpc_tests_ethereum_latest.sh b/.github/workflows/scripts/run_rpc_tests_ethereum_latest.sh index 0e1853dae2f..2bc734bf0c2 100755 --- a/.github/workflows/scripts/run_rpc_tests_ethereum_latest.sh +++ b/.github/workflows/scripts/run_rpc_tests_ethereum_latest.sh @@ -34,4 +34,4 @@ DISABLED_TEST_LIST=( DISABLED_TESTS=$(IFS=,; echo "${DISABLED_TEST_LIST[*]}") # Call the main test runner script with the required and optional parameters -"$(dirname "$0")/run_rpc_tests.sh" mainnet v2.5.0 "$DISABLED_TESTS" "$WORKSPACE" "$RESULT_DIR" "latest" "$REFERENCE_HOST" "do-not-compare-error-message" "$DUMP_RESPONSE" +"$(dirname "$0")/run_rpc_tests.sh" mainnet v2.6.0 "$DISABLED_TESTS" "$WORKSPACE" "$RESULT_DIR" "latest" "$REFERENCE_HOST" "do-not-compare-error-message" "$DUMP_RESPONSE" diff --git a/.github/workflows/scripts/run_rpc_tests_gnosis.sh b/.github/workflows/scripts/run_rpc_tests_gnosis.sh index 03acbaf6ba5..230d085799d 100755 --- a/.github/workflows/scripts/run_rpc_tests_gnosis.sh +++ b/.github/workflows/scripts/run_rpc_tests_gnosis.sh @@ -22,5 +22,5 @@ DISABLED_TEST_LIST=( DISABLED_TESTS=$(IFS=,; echo "${DISABLED_TEST_LIST[*]}") # Call the main test runner script with the required and optional parameters -"$(dirname "$0")/run_rpc_tests.sh" gnosis v2.5.0 "$DISABLED_TESTS" "$WORKSPACE" "$RESULT_DIR" +"$(dirname "$0")/run_rpc_tests.sh" gnosis v2.6.0 "$DISABLED_TESTS" "$WORKSPACE" "$RESULT_DIR" diff --git a/.github/workflows/scripts/run_rpc_tests_remote_ethereum.sh b/.github/workflows/scripts/run_rpc_tests_remote_ethereum.sh index a3d66b1318b..819f530c9af 100755 --- a/.github/workflows/scripts/run_rpc_tests_remote_ethereum.sh +++ b/.github/workflows/scripts/run_rpc_tests_remote_ethereum.sh @@ -31,4 +31,4 @@ DISABLED_TEST_LIST=( DISABLED_TESTS=$(IFS=,; echo "${DISABLED_TEST_LIST[*]}") # Call the main test runner script with the required and optional parameters -"$(dirname "$0")/run_rpc_tests.sh" mainnet v2.5.0 "$DISABLED_TESTS" "$WORKSPACE" "$RESULT_DIR" +"$(dirname "$0")/run_rpc_tests.sh" mainnet v2.6.0 "$DISABLED_TESTS" "$WORKSPACE" "$RESULT_DIR" diff --git a/execution/tracing/tracers/logger/gen_structlog.go b/execution/tracing/tracers/logger/gen_structlog.go index 1f8425b1466..68d9ffbebca 100644 --- a/execution/tracing/tracers/logger/gen_structlog.go +++ b/execution/tracing/tracers/logger/gen_structlog.go @@ -30,7 +30,7 @@ func (s StructLog) MarshalJSON() ([]byte, error) { RefundCounter uint64 `json:"refund"` Err error `json:"-"` OpName string `json:"opName"` - ErrorString string `json:"error"` + ErrorString string `json:"error,omitempty"` } var enc StructLog enc.Pc = s.Pc diff --git a/execution/tracing/tracers/logger/json_stream.go b/execution/tracing/tracers/logger/json_stream.go index ad3274918be..727dabea2b4 100644 --- a/execution/tracing/tracers/logger/json_stream.go +++ b/execution/tracing/tracers/logger/json_stream.go @@ -76,6 +76,25 @@ func (l *JsonStreamLogger) OnTxStart(env *tracing.VMContext, tx types.Transactio l.env = env } +// hexWithPrefix encodes b as a 0x-prefixed hex string using the internal buffer. +func (l *JsonStreamLogger) hexWithPrefix(b []byte) string { + l.hexEncodeBuf[0] = '0' + l.hexEncodeBuf[1] = 'x' + n := hex.Encode(l.hexEncodeBuf[2:], b) + return string(l.hexEncodeBuf[:2+n]) +} + +// formatMemoryWord encodes a memory chunk as a 0x-prefixed 64-char hex string, +// padding the last word to 32 bytes if needed. +func (l *JsonStreamLogger) formatMemoryWord(chunk []byte) string { + if len(chunk) == 32 { + return l.hexWithPrefix(chunk) + } + var word [32]byte + copy(word[:], chunk) + return l.hexWithPrefix(word[:]) +} + func (l *JsonStreamLogger) OnExit(depth int, output []byte, gasUsed uint64, err error, reverted bool) { // no log entry are producer if l.firstCapture { @@ -178,11 +197,15 @@ func (l *JsonStreamLogger) OnOpcode(pc uint64, typ byte, gas, cost uint64, scope l.stream.WriteMore() l.stream.WriteObjectField("memory") l.stream.WriteArrayStart() - for i := 0; i+32 <= len(memData); i += 32 { + for i := 0; i < len(memData); i += 32 { + end := i + 32 + if end > len(memData) { + end = len(memData) + } if i > 0 { l.stream.WriteMore() } - l.stream.WriteString(string(l.hexEncodeBuf[0:hex.Encode(l.hexEncodeBuf[:], memData[i:i+32])])) + l.stream.WriteString(l.formatMemoryWord(memData[i:end])) } l.stream.WriteArrayEnd() } @@ -207,8 +230,8 @@ func (l *JsonStreamLogger) OnOpcode(pc uint64, typ byte, gas, cost uint64, scope } else { l.stream.WriteMore() } - l.stream.WriteObjectField(string(l.hexEncodeBuf[0:hex.Encode(l.hexEncodeBuf[:], loc[:])])) - l.stream.WriteString(string(l.hexEncodeBuf[0:hex.Encode(l.hexEncodeBuf[:], value[:])])) + l.stream.WriteObjectField(l.hexWithPrefix(loc[:])) + l.stream.WriteString(l.hexWithPrefix(value[:])) } l.stream.WriteObjectEnd() } diff --git a/execution/tracing/tracers/logger/json_stream_test.go b/execution/tracing/tracers/logger/json_stream_test.go new file mode 100644 index 00000000000..ce9ba96d6d6 --- /dev/null +++ b/execution/tracing/tracers/logger/json_stream_test.go @@ -0,0 +1,244 @@ +// Copyright 2024 The Erigon Authors +// This file is part of Erigon. +// +// Erigon is free software: you can redistribute it and/or modify +// it under the terms of the GNU Lesser General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// Erigon is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public License +// along with Erigon. If not, see . + +package logger + +import ( + "bytes" + "context" + "encoding/json" + "errors" + "strings" + "testing" + + "github.com/holiman/uint256" + + "github.com/erigontech/erigon/common" + "github.com/erigontech/erigon/execution/protocol/mdgas" + "github.com/erigontech/erigon/execution/tracing" + "github.com/erigontech/erigon/execution/types/accounts" + "github.com/erigontech/erigon/execution/vm" + "github.com/erigontech/erigon/rpc/jsonstream" +) + +// mockOpContext implements tracing.OpContext for tests. +type mockOpContext struct { + memory []byte + stack []uint256.Int + address accounts.Address +} + +func (m *mockOpContext) MemoryData() []byte { return m.memory } +func (m *mockOpContext) StackData() []uint256.Int { return m.stack } +func (m *mockOpContext) Caller() accounts.Address { return m.address } +func (m *mockOpContext) Address() accounts.Address { return m.address } +func (m *mockOpContext) CallValue() uint256.Int { return uint256.Int{} } +func (m *mockOpContext) CallInput() []byte { return nil } +func (m *mockOpContext) Code() []byte { return nil } +func (m *mockOpContext) CodeHash() accounts.CodeHash { return accounts.CodeHash{} } + +// mockIBS implements tracing.IntraBlockState for tests. +type mockIBS struct{} + +func (m *mockIBS) GetBalance(accounts.Address) (uint256.Int, error) { return uint256.Int{}, nil } +func (m *mockIBS) GetNonce(accounts.Address) (uint64, error) { return 0, nil } +func (m *mockIBS) GetCode(accounts.Address) ([]byte, error) { return nil, nil } +func (m *mockIBS) GetState(accounts.Address, accounts.StorageKey) (uint256.Int, error) { + return uint256.Int{}, nil +} +func (m *mockIBS) Exist(accounts.Address) (bool, error) { return false, nil } +func (m *mockIBS) GetRefund() mdgas.MdGas { return mdgas.MdGas{} } + +// captureOnOpcode runs a single OnOpcode call and returns the parsed structLog entry. +// It closes the stream the same way ExecuteTraceTx does after execution. +// storageKey/storageVal are pushed onto the stack for SSTORE (top=key, below=val). +func captureOnOpcode(t *testing.T, cfg *LogConfig, memory []byte, storageKey, storageVal *common.Hash) map[string]json.RawMessage { + t.Helper() + var buf bytes.Buffer + stream := jsonstream.New(&buf) + l := NewJsonStreamLogger(cfg, context.Background(), stream) + l.env = &tracing.VMContext{IntraBlockState: &mockIBS{}} + + scope := &mockOpContext{memory: memory} + + op := vm.MLOAD + if storageKey != nil { + op = vm.SSTORE + // SSTORE reads stack[top-1]=address, stack[top-2]=value. + var key, val uint256.Int + key.SetBytes(storageKey[:]) + val.SetBytes(storageVal[:]) + scope.stack = []uint256.Int{val, key} // bottom=val, top=key + } + + l.OnOpcode(0, byte(op), 100, 3, scope, nil, 1, nil) + + // Mirror what ExecuteTraceTx does to close the stream after execution. + stream.WriteArrayEnd() + stream.WriteObjectEnd() + stream.Flush() + + // Parse the outer object and extract the first structLog entry. + var outer struct { + StructLogs []map[string]json.RawMessage `json:"structLogs"` + } + if err := json.Unmarshal(buf.Bytes(), &outer); err != nil { + t.Fatalf("invalid JSON output: %v\nraw: %s", err, buf.Bytes()) + } + if len(outer.StructLogs) == 0 { + t.Fatal("no structLog entry in output") + } + return outer.StructLogs[0] +} + +// TestJsonStreamLogger_MemoryEncoding verifies that memory words are emitted as +// 0x-prefixed 64-char hex strings and that a partial last word is padded to 32 bytes. +func TestJsonStreamLogger_MemoryEncoding(t *testing.T) { + zeros64 := "0x" + strings.Repeat("00", 32) + tests := []struct { + name string + memory []byte + want []string + }{ + { + name: "full 32-byte word", + memory: bytes.Repeat([]byte{0xab}, 32), + want: []string{"0x" + strings.Repeat("ab", 32)}, + }, + { + name: "partial last word padded to 32 bytes", + memory: []byte{0xaa, 0xbb}, + want: []string{"0xaabb" + strings.Repeat("00", 30)}, + }, + { + name: "two full words", + memory: func() []byte { + b := make([]byte, 64) + b[0] = 0x01 + b[32] = 0x02 + return b + }(), + want: []string{ + "0x01" + strings.Repeat("00", 31), + "0x02" + strings.Repeat("00", 31), + }, + }, + { + name: "two full words plus partial third", + memory: func() []byte { + b := make([]byte, 65) + b[64] = 0xff + return b + }(), + want: []string{ + zeros64, + zeros64, + "0xff" + strings.Repeat("00", 31), + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + obj := captureOnOpcode(t, nil, tt.memory, nil, nil) + raw, ok := obj["memory"] + if !ok { + t.Fatal("missing 'memory' field") + } + var got []string + if err := json.Unmarshal(raw, &got); err != nil { + t.Fatalf("cannot parse memory: %v", err) + } + if len(got) != len(tt.want) { + t.Fatalf("memory word count: got %d, want %d\ngot: %v", len(got), len(tt.want), got) + } + for i := range tt.want { + if got[i] != tt.want[i] { + t.Errorf("word[%d]: got %s, want %s", i, got[i], tt.want[i]) + } + } + }) + } +} + +// TestJsonStreamLogger_StorageEncoding verifies that storage keys and values are +// emitted with the 0x prefix. +func TestJsonStreamLogger_StorageEncoding(t *testing.T) { + key := common.BigToHash(common.Big1) + val := common.BigToHash(common.Big2) + + obj := captureOnOpcode(t, nil, nil, &key, &val) + + raw, ok := obj["storage"] + if !ok { + t.Fatal("missing 'storage' field") + } + var storage map[string]string + if err := json.Unmarshal(raw, &storage); err != nil { + t.Fatalf("cannot parse storage: %v", err) + } + wantKey := "0x0000000000000000000000000000000000000000000000000000000000000001" + wantVal := "0x0000000000000000000000000000000000000000000000000000000000000002" + gotVal, found := storage[wantKey] + if !found { + t.Fatalf("storage key %s not found; got: %v", wantKey, storage) + } + if gotVal != wantVal { + t.Errorf("storage value: got %s, want %s", gotVal, wantVal) + } +} + +// TestStructLog_ErrorOmitempty verifies that the 'error' field is omitted from +// MarshalJSON output when there is no error, and present when there is. +func TestStructLog_ErrorOmitempty(t *testing.T) { + t.Run("no error omitted", func(t *testing.T) { + log := StructLog{Pc: 1, Op: vm.STOP, Gas: 10, GasCost: 1, Depth: 1} + b, err := log.MarshalJSON() + if err != nil { + t.Fatal(err) + } + var obj map[string]json.RawMessage + if err := json.Unmarshal(b, &obj); err != nil { + t.Fatal(err) + } + if _, found := obj["error"]; found { + t.Errorf("expected 'error' field to be absent, but it was present: %s", obj["error"]) + } + }) + + t.Run("error included when present", func(t *testing.T) { + log := StructLog{Pc: 1, Op: vm.STOP, Gas: 10, GasCost: 1, Depth: 1, Err: errors.New("out of gas")} + b, err := log.MarshalJSON() + if err != nil { + t.Fatal(err) + } + var obj map[string]json.RawMessage + if err := json.Unmarshal(b, &obj); err != nil { + t.Fatal(err) + } + raw, found := obj["error"] + if !found { + t.Fatal("expected 'error' field but it was absent") + } + var msg string + if err := json.Unmarshal(raw, &msg); err != nil { + t.Fatalf("cannot parse error field: %v", err) + } + if msg != "out of gas" { + t.Errorf("error message: got %q, want %q", msg, "out of gas") + } + }) +} diff --git a/execution/tracing/tracers/logger/logger.go b/execution/tracing/tracers/logger/logger.go index 65163d2e810..26832adafbe 100644 --- a/execution/tracing/tracers/logger/logger.go +++ b/execution/tracing/tracers/logger/logger.go @@ -88,8 +88,8 @@ type structLogMarshaling struct { GasCost math.HexOrDecimal64 Memory hexutil.Bytes ReturnData hexutil.Bytes - OpName string `json:"opName"` // adds call to OpName() in MarshalJSON - ErrorString string `json:"error"` // adds call to ErrorString() in MarshalJSON + OpName string `json:"opName"` // adds call to OpName() in MarshalJSON + ErrorString string `json:"error,omitempty"` // adds call to ErrorString() in MarshalJSON } // OpName formats the operand name in a human-readable format.