Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

eth/tracer: extend call tracer #22245

Merged
merged 1 commit into from
Apr 21, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 30 additions & 3 deletions eth/tracers/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -161,6 +161,16 @@ type TraceConfig struct {
Reexec *uint64
}

// TraceCallConfig is the config for traceCall API. It holds one more
// field to override the state for tracing.
type TraceCallConfig struct {
*vm.LogConfig
Tracer *string
Timeout *string
Reexec *uint64
StateOverrides *ethapi.StateOverride
}

// StdTraceConfig holds extra parameters to standard-json trace functions.
type StdTraceConfig struct {
vm.LogConfig
Expand Down Expand Up @@ -720,7 +730,7 @@ func (api *API) TraceTransaction(ctx context.Context, hash common.Hash, config *
// 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.
func (api *API) TraceCall(ctx context.Context, args ethapi.CallArgs, blockNrOrHash rpc.BlockNumberOrHash, config *TraceConfig) (interface{}, error) {
func (api *API) TraceCall(ctx context.Context, args ethapi.CallArgs, blockNrOrHash rpc.BlockNumberOrHash, config *TraceCallConfig) (interface{}, error) {
// Try to retrieve the specified block
var (
err error
Expand All @@ -730,6 +740,8 @@ func (api *API) TraceCall(ctx context.Context, args ethapi.CallArgs, blockNrOrHa
block, err = api.blockByHash(ctx, hash)
} else if number, ok := blockNrOrHash.Number(); ok {
block, err = api.blockByNumber(ctx, number)
} else {
return nil, errors.New("invalid arguments; neither block nor hash specified")
}
if err != nil {
return nil, err
Expand All @@ -743,11 +755,26 @@ func (api *API) TraceCall(ctx context.Context, args ethapi.CallArgs, blockNrOrHa
if err != nil {
return nil, err
}
// Apply the customized state rules if required.
if config != nil {
if err := config.StateOverrides.Apply(statedb); err != nil {
return nil, err
}
}
// Execute the trace
msg := args.ToMessage(api.backend.RPCGasCap())
vmctx := core.NewEVMBlockContext(block.Header(), api.chainContext(ctx), nil)

return api.traceTx(ctx, msg, new(txTraceContext), vmctx, statedb, config)
var traceConfig *TraceConfig
if config != nil {
Comment on lines +768 to +769
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I had to think a while to figure out why this is needed. So essentially, you do this to clear out the StateOverrides, so it doesn't get applied a second time later on?
If that's the case, why not just set it to nil?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Because api.traceTx only accepts the TraceConfig, while TraceCall use the TraceCallConfig. Here just do the parameter conversion.

traceConfig = &TraceConfig{
LogConfig: config.LogConfig,
Tracer: config.Tracer,
Timeout: config.Timeout,
Reexec: config.Reexec,
}
}
return api.traceTx(ctx, msg, new(txTraceContext), vmctx, statedb, traceConfig)
}

// traceTx configures a new tracer according to the provided configuration, and
Expand Down Expand Up @@ -797,7 +824,7 @@ func (api *API) traceTx(ctx context.Context, message core.Message, txctx *txTrac

result, err := core.ApplyMessage(vmenv, message, new(core.GasPool).AddGas(message.Gas()))
if err != nil {
return nil, fmt.Errorf("tracing failed: %v", err)
return nil, fmt.Errorf("tracing failed: %w", err)
}

// Depending on the tracer type, format and return the output.
Expand Down
170 changes: 169 additions & 1 deletion eth/tracers/api_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import (
"bytes"
"context"
"crypto/ecdsa"
"encoding/json"
"errors"
"fmt"
"math/big"
Expand Down Expand Up @@ -198,7 +199,7 @@ func TestTraceCall(t *testing.T) {
var testSuite = []struct {
blockNumber rpc.BlockNumber
call ethapi.CallArgs
config *TraceConfig
config *TraceCallConfig
expectErr error
expect interface{}
}{
Expand Down Expand Up @@ -305,6 +306,147 @@ func TestTraceCall(t *testing.T) {
}
}

func TestOverridenTraceCall(t *testing.T) {
t.Parallel()

// Initialize test accounts
accounts := newAccounts(3)
genesis := &core.Genesis{Alloc: core.GenesisAlloc{
accounts[0].addr: {Balance: big.NewInt(params.Ether)},
accounts[1].addr: {Balance: big.NewInt(params.Ether)},
accounts[2].addr: {Balance: big.NewInt(params.Ether)},
}}
genBlocks := 10
signer := types.HomesteadSigner{}
api := NewAPI(newTestBackend(t, genBlocks, genesis, func(i int, b *core.BlockGen) {
// Transfer from account[0] to account[1]
// value: 1000 wei
// fee: 0 wei
tx, _ := types.SignTx(types.NewTransaction(uint64(i), accounts[1].addr, big.NewInt(1000), params.TxGas, big.NewInt(0), nil), signer, accounts[0].key)
b.AddTx(tx)
}))
randomAccounts, tracer := newAccounts(3), "callTracer"

var testSuite = []struct {
blockNumber rpc.BlockNumber
call ethapi.CallArgs
config *TraceCallConfig
expectErr error
expect *callTrace
}{
// Succcessful call with state overriding
{
blockNumber: rpc.PendingBlockNumber,
call: ethapi.CallArgs{
From: &randomAccounts[0].addr,
To: &randomAccounts[1].addr,
Value: (*hexutil.Big)(big.NewInt(1000)),
},
config: &TraceCallConfig{
Tracer: &tracer,
StateOverrides: &ethapi.StateOverride{
randomAccounts[0].addr: ethapi.OverrideAccount{Balance: newRPCBalance(new(big.Int).Mul(big.NewInt(1), big.NewInt(params.Ether)))},
},
},
expectErr: nil,
expect: &callTrace{
Type: "CALL",
From: randomAccounts[0].addr,
To: randomAccounts[1].addr,
Gas: newRPCUint64(24979000),
GasUsed: newRPCUint64(0),
Value: (*hexutil.Big)(big.NewInt(1000)),
},
},
// Invalid call without state overriding
{
blockNumber: rpc.PendingBlockNumber,
call: ethapi.CallArgs{
From: &randomAccounts[0].addr,
To: &randomAccounts[1].addr,
Value: (*hexutil.Big)(big.NewInt(1000)),
},
config: &TraceCallConfig{
Tracer: &tracer,
},
expectErr: core.ErrInsufficientFundsForTransfer,
expect: nil,
},
// Sucessful simple contract call
//
// // SPDX-License-Identifier: GPL-3.0
//
// pragma solidity >=0.7.0 <0.8.0;
//
// /**
// * @title Storage
// * @dev Store & retrieve value in a variable
// */
// contract Storage {
// uint256 public number;
// constructor() {
// number = block.number;
// }
// }
{
blockNumber: rpc.PendingBlockNumber,
call: ethapi.CallArgs{
From: &randomAccounts[0].addr,
To: &randomAccounts[2].addr,
Data: newRPCBytes(common.Hex2Bytes("8381f58a")), // call number()
},
config: &TraceCallConfig{
Tracer: &tracer,
StateOverrides: &ethapi.StateOverride{
randomAccounts[2].addr: ethapi.OverrideAccount{
Code: newRPCBytes(common.Hex2Bytes("6080604052348015600f57600080fd5b506004361060285760003560e01c80638381f58a14602d575b600080fd5b60336049565b6040518082815260200191505060405180910390f35b6000548156fea2646970667358221220eab35ffa6ab2adfe380772a48b8ba78e82a1b820a18fcb6f59aa4efb20a5f60064736f6c63430007040033")),
StateDiff: newStates([]common.Hash{{}}, []common.Hash{common.BigToHash(big.NewInt(123))}),
},
},
},
expectErr: nil,
expect: &callTrace{
Type: "CALL",
From: randomAccounts[0].addr,
To: randomAccounts[2].addr,
Input: hexutil.Bytes(common.Hex2Bytes("8381f58a")),
Output: hexutil.Bytes(common.BigToHash(big.NewInt(123)).Bytes()),
Gas: newRPCUint64(24978936),
GasUsed: newRPCUint64(2283),
Value: (*hexutil.Big)(big.NewInt(0)),
},
},
}
for _, testspec := range testSuite {
result, err := api.TraceCall(context.Background(), testspec.call, rpc.BlockNumberOrHash{BlockNumber: &testspec.blockNumber}, testspec.config)
if testspec.expectErr != nil {
if err == nil {
t.Errorf("Expect error %v, get nothing", testspec.expectErr)
continue
}
if !errors.Is(err, testspec.expectErr) {
t.Errorf("Error mismatch, want %v, get %v", testspec.expectErr, err)
}
} else {
if err != nil {
t.Errorf("Expect no error, get %v", err)
continue
}
ret := new(callTrace)
if err := json.Unmarshal(result.(json.RawMessage), ret); err != nil {
t.Fatalf("failed to unmarshal trace result: %v", err)
}
if !jsonEqual(ret, testspec.expect) {
// uncomment this for easier debugging
//have, _ := json.MarshalIndent(ret, "", " ")
//want, _ := json.MarshalIndent(testspec.expect, "", " ")
//t.Fatalf("trace mismatch: \nhave %+v\nwant %+v", string(have), string(want))
t.Fatalf("trace mismatch: \nhave %+v\nwant %+v", ret, testspec.expect)
}
}
}
}

func TestTraceTransaction(t *testing.T) {
t.Parallel()

Expand Down Expand Up @@ -469,3 +611,29 @@ func newAccounts(n int) (accounts Accounts) {
sort.Sort(accounts)
return accounts
}

func newRPCBalance(balance *big.Int) **hexutil.Big {
rpcBalance := (*hexutil.Big)(balance)
return &rpcBalance
}

func newRPCUint64(number uint64) *hexutil.Uint64 {
rpcUint64 := hexutil.Uint64(number)
return &rpcUint64
}

func newRPCBytes(bytes []byte) *hexutil.Bytes {
rpcBytes := hexutil.Bytes(bytes)
return &rpcBytes
}

func newStates(keys []common.Hash, vals []common.Hash) *map[common.Hash]common.Hash {
if len(keys) != len(vals) {
panic("invalid input")
}
m := make(map[common.Hash]common.Hash)
for i := 0; i < len(keys); i++ {
m[keys[i]] = vals[i]
}
return &m
}
44 changes: 27 additions & 17 deletions internal/ethapi/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ import (
"github.com/ethereum/go-ethereum/consensus/clique"
"github.com/ethereum/go-ethereum/consensus/ethash"
"github.com/ethereum/go-ethereum/core"
"github.com/ethereum/go-ethereum/core/state"
"github.com/ethereum/go-ethereum/core/types"
"github.com/ethereum/go-ethereum/core/vm"
"github.com/ethereum/go-ethereum/crypto"
Expand Down Expand Up @@ -802,29 +803,29 @@ func (args *CallArgs) ToMessage(globalGasCap uint64) types.Message {
return msg
}

// account indicates the overriding fields of account during the execution of
// a message call.
// OverrideAccount indicates the overriding fields of account during the execution
// of a message call.
// Note, state and stateDiff can't be specified at the same time. If state is
// set, message execution will only use the data in the given state. Otherwise
// if statDiff is set, all diff will be applied first and then execute the call
// message.
type account struct {
type OverrideAccount struct {
Nonce *hexutil.Uint64 `json:"nonce"`
Code *hexutil.Bytes `json:"code"`
Balance **hexutil.Big `json:"balance"`
State *map[common.Hash]common.Hash `json:"state"`
StateDiff *map[common.Hash]common.Hash `json:"stateDiff"`
}

func DoCall(ctx context.Context, b Backend, args CallArgs, blockNrOrHash rpc.BlockNumberOrHash, overrides map[common.Address]account, vmCfg vm.Config, timeout time.Duration, globalGasCap uint64) (*core.ExecutionResult, error) {
defer func(start time.Time) { log.Debug("Executing EVM call finished", "runtime", time.Since(start)) }(time.Now())
// StateOverride is the collection of overriden accounts.
type StateOverride map[common.Address]OverrideAccount

state, header, err := b.StateAndHeaderByNumberOrHash(ctx, blockNrOrHash)
if state == nil || err != nil {
return nil, err
// Apply overrides the fields of specified accounts into the given state.
func (diff *StateOverride) Apply(state *state.StateDB) error {
if diff == nil {
return nil
}
// Override the fields of specified contracts before execution.
for addr, account := range overrides {
for addr, account := range *diff {
// Override account nonce.
if account.Nonce != nil {
state.SetNonce(addr, uint64(*account.Nonce))
Expand All @@ -838,7 +839,7 @@ func DoCall(ctx context.Context, b Backend, args CallArgs, blockNrOrHash rpc.Blo
state.SetBalance(addr, (*big.Int)(*account.Balance))
}
if account.State != nil && account.StateDiff != nil {
return nil, fmt.Errorf("account %s has both 'state' and 'stateDiff'", addr.Hex())
return fmt.Errorf("account %s has both 'state' and 'stateDiff'", addr.Hex())
}
// Replace entire state if caller requires.
if account.State != nil {
Expand All @@ -851,6 +852,19 @@ func DoCall(ctx context.Context, b Backend, args CallArgs, blockNrOrHash rpc.Blo
}
}
}
return nil
}

func DoCall(ctx context.Context, b Backend, args CallArgs, blockNrOrHash rpc.BlockNumberOrHash, overrides *StateOverride, vmCfg vm.Config, timeout time.Duration, globalGasCap uint64) (*core.ExecutionResult, error) {
defer func(start time.Time) { log.Debug("Executing EVM call finished", "runtime", time.Since(start)) }(time.Now())

state, header, err := b.StateAndHeaderByNumberOrHash(ctx, blockNrOrHash)
if state == nil || err != nil {
return nil, err
}
if err := overrides.Apply(state); err != nil {
return nil, err
}
// Setup context so it may be cancelled the call has completed
// or, in case of unmetered gas, setup a context with a timeout.
var cancel context.CancelFunc
Expand Down Expand Up @@ -929,12 +943,8 @@ func (e *revertError) ErrorData() interface{} {
//
// Note, this function doesn't make and changes in the state/blockchain and is
// useful to execute and retrieve values.
func (s *PublicBlockChainAPI) Call(ctx context.Context, args CallArgs, blockNrOrHash rpc.BlockNumberOrHash, overrides *map[common.Address]account) (hexutil.Bytes, error) {
var accounts map[common.Address]account
if overrides != nil {
accounts = *overrides
}
result, err := DoCall(ctx, s.b, args, blockNrOrHash, accounts, vm.Config{}, 5*time.Second, s.b.RPCGasCap())
func (s *PublicBlockChainAPI) Call(ctx context.Context, args CallArgs, blockNrOrHash rpc.BlockNumberOrHash, overrides *StateOverride) (hexutil.Bytes, error) {
result, err := DoCall(ctx, s.b, args, blockNrOrHash, overrides, vm.Config{}, 5*time.Second, s.b.RPCGasCap())
if err != nil {
return nil, err
}
Expand Down