diff --git a/.gitignore b/.gitignore index 1ee8b83022ef..fef885502c6f 100644 --- a/.gitignore +++ b/.gitignore @@ -47,3 +47,4 @@ profile.cov /dashboard/assets/package-lock.json **/yarn-error.log + diff --git a/core/blockchain.go b/core/blockchain.go index 38c37a432525..2cfb1d8f6b15 100644 --- a/core/blockchain.go +++ b/core/blockchain.go @@ -1182,17 +1182,17 @@ func (bc *BlockChain) writeKnownBlock(block *types.Block) error { } // WriteBlockWithState writes the block and all associated state to the database. -func (bc *BlockChain) WriteBlockWithState(block *types.Block, receipts []*types.Receipt, logs []*types.Log, state *state.StateDB, emitHeadEvent bool) (status WriteStatus, err error) { +func (bc *BlockChain) WriteBlockWithState(block *types.Block, receipts []*types.Receipt, logs []*types.Log, blockResult *types.BlockResult, state *state.StateDB, emitHeadEvent bool) (status WriteStatus, err error) { if !bc.chainmu.TryLock() { return NonStatTy, errInsertionInterrupted } defer bc.chainmu.Unlock() - return bc.writeBlockWithState(block, receipts, logs, state, emitHeadEvent) + return bc.writeBlockWithState(block, receipts, logs, blockResult, state, emitHeadEvent) } // writeBlockWithState writes the block and all associated state to the database, // but is expects the chain mutex to be held. -func (bc *BlockChain) writeBlockWithState(block *types.Block, receipts []*types.Receipt, logs []*types.Log, state *state.StateDB, emitHeadEvent bool) (status WriteStatus, err error) { +func (bc *BlockChain) writeBlockWithState(block *types.Block, receipts []*types.Receipt, logs []*types.Log, blockResult *types.BlockResult, state *state.StateDB, emitHeadEvent bool) (status WriteStatus, err error) { if bc.insertStopped() { return NonStatTy, errInsertionInterrupted } @@ -1215,6 +1215,7 @@ func (bc *BlockChain) writeBlockWithState(block *types.Block, receipts []*types. rawdb.WriteTd(blockBatch, block.Hash(), block.NumberU64(), externTd) rawdb.WriteBlock(blockBatch, block) rawdb.WriteReceipts(blockBatch, block.Hash(), block.NumberU64(), receipts) + rawdb.WriteBlockResult(blockBatch, block.Hash(), blockResult) rawdb.WritePreimages(blockBatch, state.Preimages()) if err := blockBatch.Write(); err != nil { log.Crit("Failed to write block into disk", "err", err) @@ -1314,7 +1315,7 @@ func (bc *BlockChain) writeBlockWithState(block *types.Block, receipts []*types. bc.futureBlocks.Remove(block.Hash()) if status == CanonStatTy { - bc.chainFeed.Send(ChainEvent{Block: block, Hash: block.Hash(), Logs: logs}) + bc.chainFeed.Send(ChainEvent{Block: block, Hash: block.Hash(), Logs: logs, BlockResult: blockResult}) if len(logs) > 0 { bc.logsFeed.Send(logs) } @@ -1644,7 +1645,8 @@ func (bc *BlockChain) insertChain(chain types.Blocks, verifySeals bool) (int, er // Write the block to the chain and get the status. substart = time.Now() - status, err := bc.writeBlockWithState(block, receipts, logs, statedb, false) + // EvmTraces is nil is safe because l2geth's p2p server is stoped and the code will not execute there. + status, err := bc.writeBlockWithState(block, receipts, logs, nil, statedb, false) atomic.StoreUint32(&followupInterrupt, 1) if err != nil { return it.index, err diff --git a/core/events.go b/core/events.go index 398bac1ba0e6..cce09dffd8ca 100644 --- a/core/events.go +++ b/core/events.go @@ -31,9 +31,10 @@ type NewMinedBlockEvent struct{ Block *types.Block } type RemovedLogsEvent struct{ Logs []*types.Log } type ChainEvent struct { - Block *types.Block - Hash common.Hash - Logs []*types.Log + Block *types.Block + Hash common.Hash + Logs []*types.Log + BlockResult *types.BlockResult } type ChainSideEvent struct { diff --git a/core/rawdb/l2trace.go b/core/rawdb/l2trace.go new file mode 100644 index 000000000000..ee82d02b4b02 --- /dev/null +++ b/core/rawdb/l2trace.go @@ -0,0 +1,39 @@ +package rawdb + +import ( + "github.com/scroll-tech/go-ethereum/common" + "github.com/scroll-tech/go-ethereum/core/types" + "github.com/scroll-tech/go-ethereum/ethdb" + "github.com/scroll-tech/go-ethereum/log" + "github.com/scroll-tech/go-ethereum/rlp" +) + +// ReadBlockResult retrieves all data required by roller. +func ReadBlockResult(db ethdb.Reader, hash common.Hash) *types.BlockResult { + data, _ := db.Get(blockResultKey(hash)) + if len(data) == 0 { + return nil + } + var blockResult types.BlockResult + if err := rlp.DecodeBytes(data, &blockResult); err != nil { + log.Error("Failed to decode BlockResult", "err", err) + return nil + } + return &blockResult +} + +// WriteBlockResult stores blockResult into leveldb. +func WriteBlockResult(db ethdb.KeyValueWriter, hash common.Hash, blockResult *types.BlockResult) { + bytes, err := rlp.EncodeToBytes(blockResult) + if err != nil { + log.Crit("Failed to RLP encode BlockResult", "err", err) + } + db.Put(blockResultKey(hash), bytes) +} + +// DeleteBlockResult removes blockResult with a block hash. +func DeleteBlockResult(db ethdb.KeyValueWriter, hash common.Hash) { + if err := db.Delete(blockResultKey(hash)); err != nil { + log.Crit("Failed to delete BlockResult", "err", err) + } +} diff --git a/core/rawdb/l2trace_test.go b/core/rawdb/l2trace_test.go new file mode 100644 index 000000000000..d9ccf91ffaf8 --- /dev/null +++ b/core/rawdb/l2trace_test.go @@ -0,0 +1,355 @@ +package rawdb + +import ( + "bytes" + "encoding/hex" + "encoding/json" + "fmt" + "testing" + + "github.com/scroll-tech/go-ethereum/common" + "github.com/scroll-tech/go-ethereum/core/types" + "github.com/scroll-tech/go-ethereum/rlp" +) + +func TestBlockEvmTracesStorage(t *testing.T) { + db := NewMemoryDatabase() + + data1 := []byte(`{ + "gas": 1, + "failed": false, + "returnValue": "", + "structLogs": [ + { + "pc": 0, + "op": "PUSH1", + "gas": 1000000, + "gasCost": 3, + "depth": 1, + "stack": [], + "memory": [] + }, + { + "pc": 2, + "op": "SLOAD", + "gas": 999997, + "gasCost": 2100, + "depth": 1, + "stack": [ + "0x1" + ], + "memory": [], + "storage": { + "0000000000000000000000000000000000000000000000000000000000000001": "0000000000000000000000000000000000000000000000000000000000000000" + } + }, + { + "pc": 3, + "op": "POP", + "gas": 997897, + "gasCost": 2, + "depth": 1, + "stack": [ + "0x0" + ], + "memory": [] + }, + { + "pc": 4, + "op": "PUSH1", + "gas": 997895, + "gasCost": 3, + "depth": 1, + "stack": [], + "memory": [] + }, + { + "pc": 6, + "op": "PUSH1", + "gas": 997892, + "gasCost": 3, + "depth": 1, + "stack": [ + "0x11" + ], + "memory": [] + }, + { + "pc": 8, + "op": "SSTORE", + "gas": 997889, + "gasCost": 20000, + "depth": 1, + "stack": [ + "0x11", + "0x1" + ], + "memory": [], + "storage": { + "0000000000000000000000000000000000000000000000000000000000000001": "0000000000000000000000000000000000000000000000000000000000000011" + } + }, + { + "pc": 9, + "op": "PUSH1", + "gas": 977889, + "gasCost": 3, + "depth": 1, + "stack": [], + "memory": [] + }, + { + "pc": 11, + "op": "PUSH1", + "gas": 977886, + "gasCost": 3, + "depth": 1, + "stack": [ + "0x11" + ], + "memory": [] + }, + { + "pc": 13, + "op": "SSTORE", + "gas": 977883, + "gasCost": 22100, + "depth": 1, + "stack": [ + "0x11", + "0x2" + ], + "memory": [], + "storage": { + "0000000000000000000000000000000000000000000000000000000000000001": "0000000000000000000000000000000000000000000000000000000000000011", + "0000000000000000000000000000000000000000000000000000000000000002": "0000000000000000000000000000000000000000000000000000000000000011" + } + }, + { + "pc": 14, + "op": "PUSH1", + "gas": 955783, + "gasCost": 3, + "depth": 1, + "stack": [], + "memory": [] + }, + { + "pc": 16, + "op": "PUSH1", + "gas": 955780, + "gasCost": 3, + "depth": 1, + "stack": [ + "0x11" + ], + "memory": [] + }, + { + "pc": 18, + "op": "SSTORE", + "gas": 955777, + "gasCost": 100, + "depth": 1, + "stack": [ + "0x11", + "0x2" + ], + "memory": [], + "storage": { + "0000000000000000000000000000000000000000000000000000000000000001": "0000000000000000000000000000000000000000000000000000000000000011", + "0000000000000000000000000000000000000000000000000000000000000002": "0000000000000000000000000000000000000000000000000000000000000011" + } + }, + { + "pc": 19, + "op": "PUSH1", + "gas": 955677, + "gasCost": 3, + "depth": 1, + "stack": [], + "memory": [] + }, + { + "pc": 21, + "op": "SLOAD", + "gas": 955674, + "gasCost": 100, + "depth": 1, + "stack": [ + "0x2" + ], + "memory": [], + "storage": { + "0000000000000000000000000000000000000000000000000000000000000001": "0000000000000000000000000000000000000000000000000000000000000011", + "0000000000000000000000000000000000000000000000000000000000000002": "0000000000000000000000000000000000000000000000000000000000000011" + } + }, + { + "pc": 22, + "op": "PUSH1", + "gas": 955574, + "gasCost": 3, + "depth": 1, + "stack": [ + "0x11" + ], + "memory": [] + }, + { + "pc": 24, + "op": "SLOAD", + "gas": 955571, + "gasCost": 100, + "depth": 1, + "stack": [ + "0x11", + "0x1" + ], + "memory": [], + "storage": { + "0000000000000000000000000000000000000000000000000000000000000001": "0000000000000000000000000000000000000000000000000000000000000011", + "0000000000000000000000000000000000000000000000000000000000000002": "0000000000000000000000000000000000000000000000000000000000000011" + } + }, + { + "pc": 25, + "op": "STOP", + "gas": 955471, + "gasCost": 0, + "depth": 1, + "stack": [ + "0x11", + "0x11" + ], + "memory": [] + } + ] +}`) + evmTrace1 := &types.ExecutionResult{ReturnValue: "0xaaa"} + if err := json.Unmarshal(data1, evmTrace1); err != nil { + t.Fatalf(err.Error()) + } + + data2 := []byte(`{ + "gas": 1, + "failed": false, + "returnValue": "000000000000000000000000000000000000000000000000000000000000000a", + "structLogs": [ + { + "pc": 0, + "op": "PUSH1", + "gas": 1000000, + "gasCost": 3, + "depth": 1, + "stack": [], + "memory": [] + }, + { + "pc": 2, + "op": "PUSH1", + "gas": 999997, + "gasCost": 3, + "depth": 1, + "stack": [ + "0xa" + ], + "memory": [] + }, + { + "pc": 4, + "op": "MSTORE", + "gas": 999994, + "gasCost": 6, + "depth": 1, + "stack": [ + "0xa", + "0x0" + ], + "memory": [ + "0000000000000000000000000000000000000000000000000000000000000000" + ] + }, + { + "pc": 5, + "op": "PUSH1", + "gas": 999988, + "gasCost": 3, + "depth": 1, + "stack": [], + "memory": [ + "000000000000000000000000000000000000000000000000000000000000000a" + ] + }, + { + "pc": 7, + "op": "PUSH1", + "gas": 999985, + "gasCost": 3, + "depth": 1, + "stack": [ + "0x20" + ], + "memory": [ + "000000000000000000000000000000000000000000000000000000000000000a" + ] + }, + { + "pc": 9, + "op": "RETURN", + "gas": 999982, + "gasCost": 0, + "depth": 1, + "stack": [ + "0x20", + "0x0" + ], + "memory": [ + "000000000000000000000000000000000000000000000000000000000000000a" + ] + } + ] +}`) + evmTrace2 := &types.ExecutionResult{ReturnValue: "0xbbb"} + if err := json.Unmarshal(data2, evmTrace2); err != nil { + t.Fatalf(err.Error()) + } + + evmTraces := []*types.ExecutionResult{evmTrace1, evmTrace2} + hash := common.BytesToHash([]byte{0x03, 0x04}) + // Insert the blockResult into the database and check presence. + WriteBlockResult(db, hash, &types.BlockResult{ExecutionResults: evmTraces}) + // Read blockResult from db. + if blockResult := ReadBlockResult(db, hash); len(blockResult.ExecutionResults) == 0 { + t.Fatalf("No evmTraces returned") + } else { + if err := checkEvmTracesRLP(blockResult.ExecutionResults, evmTraces); err != nil { + t.Fatalf(err.Error()) + } + } + // Delete blockResult by blockHash. + DeleteBlockResult(db, hash) + if blockResult := ReadBlockResult(db, hash); blockResult != nil && len(blockResult.ExecutionResults) != 0 { + t.Fatalf("The evmTrace list should be empty.") + } +} + +func checkEvmTracesRLP(have, want []*types.ExecutionResult) error { + if len(have) != len(want) { + return fmt.Errorf("evmTraces sizes mismatch: have: %d, want: %d", len(have), len(want)) + } + for i := 0; i < len(want); i++ { + rlpHave, err := rlp.EncodeToBytes(have[i]) + if err != nil { + return err + } + rlpWant, err := rlp.EncodeToBytes(want[i]) + if err != nil { + return err + } + if !bytes.Equal(rlpHave, rlpWant) { + return fmt.Errorf("evmTrace #%d: evmTrace mismatch: have %s, want %s", i, hex.EncodeToString(rlpHave), hex.EncodeToString(rlpWant)) + } + } + return nil +} diff --git a/core/rawdb/schema.go b/core/rawdb/schema.go index 73dc69ea3122..eb9e2808ffb0 100644 --- a/core/rawdb/schema.go +++ b/core/rawdb/schema.go @@ -89,6 +89,7 @@ var ( SnapshotAccountPrefix = []byte("a") // SnapshotAccountPrefix + account hash -> account trie value SnapshotStoragePrefix = []byte("o") // SnapshotStoragePrefix + account hash + storage hash -> storage trie value CodePrefix = []byte("c") // CodePrefix + code hash -> account code + blockResultPrefix = []byte("T") // blockResultPrefix + hash -> blockResult PreimagePrefix = []byte("secure-key-") // PreimagePrefix + hash -> preimage configPrefix = []byte("ethereum-config-") // config prefix for the db @@ -230,3 +231,8 @@ func IsCodeKey(key []byte) (bool, []byte) { func configKey(hash common.Hash) []byte { return append(configPrefix, hash.Bytes()...) } + +// blockResultKey = blockResultPrefix + hash +func blockResultKey(hash common.Hash) []byte { + return append(blockResultPrefix, hash.Bytes()...) +} diff --git a/core/state_processor.go b/core/state_processor.go index f5898cbf8122..5d8d55726753 100644 --- a/core/state_processor.go +++ b/core/state_processor.go @@ -112,9 +112,14 @@ func applyTransaction(msg types.Message, config *params.ChainConfig, bc ChainCon } *usedGas += result.UsedGas + // If the result contains a revert reason, return it. + returnVal := result.Return() + if len(result.Revert()) > 0 { + returnVal = result.Revert() + } // Create a new receipt for the transaction, storing the intermediate root and gas used // by the tx. - receipt := &types.Receipt{Type: tx.Type(), PostState: root, CumulativeGasUsed: *usedGas} + receipt := &types.Receipt{Type: tx.Type(), PostState: root, CumulativeGasUsed: *usedGas, ReturnValue: returnVal} if result.Failed() { receipt.Status = types.ReceiptStatusFailed } else { diff --git a/core/types/l2trace.go b/core/types/l2trace.go new file mode 100644 index 000000000000..4537a48cac03 --- /dev/null +++ b/core/types/l2trace.go @@ -0,0 +1,165 @@ +package types + +import ( + "io" + "sort" + "strings" + + "github.com/scroll-tech/go-ethereum/rlp" +) + +// BlockResult contains block execution traces and results required for rollers. +type BlockResult struct { + ExecutionResults []*ExecutionResult `json:"executionResults"` +} + +type rlpBlockResult struct { + ExecutionResults []*ExecutionResult +} + +func (b *BlockResult) EncodeRLP(w io.Writer) error { + return rlp.Encode(w, &rlpBlockResult{ + ExecutionResults: b.ExecutionResults, + }) +} + +func (b *BlockResult) DecodeRLP(s *rlp.Stream) error { + var dec rlpBlockResult + err := s.Decode(&dec) + if err == nil { + b.ExecutionResults = dec.ExecutionResults + } + return err +} + +// ExecutionResult groups all structured logs emitted by the EVM +// while replaying a transaction in debug mode as well as transaction +// execution status, the amount of gas used and the return value +type ExecutionResult struct { + Gas uint64 `json:"gas"` + Failed bool `json:"failed"` + ReturnValue string `json:"returnValue,omitempty"` + StructLogs []StructLogRes `json:"structLogs"` +} + +type rlpExecutionResult struct { + Gas uint64 + Failed bool + ReturnValue string + StructLogs []StructLogRes +} + +func (e *ExecutionResult) EncodeRLP(w io.Writer) error { + return rlp.Encode(w, rlpExecutionResult{ + Gas: e.Gas, + Failed: e.Failed, + ReturnValue: e.ReturnValue, + StructLogs: e.StructLogs, + }) +} + +func (e *ExecutionResult) DecodeRLP(s *rlp.Stream) error { + var dec rlpExecutionResult + err := s.Decode(&dec) + if err == nil { + e.Gas, e.Failed, e.ReturnValue, e.StructLogs = dec.Gas, dec.Failed, dec.ReturnValue, dec.StructLogs + } + return err +} + +// StructLogRes stores a structured log emitted by the EVM while replaying a +// transaction in debug mode +type StructLogRes struct { + Pc uint64 `json:"pc"` + Op string `json:"op"` + Gas uint64 `json:"gas"` + GasCost uint64 `json:"gasCost"` + Depth int `json:"depth"` + Error string `json:"error,omitempty"` + Stack *[]string `json:"stack,omitempty"` + Memory *[]string `json:"memory,omitempty"` + Storage *map[string]string `json:"storage,omitempty"` +} + +type rlpStructLogRes struct { + Pc uint64 + Op string + Gas uint64 + GasCost uint64 + Depth uint + Error string + Stack []string + Memory []string + Storage []string +} + +// EncodeRLP implements rlp.Encoder. +func (r *StructLogRes) EncodeRLP(w io.Writer) error { + data := rlpStructLogRes{ + Pc: r.Pc, + Op: r.Op, + Gas: r.Gas, + GasCost: r.GasCost, + Depth: uint(r.Depth), + Error: r.Error, + } + if r.Stack != nil { + data.Stack = make([]string, len(*r.Stack)) + for i, val := range *r.Stack { + data.Stack[i] = val + } + } + if r.Memory != nil { + data.Memory = make([]string, len(*r.Memory)) + for i, val := range *r.Memory { + data.Memory[i] = val + } + } + if r.Storage != nil { + keys := make([]string, 0, len(*r.Storage)) + for key := range *r.Storage { + keys = append(keys, key) + } + sort.Slice(keys, func(i, j int) bool { + return strings.Compare(keys[i], keys[j]) >= 0 + }) + data.Storage = make([]string, 0, len(*r.Storage)*2) + for _, key := range keys { + data.Storage = append(data.Storage, []string{key, (*r.Storage)[key]}...) + } + } + return rlp.Encode(w, data) +} + +// DecodeRLP implements rlp.Decoder. +func (r *StructLogRes) DecodeRLP(s *rlp.Stream) error { + var dec rlpStructLogRes + err := s.Decode(&dec) + if err != nil { + return err + } + r.Pc, r.Op, r.Gas, r.GasCost, r.Depth, r.Error = dec.Pc, dec.Op, dec.Gas, dec.GasCost, int(dec.Depth), dec.Error + if len(dec.Stack) != 0 { + stack := make([]string, len(dec.Stack)) + for i, val := range dec.Stack { + stack[i] = val + } + r.Stack = &stack + } + if len(dec.Memory) != 0 { + memory := make([]string, len(dec.Memory)) + for i, val := range dec.Memory { + memory[i] = val + } + r.Memory = &memory + } + if len(dec.Storage) != 0 { + storage := make(map[string]string, len(dec.Storage)*2) + for i := 0; i < len(dec.Storage); i += 2 { + key, val := dec.Storage[i], dec.Storage[i+1] + storage[key] = val + } + r.Storage = &storage + } + return nil +} diff --git a/core/types/receipt.go b/core/types/receipt.go index 28cab6db0542..bf20cfbc65d8 100644 --- a/core/types/receipt.go +++ b/core/types/receipt.go @@ -70,6 +70,9 @@ type Receipt struct { BlockHash common.Hash `json:"blockHash,omitempty"` BlockNumber *big.Int `json:"blockNumber,omitempty"` TransactionIndex uint `json:"transactionIndex"` + + // The value of evm execution result. + ReturnValue []byte `json:"returnValue,omitempty"` } type receiptMarshaling struct { diff --git a/core/vm/logger.go b/core/vm/logger.go index 9e547fd29474..196cf0aefd6a 100644 --- a/core/vm/logger.go +++ b/core/vm/logger.go @@ -358,3 +358,40 @@ func (t *mdLogger) CaptureEnter(typ OpCode, from common.Address, to common.Addre } func (t *mdLogger) CaptureExit(output []byte, gasUsed uint64, err error) {} + +// FormatLogs formats EVM returned structured logs for json output +func FormatLogs(logs []StructLog) []types.StructLogRes { + formatted := make([]types.StructLogRes, len(logs)) + for index, trace := range logs { + formatted[index] = types.StructLogRes{ + Pc: trace.Pc, + Op: trace.Op.String(), + Gas: trace.Gas, + GasCost: trace.GasCost, + Depth: trace.Depth, + Error: trace.ErrorString(), + } + if trace.Stack != nil { + stack := make([]string, len(trace.Stack)) + for i, stackValue := range trace.Stack { + stack[i] = stackValue.Hex() + } + formatted[index].Stack = &stack + } + if trace.Memory != nil { + memory := make([]string, 0, (len(trace.Memory)+31)/32) + for i := 0; i+32 <= len(trace.Memory); i += 32 { + memory = append(memory, fmt.Sprintf("%x", trace.Memory[i:i+32])) + } + formatted[index].Memory = &memory + } + if trace.Storage != nil { + storage := make(map[string]string) + for i, storageValue := range trace.Storage { + storage[fmt.Sprintf("%x", i)] = fmt.Sprintf("%x", storageValue) + } + formatted[index].Storage = &storage + } + } + return formatted +} diff --git a/eth/api.go b/eth/api.go index 2e9b91246043..41dcb0683be4 100644 --- a/eth/api.go +++ b/eth/api.go @@ -607,3 +607,18 @@ func (api *PrivateDebugAPI) GetAccessibleState(from, to rpc.BlockNumber) (uint64 } return 0, fmt.Errorf("No state found") } + +// PublicTraceAPI provides an API to get evmTrace, mpt proof. +type PublicTraceAPI struct { + e *Ethereum +} + +// NewPublicTraceAPI creates a new Ethereum trace API. +func NewPublicTraceAPI(eth *Ethereum) *PublicTraceAPI { + return &PublicTraceAPI{eth} +} + +// BlockResultByHash returns the blockResult by blockHash. +func (api *PublicTraceAPI) BlockResultByHash(blockHash common.Hash) (*types.BlockResult, error) { + return rawdb.ReadBlockResult(api.e.chainDb, blockHash), nil +} diff --git a/eth/backend.go b/eth/backend.go index 2adee3f2cf16..4e3ae461a7c0 100644 --- a/eth/backend.go +++ b/eth/backend.go @@ -175,6 +175,8 @@ func New(stack *node.Node, config *ethconfig.Config) (*Ethereum, error) { var ( vmConfig = vm.Config{ EnablePreimageRecording: config.EnablePreimageRecording, + Debug: true, + Tracer: vm.NewStructLogger(&vm.LogConfig{EnableMemory: true}), } cacheConfig = &core.CacheConfig{ TrieCleanLimit: config.TrieCleanCache, @@ -314,6 +316,11 @@ func (s *Ethereum) APIs() []rpc.API { Version: "1.0", Service: downloader.NewPublicDownloaderAPI(s.handler.downloader, s.eventMux), Public: true, + }, { + Namespace: "eth", + Version: "1.0", + Service: NewPublicTraceAPI(s), + Public: true, }, { Namespace: "miner", Version: "1.0", diff --git a/eth/filters/api.go b/eth/filters/api.go index 66d62a86de64..52dbcca9d33c 100644 --- a/eth/filters/api.go +++ b/eth/filters/api.go @@ -278,6 +278,35 @@ func (api *PublicFilterAPI) Logs(ctx context.Context, crit FilterCriteria) (*rpc return rpcSub, nil } +// NewBlockResult sends the block execution result when a new block is created. +func (api *PublicFilterAPI) NewBlockResult(ctx context.Context) (*rpc.Subscription, error) { + notifier, supported := rpc.NotifierFromContext(ctx) + if !supported { + return &rpc.Subscription{}, rpc.ErrNotificationsUnsupported + } + + rpcSub := notifier.CreateSubscription() + + go func() { + blockResults := make(chan *types.BlockResult) + blockResultsSub := api.events.SubscribeBlockResult(blockResults) + + for { + select { + case blockResult := <-blockResults: + notifier.Notify(rpcSub.ID, blockResult) + case <-rpcSub.Err(): + blockResultsSub.Unsubscribe() + return + case <-notifier.Closed(): + blockResultsSub.Unsubscribe() + return + } + } + }() + return rpcSub, nil +} + // FilterCriteria represents a request to create a new filter. // Same as ethereum.FilterQuery but with UnmarshalJSON() method. type FilterCriteria ethereum.FilterQuery diff --git a/eth/filters/filter_system.go b/eth/filters/filter_system.go index 6275a57bb82a..c9ca0a6e110f 100644 --- a/eth/filters/filter_system.go +++ b/eth/filters/filter_system.go @@ -52,6 +52,8 @@ const ( PendingTransactionsSubscription // BlocksSubscription queries hashes for blocks that are imported BlocksSubscription + // BlockResultsSubscription queries for block execution traces + BlockResultsSubscription // LastSubscription keeps track of the last index LastIndexSubscription ) @@ -69,15 +71,16 @@ const ( ) type subscription struct { - id rpc.ID - typ Type - created time.Time - logsCrit ethereum.FilterQuery - logs chan []*types.Log - hashes chan []common.Hash - headers chan *types.Header - installed chan struct{} // closed when the filter is installed - err chan error // closed when the filter is uninstalled + id rpc.ID + typ Type + created time.Time + logsCrit ethereum.FilterQuery + logs chan []*types.Log + hashes chan []common.Hash + headers chan *types.Header + blockResults chan *types.BlockResult + installed chan struct{} // closed when the filter is installed + err chan error // closed when the filter is uninstalled } // EventSystem creates subscriptions, processes events and broadcasts them to the @@ -290,6 +293,19 @@ func (es *EventSystem) SubscribeNewHeads(headers chan *types.Header) *Subscripti return es.subscribe(sub) } +// SubscribeBlockResult creates a subscription that writes the block trace when a new block is created. +func (es *EventSystem) SubscribeBlockResult(blockResult chan *types.BlockResult) *Subscription { + sub := &subscription{ + id: rpc.NewID(), + typ: BlockResultsSubscription, + created: time.Now(), + blockResults: blockResult, + installed: make(chan struct{}), + err: make(chan error), + } + return es.subscribe(sub) +} + // SubscribePendingTxs creates a subscription that writes transaction hashes for // transactions that enter the transaction pool. func (es *EventSystem) SubscribePendingTxs(hashes chan []common.Hash) *Subscription { @@ -355,6 +371,9 @@ func (es *EventSystem) handleChainEvent(filters filterIndex, ev core.ChainEvent) for _, f := range filters[BlocksSubscription] { f.headers <- ev.Block.Header() } + for _, f := range filters[BlockResultsSubscription] { + f.blockResults <- ev.BlockResult + } if es.lightMode && len(filters[LogsSubscription]) > 0 { es.lightFilterNewHead(ev.Block.Header(), func(header *types.Header, remove bool) { for _, f := range filters[LogsSubscription] { diff --git a/eth/tracers/api.go b/eth/tracers/api.go index fd6b622519b9..cf209d7f721d 100644 --- a/eth/tracers/api.go +++ b/eth/tracers/api.go @@ -912,11 +912,11 @@ func (api *API) traceTx(ctx context.Context, message core.Message, txctx *Contex if len(result.Revert()) > 0 { returnVal = fmt.Sprintf("%x", result.Revert()) } - return ðapi.ExecutionResult{ + return &types.ExecutionResult{ Gas: result.UsedGas, Failed: result.Failed(), ReturnValue: returnVal, - StructLogs: ethapi.FormatLogs(tracer.StructLogs()), + StructLogs: vm.FormatLogs(tracer.StructLogs()), }, nil case Tracer: diff --git a/eth/tracers/api_test.go b/eth/tracers/api_test.go index 6a917e8fd7d9..284b4c59c780 100644 --- a/eth/tracers/api_test.go +++ b/eth/tracers/api_test.go @@ -213,11 +213,11 @@ func TestTraceCall(t *testing.T) { }, config: nil, expectErr: nil, - expect: ðapi.ExecutionResult{ + expect: &types.ExecutionResult{ Gas: params.TxGas, Failed: false, ReturnValue: "", - StructLogs: []ethapi.StructLogRes{}, + StructLogs: []types.StructLogRes{}, }, }, // Standard JSON trace upon the head, plain transfer. @@ -230,11 +230,11 @@ func TestTraceCall(t *testing.T) { }, config: nil, expectErr: nil, - expect: ðapi.ExecutionResult{ + expect: &types.ExecutionResult{ Gas: params.TxGas, Failed: false, ReturnValue: "", - StructLogs: []ethapi.StructLogRes{}, + StructLogs: []types.StructLogRes{}, }, }, // Standard JSON trace upon the non-existent block, error expects @@ -259,11 +259,11 @@ func TestTraceCall(t *testing.T) { }, config: nil, expectErr: nil, - expect: ðapi.ExecutionResult{ + expect: &types.ExecutionResult{ Gas: params.TxGas, Failed: false, ReturnValue: "", - StructLogs: []ethapi.StructLogRes{}, + StructLogs: []types.StructLogRes{}, }, }, // Standard JSON trace upon the pending block @@ -276,11 +276,11 @@ func TestTraceCall(t *testing.T) { }, config: nil, expectErr: nil, - expect: ðapi.ExecutionResult{ + expect: &types.ExecutionResult{ Gas: params.TxGas, Failed: false, ReturnValue: "", - StructLogs: []ethapi.StructLogRes{}, + StructLogs: []types.StructLogRes{}, }, }, } @@ -329,11 +329,11 @@ func TestTraceTransaction(t *testing.T) { if err != nil { t.Errorf("Failed to trace transaction %v", err) } - if !reflect.DeepEqual(result, ðapi.ExecutionResult{ + if !reflect.DeepEqual(result, &types.ExecutionResult{ Gas: params.TxGas, Failed: false, ReturnValue: "", - StructLogs: []ethapi.StructLogRes{}, + StructLogs: []types.StructLogRes{}, }) { t.Error("Transaction tracing result is different") } diff --git a/ethclient/ethclient.go b/ethclient/ethclient.go index 0bad4aa44ca4..040962dcc442 100644 --- a/ethclient/ethclient.go +++ b/ethclient/ethclient.go @@ -325,6 +325,20 @@ func (ec *Client) SubscribeNewHead(ctx context.Context, ch chan<- *types.Header) return ec.c.EthSubscribe(ctx, ch, "newHeads") } +// BlockResultByHash returns the blockResult. +func (ec *Client) BlockResultByHash(ctx context.Context, blockHash common.Hash) (*types.BlockResult, error) { + var blockResult types.BlockResult + if err := ec.c.CallContext(ctx, &blockResult, "eth_blockResultByHash", blockHash); err != nil { + return nil, err + } + return &blockResult, nil +} + +// SubscribeNewBlockResult subscribes to block execution trace when a new block is created. +func (ec *Client) SubscribeNewBlockResult(ctx context.Context, ch chan<- *types.BlockResult) (ethereum.Subscription, error) { + return ec.c.EthSubscribe(ctx, ch, "newBlockResult") +} + // State Access // NetworkID returns the network ID (also known as the chain ID) for this chain. diff --git a/go.mod b/go.mod index 9d896b5279f9..a3a501ff8c1e 100644 --- a/go.mod +++ b/go.mod @@ -52,6 +52,7 @@ require ( github.com/naoina/toml v0.1.2-0.20170918210437-9fafd6967416 github.com/olekukonko/tablewriter v0.0.5 github.com/peterh/liner v1.1.1-0.20190123174540-a2c9a5303de7 + github.com/pkg/errors v0.9.1 github.com/prometheus/tsdb v0.7.1 github.com/rjeczalik/notify v0.9.1 github.com/rs/cors v1.7.0 diff --git a/internal/ethapi/api.go b/internal/ethapi/api.go index a998af86ea74..5c957dc0afcf 100644 --- a/internal/ethapi/api.go +++ b/internal/ethapi/api.go @@ -1113,67 +1113,6 @@ func (s *PublicBlockChainAPI) EstimateGas(ctx context.Context, args TransactionA return DoEstimateGas(ctx, s.b, args, bNrOrHash, s.b.RPCGasCap()) } -// ExecutionResult groups all structured logs emitted by the EVM -// while replaying a transaction in debug mode as well as transaction -// execution status, the amount of gas used and the return value -type ExecutionResult struct { - Gas uint64 `json:"gas"` - Failed bool `json:"failed"` - ReturnValue string `json:"returnValue"` - StructLogs []StructLogRes `json:"structLogs"` -} - -// StructLogRes stores a structured log emitted by the EVM while replaying a -// transaction in debug mode -type StructLogRes struct { - Pc uint64 `json:"pc"` - Op string `json:"op"` - Gas uint64 `json:"gas"` - GasCost uint64 `json:"gasCost"` - Depth int `json:"depth"` - Error string `json:"error,omitempty"` - Stack *[]string `json:"stack,omitempty"` - Memory *[]string `json:"memory,omitempty"` - Storage *map[string]string `json:"storage,omitempty"` -} - -// FormatLogs formats EVM returned structured logs for json output -func FormatLogs(logs []vm.StructLog) []StructLogRes { - formatted := make([]StructLogRes, len(logs)) - for index, trace := range logs { - formatted[index] = StructLogRes{ - Pc: trace.Pc, - Op: trace.Op.String(), - Gas: trace.Gas, - GasCost: trace.GasCost, - Depth: trace.Depth, - Error: trace.ErrorString(), - } - if trace.Stack != nil { - stack := make([]string, len(trace.Stack)) - for i, stackValue := range trace.Stack { - stack[i] = stackValue.Hex() - } - formatted[index].Stack = &stack - } - if trace.Memory != nil { - memory := make([]string, 0, (len(trace.Memory)+31)/32) - for i := 0; i+32 <= len(trace.Memory); i += 32 { - memory = append(memory, fmt.Sprintf("%x", trace.Memory[i:i+32])) - } - formatted[index].Memory = &memory - } - if trace.Storage != nil { - storage := make(map[string]string) - for i, storageValue := range trace.Storage { - storage[fmt.Sprintf("%x", i)] = fmt.Sprintf("%x", storageValue) - } - formatted[index].Storage = &storage - } - } - return formatted -} - // RPCMarshalHeader converts the given header to the RPC output . func RPCMarshalHeader(head *types.Header) map[string]interface{} { result := map[string]interface{}{ diff --git a/miner/worker.go b/miner/worker.go index 8a3cdc3905e5..e0e56f8f3f99 100644 --- a/miner/worker.go +++ b/miner/worker.go @@ -19,6 +19,7 @@ package miner import ( "bytes" "errors" + "fmt" "math/big" "sync" "sync/atomic" @@ -31,6 +32,7 @@ import ( "github.com/scroll-tech/go-ethereum/core" "github.com/scroll-tech/go-ethereum/core/state" "github.com/scroll-tech/go-ethereum/core/types" + "github.com/scroll-tech/go-ethereum/core/vm" "github.com/scroll-tech/go-ethereum/event" "github.com/scroll-tech/go-ethereum/log" "github.com/scroll-tech/go-ethereum/params" @@ -88,17 +90,19 @@ type environment struct { tcount int // tx count in cycle gasPool *core.GasPool // available gas used to pack transactions - header *types.Header - txs []*types.Transaction - receipts []*types.Receipt + header *types.Header + txs []*types.Transaction + receipts []*types.Receipt + executionResults []*types.ExecutionResult } // task contains all information for consensus engine sealing and result submitting. type task struct { - receipts []*types.Receipt - state *state.StateDB - block *types.Block - createdAt time.Time + receipts []*types.Receipt + executionResults []*types.ExecutionResult + state *state.StateDB + block *types.Block + createdAt time.Time } const ( @@ -393,10 +397,10 @@ func (w *worker) newWorkLoop(recommit time.Duration) { timestamp = time.Now().Unix() commit(false, commitInterruptNewHead) - /*case head := <-w.chainHeadCh: - clearPending(head.Block.NumberU64()) - timestamp = time.Now().Unix() - commit(false, commitInterruptNewHead)*/ + case head := <-w.chainHeadCh: + clearPending(head.Block.NumberU64()) + timestamp = time.Now().Unix() + // commit(false, commitInterruptNewHead) case <-timer.C: // If mining is running resubmit a new work cycle periodically to pull in @@ -462,6 +466,7 @@ func (w *worker) mainLoop() { select { case req := <-w.newWorkCh: w.commitNewWork(req.interrupt, req.noempty, req.timestamp) + // new block created. case ev := <-w.chainSideCh: // Short circuit for duplicate side blocks @@ -632,14 +637,19 @@ func (w *worker) resultLoop() { } // Different block could share same sealhash, deep copy here to prevent write-write conflict. var ( - receipts = make([]*types.Receipt, len(task.receipts)) - logs []*types.Log + receipts = make([]*types.Receipt, len(task.receipts)) + evmTraces = make([]*types.ExecutionResult, len(task.executionResults)) + logs []*types.Log ) for i, taskReceipt := range task.receipts { receipt := new(types.Receipt) receipts[i] = receipt *receipt = *taskReceipt + evmTrace := new(types.ExecutionResult) + evmTraces[i] = evmTrace + *evmTrace = *task.executionResults[i] + // add block location fields receipt.BlockHash = hash receipt.BlockNumber = block.Number() @@ -657,7 +667,7 @@ func (w *worker) resultLoop() { logs = append(logs, receipt.Logs...) } // Commit block and state to database. - _, err := w.chain.WriteBlockWithState(block, receipts, logs, task.state, true) + _, err := w.chain.WriteBlockWithState(block, receipts, logs, &types.BlockResult{ExecutionResults: evmTraces}, task.state, true) if err != nil { log.Error("Failed writing block to chain", "err", err) continue @@ -771,6 +781,10 @@ func (w *worker) updateSnapshot() { func (w *worker) commitTransaction(tx *types.Transaction, coinbase common.Address) ([]*types.Log, error) { snap := w.current.state.Snapshot() + // reset tracer. + tracer := w.chain.GetVMConfig().Tracer.(*vm.StructLogger) + tracer.Reset() + receipt, err := core.ApplyTransaction(w.chainConfig, w.chain, &coinbase, w.current.gasPool, w.current.state, w.current.header, tx, &w.current.header.GasUsed, *w.chain.GetVMConfig()) if err != nil { w.current.state.RevertToSnapshot(snap) @@ -778,6 +792,12 @@ func (w *worker) commitTransaction(tx *types.Transaction, coinbase common.Addres } w.current.txs = append(w.current.txs, tx) w.current.receipts = append(w.current.receipts, receipt) + w.current.executionResults = append(w.current.executionResults, &types.ExecutionResult{ + Gas: receipt.GasUsed, + Failed: receipt.Status == types.ReceiptStatusSuccessful, + ReturnValue: fmt.Sprintf("%x", receipt.ReturnValue), + StructLogs: vm.FormatLogs(tracer.StructLogs()), + }) return receipt.Logs, nil } @@ -1042,7 +1062,7 @@ func (w *worker) commit(uncles []*types.Header, interval func(), update bool, st interval() } select { - case w.taskCh <- &task{receipts: receipts, state: s, block: block, createdAt: time.Now()}: + case w.taskCh <- &task{receipts: receipts, executionResults: w.current.executionResults, state: s, block: block, createdAt: time.Now()}: w.unconfirmed.Shift(block.NumberU64() - 1) log.Info("Commit new mining work", "number", block.Number(), "sealhash", w.engine.SealHash(block.Header()), "uncles", len(uncles), "txs", w.current.tcount, diff --git a/miner/worker_test.go b/miner/worker_test.go index 010eb6cd16c9..49324cc92edc 100644 --- a/miner/worker_test.go +++ b/miner/worker_test.go @@ -135,7 +135,9 @@ func newTestWorkerBackend(t *testing.T, chainConfig *params.ChainConfig, engine } genesis := gspec.MustCommit(db) - chain, _ := core.NewBlockChain(db, &core.CacheConfig{TrieDirtyDisabled: true}, gspec.Config, engine, vm.Config{}, nil, nil) + chain, _ := core.NewBlockChain(db, &core.CacheConfig{TrieDirtyDisabled: true}, gspec.Config, engine, vm.Config{ + Debug: true, + Tracer: vm.NewStructLogger(&vm.LogConfig{EnableMemory: true})}, nil, nil) txpool := core.NewTxPool(testTxPoolConfig, chainConfig, chain) // Generate a small n-block chain and an uncle block for it @@ -232,7 +234,9 @@ func testGenerateBlockAndImport(t *testing.T, isClique bool) { // This test chain imports the mined blocks. db2 := rawdb.NewMemoryDatabase() b.genesis.MustCommit(db2) - chain, _ := core.NewBlockChain(db2, nil, b.chain.Config(), engine, vm.Config{}, nil, nil) + chain, _ := core.NewBlockChain(db2, nil, b.chain.Config(), engine, vm.Config{ + Debug: true, + Tracer: vm.NewStructLogger(&vm.LogConfig{EnableMemory: true})}, nil, nil) defer chain.Stop() // Ignore empty commit here for less noise.