diff --git a/internal/ethapi/api.go b/internal/ethapi/api.go index 6a30fb60dc..a64d89862d 100644 --- a/internal/ethapi/api.go +++ b/internal/ethapi/api.go @@ -19,6 +19,7 @@ package ethapi import ( "context" "encoding/hex" + "encoding/json" "errors" "fmt" "math/big" @@ -934,15 +935,104 @@ func (s *BlockChainAPI) GetBlockReceipts(ctx context.Context, blockNrOrHash rpc. } // GetRequiredBlockState returns all state required to execute a single historical block. -func (s *BlockChainAPI) GetRequiredBlockState(ctx context.Context, blockNrOrHash rpc.BlockNumberOrHash) ([]map[string]interface{}, error) { +func (s *BlockChainAPI) GetRequiredBlockState(ctx context.Context, blockNrOrHash rpc.BlockNumberOrHash) ([]*txTraceResult, error) { block, err := s.b.BlockByNumberOrHash(ctx, blockNrOrHash) if block == nil || err != nil { // When the block doesn't exist, the RPC method should return JSON null return nil, nil } + return s.traceBlock(ctx, block) +} - result := make([]map[string]interface{}, 1) - return result, nil +// txTraceResult is the result of a single transaction trace. +type txTraceResult struct { + TxHash common.Hash `json:"txHash"` // transaction hash + Result interface{} `json:"result,omitempty"` // Trace results produced by the tracer + Error string `json:"error,omitempty"` // Trace failure produced by the tracer +} + +const ( + // defaultTraceTimeout is the amount of time a single transaction can execute + // by default before being forcefully aborted. + defaultTraceTimeout = 5 * time.Second +) + +// traceBlock configures a new tracer according to the provided configuration, and +// executes all the transactions contained within. The return value will be one item +// per transaction, dependent on the requested tracer. +func (s *BlockChainAPI) traceBlock(ctx context.Context, block *types.Block) ([]*txTraceResult, error) { + if block.NumberU64() == 0 { + return nil, errors.New("genesis is not traceable") + } + // Prepare base state + statedb, _, err := s.b.StateAndHeaderByNumber(ctx, rpc.BlockNumber(block.NumberU64()-1)) + if err != nil { + return nil, err + } + + // Native tracers have low overhead + var ( + txs = block.Transactions() + is158 = s.b.ChainConfig().IsEIP158(block.Number()) + blockCtx = core.NewEVMBlockContext(block.Header(), NewChainContext(ctx, s.b), nil) + signer = types.MakeSigner(s.b.ChainConfig(), block.Number(), block.Time()) + results = make([]*txTraceResult, len(txs)) + ) + for i, tx := range txs { + // Generate the next state snapshot fast without tracing + msg, _ := core.TransactionToMessage(tx, signer, block.BaseFee()) + res, err := s.traceTx(ctx, msg, blockCtx, statedb) + if err != nil { + return nil, err + } + results[i] = &txTraceResult{TxHash: tx.Hash(), Result: res} + // Finalize the state so any modifications are written to the trie + // Only delete empty objects if EIP158/161 (a.k.a Spurious Dragon) is in effect + statedb.Finalise(is158) + } + return results, nil +} + +// Tracer interface extends vm.EVMLogger and additionally +// allows collecting the tracing result. +type Tracer interface { + vm.EVMLogger + GetResult() (json.RawMessage, error) + // Stop terminates execution of the tracer at the first opportune moment. + Stop(err error) +} + +// 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 (s *BlockChainAPI) traceTx(ctx context.Context, message *core.Message, vmctx vm.BlockContext, statedb *state.StateDB) (interface{}, error) { + var ( + tracer Tracer + err error + timeout = defaultTraceTimeout + txContext = core.NewEVMTxContext(message) + ) + // Default tracer is the struct logger + tracer = logger.NewStructLogger(&logger.Config{}) + vmenv := vm.NewEVM(vmctx, txContext, statedb, s.b.ChainConfig(), vm.Config{Tracer: tracer, NoBaseFee: true}) + + // Define a meaningful timeout of a single transaction trace + deadlineCtx, cancel := context.WithTimeout(ctx, timeout) + go func() { + <-deadlineCtx.Done() + if errors.Is(deadlineCtx.Err(), context.DeadlineExceeded) { + tracer.Stop(errors.New("execution timeout")) + // Stop evm execution. Note cancellation is not necessarily immediate. + vmenv.Cancel() + } + }() + defer cancel() + + // Call Prepare to clear out the statedb access list + if _, err = core.ApplyMessage(vmenv, message, new(core.GasPool).AddGas(message.GasLimit)); err != nil { + return nil, fmt.Errorf("tracing failed: %w", err) + } + return tracer.GetResult() } // OverrideAccount indicates the overriding fields of account during the execution diff --git a/internal/ethapi/api_test.go b/internal/ethapi/api_test.go index 9caf0ecfc8..9c62011907 100644 --- a/internal/ethapi/api_test.go +++ b/internal/ethapi/api_test.go @@ -1620,7 +1620,86 @@ func TestRPCGetBlockReceipts(t *testing.T) { } func TestRPCGetRequiredBlockState(t *testing.T) { - // + t.Parallel() + + var ( + genBlocks = 6 + backend, _ = setupReceiptBackend(t, genBlocks) + api = NewBlockChainAPI(backend) + ) + blockHashes := make([]common.Hash, genBlocks+1) + ctx := context.Background() + for i := 0; i <= genBlocks; i++ { + header, err := backend.HeaderByNumber(ctx, rpc.BlockNumber(i)) + if err != nil { + t.Errorf("failed to get block: %d err: %v", i, err) + } + blockHashes[i] = header.Hash() + } + + var testSuite = []struct { + test rpc.BlockNumberOrHash + file string + }{ + // 3. latest tag + { + test: rpc.BlockNumberOrHashWithNumber(rpc.LatestBlockNumber), + file: "tag-latest", + }, + // // 4. block with legacy transfer tx(hash) + // { + // test: rpc.BlockNumberOrHashWithHash(blockHashes[1], false), + // file: "block-with-legacy-transfer-tx", + // }, + // // 5. block with contract create tx(number) + // { + // test: rpc.BlockNumberOrHashWithNumber(rpc.BlockNumber(2)), + // file: "block-with-contract-create-tx", + // }, + // // 6. block with legacy contract call tx(hash) + // { + // test: rpc.BlockNumberOrHashWithHash(blockHashes[3], false), + // file: "block-with-legacy-contract-call-tx", + // }, + // // 7. block with dynamic fee tx(number) + // { + // test: rpc.BlockNumberOrHashWithNumber(rpc.BlockNumber(4)), + // file: "block-with-dynamic-fee-tx", + // }, + // // 8. block is empty + // { + // test: rpc.BlockNumberOrHashWithHash(common.Hash{}, false), + // file: "hash-empty", + // }, + // // 9. block is not found + // { + // test: rpc.BlockNumberOrHashWithHash(common.HexToHash("deadbeef"), false), + // file: "hash-notfound", + // }, + // // 10. block is not found + // { + // test: rpc.BlockNumberOrHashWithNumber(rpc.BlockNumber(genBlocks + 1)), + // file: "block-notfound", + // }, + // // 11. block with blob tx + // { + // test: rpc.BlockNumberOrHashWithNumber(rpc.BlockNumber(6)), + // file: "block-with-blob-tx", + // }, + } + + for i, tt := range testSuite { + var ( + result interface{} + err error + ) + result, err = api.GetRequiredBlockState(context.Background(), tt.test) + if err != nil { + t.Errorf("test %d: want no error, have %v", i, err) + continue + } + testRPCResponseWithFile(t, i, result, "eth_getRequiredBlockState", tt.file) + } } func testRPCResponseWithFile(t *testing.T, testid int, result interface{}, rpc string, file string) { diff --git a/internal/ethapi/testdata/eth_getRequiredBlockState-tag-latest.json b/internal/ethapi/testdata/eth_getRequiredBlockState-tag-latest.json new file mode 100644 index 0000000000..71b397d1f1 --- /dev/null +++ b/internal/ethapi/testdata/eth_getRequiredBlockState-tag-latest.json @@ -0,0 +1,11 @@ +[ + { + "txHash": "0xb51ee3d2a89ba5d5623c73133c8d7a6ba9fb41194c17f4302c21b30994a1180f", + "result": { + "gas": 21001, + "failed": false, + "returnValue": "", + "structLogs": [] + } + } +] \ No newline at end of file