diff --git a/eth/tracers/api.go b/eth/tracers/api.go index 2886fcd0cb..67e4ce98bb 100644 --- a/eth/tracers/api.go +++ b/eth/tracers/api.go @@ -944,13 +944,20 @@ func (api *API) TraceCall(ctx context.Context, args ethapi.TransactionArgs, bloc // traceTx configures a new tracer according to the provided configuration, and // executes the given message in the provided environment. The return value will // be tracer dependent. -func (api *API) traceTx(ctx context.Context, tx *types.Transaction, message *core.Message, txctx *Context, vmctx vm.BlockContext, statedb vm.StateDB, config *TraceConfig) (interface{}, error) { +func (api *API) traceTx(ctx context.Context, tx *types.Transaction, message *core.Message, txctx *Context, vmctx vm.BlockContext, statedb vm.StateDB, config *TraceConfig) (value interface{}, returnErr error) { + // Note that traceTx could panic, so we want to catch it and send a generic error message var ( tracer *Tracer err error timeout = defaultTraceTimeout usedGas uint64 ) + defer func() { + if r := recover(); r != nil { + value = nil + returnErr = fmt.Errorf("panic occured: %v, could not trace tx: %s", r, tx.Hash()) + } + }() if config == nil { config = &TraceConfig{} } diff --git a/eth/tracers/api_test.go b/eth/tracers/api_test.go index 2f5bf4f7d5..fba30b018f 100644 --- a/eth/tracers/api_test.go +++ b/eth/tracers/api_test.go @@ -457,6 +457,74 @@ func TestTraceTransaction(t *testing.T) { } } +func TestTracePanicTransaction(t *testing.T) { + t.Parallel() + + // Initialize test accounts + accounts := newAccounts(2) + genesis := &core.Genesis{ + Config: params.TestChainConfig, + Alloc: types.GenesisAlloc{ + accounts[0].addr: {Balance: big.NewInt(params.Ether)}, + accounts[1].addr: {Balance: big.NewInt(params.Ether)}, + }, + } + target := common.Hash{} + signer := types.HomesteadSigner{} + backend := newTestBackend(t, 1, genesis, func(i int, b *core.BlockGen) { + // Transfer from account[0] to account[1] + tx, _ := types.SignTx(types.NewTx(&types.LegacyTx{ + Nonce: uint64(i), + To: &accounts[1].addr, + Value: big.NewInt(1000), + Gas: params.TxGas, + GasPrice: b.BaseFee(), + Data: nil}), + signer, accounts[0].key) + b.AddTx(tx) + target = tx.Hash() + }) + defer backend.chain.Stop() + + // Create API with a backend that uses a panic-inducing StateDB + panicBackend := &panicBackend{backend} + api := NewAPI(panicBackend) + + result, err := api.TraceTransaction(context.Background(), target, nil) + + // Verify panic was caught and handled + if err == nil { + t.Fatal("Expected error from panic recovery, got nil") + } + if result != nil { + t.Errorf("Expected nil result after panic, got %v", result) + } +} + +type panicBackend struct { + Backend +} + +// StateAtTransaction overrides the backend's StateAtTransaction to use a panic-inducing StateDB +func (b *panicBackend) StateAtTransaction(ctx context.Context, block *types.Block, txIndex int, reexec uint64) (*types.Transaction, vm.BlockContext, vm.StateDB, StateReleaseFunc, error) { + tx, vmctx, _, release, err := b.Backend.StateAtTransaction(ctx, block, txIndex, reexec) + if err != nil { + return nil, vm.BlockContext{}, nil, nil, err + } + // Return a StateDB that panics on ApplyTransactionWithEVM + return tx, vmctx, &panicStateDB{}, release, nil +} + +// panicStateDB is a mock StateDB that panics during ApplyTransactionWithEVM +type panicStateDB struct { + vm.StateDB +} + +// ApplyTransactionWithEVM is overridden to panic +func (s *panicStateDB) ApplyTransactionWithEVM(message *core.Message, config *params.ChainConfig, gasPool *core.GasPool, statedb vm.StateDB, blockNumber *big.Int, blockHash common.Hash, tx *types.Transaction, usedGas *uint64, evm *vm.EVM) ([]byte, error) { + panic("intentional panic for testing") +} + func TestTraceBlock(t *testing.T) { t.Parallel()