github.com/calmw/ethereum@v0.1.1/eth/tracers/internal/tracetest/calltrace_test.go (about)

     1  // Copyright 2021 The go-ethereum Authors
     2  // This file is part of the go-ethereum library.
     3  //
     4  // The go-ethereum library is free software: you can redistribute it and/or modify
     5  // it under the terms of the GNU Lesser General Public License as published by
     6  // the Free Software Foundation, either version 3 of the License, or
     7  // (at your option) any later version.
     8  //
     9  // The go-ethereum library is distributed in the hope that it will be useful,
    10  // but WITHOUT ANY WARRANTY; without even the implied warranty of
    11  // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
    12  // GNU Lesser General Public License for more details.
    13  //
    14  // You should have received a copy of the GNU Lesser General Public License
    15  // along with the go-ethereum library. If not, see <http://www.gnu.org/licenses/>.
    16  
    17  package tracetest
    18  
    19  import (
    20  	"encoding/json"
    21  	"math/big"
    22  	"os"
    23  	"path/filepath"
    24  	"strings"
    25  	"testing"
    26  
    27  	"github.com/calmw/ethereum/common"
    28  	"github.com/calmw/ethereum/common/hexutil"
    29  	"github.com/calmw/ethereum/common/math"
    30  	"github.com/calmw/ethereum/core"
    31  	"github.com/calmw/ethereum/core/rawdb"
    32  	"github.com/calmw/ethereum/core/types"
    33  	"github.com/calmw/ethereum/core/vm"
    34  	"github.com/calmw/ethereum/eth/tracers"
    35  	"github.com/calmw/ethereum/params"
    36  	"github.com/calmw/ethereum/rlp"
    37  	"github.com/calmw/ethereum/tests"
    38  )
    39  
    40  type callContext struct {
    41  	Number     math.HexOrDecimal64   `json:"number"`
    42  	Difficulty *math.HexOrDecimal256 `json:"difficulty"`
    43  	Time       math.HexOrDecimal64   `json:"timestamp"`
    44  	GasLimit   math.HexOrDecimal64   `json:"gasLimit"`
    45  	Miner      common.Address        `json:"miner"`
    46  }
    47  
    48  // callLog is the result of LOG opCode
    49  type callLog struct {
    50  	Address common.Address `json:"address"`
    51  	Topics  []common.Hash  `json:"topics"`
    52  	Data    hexutil.Bytes  `json:"data"`
    53  }
    54  
    55  // callTrace is the result of a callTracer run.
    56  type callTrace struct {
    57  	From         common.Address  `json:"from"`
    58  	Gas          *hexutil.Uint64 `json:"gas"`
    59  	GasUsed      *hexutil.Uint64 `json:"gasUsed"`
    60  	To           *common.Address `json:"to,omitempty"`
    61  	Input        hexutil.Bytes   `json:"input"`
    62  	Output       hexutil.Bytes   `json:"output,omitempty"`
    63  	Error        string          `json:"error,omitempty"`
    64  	RevertReason string          `json:"revertReason,omitempty"`
    65  	Calls        []callTrace     `json:"calls,omitempty"`
    66  	Logs         []callLog       `json:"logs,omitempty"`
    67  	Value        *hexutil.Big    `json:"value,omitempty"`
    68  	// Gencodec adds overridden fields at the end
    69  	Type string `json:"type"`
    70  }
    71  
    72  // callTracerTest defines a single test to check the call tracer against.
    73  type callTracerTest struct {
    74  	Genesis      *core.Genesis   `json:"genesis"`
    75  	Context      *callContext    `json:"context"`
    76  	Input        string          `json:"input"`
    77  	TracerConfig json.RawMessage `json:"tracerConfig"`
    78  	Result       *callTrace      `json:"result"`
    79  }
    80  
    81  // Iterates over all the input-output datasets in the tracer test harness and
    82  // runs the JavaScript tracers against them.
    83  func TestCallTracerLegacy(t *testing.T) {
    84  	testCallTracer("callTracerLegacy", "call_tracer_legacy", t)
    85  }
    86  
    87  func TestCallTracerNative(t *testing.T) {
    88  	testCallTracer("callTracer", "call_tracer", t)
    89  }
    90  
    91  func TestCallTracerNativeWithLog(t *testing.T) {
    92  	testCallTracer("callTracer", "call_tracer_withLog", t)
    93  }
    94  
    95  func testCallTracer(tracerName string, dirPath string, t *testing.T) {
    96  	isLegacy := strings.HasSuffix(dirPath, "_legacy")
    97  	files, err := os.ReadDir(filepath.Join("testdata", dirPath))
    98  	if err != nil {
    99  		t.Fatalf("failed to retrieve tracer test suite: %v", err)
   100  	}
   101  	for _, file := range files {
   102  		if !strings.HasSuffix(file.Name(), ".json") {
   103  			continue
   104  		}
   105  		file := file // capture range variable
   106  		t.Run(camel(strings.TrimSuffix(file.Name(), ".json")), func(t *testing.T) {
   107  			t.Parallel()
   108  
   109  			var (
   110  				test = new(callTracerTest)
   111  				tx   = new(types.Transaction)
   112  			)
   113  			// Call tracer test found, read if from disk
   114  			if blob, err := os.ReadFile(filepath.Join("testdata", dirPath, file.Name())); err != nil {
   115  				t.Fatalf("failed to read testcase: %v", err)
   116  			} else if err := json.Unmarshal(blob, test); err != nil {
   117  				t.Fatalf("failed to parse testcase: %v", err)
   118  			}
   119  			if err := tx.UnmarshalBinary(common.FromHex(test.Input)); err != nil {
   120  				t.Fatalf("failed to parse testcase input: %v", err)
   121  			}
   122  			// Configure a blockchain with the given prestate
   123  			var (
   124  				signer    = types.MakeSigner(test.Genesis.Config, new(big.Int).SetUint64(uint64(test.Context.Number)), uint64(test.Context.Time))
   125  				origin, _ = signer.Sender(tx)
   126  				txContext = vm.TxContext{
   127  					Origin:   origin,
   128  					GasPrice: tx.GasPrice(),
   129  				}
   130  				context = vm.BlockContext{
   131  					CanTransfer: core.CanTransfer,
   132  					Transfer:    core.Transfer,
   133  					Coinbase:    test.Context.Miner,
   134  					BlockNumber: new(big.Int).SetUint64(uint64(test.Context.Number)),
   135  					Time:        uint64(test.Context.Time),
   136  					Difficulty:  (*big.Int)(test.Context.Difficulty),
   137  					GasLimit:    uint64(test.Context.GasLimit),
   138  					BaseFee:     test.Genesis.BaseFee,
   139  				}
   140  				_, statedb = tests.MakePreState(rawdb.NewMemoryDatabase(), test.Genesis.Alloc, false)
   141  			)
   142  			tracer, err := tracers.DefaultDirectory.New(tracerName, new(tracers.Context), test.TracerConfig)
   143  			if err != nil {
   144  				t.Fatalf("failed to create call tracer: %v", err)
   145  			}
   146  			evm := vm.NewEVM(context, txContext, statedb, test.Genesis.Config, vm.Config{Tracer: tracer})
   147  			msg, err := core.TransactionToMessage(tx, signer, nil)
   148  			if err != nil {
   149  				t.Fatalf("failed to prepare transaction for tracing: %v", err)
   150  			}
   151  			vmRet, err := core.ApplyMessage(evm, msg, new(core.GasPool).AddGas(tx.Gas()))
   152  			if err != nil {
   153  				t.Fatalf("failed to execute transaction: %v", err)
   154  			}
   155  			// Retrieve the trace result and compare against the expected.
   156  			res, err := tracer.GetResult()
   157  			if err != nil {
   158  				t.Fatalf("failed to retrieve trace result: %v", err)
   159  			}
   160  			// The legacy javascript calltracer marshals json in js, which
   161  			// is not deterministic (as opposed to the golang json encoder).
   162  			if isLegacy {
   163  				// This is a tweak to make it deterministic. Can be removed when
   164  				// we remove the legacy tracer.
   165  				var x callTrace
   166  				json.Unmarshal(res, &x)
   167  				res, _ = json.Marshal(x)
   168  			}
   169  			want, err := json.Marshal(test.Result)
   170  			if err != nil {
   171  				t.Fatalf("failed to marshal test: %v", err)
   172  			}
   173  			if string(want) != string(res) {
   174  				t.Fatalf("trace mismatch\n have: %v\n want: %v\n", string(res), string(want))
   175  			}
   176  			// Sanity check: compare top call's gas used against vm result
   177  			type simpleResult struct {
   178  				GasUsed hexutil.Uint64
   179  			}
   180  			var topCall simpleResult
   181  			if err := json.Unmarshal(res, &topCall); err != nil {
   182  				t.Fatalf("failed to unmarshal top calls gasUsed: %v", err)
   183  			}
   184  			if uint64(topCall.GasUsed) != vmRet.UsedGas {
   185  				t.Fatalf("top call has invalid gasUsed. have: %d want: %d", topCall.GasUsed, vmRet.UsedGas)
   186  			}
   187  		})
   188  	}
   189  }
   190  
   191  func BenchmarkTracers(b *testing.B) {
   192  	files, err := os.ReadDir(filepath.Join("testdata", "call_tracer"))
   193  	if err != nil {
   194  		b.Fatalf("failed to retrieve tracer test suite: %v", err)
   195  	}
   196  	for _, file := range files {
   197  		if !strings.HasSuffix(file.Name(), ".json") {
   198  			continue
   199  		}
   200  		file := file // capture range variable
   201  		b.Run(camel(strings.TrimSuffix(file.Name(), ".json")), func(b *testing.B) {
   202  			blob, err := os.ReadFile(filepath.Join("testdata", "call_tracer", file.Name()))
   203  			if err != nil {
   204  				b.Fatalf("failed to read testcase: %v", err)
   205  			}
   206  			test := new(callTracerTest)
   207  			if err := json.Unmarshal(blob, test); err != nil {
   208  				b.Fatalf("failed to parse testcase: %v", err)
   209  			}
   210  			benchTracer("callTracer", test, b)
   211  		})
   212  	}
   213  }
   214  
   215  func benchTracer(tracerName string, test *callTracerTest, b *testing.B) {
   216  	// Configure a blockchain with the given prestate
   217  	tx := new(types.Transaction)
   218  	if err := rlp.DecodeBytes(common.FromHex(test.Input), tx); err != nil {
   219  		b.Fatalf("failed to parse testcase input: %v", err)
   220  	}
   221  	signer := types.MakeSigner(test.Genesis.Config, new(big.Int).SetUint64(uint64(test.Context.Number)), uint64(test.Context.Time))
   222  	msg, err := core.TransactionToMessage(tx, signer, nil)
   223  	if err != nil {
   224  		b.Fatalf("failed to prepare transaction for tracing: %v", err)
   225  	}
   226  	origin, _ := signer.Sender(tx)
   227  	txContext := vm.TxContext{
   228  		Origin:   origin,
   229  		GasPrice: tx.GasPrice(),
   230  	}
   231  	context := vm.BlockContext{
   232  		CanTransfer: core.CanTransfer,
   233  		Transfer:    core.Transfer,
   234  		Coinbase:    test.Context.Miner,
   235  		BlockNumber: new(big.Int).SetUint64(uint64(test.Context.Number)),
   236  		Time:        uint64(test.Context.Time),
   237  		Difficulty:  (*big.Int)(test.Context.Difficulty),
   238  		GasLimit:    uint64(test.Context.GasLimit),
   239  	}
   240  	_, statedb := tests.MakePreState(rawdb.NewMemoryDatabase(), test.Genesis.Alloc, false)
   241  
   242  	b.ReportAllocs()
   243  	b.ResetTimer()
   244  	for i := 0; i < b.N; i++ {
   245  		tracer, err := tracers.DefaultDirectory.New(tracerName, new(tracers.Context), nil)
   246  		if err != nil {
   247  			b.Fatalf("failed to create call tracer: %v", err)
   248  		}
   249  		evm := vm.NewEVM(context, txContext, statedb, test.Genesis.Config, vm.Config{Tracer: tracer})
   250  		snap := statedb.Snapshot()
   251  		st := core.NewStateTransition(evm, msg, new(core.GasPool).AddGas(tx.Gas()))
   252  		if _, err = st.TransitionDb(); err != nil {
   253  			b.Fatalf("failed to execute transaction: %v", err)
   254  		}
   255  		if _, err = tracer.GetResult(); err != nil {
   256  			b.Fatal(err)
   257  		}
   258  		statedb.RevertToSnapshot(snap)
   259  	}
   260  }
   261  
   262  func TestInternals(t *testing.T) {
   263  	var (
   264  		to        = common.HexToAddress("0x00000000000000000000000000000000deadbeef")
   265  		origin    = common.HexToAddress("0x00000000000000000000000000000000feed")
   266  		txContext = vm.TxContext{
   267  			Origin:   origin,
   268  			GasPrice: big.NewInt(1),
   269  		}
   270  		context = vm.BlockContext{
   271  			CanTransfer: core.CanTransfer,
   272  			Transfer:    core.Transfer,
   273  			Coinbase:    common.Address{},
   274  			BlockNumber: new(big.Int).SetUint64(8000000),
   275  			Time:        5,
   276  			Difficulty:  big.NewInt(0x30000),
   277  			GasLimit:    uint64(6000000),
   278  		}
   279  	)
   280  	mkTracer := func(name string, cfg json.RawMessage) tracers.Tracer {
   281  		tr, err := tracers.DefaultDirectory.New(name, nil, cfg)
   282  		if err != nil {
   283  			t.Fatalf("failed to create call tracer: %v", err)
   284  		}
   285  		return tr
   286  	}
   287  
   288  	for _, tc := range []struct {
   289  		name   string
   290  		code   []byte
   291  		tracer tracers.Tracer
   292  		want   string
   293  	}{
   294  		{
   295  			// TestZeroValueToNotExitCall tests the calltracer(s) on the following:
   296  			// Tx to A, A calls B with zero value. B does not already exist.
   297  			// Expected: that enter/exit is invoked and the inner call is shown in the result
   298  			name: "ZeroValueToNotExitCall",
   299  			code: []byte{
   300  				byte(vm.PUSH1), 0x0, byte(vm.DUP1), byte(vm.DUP1), byte(vm.DUP1), // in and outs zero
   301  				byte(vm.DUP1), byte(vm.PUSH1), 0xff, byte(vm.GAS), // value=0,address=0xff, gas=GAS
   302  				byte(vm.CALL),
   303  			},
   304  			tracer: mkTracer("callTracer", nil),
   305  			want:   `{"from":"0x000000000000000000000000000000000000feed","gas":"0xc350","gasUsed":"0x54d8","to":"0x00000000000000000000000000000000deadbeef","input":"0x","calls":[{"from":"0x00000000000000000000000000000000deadbeef","gas":"0x6cbf","gasUsed":"0x0","to":"0x00000000000000000000000000000000000000ff","input":"0x","value":"0x0","type":"CALL"}],"value":"0x0","type":"CALL"}`,
   306  		},
   307  		{
   308  			name:   "Stack depletion in LOG0",
   309  			code:   []byte{byte(vm.LOG3)},
   310  			tracer: mkTracer("callTracer", json.RawMessage(`{ "withLog": true }`)),
   311  			want:   `{"from":"0x000000000000000000000000000000000000feed","gas":"0xc350","gasUsed":"0xc350","to":"0x00000000000000000000000000000000deadbeef","input":"0x","error":"stack underflow (0 \u003c=\u003e 5)","value":"0x0","type":"CALL"}`,
   312  		},
   313  		{
   314  			name: "Mem expansion in LOG0",
   315  			code: []byte{
   316  				byte(vm.PUSH1), 0x1,
   317  				byte(vm.PUSH1), 0x0,
   318  				byte(vm.MSTORE),
   319  				byte(vm.PUSH1), 0xff,
   320  				byte(vm.PUSH1), 0x0,
   321  				byte(vm.LOG0),
   322  			},
   323  			tracer: mkTracer("callTracer", json.RawMessage(`{ "withLog": true }`)),
   324  			want:   `{"from":"0x000000000000000000000000000000000000feed","gas":"0xc350","gasUsed":"0x5b9e","to":"0x00000000000000000000000000000000deadbeef","input":"0x","logs":[{"address":"0x00000000000000000000000000000000deadbeef","topics":[],"data":"0x000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000"}],"value":"0x0","type":"CALL"}`,
   325  		},
   326  		{
   327  			// Leads to OOM on the prestate tracer
   328  			name: "Prestate-tracer - mem expansion in CREATE2",
   329  			code: []byte{
   330  				byte(vm.PUSH1), 0x1,
   331  				byte(vm.PUSH1), 0x0,
   332  				byte(vm.MSTORE),
   333  				byte(vm.PUSH1), 0x1,
   334  				byte(vm.PUSH5), 0xff, 0xff, 0xff, 0xff, 0xff,
   335  				byte(vm.PUSH1), 0x1,
   336  				byte(vm.PUSH1), 0x0,
   337  				byte(vm.CREATE2),
   338  				byte(vm.PUSH1), 0xff,
   339  				byte(vm.PUSH1), 0x0,
   340  				byte(vm.LOG0),
   341  			},
   342  			tracer: mkTracer("prestateTracer", json.RawMessage(`{ "withLog": true }`)),
   343  			want:   `{"0x0000000000000000000000000000000000000000":{"balance":"0x0"},"0x000000000000000000000000000000000000feed":{"balance":"0x1c6bf52640350"},"0x00000000000000000000000000000000deadbeef":{"balance":"0x0","code":"0x6001600052600164ffffffffff60016000f560ff6000a0"}}`,
   344  		},
   345  	} {
   346  		_, statedb := tests.MakePreState(rawdb.NewMemoryDatabase(),
   347  			core.GenesisAlloc{
   348  				to: core.GenesisAccount{
   349  					Code: tc.code,
   350  				},
   351  				origin: core.GenesisAccount{
   352  					Balance: big.NewInt(500000000000000),
   353  				},
   354  			}, false)
   355  		evm := vm.NewEVM(context, txContext, statedb, params.MainnetChainConfig, vm.Config{Tracer: tc.tracer})
   356  		msg := &core.Message{
   357  			To:                &to,
   358  			From:              origin,
   359  			Value:             big.NewInt(0),
   360  			GasLimit:          50000,
   361  			GasPrice:          big.NewInt(0),
   362  			GasFeeCap:         big.NewInt(0),
   363  			GasTipCap:         big.NewInt(0),
   364  			SkipAccountChecks: false,
   365  		}
   366  		st := core.NewStateTransition(evm, msg, new(core.GasPool).AddGas(msg.GasLimit))
   367  		if _, err := st.TransitionDb(); err != nil {
   368  			t.Fatalf("test %v: failed to execute transaction: %v", tc.name, err)
   369  		}
   370  		// Retrieve the trace result and compare against the expected
   371  		res, err := tc.tracer.GetResult()
   372  		if err != nil {
   373  			t.Fatalf("test %v: failed to retrieve trace result: %v", tc.name, err)
   374  		}
   375  		if string(res) != tc.want {
   376  			t.Fatalf("test %v: trace mismatch\n have: %v\n want: %v\n", tc.name, string(res), tc.want)
   377  		}
   378  	}
   379  }