Skip to content

Commit 260bc5d

Browse files
authored
Merge pull request #52 from OffchainLabs/guard-tracer-empty-callstack
eth: Guard callTracer and erc7562Tracer OnTxEnd against empty callstack
2 parents c7225fd + 0f618f3 commit 260bc5d

4 files changed

Lines changed: 87 additions & 0 deletions

File tree

eth/tracers/native/call.go

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -239,6 +239,11 @@ func (t *callTracer) OnTxEnd(receipt *types.Receipt, err error) {
239239
if err != nil {
240240
return
241241
}
242+
// Tracing may have been interrupted (e.g. a timeout) before the top-level frame was
243+
// captured, leaving nothing to finalize.
244+
if len(t.callstack) == 0 {
245+
return
246+
}
242247
if receipt != nil {
243248
t.callstack[0].GasUsed = receipt.GasUsed
244249
}
@@ -273,6 +278,10 @@ func (t *callTracer) OnLog(log *types.Log) {
273278
// GetResult returns the json-encoded nested list of call traces, and any
274279
// error arising from the encoding or forceful termination (via `Stop`).
275280
func (t *callTracer) GetResult() (json.RawMessage, error) {
281+
// Tracing was interrupted (e.g. a timeout): report the interruption instead of continuing
282+
if t.interrupt.Load() {
283+
return nil, t.reason
284+
}
276285
if len(t.callstack) != 1 {
277286
return nil, errors.New("incorrect number of top-level calls")
278287
}

eth/tracers/native/call_flat.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -232,6 +232,10 @@ func (t *flatCallTracer) OnTxEnd(receipt *types.Receipt, err error) {
232232

233233
// GetResult returns an empty json object.
234234
func (t *flatCallTracer) GetResult() (json.RawMessage, error) {
235+
// Tracing was interrupted (e.g. a timeout): report the interruption instead of continuing
236+
if t.interrupt.Load() {
237+
return nil, t.tracer.reason
238+
}
235239
if len(t.tracer.callstack) < 1 {
236240
return nil, errors.New("invalid number of calls")
237241
}

eth/tracers/native/erc7562.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -288,6 +288,10 @@ func (t *erc7562Tracer) OnTxEnd(receipt *types.Receipt, err error) {
288288
if err != nil {
289289
return
290290
}
291+
// An interrupted trace may leave no top-level frame to finalize.
292+
if len(t.callstackWithOpcodes) == 0 {
293+
return
294+
}
291295
t.callstackWithOpcodes[0].GasUsed = receipt.GasUsed
292296
if t.config.WithLog {
293297
// Logs are not emitted when the call fails
Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
// Copyright 2025 The go-ethereum Authors
2+
// This file is part of the go-ethereum library.
3+
//
4+
// The go-ethereum library 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+
// The go-ethereum library 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 the go-ethereum library. If not, see <http://www.gnu.org/licenses/>.
16+
17+
package native_test
18+
19+
import (
20+
"errors"
21+
"math/big"
22+
"testing"
23+
24+
"github.com/ethereum/go-ethereum/common"
25+
"github.com/ethereum/go-ethereum/core/tracing"
26+
"github.com/ethereum/go-ethereum/core/types"
27+
"github.com/ethereum/go-ethereum/core/vm"
28+
"github.com/ethereum/go-ethereum/eth/tracers"
29+
"github.com/ethereum/go-ethereum/params"
30+
"github.com/stretchr/testify/require"
31+
)
32+
33+
// TestTracerInterruptBeforeTopFrame guards against the empty-callstack panic when a trace
34+
// is interrupted before its top-level frame is captured. Calling Stop before OnEnter makes
35+
// that window deterministic, unlike the scheduler-dependent system-test repro.
36+
func TestTracerInterruptBeforeTopFrame(t *testing.T) {
37+
stopError := errors.New("execution timeout")
38+
39+
tx := types.NewTx(&types.LegacyTx{
40+
Nonce: 0,
41+
To: &common.Address{},
42+
Value: big.NewInt(0),
43+
Gas: 0,
44+
GasPrice: big.NewInt(0),
45+
Data: nil,
46+
})
47+
48+
for _, name := range []string{"callTracer", "flatCallTracer", "erc7562Tracer"} {
49+
t.Run(name, func(t *testing.T) {
50+
tracer, err := tracers.DefaultDirectory.New(name, &tracers.Context{}, nil, params.MainnetChainConfig)
51+
require.NoError(t, err)
52+
53+
tracer.OnTxStart(&tracing.VMContext{}, tx, common.Address{})
54+
55+
// Interrupt before the top-level frame is captured; OnEnter then no-ops on the
56+
// interrupt flag, leaving the callstack empty.
57+
tracer.Stop(stopError)
58+
tracer.OnEnter(0, byte(vm.CALL), common.Address{}, common.Address{}, nil, 0, big.NewInt(0))
59+
60+
var res []byte
61+
require.NotPanics(t, func() {
62+
tracer.OnTxEnd(&types.Receipt{GasUsed: 0}, nil)
63+
var tracerErr error
64+
res, tracerErr = tracer.GetResult()
65+
require.Equal(t, stopError, tracerErr)
66+
})
67+
require.Nil(t, res)
68+
})
69+
}
70+
}

0 commit comments

Comments
 (0)