diff --git a/core/blockchain.go b/core/blockchain.go index 6fd34e1c6..b98065a22 100644 --- a/core/blockchain.go +++ b/core/blockchain.go @@ -2219,13 +2219,6 @@ func (bc *BlockChain) IsSameLeaderAsPreviousBlock(block *types.Block) bool { return block.Coinbase() == previousHeader.Coinbase() } -// ChainDB ... -// TODO(ricl): in eth, this is not exposed. I expose it here because I need it in Harmony object. -// In eth, chainDB is initialized within Ethereum object -func (bc *BlockChain) ChainDB() ethdb.Database { - return bc.db -} - // GetVMConfig returns the block chain VM config. func (bc *BlockChain) GetVMConfig() *vm.Config { return &bc.vmConfig diff --git a/hmy/hmy.go b/hmy/hmy.go index aa8b389df..ccf47f3e4 100644 --- a/hmy/hmy.go +++ b/hmy/hmy.go @@ -111,7 +111,7 @@ type NodeAPI interface { func New( nodeAPI NodeAPI, txPool *core.TxPool, cxPool *core.CxPool, shardID uint32, ) *Harmony { - chainDb := nodeAPI.Blockchain().ChainDB() + chainDb := nodeAPI.Blockchain().ChainDb() leaderCache, _ := lru.New(leaderCacheSize) undelegationPayoutsCache, _ := lru.New(undelegationPayoutsCacheSize) preStakingBlockRewardsCache, _ := lru.New(preStakingBlockRewardsCacheSize) diff --git a/hmy/tracer.go b/hmy/tracer.go new file mode 100644 index 000000000..bd60a4794 --- /dev/null +++ b/hmy/tracer.go @@ -0,0 +1,743 @@ +// Copyright 2017 The go-ethereum Authors +// This file is part of the go-ethereum library. +// +// The go-ethereum library 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. +// +// The go-ethereum library 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 the go-ethereum library. If not, see . + +package hmy + +import ( + "bufio" + "context" + "fmt" + "io/ioutil" + "os" + "runtime" + "sync" + "time" + + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/common/hexutil" + "github.com/ethereum/go-ethereum/common/math" + "github.com/ethereum/go-ethereum/rpc" + "github.com/ethereum/go-ethereum/trie" + "github.com/harmony-one/harmony/core" + "github.com/harmony-one/harmony/core/state" + "github.com/harmony-one/harmony/core/types" + "github.com/harmony-one/harmony/core/vm" + "github.com/harmony-one/harmony/internal/utils" +) + +const ( + // defaultTraceTimeout is the amount of time a single transaction can execute + // by default before being forcefully aborted. + defaultTraceTimeout = 5 * time.Second + + // defaultTraceReExec is the number of blocks the tracer is willing to go back + // and re-execute to produce missing historical state necessary to run a specific + // trace. + defaultTraceReexec = uint64(128) + + err +) + +// TraceConfig holds extra parameters to trace functions. +type TraceConfig struct { + *vm.LogConfig + Tracer *string + Timeout *string + Reexec *uint64 +} + +// StdTraceConfig holds extra parameters to standard-json trace functions. +type StdTraceConfig struct { + *vm.LogConfig + Reexec *uint64 + TxHash common.Hash +} + +// TxTraceResult is the result of a single transaction trace. +type TxTraceResult struct { + Result interface{} `json:"result,omitempty"` // Trace results produced by the tracer + Error string `json:"error,omitempty"` // Trace failure produced by the tracer +} + +// blockTraceTask represents a single block trace task when an entire chain is +// being traced. +type blockTraceTask struct { + statedb *state.DB // Intermediate state prepped for tracing + block *types.Block // Block to trace the transactions from + rootRef common.Hash // Trie root reference held for this task + results []*TxTraceResult // Trace results produced by the task +} + +// blockTraceResult represets the results of tracing a single block when an entire +// chain is being traced. +type blockTraceResult struct { + Block hexutil.Uint64 `json:"block"` // Block number corresponding to this trace + Hash common.Hash `json:"hash"` // Block hash corresponding to this trace + Traces []*TxTraceResult `json:"traces"` // Trace results produced by the task +} + +// txTraceTask represents a single transaction trace task when an entire block +// is being traced. +type txTraceTask struct { + statedb *state.DB // Intermediate state prepped for tracing + index int // Transaction offset in the block +} + +// TraceChain 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 (hmy *Harmony) TraceChain(ctx context.Context, start, end *types.Block, config *TraceConfig) (*rpc.Subscription, error) { + // Tracing a chain is a **long** operation, only do with subscriptions + notifier, supported := rpc.NotifierFromContext(ctx) + if !supported { + return nil, rpc.ErrNotificationsUnsupported + } + sub := notifier.CreateSubscription() + + // Ensure we have a valid starting state before doing any work + origin := start.NumberU64() + database := state.NewDatabaseWithCache(hmy.ChainDb(), 16) + + if origin > 0 { + start = hmy.BlockChain.GetBlock(start.ParentHash(), origin-1) + if start == nil { + return nil, fmt.Errorf("parent block #%d not found", origin-1) + } + } + + statedb, err := state.New(start.Root(), database) + if err != nil { + // If the starting state is missing, allow some number of blocks to be executed + reexec := defaultTraceReexec + if config != nil && config.Reexec != nil { + reexec = *config.Reexec + } + // Find the most recent block that has state available + for i := uint64(0); i < reexec; i++ { + start = hmy.BlockChain.GetBlock(start.ParentHash(), start.NumberU64()-1) + if start == nil { + break + } + if statedb, err = state.New(start.Root(), database); err == nil { + break + } + } + // If we still don't have the state available, bail + if err != nil { + return nil, err + } + } + + // Execute all the transactions contained within the chain concurrently for each block + blocks := int(end.NumberU64() - origin) + + threads := runtime.NumCPU() + if threads > blocks { + threads = blocks + } + var ( + pending = new(sync.WaitGroup) + tasks = make(chan *blockTraceTask, threads) + results = make(chan *blockTraceTask, threads) + ) + for th := 0; th < threads; th++ { + pending.Add(1) + go func() { + defer pending.Done() + + // Fetch and execute the next block trace tasks + for task := range tasks { + signer := types.MakeSigner(hmy.BlockChain.Config(), task.block.Number()) + + // Trace all the transactions contained within + for i, tx := range task.block.Transactions() { + msg, _ := tx.AsMessage(signer) + vmCtx := core.NewEVMContext(msg, task.block.Header(), hmy.BlockChain, nil) + + res, err := hmy.TraceTx(ctx, msg, vmCtx, task.statedb, config) + if err != nil { + task.results[i] = &TxTraceResult{Error: err.Error()} + utils.Logger().Warn().Msg("Tracing failed") + break + } + // EIP 158/161 (Spurious Dragon) does not apply to Harmony + task.statedb.Finalise(true) + task.results[i] = &TxTraceResult{Result: res} + } + // Stream the result back to the user or abort on teardown + select { + case results <- task: + case <-notifier.Closed(): + return + } + } + }() + } + // Start a goroutine that feeds all the blocks into the tracers + begin := time.Now() + + go func() { + var ( + logged time.Time + number uint64 + traced uint64 + failed error + proot common.Hash + ) + // Ensure everything is properly cleaned up on any exit path + defer func() { + close(tasks) + pending.Wait() + + switch { + case failed != nil: + utils.Logger().Warn(). + Uint64("start", start.NumberU64()). + Uint64("end", end.NumberU64()). + Uint64("transactions", traced). + Float64("elapsed", time.Since(begin).Seconds()). + Err(failed). + Msg("Chain tracing failed") + case number < end.NumberU64(): + utils.Logger().Warn(). + Uint64("start", start.NumberU64()). + Uint64("end", end.NumberU64()). + Uint64("abort", number). + Uint64("transactions", traced). + Float64("elapsed", time.Since(begin).Seconds()). + Msg("Chain tracing aborted") + default: + utils.Logger().Info(). + Uint64("start", start.NumberU64()). + Uint64("end", end.NumberU64()). + Uint64("transactions", traced). + Float64("elapsed", time.Since(begin).Seconds()). + Msg("Chain tracing finished") + } + close(results) + }() + // Feed all the blocks both into the tracer, as well as fast process concurrently + for number = start.NumberU64() + 1; number <= end.NumberU64(); number++ { + // Stop tracing if interrupt was requested + select { + case <-notifier.Closed(): + return + default: + } + + // Print progress logs if long enough time elapsed + if time.Since(logged) > 8*time.Second { + if number > origin { + nodes, imgs := database.TrieDB().Size() + utils.Logger().Info(). + Uint64("start", origin). + Uint64("end", end.NumberU64()). + Uint64("current", number). + Uint64("transactions", traced). + Float64("elapsed", time.Since(begin).Seconds()). + Float64("memory", float64(nodes)+float64(imgs)). + Msg("Tracing chain segment") + } else { + utils.Logger().Info().Msg("Preparing state for chain trace") + } + logged = time.Now() + } + // Retrieve the next block to trace + block := hmy.BlockChain.GetBlockByNumber(number) + if block == nil { + failed = fmt.Errorf("block #%d not found", number) + break + } + // Send the block over to the concurrent tracers (if not in the fast-forward phase) + if number > origin { + txs := block.Transactions() + + select { + case tasks <- &blockTraceTask{statedb: statedb.Copy(), block: block, rootRef: proot, results: make([]*TxTraceResult, len(txs))}: + case <-notifier.Closed(): + return + } + traced += uint64(len(txs)) + } + // Generate the next state snapshot fast without tracing + _, _, _, _, _, err := hmy.BlockChain.Processor().Process(block, statedb, vm.Config{}) + if err != nil { + failed = err + break + } + // Finalize the state so any modifications are written to the trie + root, err := statedb.Commit(true) + if err != nil { + failed = err + break + } + if err := statedb.Reset(root); err != nil { + failed = err + break + } + // Reference the trie twice, once for us, once for the tracer + database.TrieDB().Reference(root, common.Hash{}) + if number >= origin { + database.TrieDB().Reference(root, common.Hash{}) + } + // Deference all past tries we ourselves are done working with + if proot != (common.Hash{}) { + database.TrieDB().Dereference(proot) + } + proot = root + + // TODO(karalabe): Do we need the preimages? Won't they accumulate too much? + } + }() + + // Keep reading the trace results and stream them to the user + go func() { + var ( + done = make(map[uint64]*blockTraceResult) + next = origin + 1 + ) + for res := range results { + // Queue up next received result + result := &blockTraceResult{ + Block: hexutil.Uint64(res.block.NumberU64()), + Hash: res.block.Hash(), + Traces: res.results, + } + done[uint64(result.Block)] = result + + // Dereference any parent tries held in memory by this task + database.TrieDB().Dereference(res.rootRef) + + // Stream completed traces to the user, aborting on the first error + for result, ok := done[next]; ok; result, ok = done[next] { + if len(result.Traces) > 0 || next == end.NumberU64() { + notifier.Notify(sub.ID, result) + } + delete(done, next) + next++ + } + } + }() + + return sub, nil +} + +// 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 (hmy *Harmony) TraceBlock(ctx context.Context, block *types.Block, config *TraceConfig) ([]*TxTraceResult, error) { + // Create the parent state database + if err := hmy.BlockChain.Engine().VerifyHeader(hmy.BlockChain, block.Header(), true); err != nil { + return nil, err + } + parent := hmy.BlockChain.GetBlock(block.ParentHash(), block.NumberU64()-1) + if parent == nil { + return nil, fmt.Errorf("parent %#x not found", block.ParentHash()) + } + reexec := defaultTraceReexec + if config != nil && config.Reexec != nil { + reexec = *config.Reexec + } + statedb, err := hmy.ComputeStateDB(parent, reexec) + if err != nil { + return nil, err + } + // Execute all the transaction contained within the block concurrently + var ( + signer = types.MakeSigner(hmy.BlockChain.Config(), block.Number()) + + txs = block.Transactions() + results = make([]*TxTraceResult, len(txs)) + + pend = new(sync.WaitGroup) + jobs = make(chan *txTraceTask, len(txs)) + ) + threads := runtime.NumCPU() + if threads > len(txs) { + threads = len(txs) + } + for th := 0; th < threads; th++ { + pend.Add(1) + go func() { + defer pend.Done() + + // Fetch and execute the next transaction trace tasks + for task := range jobs { + msg, _ := txs[task.index].AsMessage(signer) + vmctx := core.NewEVMContext(msg, block.Header(), hmy.BlockChain, nil) + + res, err := hmy.TraceTx(ctx, msg, vmctx, task.statedb, config) + if err != nil { + results[task.index] = &TxTraceResult{Error: err.Error()} + continue + } + results[task.index] = &TxTraceResult{Result: res} + } + }() + } + // Feed the transactions into the tracers and return + var failed error + for i, tx := range txs { + // Send the trace task over for execution + jobs <- &txTraceTask{statedb: statedb.Copy(), index: i} + + // Generate the next state snapshot fast without tracing + msg, _ := tx.AsMessage(signer) + vmctx := core.NewEVMContext(msg, block.Header(), hmy.BlockChain, nil) + + vmenv := vm.NewEVM(vmctx, statedb, hmy.BlockChain.Config(), vm.Config{}) + if _, err := core.ApplyMessage(vmenv, msg, new(core.GasPool).AddGas(msg.Gas())); err != nil { + failed = err + break + } + // Finalize the state so any modifications are written to the trie + statedb.Finalise(true) + } + close(jobs) + pend.Wait() + + // If execution failed in between, abort + if failed != nil { + return nil, failed + } + return results, nil +} + +// standardTraceBlockToFile configures a new tracer which uses standard JSON output, +// and traces either a full block or an individual transaction. The return value will +// be one filename per transaction traced. +func (hmy *Harmony) standardTraceBlockToFile(ctx context.Context, block *types.Block, config *StdTraceConfig) ([]string, error) { + // If we're tracing a single transaction, make sure it's present + if config != nil && config.TxHash != (common.Hash{}) { + if !containsTx(block, config.TxHash) { + return nil, fmt.Errorf("transaction %#x not found in block", config.TxHash) + } + } + // Create the parent state database + if err := hmy.BlockChain.Engine().VerifyHeader(hmy.BlockChain, block.Header(), true); err != nil { + return nil, err + } + parent := hmy.BlockChain.GetBlock(block.ParentHash(), block.NumberU64()-1) + if parent == nil { + return nil, fmt.Errorf("parent %#x not found", block.ParentHash()) + } + reexec := defaultTraceReexec + if config != nil && config.Reexec != nil { + reexec = *config.Reexec + } + statedb, err := hmy.ComputeStateDB(parent, reexec) + if err != nil { + return nil, err + } + // Retrieve the tracing configurations, or use default values + var ( + logConfig vm.LogConfig + txHash common.Hash + ) + if config != nil { + if config.LogConfig != nil { + logConfig = *config.LogConfig + } + txHash = config.TxHash + } + logConfig.Debug = true + + // Execute transaction, either tracing all or just the requested one + var ( + signer = types.MakeSigner(hmy.BlockChain.Config(), block.Number()) + dumps []string + ) + for i, tx := range block.Transactions() { + // Prepare the transaction for un-traced execution + var ( + msg, _ = tx.AsMessage(signer) + vmctx = core.NewEVMContext(msg, block.Header(), hmy.BlockChain, nil) + + vmConf vm.Config + dump *os.File + writer *bufio.Writer + err error + ) + // If the transaction needs tracing, swap out the configs + if tx.Hash() == txHash || txHash == (common.Hash{}) { + // Generate a unique temporary file to dump it into + prefix := fmt.Sprintf("block_%#x-%d-%#x-", block.Hash().Bytes()[:4], i, tx.Hash().Bytes()[:4]) + + dump, err = ioutil.TempFile(os.TempDir(), prefix) + if err != nil { + return nil, err + } + dumps = append(dumps, dump.Name()) + + // Swap out the noop logger to the standard tracer + writer = bufio.NewWriter(dump) + vmConf = vm.Config{ + Debug: true, + Tracer: vm.NewJSONLogger(&logConfig, writer), + EnablePreimageRecording: true, + } + } + // Execute the transaction and flush any traces to disk + vmenv := vm.NewEVM(vmctx, statedb, hmy.BlockChain.Config(), vmConf) + _, err = core.ApplyMessage(vmenv, msg, new(core.GasPool).AddGas(msg.Gas())) + if writer != nil { + writer.Flush() + } + if dump != nil { + dump.Close() + utils.Logger().Info().Msg(fmt.Sprintf("Wrote standard trace file %s", dump.Name())) + } + if err != nil { + return dumps, err + } + // Finalize the state so any modifications are written to the trie + statedb.Finalise(true) + + // If we've traced the transaction we were looking for, abort + if tx.Hash() == txHash { + break + } + } + return dumps, nil +} + +// containsTx reports whether the transaction with a certain hash +// is contained within the specified block. +func containsTx(block *types.Block, hash common.Hash) bool { + for _, tx := range block.Transactions() { + if tx.Hash() == hash { + return true + } + } + return false +} + +// ComputeStateDB retrieves the state database associated with a certain block. +// If no state is locally available for the given block, a number of blocks are +// attempted to be reexecuted to generate the desired state. +func (hmy *Harmony) ComputeStateDB(block *types.Block, reexec uint64) (*state.DB, error) { + // If we have the state fully available, use that + statedb, err := hmy.BlockChain.StateAt(block.Root()) + if err == nil { + return statedb, nil + } + // Otherwise try to reexec blocks until we find a state or reach our limit + origin := block.NumberU64() + database := state.NewDatabaseWithCache(hmy.BlockChain.ChainDb(), 16) + + for i := uint64(0); i < reexec; i++ { + block = hmy.BlockChain.GetBlock(block.ParentHash(), block.NumberU64()-1) + if block == nil { + break + } + if statedb, err = state.New(block.Root(), database); err == nil { + break + } + } + if err != nil { + switch err.(type) { + case *trie.MissingNodeError: + return nil, fmt.Errorf("required historical state unavailable (reexec=%d)", reexec) + default: + return nil, err + } + } + // State was available at historical point, regenerate + var ( + start = time.Now() + logged time.Time + proot common.Hash + ) + for block.NumberU64() < origin { + // Print progress logs if long enough time elapsed + if time.Since(logged) > 8*time.Second { + utils.Logger().Info(). + Uint64("block", block.NumberU64()). + Uint64("target", origin). + Uint64("remaining", origin-block.NumberU64()). + Float64("elasped", time.Since(start).Seconds()). + Msg(fmt.Sprintf("Regenerating historical state")) + logged = time.Now() + } + // Retrieve the next block to regenerate and process it + if block = hmy.BlockChain.GetBlockByNumber(block.NumberU64() + 1); block == nil { + return nil, fmt.Errorf("block #%d not found", block.NumberU64()+1) + } + _, _, _, _, _, err := hmy.BlockChain.Processor().Process(block, statedb, vm.Config{}) + if err != nil { + return nil, fmt.Errorf("processing block %d failed: %v", block.NumberU64(), err) + } + // Finalize the state so any modifications are written to the trie + root, err := statedb.Commit(true) + if err != nil { + return nil, err + } + if err := statedb.Reset(root); err != nil { + return nil, fmt.Errorf("state reset after block %d failed: %v", block.NumberU64(), err) + } + database.TrieDB().Reference(root, common.Hash{}) + if proot != (common.Hash{}) { + database.TrieDB().Dereference(proot) + } + proot = root + } + nodes, imgs := database.TrieDB().Size() + utils.Logger().Info(). + Uint64("block", block.NumberU64()). + Float64("elasped", time.Since(start).Seconds()). + Float64("nodes", float64(nodes)). + Float64("preimages", float64(imgs)). + Msg("Historical state regenerated") + return statedb, nil +} + +// 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. +// NOTE: Only support default StructLogger tracer +func (hmy *Harmony) TraceTx(ctx context.Context, message core.Message, vmctx vm.Context, statedb *state.DB, config *TraceConfig) (interface{}, error) { + // Assemble the structured logger or the JavaScript tracer + var ( + tracer vm.Tracer + err error + ) + + if config == nil { + tracer = vm.NewStructLogger(nil) + } else { + tracer = vm.NewStructLogger(config.LogConfig) + } + + // Run the transaction with tracing enabled. + vmenv := vm.NewEVM(vmctx, statedb, hmy.BlockChain.Config(), vm.Config{Debug: true, Tracer: tracer}) + + result, err := core.ApplyMessage(vmenv, message, new(core.GasPool).AddGas(message.Gas())) + if err != nil { + return nil, fmt.Errorf("tracing failed: %v", err) + } + // Depending on the tracer type, format and return the output + switch tracer := tracer.(type) { + case *vm.StructLogger: + return &ExecutionResult{ + Gas: result.UsedGas, + Failed: result.VMErr != nil, + ReturnValue: fmt.Sprintf("%x", result.ReturnData), + StructLogs: FormatLogs(tracer.StructLogs()), + }, nil + + default: + panic(fmt.Sprintf("bad tracer type %T", tracer)) + } +} + +// ComputeTxEnv returns the execution environment of a certain transaction. +func (hmy *Harmony) ComputeTxEnv(block *types.Block, txIndex int, reexec uint64) (core.Message, vm.Context, *state.DB, error) { + // Create the parent state database + parent := hmy.BlockChain.GetBlock(block.ParentHash(), block.NumberU64()-1) + if parent == nil { + return nil, vm.Context{}, nil, fmt.Errorf("parent %#x not found", block.ParentHash()) + } + statedb, err := hmy.ComputeStateDB(parent, reexec) + if err != nil { + return nil, vm.Context{}, nil, err + } + + if txIndex == 0 && len(block.Transactions()) == 0 { + return nil, vm.Context{}, statedb, nil + } + + // Recompute transactions up to the target index. + signer := types.MakeSigner(hmy.BlockChain.Config(), block.Number()) + + for idx, tx := range block.Transactions() { + // Assemble the transaction call message and return if the requested offset + msg, _ := tx.AsMessage(signer) + context := core.NewEVMContext(msg, block.Header(), hmy.BlockChain, nil) + if idx == txIndex { + return msg, context, statedb, nil + } + // Not yet the searched for transaction, execute on top of the current state + vmenv := vm.NewEVM(context, statedb, hmy.BlockChain.Config(), vm.Config{}) + if _, err := core.ApplyMessage(vmenv, msg, new(core.GasPool).AddGas(tx.Gas())); err != nil { + return nil, vm.Context{}, nil, fmt.Errorf("transaction %#x failed: %v", tx.Hash(), err) + } + // Ensure any modifications are committed to the state + statedb.Finalise(true) + } + return nil, vm.Context{}, nil, fmt.Errorf("transaction index %d out of range for block %#x", txIndex, block.Hash()) +} + +// 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 +// Taken from go-ethereum/internal/ethapi/api.go +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 error `json:"error,omitempty"` + Stack *[]string `json:"stack,omitempty"` + Memory *[]string `jsogun:"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.Err, + } + if trace.Stack != nil { + stack := make([]string, len(trace.Stack)) + for i, stackValue := range trace.Stack { + stack[i] = fmt.Sprintf("%x", math.PaddedBigBytes(stackValue, 32)) + } + 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/rosetta/services/construction_check.go b/rosetta/services/construction_check.go index e38eb6a36..b27ccb3a5 100644 --- a/rosetta/services/construction_check.go +++ b/rosetta/services/construction_check.go @@ -12,7 +12,6 @@ import ( ethRpc "github.com/ethereum/go-ethereum/rpc" "github.com/pkg/errors" - "github.com/harmony-one/harmony/core/vm" "github.com/harmony-one/harmony/internal/params" "github.com/harmony-one/harmony/rosetta/common" "github.com/harmony-one/harmony/rpc" @@ -252,7 +251,7 @@ func (s *ConstructAPI) ConstructionMetadata( callArgs.To = &contractAddress } evmExe, err := rpc.DoEVMCall( - ctx, s.hmy, callArgs, ethRpc.LatestBlockNumber, vm.Config{}, rpc.CallTimeout, s.hmy.RPCGasCap, + ctx, s.hmy, callArgs, ethRpc.LatestBlockNumber, rpc.CallTimeout, ) if err != nil { return nil, common.NewError(common.CatchAllError, map[string]interface{}{ diff --git a/rpc/contract.go b/rpc/contract.go index 43d9fb6df..9c2c56cc3 100644 --- a/rpc/contract.go +++ b/rpc/contract.go @@ -4,7 +4,6 @@ import ( "context" "fmt" "math" - "math/big" "time" "github.com/ethereum/go-ethereum/common" @@ -12,8 +11,6 @@ import ( "github.com/ethereum/go-ethereum/rpc" "github.com/harmony-one/harmony/common/denominations" "github.com/harmony-one/harmony/core" - "github.com/harmony-one/harmony/core/types" - "github.com/harmony-one/harmony/core/vm" "github.com/harmony-one/harmony/hmy" hmyCommon "github.com/harmony-one/harmony/internal/common" "github.com/harmony-one/harmony/internal/utils" @@ -50,7 +47,7 @@ func (s *PublicContractService) Call( blockNum := blockNumber.EthBlockNumber() // Execute call - result, err := DoEVMCall(ctx, s.hmy, args, blockNum, vm.Config{}, CallTimeout, s.hmy.RPCGasCap) + result, err := DoEVMCall(ctx, s.hmy, args, blockNum, CallTimeout) if err != nil { return nil, err } @@ -102,7 +99,7 @@ func (s *PublicContractService) GetStorageAt( // DoEVMCall executes an EVM call func DoEVMCall( ctx context.Context, hmy *hmy.Harmony, args CallArgs, blockNum rpc.BlockNumber, - vmCfg vm.Config, timeout time.Duration, globalGasCap *big.Int, + timeout time.Duration, ) (core.ExecutionResult, error) { defer func(start time.Time) { utils.Logger().Debug(). @@ -116,44 +113,8 @@ func DoEVMCall( return core.ExecutionResult{}, err } - // Set sender address or use a default if none specified - var addr common.Address - if args.From == nil { - // Any address does not affect the logic of this call. - addr = common.HexToAddress(defaultFromAddress) - } else { - addr = *args.From - } - - // Set default gas & gas price if none were set - gas := uint64(math.MaxUint64 / 2) - if args.Gas != nil { - gas = uint64(*args.Gas) - } - if globalGasCap != nil && globalGasCap.Uint64() < gas { - utils.Logger().Warn(). - Uint64("requested", gas). - Uint64("cap", globalGasCap.Uint64()). - Msg("Caller gas above allowance, capping") - gas = globalGasCap.Uint64() - } - gasPrice := new(big.Int).SetUint64(defaultGasPrice) - if args.GasPrice != nil { - gasPrice = args.GasPrice.ToInt() - } - - // Set value & data - value := new(big.Int) - if args.Value != nil { - value = args.Value.ToInt() - } - var data []byte - if args.Data != nil { - data = *args.Data - } - // Create new call message - msg := types.NewMessage(addr, args.To, 0, value, gas, gasPrice, data, false) + msg := args.ToMessage(hmy.RPCGasCap) // Setup context so it may be cancelled the call has completed // or, in case of unmetered gas, setup a context with a timeout. diff --git a/rpc/debug.go b/rpc/debug.go index 4d7c86d70..9d05a113e 100644 --- a/rpc/debug.go +++ b/rpc/debug.go @@ -27,9 +27,7 @@ func NewPrivateDebugAPI(hmy *hmy.Harmony, version Version) rpc.API { } // SetLogVerbosity Sets log verbosity on runtime -// Example usage: -// curl -H "Content-Type: application/json" -d '{"method":"debug_setLogVerbosity","params":[0],"id":1}' http://localhost:9123 -func (*PrivateDebugService) SetLogVerbosity(ctx context.Context, level int) (map[string]interface{}, error) { +func (s *PrivateDebugService) SetLogVerbosity(ctx context.Context, level int) (map[string]interface{}, error) { if level < int(log.LvlCrit) || level > int(log.LvlTrace) { return nil, ErrInvalidLogLevel } diff --git a/rpc/error.go b/rpc/error.go index cfe653592..5d3f4382a 100644 --- a/rpc/error.go +++ b/rpc/error.go @@ -17,6 +17,6 @@ var ( ErrRequestedBlockTooHigh = errors.New("requested block number greater than current block number") // ErrUnknownRPCVersion when rpc method has an unknown or unhandled version ErrUnknownRPCVersion = errors.New("API service has an unknown version") - // ErrTransactionNotFound when attempting to get a transaction that does exist or has not been finalized + // ErrTransactionNotFound when attempting to get a transaction that does not exist or has not been finalized ErrTransactionNotFound = errors.New("transaction not found") ) diff --git a/rpc/rpc.go b/rpc/rpc.go index 4650616b5..f414aeb2a 100644 --- a/rpc/rpc.go +++ b/rpc/rpc.go @@ -18,6 +18,7 @@ import ( const ( V1 Version = iota V2 + Debug ) const ( @@ -38,9 +39,9 @@ const ( var ( // HTTPModules .. - HTTPModules = []string{"hmy", "hmyv2", netV1Namespace, netV2Namespace, "explorer"} + HTTPModules = []string{"hmy", "hmyv2", "debug", netV1Namespace, netV2Namespace, "explorer"} // WSModules .. - WSModules = []string{"hmy", "hmyv2", netV1Namespace, netV2Namespace, "web3"} + WSModules = []string{"hmy", "hmyv2", "debug", netV1Namespace, netV2Namespace, "web3"} httpListener net.Listener httpHandler *rpc.Server @@ -130,6 +131,7 @@ func getAPIs(hmy *hmy.Harmony, debugEnable bool) []rpc.API { NewPublicPoolAPI(hmy, V2), NewPublicStakingAPI(hmy, V1), NewPublicStakingAPI(hmy, V2), + NewPublicTracerAPI(hmy, Debug), // Legacy methods (subject to removal) v1.NewPublicLegacyAPI(hmy), v2.NewPublicLegacyAPI(hmy), diff --git a/rpc/tracer.go b/rpc/tracer.go new file mode 100644 index 000000000..beb3b489b --- /dev/null +++ b/rpc/tracer.go @@ -0,0 +1,177 @@ +// Copyright 2017 The go-ethereum Authors +// This file is part of the go-ethereum library. +// +// The go-ethereum library 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. +// +// The go-ethereum library 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 the go-ethereum library. If not, see . + +package rpc + +import ( + "bytes" + "context" + "errors" + "fmt" + "time" + + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/rlp" + "github.com/ethereum/go-ethereum/rpc" + "github.com/harmony-one/harmony/core" + "github.com/harmony-one/harmony/core/rawdb" + "github.com/harmony-one/harmony/core/types" + "github.com/harmony-one/harmony/hmy" +) + +const ( + // defaultTraceTimeout is the amount of time a single transaction can execute + // by default before being forcefully aborted. + defaultTraceTimeout = 5 * time.Second + + // defaultTraceReExec is the number of blocks the tracer is willing to go back + // and re-execute to produce missing historical state necessary to run a specific + // trace. + defaultTraceReexec = uint64(128) +) + +var ( + // ErrNotAvailable to indicate the RPC is not ready for public use + ErrNotAvailable = errors.New("RPC not available yet") +) + +// PublicTracerService provides an API to access Harmony's staking services. +// It offers only methods that operate on public data that is freely available to anyone. +type PublicTracerService struct { + hmy *hmy.Harmony + version Version +} + +// NewPublicTracerAPI creates a new API for the RPC interface +func NewPublicTracerAPI(hmy *hmy.Harmony, version Version) rpc.API { + return rpc.API{ + Namespace: version.Namespace(), + Version: APIVersion, + Service: &PublicTracerService{hmy, version}, + Public: true, + } +} + +// TraceChain returns the structured logs created during the execution of EVM +// between two blocks (excluding start) and returns them as a JSON object. +func (s *PublicTracerService) TraceChain(ctx context.Context, start, end rpc.BlockNumber, config *hmy.TraceConfig) (*rpc.Subscription, error) { + // TODO (JL): Make API available after DoS testing + return nil, ErrNotAvailable + if uint64(start) >= uint64(end) { + return nil, fmt.Errorf("start block can not be equal or greater than the end block") + } + + currentBlock := s.hmy.BlockChain.CurrentBlock().NumberU64() + if uint64(start) > currentBlock || uint64(end) > currentBlock { + return nil, ErrRequestedBlockTooHigh + } + + from := s.hmy.BlockChain.GetBlockByNumber(uint64(start)) + if from == nil { + return nil, fmt.Errorf("start block #%d not found", start) + } + to := s.hmy.BlockChain.GetBlockByNumber(uint64(end)) + if to == nil { + return nil, fmt.Errorf("end block #%d not found", end) + } + + return s.hmy.TraceChain(ctx, from, to, config) +} + +// TraceBlockByNumber returns the structured logs created during the execution of +// EVM and returns them as a JSON object. +func (s *PublicTracerService) TraceBlockByNumber(ctx context.Context, number rpc.BlockNumber, config *hmy.TraceConfig) ([]*hmy.TxTraceResult, error) { + // Fetch the block that we want to trace + block := s.hmy.BlockChain.GetBlockByNumber(uint64(number)) + + return s.hmy.TraceBlock(ctx, block, config) +} + +// TraceBlockByHash returns the structured logs created during the execution of +// EVM and returns them as a JSON object. +func (s *PublicTracerService) TraceBlockByHash(ctx context.Context, hash common.Hash, config *hmy.TraceConfig) ([]*hmy.TxTraceResult, error) { + block := s.hmy.BlockChain.GetBlockByHash(hash) + if block == nil { + return nil, fmt.Errorf("block %#x not found", hash) + } + return s.hmy.TraceBlock(ctx, block, config) +} + +// TraceBlock returns the structured logs created during the execution of EVM +// and returns them as a JSON object. +func (s *PublicTracerService) TraceBlock(ctx context.Context, blob []byte, config *hmy.TraceConfig) ([]*hmy.TxTraceResult, error) { + block := new(types.Block) + if err := rlp.Decode(bytes.NewReader(blob), block); err != nil { + return nil, fmt.Errorf("could not decode block: %v", err) + } + return s.hmy.TraceBlock(ctx, block, config) +} + +// TraceTransaction returns the structured logs created during the execution of EVM +// and returns them as a JSON object. +func (s *PublicTracerService) TraceTransaction(ctx context.Context, hash common.Hash, config *hmy.TraceConfig) (interface{}, error) { + // Retrieve the transaction and assemble its EVM context + tx, blockHash, _, index := rawdb.ReadTransaction(s.hmy.ChainDb(), hash) + if tx == nil { + return nil, fmt.Errorf("transaction %#x not found", hash) + } + reexec := defaultTraceReexec + if config != nil && config.Reexec != nil { + reexec = *config.Reexec + } + // Retrieve the block + block := s.hmy.BlockChain.GetBlockByHash(blockHash) + if block == nil { + return nil, fmt.Errorf("block %#x not found", blockHash) + } + msg, vmctx, statedb, err := s.hmy.ComputeTxEnv(block, int(index), reexec) + if err != nil { + return nil, err + } + // Trace the transaction and return + return s.hmy.TraceTx(ctx, msg, vmctx, statedb, config) +} + +// TraceCall lets you trace a given eth_call. It collects the structured logs created during the execution of EVM +// if the given transaction was added on top of the provided block and returns them as a JSON object. +// You can provide -2 as a block number to trace on top of the pending block. +// NOTE: Our version only supports block number as an input +func (s *PublicTracerService) TraceCall(ctx context.Context, args CallArgs, blockNr rpc.BlockNumber, config *hmy.TraceConfig) (interface{}, error) { + // First try to retrieve the state + statedb, header, err := s.hmy.StateAndHeaderByNumber(ctx, blockNr) + if err != nil { + // Try to retrieve the specified block + block := s.hmy.BlockChain.GetBlockByNumber(uint64(blockNr)) + if block == nil { + return nil, fmt.Errorf("block %v not found: %v", blockNr, err) + } + // try to recompute the state + reexec := defaultTraceReexec + if config != nil && config.Reexec != nil { + reexec = *config.Reexec + } + _, _, statedb, err = s.hmy.ComputeTxEnv(block, 0, reexec) + if err != nil { + return nil, err + } + } + + // Execute the trace + msg := args.ToMessage(s.hmy.RPCGasCap) + vmctx := core.NewEVMContext(msg, header, s.hmy.BlockChain, nil) + // Trace the transaction and return + return s.hmy.TraceTx(ctx, msg, vmctx, statedb, config) +} diff --git a/rpc/transaction.go b/rpc/transaction.go index f95e6589b..1d46e16ed 100644 --- a/rpc/transaction.go +++ b/rpc/transaction.go @@ -725,7 +725,7 @@ func EstimateGas( executable := func(gas uint64) bool { args.Gas = (*hexutil.Uint64)(&gas) - result, err := DoEVMCall(ctx, hmy, args, blockNum, vm.Config{}, 0, big.NewInt(int64(max))) + result, err := DoEVMCall(ctx, hmy, args, blockNum, 0) if err != nil || result.VMErr == vm.ErrCodeStoreOutOfGas || result.VMErr == vm.ErrOutOfGas { return false } diff --git a/rpc/types.go b/rpc/types.go index e353a8267..f0691ddfd 100644 --- a/rpc/types.go +++ b/rpc/types.go @@ -11,10 +11,12 @@ import ( "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/common/hexutil" + "github.com/ethereum/go-ethereum/common/math" "github.com/ethereum/go-ethereum/rlp" "github.com/ethereum/go-ethereum/rpc" "github.com/harmony-one/harmony/block" "github.com/harmony-one/harmony/core/types" + "github.com/harmony-one/harmony/internal/utils" "github.com/harmony-one/harmony/numeric" "github.com/harmony-one/harmony/shard" ) @@ -29,6 +31,46 @@ type CallArgs struct { Data *hexutil.Bytes `json:"data"` } +// ToMessage converts CallArgs to the Message type used by the core evm +// Adapted from go-ethereum/internal/ethapi/api.go +func (args *CallArgs) ToMessage(globalGasCap *big.Int) types.Message { + // Set sender address or use zero address if none specified. + var addr common.Address + if args.From != nil { + addr = *args.From + } + + // Set default gas & gas price if none were set + gas := uint64(math.MaxUint64 / 2) + if args.Gas != nil { + gas = uint64(*args.Gas) + } + if globalGasCap != nil && globalGasCap.Uint64() < gas { + utils.Logger().Warn(). + Uint64("requested", gas). + Uint64("cap", globalGasCap.Uint64()). + Msg("Caller gas above allowance, capping") + gas = globalGasCap.Uint64() + } + gasPrice := new(big.Int) + if args.GasPrice != nil { + gasPrice = args.GasPrice.ToInt() + } + + value := new(big.Int) + if args.Value != nil { + value = args.Value.ToInt() + } + + var data []byte + if args.Data != nil { + data = []byte(*args.Data) + } + + msg := types.NewMessage(addr, args.To, 0, value, gas, gasPrice, data, false) + return msg +} + // StakingNetworkInfo returns global staking info. type StakingNetworkInfo struct { TotalSupply numeric.Dec `json:"total-supply"`