mirror of
https://github.com/cerc-io/watcher-ts
synced 2026-09-08 08:54:05 +00:00
Call tracer with address tracing support (#65)
* Tracing client package, basic utils. * Start of address tracer. * Rename trace CLI. * Call tracer API. * Tracing timeout, more traces. * Integrate address tracer into call address tracer.
This commit is contained in:
@@ -0,0 +1,52 @@
|
||||
import fs from 'fs';
|
||||
import yargs from 'yargs';
|
||||
import { ethers } from 'ethers';
|
||||
|
||||
interface StructLog {
|
||||
stack: string[]
|
||||
}
|
||||
|
||||
(async () => {
|
||||
const argv = await yargs.parserConfiguration({
|
||||
'parse-numbers': false
|
||||
}).options({
|
||||
traceFile: {
|
||||
type: 'string',
|
||||
require: true,
|
||||
demandOption: true
|
||||
}
|
||||
}).argv;
|
||||
|
||||
const { structLogs }: { structLogs: StructLog[] } = JSON.parse(fs.readFileSync(argv.traceFile).toString("utf-8"));
|
||||
|
||||
const addressMap: any = {};
|
||||
|
||||
structLogs.forEach((log: StructLog) => {
|
||||
if (!log.stack.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
let maybeAddress = log.stack[log.stack.length - 1];
|
||||
|
||||
// Address are 40 bytes.
|
||||
// Example: 000000000000000000000000ca6d29232d1435d8198e3e5302495417dd073d61
|
||||
if (!maybeAddress.startsWith("000000000000000000000000")) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (addressMap[maybeAddress]) {
|
||||
return;
|
||||
}
|
||||
|
||||
maybeAddress = maybeAddress.substr("000000000000000000000000".length);
|
||||
|
||||
if (!ethers.utils.isAddress(maybeAddress)) {
|
||||
return;
|
||||
}
|
||||
|
||||
addressMap[ethers.utils.getAddress(maybeAddress)] = true;
|
||||
Object.keys(addressMap).forEach(address => {
|
||||
console.log(address);
|
||||
});
|
||||
});
|
||||
})();
|
||||
@@ -0,0 +1,51 @@
|
||||
import fs from 'fs';
|
||||
import yargs from 'yargs';
|
||||
|
||||
import { getCallTrace } from '../tracing';
|
||||
|
||||
(async () => {
|
||||
const argv = await yargs.parserConfiguration({
|
||||
'parse-numbers': false
|
||||
}).options({
|
||||
providerUrl: {
|
||||
type: 'string',
|
||||
require: true,
|
||||
demandOption: true,
|
||||
default: 'http://localhost:8545',
|
||||
describe: 'ETH JSON-RPC provider URL'
|
||||
},
|
||||
block: {
|
||||
type: 'string',
|
||||
require: true,
|
||||
demandOption: true,
|
||||
describe: 'Block hash or number'
|
||||
},
|
||||
txFile: {
|
||||
type: 'string',
|
||||
require: true,
|
||||
demandOption: true,
|
||||
describe: 'File with tx data for call tracing'
|
||||
},
|
||||
tracer: {
|
||||
type: 'string',
|
||||
describe: 'The tracer to use'
|
||||
},
|
||||
tracerFile: {
|
||||
type: 'string',
|
||||
"describe": 'File with custom tracing JS code'
|
||||
}
|
||||
}).argv;
|
||||
|
||||
let tracer = argv.tracer;
|
||||
|
||||
const tracerFile = argv.tracerFile;
|
||||
if (tracerFile) {
|
||||
tracer = fs.readFileSync(tracerFile).toString("utf-8");
|
||||
}
|
||||
|
||||
const txData = JSON.parse(fs.readFileSync(argv.txFile).toString("utf-8"));
|
||||
|
||||
const result = await getCallTrace(argv.providerUrl, argv.block, txData, tracer);
|
||||
|
||||
console.log(JSON.stringify(result, null, 2));
|
||||
})();
|
||||
@@ -0,0 +1,48 @@
|
||||
import fs from 'fs';
|
||||
import yargs from 'yargs';
|
||||
|
||||
import { getTxTrace } from '../tracing';
|
||||
|
||||
(async () => {
|
||||
const argv = await yargs.parserConfiguration({
|
||||
'parse-numbers': false
|
||||
}).options({
|
||||
providerUrl: {
|
||||
type: 'string',
|
||||
require: true,
|
||||
demandOption: true,
|
||||
default: 'http://localhost:8545',
|
||||
describe: 'ETH JSON-RPC provider URL'
|
||||
},
|
||||
txHash: {
|
||||
type: 'string',
|
||||
require: true,
|
||||
demandOption: true,
|
||||
describe: 'Transaction hash'
|
||||
},
|
||||
tracer: {
|
||||
type: 'string',
|
||||
describe: 'The tracer to use'
|
||||
},
|
||||
tracerFile: {
|
||||
type: 'string',
|
||||
describe: 'File with custom tracing JS code'
|
||||
},
|
||||
timeout: {
|
||||
type: 'string',
|
||||
default: "10s",
|
||||
describe: 'Trace execution timeout'
|
||||
}
|
||||
}).argv;
|
||||
|
||||
let tracer = argv.tracer;
|
||||
|
||||
const tracerFile = argv.tracerFile;
|
||||
if (tracerFile) {
|
||||
tracer = fs.readFileSync(tracerFile).toString("utf-8");
|
||||
}
|
||||
|
||||
const result = await getTxTrace(argv.providerUrl, argv.txHash, tracer, argv.timeout);
|
||||
|
||||
console.log(JSON.stringify(result, null, 2));
|
||||
})();
|
||||
@@ -0,0 +1,73 @@
|
||||
{
|
||||
minVanityAddressLength: 35,
|
||||
|
||||
excludedAddresses: [
|
||||
"0x0000000000000000000000000000000000000000",
|
||||
"0xffffffffffffffffffffffffffffffffffffffff"
|
||||
],
|
||||
|
||||
// Known vanity addresses. Empty by default, but can replace this object when making dynamic requests.
|
||||
// Burner addresses go here too.
|
||||
knownVanityAddresses: {
|
||||
// "0x000026b86Ac8B3c08ADDEeacd7ee19e807D94742": true
|
||||
},
|
||||
|
||||
data: {},
|
||||
|
||||
isAddress: function(log, db, value) {
|
||||
// More than 40 chars or too small in length, so not an address.
|
||||
if (value.length > 40 || value.length < this.minVanityAddressLength) {
|
||||
return { isAddress: false };
|
||||
}
|
||||
|
||||
var address = toAddress(value);
|
||||
var addressAsHex = toHex(address);
|
||||
|
||||
// Check list of known exclusions.
|
||||
if (this.excludedAddresses.indexOf(addressAsHex) != -1) {
|
||||
return { isAddress: false };
|
||||
}
|
||||
|
||||
// Address exists in db, so definitely an address.
|
||||
if (db.exists(address)) {
|
||||
return { isAddress: true, address: addressAsHex, confidence: 1 };
|
||||
}
|
||||
|
||||
// May still be a valid address (e.g. for ERC20 transfer).
|
||||
// It won't exist in DB e.g. if no ETH was sent to it directly.
|
||||
// Apply heuristics.
|
||||
|
||||
// Length heuristic - addresses are usually 40 bytes.
|
||||
if (value.length == 40 && log.op.isPush()) {
|
||||
return { isAddress: true, address: addressAsHex, confidence: 0.75 };
|
||||
}
|
||||
|
||||
// Vanity addresses might start with leading zeros, so length will be < 40.
|
||||
// But we use a min length of addresses, otherwise there are too many false positives.
|
||||
// Also use a known vanity address list to override the normal logic.
|
||||
if (this.knownVanityAddresses[addressAsHex] || (log.op.isPush() && value.length > this.minVanityAddressLength)) {
|
||||
return { isAddress: true, address: addressAsHex, confidence: 0.60 };
|
||||
}
|
||||
|
||||
return { isAddress: false };
|
||||
},
|
||||
|
||||
// step is invoked for every opcode that the VM executes.
|
||||
step: function(log, db) {
|
||||
var topOfStack = log.stack.peek(0).toString(16);
|
||||
var result = this.isAddress(log, db, topOfStack);
|
||||
|
||||
if (result.isAddress) {
|
||||
this.data[result.address] = result.confidence;
|
||||
}
|
||||
},
|
||||
|
||||
// fault is invoked when the actual execution of an opcode fails.
|
||||
fault: function(log, db) { },
|
||||
|
||||
// result is invoked when all the opcodes have been iterated over and returns
|
||||
// the final result of the tracing.
|
||||
result: function(ctx, db) {
|
||||
return this.data;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,320 @@
|
||||
// Based on call_tracer.js in go-ethereum
|
||||
//
|
||||
// Original Notice:
|
||||
//
|
||||
// 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 <http://www.gnu.org/licenses/>.
|
||||
|
||||
// callTracer is a full blown transaction tracer that extracts and reports all
|
||||
// the internal calls made by a transaction, along with any useful information.
|
||||
|
||||
// addressTracer is a tracer that watches for address like strings for each call.
|
||||
{
|
||||
minVanityAddressLength: 35,
|
||||
|
||||
excludedAddresses: [
|
||||
"0x0000000000000000000000000000000000000000",
|
||||
"0xffffffffffffffffffffffffffffffffffffffff"
|
||||
],
|
||||
|
||||
// Known vanity addresses. Empty by default, but can replace this object when making dynamic requests.
|
||||
// Burner addresses go here too.
|
||||
knownVanityAddresses: {
|
||||
// "0x000026b86Ac8B3c08ADDEeacd7ee19e807D94742": true
|
||||
},
|
||||
|
||||
isAddress: function(log, db, value) {
|
||||
// More than 40 chars or too small in length, so not an address.
|
||||
if (value.length > 40 || value.length < this.minVanityAddressLength) {
|
||||
return { isAddress: false };
|
||||
}
|
||||
|
||||
var address = toAddress(value);
|
||||
var addressAsHex = toHex(address);
|
||||
|
||||
// Check list of known exclusions.
|
||||
if (this.excludedAddresses.indexOf(addressAsHex) != -1) {
|
||||
return { isAddress: false };
|
||||
}
|
||||
|
||||
// Address exists in db, so definitely an address.
|
||||
if (db.exists(address)) {
|
||||
return { isAddress: true, address: addressAsHex, confidence: 1 };
|
||||
}
|
||||
|
||||
// May still be a valid address (e.g. for ERC20 transfer).
|
||||
// It won't exist in DB e.g. if no ETH was sent to it directly.
|
||||
// Apply heuristics.
|
||||
|
||||
// Length heuristic - addresses are usually 40 bytes.
|
||||
if (value.length == 40 && log.op.isPush()) {
|
||||
return { isAddress: true, address: addressAsHex, confidence: 0.75 };
|
||||
}
|
||||
|
||||
// Vanity addresses might start with leading zeros, so length will be < 40.
|
||||
// But we use a min length of addresses, otherwise there are too many false positives.
|
||||
// Also use a known vanity address list to override the normal logic.
|
||||
if (this.knownVanityAddresses[addressAsHex] || (log.op.isPush() && value.length > this.minVanityAddressLength)) {
|
||||
return { isAddress: true, address: addressAsHex, confidence: 0.60 };
|
||||
}
|
||||
|
||||
return { isAddress: false };
|
||||
},
|
||||
|
||||
// callstack is the current recursive call stack of the EVM execution.
|
||||
callstack: [{}],
|
||||
|
||||
// descended tracks whether we've just descended from an outer transaction into
|
||||
// an inner call.
|
||||
descended: false,
|
||||
|
||||
// step is invoked for every opcode that the VM executes.
|
||||
step: function(log, db) {
|
||||
// Capture any errors immediately
|
||||
var error = log.getError();
|
||||
if (error !== undefined) {
|
||||
this.fault(log, db);
|
||||
return;
|
||||
}
|
||||
// We only care about system opcodes, faster if we pre-check once
|
||||
var syscall = (log.op.toNumber() & 0xf0) == 0xf0;
|
||||
if (syscall) {
|
||||
var op = log.op.toString();
|
||||
}
|
||||
// If a new contract is being created, add to the call stack
|
||||
if (syscall && (op == 'CREATE' || op == "CREATE2")) {
|
||||
var inOff = log.stack.peek(1).valueOf();
|
||||
var inEnd = inOff + log.stack.peek(2).valueOf();
|
||||
|
||||
// Assemble the internal call report and store for completion
|
||||
var call = {
|
||||
type: op,
|
||||
from: toHex(log.contract.getAddress()),
|
||||
input: toHex(log.memory.slice(inOff, inEnd)),
|
||||
gasIn: log.getGas(),
|
||||
gasCost: log.getCost(),
|
||||
value: '0x' + log.stack.peek(0).toString(16)
|
||||
};
|
||||
this.callstack.push(call);
|
||||
this.descended = true
|
||||
return;
|
||||
}
|
||||
// If a contract is being self destructed, gather that as a subcall too
|
||||
if (syscall && op == 'SELFDESTRUCT') {
|
||||
var left = this.callstack.length;
|
||||
if (this.callstack[left-1].calls === undefined) {
|
||||
this.callstack[left-1].calls = [];
|
||||
}
|
||||
this.callstack[left-1].calls.push({
|
||||
type: op,
|
||||
from: toHex(log.contract.getAddress()),
|
||||
to: toHex(toAddress(log.stack.peek(0).toString(16))),
|
||||
gasIn: log.getGas(),
|
||||
gasCost: log.getCost(),
|
||||
value: '0x' + db.getBalance(log.contract.getAddress()).toString(16)
|
||||
});
|
||||
return
|
||||
}
|
||||
// If a new method invocation is being done, add to the call stack
|
||||
if (syscall && (op == 'CALL' || op == 'CALLCODE' || op == 'DELEGATECALL' || op == 'STATICCALL')) {
|
||||
// Skip any pre-compile invocations, those are just fancy opcodes
|
||||
var to = toAddress(log.stack.peek(1).toString(16));
|
||||
if (isPrecompiled(to)) {
|
||||
return
|
||||
}
|
||||
var off = (op == 'DELEGATECALL' || op == 'STATICCALL' ? 0 : 1);
|
||||
|
||||
var inOff = log.stack.peek(2 + off).valueOf();
|
||||
var inEnd = inOff + log.stack.peek(3 + off).valueOf();
|
||||
|
||||
// Assemble the internal call report and store for completion
|
||||
var call = {
|
||||
type: op,
|
||||
from: toHex(log.contract.getAddress()),
|
||||
to: toHex(to),
|
||||
input: toHex(log.memory.slice(inOff, inEnd)),
|
||||
gasIn: log.getGas(),
|
||||
gasCost: log.getCost(),
|
||||
outOff: log.stack.peek(4 + off).valueOf(),
|
||||
outLen: log.stack.peek(5 + off).valueOf()
|
||||
};
|
||||
if (op != 'DELEGATECALL' && op != 'STATICCALL') {
|
||||
call.value = '0x' + log.stack.peek(2).toString(16);
|
||||
}
|
||||
this.callstack.push(call);
|
||||
this.descended = true
|
||||
return;
|
||||
}
|
||||
// If we've just descended into an inner call, retrieve it's true allowance. We
|
||||
// need to extract if from within the call as there may be funky gas dynamics
|
||||
// with regard to requested and actually given gas (2300 stipend, 63/64 rule).
|
||||
if (this.descended) {
|
||||
if (log.getDepth() >= this.callstack.length) {
|
||||
this.callstack[this.callstack.length - 1].gas = log.getGas();
|
||||
} else {
|
||||
// TODO(karalabe): The call was made to a plain account. We currently don't
|
||||
// have access to the true gas amount inside the call and so any amount will
|
||||
// mostly be wrong since it depends on a lot of input args. Skip gas for now.
|
||||
}
|
||||
this.descended = false;
|
||||
}
|
||||
// If an existing call is returning, pop off the call stack
|
||||
if (syscall && op == 'REVERT') {
|
||||
this.callstack[this.callstack.length - 1].error = "execution reverted";
|
||||
return;
|
||||
}
|
||||
if (log.getDepth() == this.callstack.length - 1) {
|
||||
// Pop off the last call and get the execution results
|
||||
var call = this.callstack.pop();
|
||||
|
||||
if (call.type == 'CREATE' || call.type == "CREATE2") {
|
||||
// If the call was a CREATE, retrieve the contract address and output code
|
||||
call.gasUsed = '0x' + bigInt(call.gasIn - call.gasCost - log.getGas()).toString(16);
|
||||
delete call.gasIn; delete call.gasCost;
|
||||
|
||||
var ret = log.stack.peek(0);
|
||||
if (!ret.equals(0)) {
|
||||
call.to = toHex(toAddress(ret.toString(16)));
|
||||
call.output = toHex(db.getCode(toAddress(ret.toString(16))));
|
||||
} else if (call.error === undefined) {
|
||||
call.error = "internal failure"; // TODO(karalabe): surface these faults somehow
|
||||
}
|
||||
} else {
|
||||
// If the call was a contract call, retrieve the gas usage and output
|
||||
if (call.gas !== undefined) {
|
||||
call.gasUsed = '0x' + bigInt(call.gasIn - call.gasCost + call.gas - log.getGas()).toString(16);
|
||||
}
|
||||
var ret = log.stack.peek(0);
|
||||
if (!ret.equals(0)) {
|
||||
call.output = toHex(log.memory.slice(call.outOff, call.outOff + call.outLen));
|
||||
} else if (call.error === undefined) {
|
||||
call.error = "internal failure"; // TODO(karalabe): surface these faults somehow
|
||||
}
|
||||
delete call.gasIn; delete call.gasCost;
|
||||
delete call.outOff; delete call.outLen;
|
||||
}
|
||||
if (call.gas !== undefined) {
|
||||
call.gas = '0x' + bigInt(call.gas).toString(16);
|
||||
}
|
||||
// Inject the call into the previous one
|
||||
var left = this.callstack.length;
|
||||
if (this.callstack[left-1].calls === undefined) {
|
||||
this.callstack[left-1].calls = [];
|
||||
}
|
||||
this.callstack[left-1].calls.push(call);
|
||||
}
|
||||
|
||||
var topOfStack = log.stack.peek(0).toString(16);
|
||||
var result = this.isAddress(log, db, topOfStack);
|
||||
if (result.isAddress) {
|
||||
var call = this.callstack[this.callstack.length - 1];
|
||||
if (!call.addresses) {
|
||||
call.addresses = {};
|
||||
}
|
||||
call.addresses[result.address] = result.confidence;
|
||||
}
|
||||
},
|
||||
|
||||
// fault is invoked when the actual execution of an opcode fails.
|
||||
fault: function(log, db) {
|
||||
// If the topmost call already reverted, don't handle the additional fault again
|
||||
if (this.callstack[this.callstack.length - 1].error !== undefined) {
|
||||
return;
|
||||
}
|
||||
// Pop off the just failed call
|
||||
var call = this.callstack.pop();
|
||||
call.error = log.getError();
|
||||
|
||||
// Consume all available gas and clean any leftovers
|
||||
if (call.gas !== undefined) {
|
||||
call.gas = '0x' + bigInt(call.gas).toString(16);
|
||||
call.gasUsed = call.gas
|
||||
}
|
||||
delete call.gasIn; delete call.gasCost;
|
||||
delete call.outOff; delete call.outLen;
|
||||
|
||||
// Flatten the failed call into its parent
|
||||
var left = this.callstack.length;
|
||||
if (left > 0) {
|
||||
if (this.callstack[left-1].calls === undefined) {
|
||||
this.callstack[left-1].calls = [];
|
||||
}
|
||||
this.callstack[left-1].calls.push(call);
|
||||
return;
|
||||
}
|
||||
// Last call failed too, leave it in the stack
|
||||
this.callstack.push(call);
|
||||
},
|
||||
|
||||
// result is invoked when all the opcodes have been iterated over and returns
|
||||
// the final result of the tracing.
|
||||
result: function(ctx, db) {
|
||||
var result = {
|
||||
type: ctx.type,
|
||||
from: toHex(ctx.from),
|
||||
to: toHex(ctx.to),
|
||||
value: '0x' + ctx.value.toString(16),
|
||||
gas: '0x' + bigInt(ctx.gas).toString(16),
|
||||
gasUsed: '0x' + bigInt(ctx.gasUsed).toString(16),
|
||||
input: toHex(ctx.input),
|
||||
output: toHex(ctx.output),
|
||||
time: ctx.time,
|
||||
};
|
||||
if (this.callstack[0].calls !== undefined) {
|
||||
result.calls = this.callstack[0].calls;
|
||||
}
|
||||
if (this.callstack[0].error !== undefined) {
|
||||
result.error = this.callstack[0].error;
|
||||
} else if (ctx.error !== undefined) {
|
||||
result.error = ctx.error;
|
||||
}
|
||||
if (result.error !== undefined && (result.error !== "execution reverted" || result.output ==="0x")) {
|
||||
delete result.output;
|
||||
}
|
||||
return this.finalize(result);
|
||||
},
|
||||
|
||||
// finalize recreates a call object using the final desired field oder for json
|
||||
// serialization. This is a nicety feature to pass meaningfully ordered results
|
||||
// to users who don't interpret it, just display it.
|
||||
finalize: function(call) {
|
||||
var sorted = {
|
||||
type: call.type,
|
||||
from: call.from,
|
||||
to: call.to,
|
||||
value: call.value,
|
||||
gas: call.gas,
|
||||
gasUsed: call.gasUsed,
|
||||
input: call.input,
|
||||
output: call.output,
|
||||
error: call.error,
|
||||
time: call.time,
|
||||
calls: call.calls,
|
||||
addresses: call.addresses,
|
||||
}
|
||||
for (var key in sorted) {
|
||||
if (sorted[key] === undefined) {
|
||||
delete sorted[key];
|
||||
}
|
||||
}
|
||||
if (sorted.calls !== undefined) {
|
||||
for (var i=0; i<sorted.calls.length; i++) {
|
||||
sorted.calls[i] = this.finalize(sorted.calls[i]);
|
||||
}
|
||||
}
|
||||
return sorted;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,252 @@
|
||||
// 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 <http://www.gnu.org/licenses/>.
|
||||
|
||||
// callTracer is a full blown transaction tracer that extracts and reports all
|
||||
// the internal calls made by a transaction, along with any useful information.
|
||||
{
|
||||
// callstack is the current recursive call stack of the EVM execution.
|
||||
callstack: [{}],
|
||||
|
||||
// descended tracks whether we've just descended from an outer transaction into
|
||||
// an inner call.
|
||||
descended: false,
|
||||
|
||||
// step is invoked for every opcode that the VM executes.
|
||||
step: function(log, db) {
|
||||
// Capture any errors immediately
|
||||
var error = log.getError();
|
||||
if (error !== undefined) {
|
||||
this.fault(log, db);
|
||||
return;
|
||||
}
|
||||
// We only care about system opcodes, faster if we pre-check once
|
||||
var syscall = (log.op.toNumber() & 0xf0) == 0xf0;
|
||||
if (syscall) {
|
||||
var op = log.op.toString();
|
||||
}
|
||||
// If a new contract is being created, add to the call stack
|
||||
if (syscall && (op == 'CREATE' || op == "CREATE2")) {
|
||||
var inOff = log.stack.peek(1).valueOf();
|
||||
var inEnd = inOff + log.stack.peek(2).valueOf();
|
||||
|
||||
// Assemble the internal call report and store for completion
|
||||
var call = {
|
||||
type: op,
|
||||
from: toHex(log.contract.getAddress()),
|
||||
input: toHex(log.memory.slice(inOff, inEnd)),
|
||||
gasIn: log.getGas(),
|
||||
gasCost: log.getCost(),
|
||||
value: '0x' + log.stack.peek(0).toString(16)
|
||||
};
|
||||
this.callstack.push(call);
|
||||
this.descended = true
|
||||
return;
|
||||
}
|
||||
// If a contract is being self destructed, gather that as a subcall too
|
||||
if (syscall && op == 'SELFDESTRUCT') {
|
||||
var left = this.callstack.length;
|
||||
if (this.callstack[left-1].calls === undefined) {
|
||||
this.callstack[left-1].calls = [];
|
||||
}
|
||||
this.callstack[left-1].calls.push({
|
||||
type: op,
|
||||
from: toHex(log.contract.getAddress()),
|
||||
to: toHex(toAddress(log.stack.peek(0).toString(16))),
|
||||
gasIn: log.getGas(),
|
||||
gasCost: log.getCost(),
|
||||
value: '0x' + db.getBalance(log.contract.getAddress()).toString(16)
|
||||
});
|
||||
return
|
||||
}
|
||||
// If a new method invocation is being done, add to the call stack
|
||||
if (syscall && (op == 'CALL' || op == 'CALLCODE' || op == 'DELEGATECALL' || op == 'STATICCALL')) {
|
||||
// Skip any pre-compile invocations, those are just fancy opcodes
|
||||
var to = toAddress(log.stack.peek(1).toString(16));
|
||||
if (isPrecompiled(to)) {
|
||||
return
|
||||
}
|
||||
var off = (op == 'DELEGATECALL' || op == 'STATICCALL' ? 0 : 1);
|
||||
|
||||
var inOff = log.stack.peek(2 + off).valueOf();
|
||||
var inEnd = inOff + log.stack.peek(3 + off).valueOf();
|
||||
|
||||
// Assemble the internal call report and store for completion
|
||||
var call = {
|
||||
type: op,
|
||||
from: toHex(log.contract.getAddress()),
|
||||
to: toHex(to),
|
||||
input: toHex(log.memory.slice(inOff, inEnd)),
|
||||
gasIn: log.getGas(),
|
||||
gasCost: log.getCost(),
|
||||
outOff: log.stack.peek(4 + off).valueOf(),
|
||||
outLen: log.stack.peek(5 + off).valueOf()
|
||||
};
|
||||
if (op != 'DELEGATECALL' && op != 'STATICCALL') {
|
||||
call.value = '0x' + log.stack.peek(2).toString(16);
|
||||
}
|
||||
this.callstack.push(call);
|
||||
this.descended = true
|
||||
return;
|
||||
}
|
||||
// If we've just descended into an inner call, retrieve it's true allowance. We
|
||||
// need to extract if from within the call as there may be funky gas dynamics
|
||||
// with regard to requested and actually given gas (2300 stipend, 63/64 rule).
|
||||
if (this.descended) {
|
||||
if (log.getDepth() >= this.callstack.length) {
|
||||
this.callstack[this.callstack.length - 1].gas = log.getGas();
|
||||
} else {
|
||||
// TODO(karalabe): The call was made to a plain account. We currently don't
|
||||
// have access to the true gas amount inside the call and so any amount will
|
||||
// mostly be wrong since it depends on a lot of input args. Skip gas for now.
|
||||
}
|
||||
this.descended = false;
|
||||
}
|
||||
// If an existing call is returning, pop off the call stack
|
||||
if (syscall && op == 'REVERT') {
|
||||
this.callstack[this.callstack.length - 1].error = "execution reverted";
|
||||
return;
|
||||
}
|
||||
if (log.getDepth() == this.callstack.length - 1) {
|
||||
// Pop off the last call and get the execution results
|
||||
var call = this.callstack.pop();
|
||||
|
||||
if (call.type == 'CREATE' || call.type == "CREATE2") {
|
||||
// If the call was a CREATE, retrieve the contract address and output code
|
||||
call.gasUsed = '0x' + bigInt(call.gasIn - call.gasCost - log.getGas()).toString(16);
|
||||
delete call.gasIn; delete call.gasCost;
|
||||
|
||||
var ret = log.stack.peek(0);
|
||||
if (!ret.equals(0)) {
|
||||
call.to = toHex(toAddress(ret.toString(16)));
|
||||
call.output = toHex(db.getCode(toAddress(ret.toString(16))));
|
||||
} else if (call.error === undefined) {
|
||||
call.error = "internal failure"; // TODO(karalabe): surface these faults somehow
|
||||
}
|
||||
} else {
|
||||
// If the call was a contract call, retrieve the gas usage and output
|
||||
if (call.gas !== undefined) {
|
||||
call.gasUsed = '0x' + bigInt(call.gasIn - call.gasCost + call.gas - log.getGas()).toString(16);
|
||||
}
|
||||
var ret = log.stack.peek(0);
|
||||
if (!ret.equals(0)) {
|
||||
call.output = toHex(log.memory.slice(call.outOff, call.outOff + call.outLen));
|
||||
} else if (call.error === undefined) {
|
||||
call.error = "internal failure"; // TODO(karalabe): surface these faults somehow
|
||||
}
|
||||
delete call.gasIn; delete call.gasCost;
|
||||
delete call.outOff; delete call.outLen;
|
||||
}
|
||||
if (call.gas !== undefined) {
|
||||
call.gas = '0x' + bigInt(call.gas).toString(16);
|
||||
}
|
||||
// Inject the call into the previous one
|
||||
var left = this.callstack.length;
|
||||
if (this.callstack[left-1].calls === undefined) {
|
||||
this.callstack[left-1].calls = [];
|
||||
}
|
||||
this.callstack[left-1].calls.push(call);
|
||||
}
|
||||
},
|
||||
|
||||
// fault is invoked when the actual execution of an opcode fails.
|
||||
fault: function(log, db) {
|
||||
// If the topmost call already reverted, don't handle the additional fault again
|
||||
if (this.callstack[this.callstack.length - 1].error !== undefined) {
|
||||
return;
|
||||
}
|
||||
// Pop off the just failed call
|
||||
var call = this.callstack.pop();
|
||||
call.error = log.getError();
|
||||
|
||||
// Consume all available gas and clean any leftovers
|
||||
if (call.gas !== undefined) {
|
||||
call.gas = '0x' + bigInt(call.gas).toString(16);
|
||||
call.gasUsed = call.gas
|
||||
}
|
||||
delete call.gasIn; delete call.gasCost;
|
||||
delete call.outOff; delete call.outLen;
|
||||
|
||||
// Flatten the failed call into its parent
|
||||
var left = this.callstack.length;
|
||||
if (left > 0) {
|
||||
if (this.callstack[left-1].calls === undefined) {
|
||||
this.callstack[left-1].calls = [];
|
||||
}
|
||||
this.callstack[left-1].calls.push(call);
|
||||
return;
|
||||
}
|
||||
// Last call failed too, leave it in the stack
|
||||
this.callstack.push(call);
|
||||
},
|
||||
|
||||
// result is invoked when all the opcodes have been iterated over and returns
|
||||
// the final result of the tracing.
|
||||
result: function(ctx, db) {
|
||||
var result = {
|
||||
type: ctx.type,
|
||||
from: toHex(ctx.from),
|
||||
to: toHex(ctx.to),
|
||||
value: '0x' + ctx.value.toString(16),
|
||||
gas: '0x' + bigInt(ctx.gas).toString(16),
|
||||
gasUsed: '0x' + bigInt(ctx.gasUsed).toString(16),
|
||||
input: toHex(ctx.input),
|
||||
output: toHex(ctx.output),
|
||||
time: ctx.time,
|
||||
};
|
||||
if (this.callstack[0].calls !== undefined) {
|
||||
result.calls = this.callstack[0].calls;
|
||||
}
|
||||
if (this.callstack[0].error !== undefined) {
|
||||
result.error = this.callstack[0].error;
|
||||
} else if (ctx.error !== undefined) {
|
||||
result.error = ctx.error;
|
||||
}
|
||||
if (result.error !== undefined && (result.error !== "execution reverted" || result.output ==="0x")) {
|
||||
delete result.output;
|
||||
}
|
||||
return this.finalize(result);
|
||||
},
|
||||
|
||||
// finalize recreates a call object using the final desired field oder for json
|
||||
// serialization. This is a nicety feature to pass meaningfully ordered results
|
||||
// to users who don't interpret it, just display it.
|
||||
finalize: function(call) {
|
||||
var sorted = {
|
||||
type: call.type,
|
||||
from: call.from,
|
||||
to: call.to,
|
||||
value: call.value,
|
||||
gas: call.gas,
|
||||
gasUsed: call.gasUsed,
|
||||
input: call.input,
|
||||
output: call.output,
|
||||
error: call.error,
|
||||
time: call.time,
|
||||
calls: call.calls,
|
||||
}
|
||||
for (var key in sorted) {
|
||||
if (sorted[key] === undefined) {
|
||||
delete sorted[key];
|
||||
}
|
||||
}
|
||||
if (sorted.calls !== undefined) {
|
||||
for (var i=0; i<sorted.calls.length; i++) {
|
||||
sorted.calls[i] = this.finalize(sorted.calls[i]);
|
||||
}
|
||||
}
|
||||
return sorted;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
import { ethers } from 'ethers';
|
||||
|
||||
export const getTxTrace = async (providerUrl: string, txHash: string, tracer: string | undefined, timeout: string | undefined): Promise<any> => {
|
||||
const provider = new ethers.providers.JsonRpcProvider(providerUrl);
|
||||
return provider.send('debug_traceTransaction', [txHash, { tracer, timeout }]);
|
||||
};
|
||||
|
||||
export const getCallTrace = async (providerUrl: string, block: string, txData: any, tracer: string | undefined): Promise<any> => {
|
||||
const provider = new ethers.providers.JsonRpcProvider(providerUrl);
|
||||
return provider.send('debug_traceCall', [ txData, block, { tracer }]);
|
||||
};
|
||||
Reference in New Issue
Block a user