github.com/dim4egster/coreth@v0.10.2/eth/tracers/internal/tracetest/calltrace_test.go (about) 1 // (c) 2020-2021, Ava Labs, Inc. 2 // 3 // This file is a derived work, based on the go-ethereum library whose original 4 // notices appear below. 5 // 6 // It is distributed under a license compatible with the licensing terms of the 7 // original code from which it is derived. 8 // 9 // Much love to the original authors for their work. 10 // ********** 11 // Copyright 2021 The go-ethereum Authors 12 // This file is part of the go-ethereum library. 13 // 14 // The go-ethereum library is free software: you can redistribute it and/or modify 15 // it under the terms of the GNU Lesser General Public License as published by 16 // the Free Software Foundation, either version 3 of the License, or 17 // (at your option) any later version. 18 // 19 // The go-ethereum library is distributed in the hope that it will be useful, 20 // but WITHOUT ANY WARRANTY; without even the implied warranty of 21 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 22 // GNU Lesser General Public License for more details. 23 // 24 // You should have received a copy of the GNU Lesser General Public License 25 // along with the go-ethereum library. If not, see <http://www.gnu.org/licenses/>. 26 27 package tracetest 28 29 import ( 30 "encoding/json" 31 "math/big" 32 "os" 33 "path/filepath" 34 "reflect" 35 "strings" 36 "testing" 37 "unicode" 38 39 "github.com/dim4egster/coreth/core" 40 "github.com/dim4egster/coreth/core/rawdb" 41 "github.com/dim4egster/coreth/core/types" 42 "github.com/dim4egster/coreth/core/vm" 43 "github.com/dim4egster/coreth/eth/tracers" 44 "github.com/dim4egster/coreth/params" 45 "github.com/dim4egster/coreth/tests" 46 "github.com/ethereum/go-ethereum/common" 47 "github.com/ethereum/go-ethereum/common/hexutil" 48 "github.com/ethereum/go-ethereum/common/math" 49 "github.com/ethereum/go-ethereum/crypto" 50 "github.com/ethereum/go-ethereum/rlp" 51 52 // Force-load native, to trigger registration 53 _ "github.com/dim4egster/coreth/eth/tracers/native" 54 ) 55 56 // To generate a new callTracer test, copy paste the makeTest method below into 57 // a Geth console and call it with a transaction hash you which to export. 58 59 /* 60 // makeTest generates a callTracer test by running a prestate reassembled and a 61 // call trace run, assembling all the gathered information into a test case. 62 var makeTest = function(tx, rewind) { 63 // Generate the genesis block from the block, transaction and prestate data 64 var block = eth.getBlock(eth.getTransaction(tx).blockHash); 65 var genesis = eth.getBlock(block.parentHash); 66 67 delete genesis.gasUsed; 68 delete genesis.logsBloom; 69 delete genesis.parentHash; 70 delete genesis.receiptsRoot; 71 delete genesis.sha3Uncles; 72 delete genesis.size; 73 delete genesis.transactions; 74 delete genesis.transactionsRoot; 75 delete genesis.uncles; 76 77 genesis.gasLimit = genesis.gasLimit.toString(); 78 genesis.number = genesis.number.toString(); 79 genesis.timestamp = genesis.timestamp.toString(); 80 81 genesis.alloc = debug.traceTransaction(tx, {tracer: "prestateTracer", rewind: rewind}); 82 for (var key in genesis.alloc) { 83 genesis.alloc[key].nonce = genesis.alloc[key].nonce.toString(); 84 } 85 genesis.config = admin.nodeInfo.protocols.eth.config; 86 87 // Generate the call trace and produce the test input 88 var result = debug.traceTransaction(tx, {tracer: "callTracer", rewind: rewind}); 89 delete result.time; 90 91 console.log(JSON.stringify({ 92 genesis: genesis, 93 context: { 94 number: block.number.toString(), 95 difficulty: block.difficulty, 96 timestamp: block.timestamp.toString(), 97 gasLimit: block.gasLimit.toString(), 98 miner: block.miner, 99 }, 100 input: eth.getRawTransaction(tx), 101 result: result, 102 }, null, 2)); 103 } 104 */ 105 106 type callContext struct { 107 Number math.HexOrDecimal64 `json:"number"` 108 Difficulty *math.HexOrDecimal256 `json:"difficulty"` 109 Time math.HexOrDecimal64 `json:"timestamp"` 110 GasLimit math.HexOrDecimal64 `json:"gasLimit"` 111 Miner common.Address `json:"miner"` 112 } 113 114 // callTrace is the result of a callTracer run. 115 type callTrace struct { 116 Type string `json:"type"` 117 From common.Address `json:"from"` 118 To common.Address `json:"to"` 119 Input hexutil.Bytes `json:"input"` 120 Output hexutil.Bytes `json:"output"` 121 Gas *hexutil.Uint64 `json:"gas,omitempty"` 122 GasUsed *hexutil.Uint64 `json:"gasUsed,omitempty"` 123 Value *hexutil.Big `json:"value,omitempty"` 124 Error string `json:"error,omitempty"` 125 Calls []callTrace `json:"calls,omitempty"` 126 } 127 128 // callTracerTest defines a single test to check the call tracer against. 129 type callTracerTest struct { 130 Genesis *core.Genesis `json:"genesis"` 131 Context *callContext `json:"context"` 132 Input string `json:"input"` 133 TracerConfig json.RawMessage `json:"tracerConfig"` 134 Result *callTrace `json:"result"` 135 } 136 137 func TestCallTracerNative(t *testing.T) { 138 testCallTracer("callTracer", "call_tracer", t) 139 } 140 141 func testCallTracer(tracerName string, dirPath string, t *testing.T) { 142 files, err := os.ReadDir(filepath.Join("testdata", dirPath)) 143 if err != nil { 144 t.Fatalf("failed to retrieve tracer test suite: %v", err) 145 } 146 for _, file := range files { 147 if !strings.HasSuffix(file.Name(), ".json") { 148 continue 149 } 150 file := file // capture range variable 151 t.Run(camel(strings.TrimSuffix(file.Name(), ".json")), func(t *testing.T) { 152 t.Parallel() 153 154 var ( 155 test = new(callTracerTest) 156 tx = new(types.Transaction) 157 ) 158 // Call tracer test found, read if from disk 159 if blob, err := os.ReadFile(filepath.Join("testdata", dirPath, file.Name())); err != nil { 160 t.Fatalf("failed to read testcase: %v", err) 161 } else if err := json.Unmarshal(blob, test); err != nil { 162 t.Fatalf("failed to parse testcase: %v", err) 163 } 164 if err := rlp.DecodeBytes(common.FromHex(test.Input), tx); err != nil { 165 t.Fatalf("failed to parse testcase input: %v", err) 166 } 167 // Configure a blockchain with the given prestate 168 var ( 169 signer = types.MakeSigner(test.Genesis.Config, new(big.Int).SetUint64(uint64(test.Context.Number)), new(big.Int).SetUint64(uint64(test.Context.Time))) 170 origin, _ = signer.Sender(tx) 171 txContext = vm.TxContext{ 172 Origin: origin, 173 GasPrice: tx.GasPrice(), 174 } 175 context = vm.BlockContext{ 176 CanTransfer: core.CanTransfer, 177 Transfer: core.Transfer, 178 Coinbase: test.Context.Miner, 179 BlockNumber: new(big.Int).SetUint64(uint64(test.Context.Number)), 180 Time: new(big.Int).SetUint64(uint64(test.Context.Time)), 181 Difficulty: (*big.Int)(test.Context.Difficulty), 182 GasLimit: uint64(test.Context.GasLimit), 183 } 184 _, statedb = tests.MakePreState(rawdb.NewMemoryDatabase(), test.Genesis.Alloc, false) 185 ) 186 tracer, err := tracers.New(tracerName, new(tracers.Context), test.TracerConfig) 187 if err != nil { 188 t.Fatalf("failed to create call tracer: %v", err) 189 } 190 evm := vm.NewEVM(context, txContext, statedb, test.Genesis.Config, vm.Config{Debug: true, Tracer: tracer}) 191 msg, err := tx.AsMessage(signer, nil) 192 if err != nil { 193 t.Fatalf("failed to prepare transaction for tracing: %v", err) 194 } 195 st := core.NewStateTransition(evm, msg, new(core.GasPool).AddGas(tx.Gas())) 196 if _, err = st.TransitionDb(); err != nil { 197 t.Fatalf("failed to execute transaction: %v", err) 198 } 199 // Retrieve the trace result and compare against the etalon 200 res, err := tracer.GetResult() 201 if err != nil { 202 t.Fatalf("failed to retrieve trace result: %v", err) 203 } 204 ret := new(callTrace) 205 if err := json.Unmarshal(res, ret); err != nil { 206 t.Fatalf("failed to unmarshal trace result: %v", err) 207 } 208 209 if !jsonEqual(ret, test.Result) { 210 // uncomment this for easier debugging 211 //have, _ := json.MarshalIndent(ret, "", " ") 212 //want, _ := json.MarshalIndent(test.Result, "", " ") 213 //t.Fatalf("trace mismatch: \nhave %+v\nwant %+v", string(have), string(want)) 214 t.Fatalf("trace mismatch: \nhave %+v\nwant %+v", ret, test.Result) 215 } 216 }) 217 } 218 } 219 220 // jsonEqual is similar to reflect.DeepEqual, but does a 'bounce' via json prior to 221 // comparison 222 func jsonEqual(x, y interface{}) bool { 223 xTrace := new(callTrace) 224 yTrace := new(callTrace) 225 if xj, err := json.Marshal(x); err == nil { 226 json.Unmarshal(xj, xTrace) 227 } else { 228 return false 229 } 230 if yj, err := json.Marshal(y); err == nil { 231 json.Unmarshal(yj, yTrace) 232 } else { 233 return false 234 } 235 return reflect.DeepEqual(xTrace, yTrace) 236 } 237 238 // camel converts a snake cased input string into a camel cased output. 239 func camel(str string) string { 240 pieces := strings.Split(str, "_") 241 for i := 1; i < len(pieces); i++ { 242 pieces[i] = string(unicode.ToUpper(rune(pieces[i][0]))) + pieces[i][1:] 243 } 244 return strings.Join(pieces, "") 245 } 246 247 func BenchmarkTracers(b *testing.B) { 248 files, err := os.ReadDir(filepath.Join("testdata", "call_tracer")) 249 if err != nil { 250 b.Fatalf("failed to retrieve tracer test suite: %v", err) 251 } 252 for _, file := range files { 253 if !strings.HasSuffix(file.Name(), ".json") { 254 continue 255 } 256 file := file // capture range variable 257 b.Run(camel(strings.TrimSuffix(file.Name(), ".json")), func(b *testing.B) { 258 blob, err := os.ReadFile(filepath.Join("testdata", "call_tracer", file.Name())) 259 if err != nil { 260 b.Fatalf("failed to read testcase: %v", err) 261 } 262 test := new(callTracerTest) 263 if err := json.Unmarshal(blob, test); err != nil { 264 b.Fatalf("failed to parse testcase: %v", err) 265 } 266 benchTracer("callTracer", test, b) 267 }) 268 } 269 } 270 271 func benchTracer(tracerName string, test *callTracerTest, b *testing.B) { 272 // Configure a blockchain with the given prestate 273 tx := new(types.Transaction) 274 if err := rlp.DecodeBytes(common.FromHex(test.Input), tx); err != nil { 275 b.Fatalf("failed to parse testcase input: %v", err) 276 } 277 signer := types.MakeSigner(test.Genesis.Config, new(big.Int).SetUint64(uint64(test.Context.Number)), new(big.Int).SetUint64(uint64(test.Context.Time))) 278 msg, err := tx.AsMessage(signer, nil) 279 if err != nil { 280 b.Fatalf("failed to prepare transaction for tracing: %v", err) 281 } 282 origin, _ := signer.Sender(tx) 283 txContext := vm.TxContext{ 284 Origin: origin, 285 GasPrice: tx.GasPrice(), 286 } 287 context := vm.BlockContext{ 288 CanTransfer: core.CanTransfer, 289 Transfer: core.Transfer, 290 Coinbase: test.Context.Miner, 291 BlockNumber: new(big.Int).SetUint64(uint64(test.Context.Number)), 292 Time: new(big.Int).SetUint64(uint64(test.Context.Time)), 293 Difficulty: (*big.Int)(test.Context.Difficulty), 294 GasLimit: uint64(test.Context.GasLimit), 295 } 296 _, statedb := tests.MakePreState(rawdb.NewMemoryDatabase(), test.Genesis.Alloc, false) 297 298 b.ReportAllocs() 299 b.ResetTimer() 300 for i := 0; i < b.N; i++ { 301 tracer, err := tracers.New(tracerName, new(tracers.Context), nil) 302 if err != nil { 303 b.Fatalf("failed to create call tracer: %v", err) 304 } 305 evm := vm.NewEVM(context, txContext, statedb, test.Genesis.Config, vm.Config{Debug: true, Tracer: tracer}) 306 snap := statedb.Snapshot() 307 st := core.NewStateTransition(evm, msg, new(core.GasPool).AddGas(tx.Gas())) 308 if _, err = st.TransitionDb(); err != nil { 309 b.Fatalf("failed to execute transaction: %v", err) 310 } 311 if _, err = tracer.GetResult(); err != nil { 312 b.Fatal(err) 313 } 314 statedb.RevertToSnapshot(snap) 315 } 316 } 317 318 // TestZeroValueToNotExitCall tests the calltracer(s) on the following: 319 // Tx to A, A calls B with zero value. B does not already exist. 320 // Expected: that enter/exit is invoked and the inner call is shown in the result 321 func TestZeroValueToNotExitCall(t *testing.T) { 322 var to = common.HexToAddress("0x00000000000000000000000000000000deadbeef") 323 privkey, err := crypto.HexToECDSA("0000000000000000deadbeef00000000000000000000000000000000deadbeef") 324 if err != nil { 325 t.Fatalf("err %v", err) 326 } 327 signer := types.NewEIP155Signer(big.NewInt(1)) 328 tx, err := types.SignNewTx(privkey, signer, &types.LegacyTx{ 329 GasPrice: big.NewInt(0), 330 Gas: 50000, 331 To: &to, 332 }) 333 if err != nil { 334 t.Fatalf("err %v", err) 335 } 336 origin, _ := signer.Sender(tx) 337 txContext := vm.TxContext{ 338 Origin: origin, 339 GasPrice: big.NewInt(1), 340 } 341 context := vm.BlockContext{ 342 CanTransfer: core.CanTransfer, 343 Transfer: core.Transfer, 344 Coinbase: common.Address{}, 345 BlockNumber: new(big.Int).SetUint64(8000000), 346 Time: new(big.Int).SetUint64(5), 347 Difficulty: big.NewInt(0x30000), 348 GasLimit: uint64(6000000), 349 } 350 var code = []byte{ 351 byte(vm.PUSH1), 0x0, byte(vm.DUP1), byte(vm.DUP1), byte(vm.DUP1), // in and outs zero 352 byte(vm.DUP1), byte(vm.PUSH1), 0xff, byte(vm.GAS), // value=0,address=0xff, gas=GAS 353 byte(vm.CALL), 354 } 355 var alloc = core.GenesisAlloc{ 356 to: core.GenesisAccount{ 357 Nonce: 1, 358 Code: code, 359 }, 360 origin: core.GenesisAccount{ 361 Nonce: 0, 362 Balance: big.NewInt(500000000000000), 363 }, 364 } 365 _, statedb := tests.MakePreState(rawdb.NewMemoryDatabase(), alloc, false) 366 // Create the tracer, the EVM environment and run it 367 tracer, err := tracers.New("callTracer", nil, nil) 368 if err != nil { 369 t.Fatalf("failed to create call tracer: %v", err) 370 } 371 evm := vm.NewEVM(context, txContext, statedb, params.AvalancheMainnetChainConfig, vm.Config{Debug: true, Tracer: tracer}) 372 msg, err := tx.AsMessage(signer, nil) 373 if err != nil { 374 t.Fatalf("failed to prepare transaction for tracing: %v", err) 375 } 376 st := core.NewStateTransition(evm, msg, new(core.GasPool).AddGas(tx.Gas())) 377 if _, err = st.TransitionDb(); err != nil { 378 t.Fatalf("failed to execute transaction: %v", err) 379 } 380 // Retrieve the trace result and compare against the etalon 381 res, err := tracer.GetResult() 382 if err != nil { 383 t.Fatalf("failed to retrieve trace result: %v", err) 384 } 385 have := new(callTrace) 386 if err := json.Unmarshal(res, have); err != nil { 387 t.Fatalf("failed to unmarshal trace result: %v", err) 388 } 389 wantStr := `{"type":"CALL","from":"0x682a80a6f560eec50d54e63cbeda1c324c5f8d1b","to":"0x00000000000000000000000000000000deadbeef","value":"0x0","gas":"0x7148","gasUsed":"0x2d0","input":"0x","output":"0x","calls":[{"type":"CALL","from":"0x00000000000000000000000000000000deadbeef","to":"0x00000000000000000000000000000000000000ff","value":"0x0","gas":"0x6cbf","gasUsed":"0x0","input":"0x","output":"0x"}]}` 390 want := new(callTrace) 391 json.Unmarshal([]byte(wantStr), want) 392 if !jsonEqual(have, want) { 393 t.Error("have != want") 394 } 395 }