Skip to content

Commit

Permalink
add debug_traceTx RPC
Browse files Browse the repository at this point in the history
  • Loading branch information
shunjizhan committed Jul 24, 2024
1 parent 7c94726 commit 0629d48
Show file tree
Hide file tree
Showing 6 changed files with 332 additions and 28 deletions.
4 changes: 2 additions & 2 deletions packages/eth-providers/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
"test:e2e": "vitest --run --config vitest.config.e2e.ts"
},
"peerDependencies": {
"@acala-network/api": "6.1.0",
"@acala-network/api": "^6.1.3",
"@polkadot/api": "^10.11.1"
},
"dependencies": {
Expand All @@ -23,7 +23,7 @@
"lru-cache": "~7.8.2"
},
"devDependencies": {
"@acala-network/api": "6.1.0",
"@acala-network/api": "^6.1.3",
"@types/bn.js": "~5.1.0",
"@types/lru-cache": "~7.6.1",
"dotenv": "~10.0.0",
Expand Down
34 changes: 34 additions & 0 deletions packages/eth-providers/src/base-provider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,7 @@ import {
import { BlockCache, CacheInspect } from './utils/BlockCache';
import { MaxSizeSet } from './utils/MaxSizeSet';
import { SubqlProvider } from './utils/subqlProvider';
import { TracerType, traceCall, traceVM } from './utils/trace';
import { _Metadata } from './utils/gqlTypes';

export interface HeadsInfo {
Expand Down Expand Up @@ -2164,4 +2165,37 @@ export abstract class BaseProvider extends AbstractProvider {
listeners = (_eventName?: EventType): Array<Listener> => throwNotImplemented('listeners');
off = (_eventName: EventType, _listener?: Listener): Provider => throwNotImplemented('off');
removeAllListeners = (_eventName?: EventType): Provider => throwNotImplemented('removeAllListeners');

traceTx = async (txHash: string, traceConf: any) => {
const tracerOptions = Object.values(TracerType);
if (!tracerOptions.includes(traceConf.tracer)) {
logger.throwError(
`traceTx: invalid tracer, must be one of { ${tracerOptions.join(', ')} }`,
Logger.errors.INVALID_ARGUMENT,
{ tracer: traceConf.tracer },
);
}

if (!this.api.call.evmTraceApi) {
logger.throwError('traceTx: evm tracing not supported', Logger.errors.NOT_IMPLEMENTED, { txHash });
}

const receipt = await this.getReceipt(txHash);
if (!receipt) {
logger.throwError('traceTx: tx not found', Logger.errors.UNKNOWN_ERROR, { txHash });
}

const { blockHash, transactionIndex } = receipt;
const blockData = await this.api.rpc.chain.getBlock(blockHash);
const evmExtrinsics = blockData.block.extrinsics.filter(isEvmExtrinsic);

const targetExtrinsic = evmExtrinsics[transactionIndex];
if (!targetExtrinsic) {
logger.throwError('traceTx: target extrinsic not found', Logger.errors.UNKNOWN_ERROR, { txHash });
}

return traceConf.tracer === TracerType.CallTracer
? await traceCall(this.api, targetExtrinsic)
: await traceVM(this.api, targetExtrinsic);
};
}
264 changes: 264 additions & 0 deletions packages/eth-providers/src/utils/trace.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,264 @@
import { ApiPromise } from '@polkadot/api';
import { Extrinsic } from '@polkadot/types/interfaces';
import { HexString } from '@polkadot/util/types';
import { IExtrinsic } from '@polkadot/types/types';

export type CallTrace = {
type: 'CALL' | 'CALLCODE' | 'STATICCALL' | 'DELEGATECALL' | 'CREATE' | 'SUICIDE';
from: HexString;
to: HexString;
input: HexString;
value: HexString;
gas: number;
gasUsed: number;
output: HexString | null;
error: string | null;
revertReason: string | null;
depth: number;
calls: CallTrace[];
}

export interface StepBase {
pc: number;
depth: number;
gas: number;
stack: HexString[];
memory: string[] | null;
}

export interface StepRaw extends StepBase {
op: number;
}

export interface Step extends StepBase {
op: string;
}

export enum TracerType {
CallTracer = 'callTracer',
OpcodeTracer = 'opcodeTracer'
}

export const OPNAME_TABLE = {
'0x00': 'STOP',
'0x01': 'ADD',
'0x02': 'MUL',
'0x03': 'SUB',
'0x04': 'DIV',
'0x05': 'SDIV',
'0x06': 'MOD',
'0x07': 'SMOD',
'0x08': 'ADDMOD',
'0x09': 'MULMOD',
'0x0a': 'EXP',
'0x0b': 'SIGNEXTEND',
'0x10': 'LT',
'0x11': 'GT',
'0x12': 'SLT',
'0x13': 'SGT',
'0x14': 'EQ',
'0x15': 'ISZERO',
'0x16': 'AND',
'0x17': 'OR',
'0x18': 'XOR',
'0x19': 'NOT',
'0x1a': 'BYTE',
'0x35': 'CALLDATALOAD',
'0x36': 'CALLDATASIZE',
'0x37': 'CALLDATACOPY',
'0x38': 'CODESIZE',
'0x39': 'CODECOPY',
'0x1b': 'SHL',
'0x1c': 'SHR',
'0x1d': 'SAR',
'0x50': 'POP',
'0x51': 'MLOAD',
'0x52': 'MSTORE',
'0x53': 'MSTORE8',
'0x56': 'JUMP',
'0x57': 'JUMPI',
'0x58': 'PC',
'0x59': 'MSIZE',
'0x5b': 'JUMPDEST',
'0x5f': 'PUSH0',
'0x60': 'PUSH1',
'0x61': 'PUSH2',
'0x62': 'PUSH3',
'0x63': 'PUSH4',
'0x64': 'PUSH5',
'0x65': 'PUSH6',
'0x66': 'PUSH7',
'0x67': 'PUSH8',
'0x68': 'PUSH9',
'0x69': 'PUSH10',
'0x6a': 'PUSH11',
'0x6b': 'PUSH12',
'0x6c': 'PUSH13',
'0x6d': 'PUSH14',
'0x6e': 'PUSH15',
'0x6f': 'PUSH16',
'0x70': 'PUSH17',
'0x71': 'PUSH18',
'0x72': 'PUSH19',
'0x73': 'PUSH20',
'0x74': 'PUSH21',
'0x75': 'PUSH22',
'0x76': 'PUSH23',
'0x77': 'PUSH24',
'0x78': 'PUSH25',
'0x79': 'PUSH26',
'0x7a': 'PUSH27',
'0x7b': 'PUSH28',
'0x7c': 'PUSH29',
'0x7d': 'PUSH30',
'0x7e': 'PUSH31',
'0x7f': 'PUSH32',
'0x80': 'DUP1',
'0x81': 'DUP2',
'0x82': 'DUP3',
'0x83': 'DUP4',
'0x84': 'DUP5',
'0x85': 'DUP6',
'0x86': 'DUP7',
'0x87': 'DUP8',
'0x88': 'DUP9',
'0x89': 'DUP10',
'0x8a': 'DUP11',
'0x8b': 'DUP12',
'0x8c': 'DUP13',
'0x8d': 'DUP14',
'0x8e': 'DUP15',
'0x8f': 'DUP16',
'0x90': 'SWAP1',
'0x91': 'SWAP2',
'0x92': 'SWAP3',
'0x93': 'SWAP4',
'0x94': 'SWAP5',
'0x95': 'SWAP6',
'0x96': 'SWAP7',
'0x97': 'SWAP8',
'0x98': 'SWAP9',
'0x99': 'SWAP10',
'0x9a': 'SWAP11',
'0x9b': 'SWAP12',
'0x9c': 'SWAP13',
'0x9d': 'SWAP14',
'0x9e': 'SWAP15',
'0x9f': 'SWAP16',
'0xf3': 'RETURN',
'0xfd': 'REVERT',
'0xfe': 'INVALID',
'0xef': 'EOFMAGIC',
'0x20': 'SHA3',
'0x30': 'ADDRESS',
'0x31': 'BALANCE',
'0x47': 'SELFBALANCE',
'0x48': 'BASEFEE',
'0x32': 'ORIGIN',
'0x33': 'CALLER',
'0x34': 'CALLVALUE',
'0x3a': 'GASPRICE',
'0x3b': 'EXTCODESIZE',
'0x3c': 'EXTCODECOPY',
'0x3f': 'EXTCODEHASH',
'0x3d': 'RETURNDATASIZE',
'0x3e': 'RETURNDATACOPY',
'0x40': 'BLOCKHASH',
'0x41': 'COINBASE',
'0x42': 'TIMESTAMP',
'0x43': 'NUMBER',
'0x44': 'DIFFICULTY',
'0x45': 'GASLIMIT',
'0x54': 'SLOAD',
'0x55': 'SSTORE',
'0x5a': 'GAS',
'0xa0': 'LOG0',
'0xa1': 'LOG1',
'0xa2': 'LOG2',
'0xa3': 'LOG3',
'0xa4': 'LOG4',
'0xf0': 'CREATE',
'0xf5': 'CREATE2',
'0xf1': 'CALL',
'0xf2': 'CALLCODE',
'0xf4': 'DELEGATECALL',
'0xfa': 'STATICCALL',
'0xff': 'SUICIDE',
'0x46': 'CHAINID',
};

export const opName = (opCode: number): string => {
const opCodeHex = `0x${opCode.toString(16).padStart(2, '0')}`;
return OPNAME_TABLE[opCodeHex] ?? opCodeHex;
};

export const traceVM = async (
api: ApiPromise,
extrinsic: Extrinsic | IExtrinsic | string | Uint8Array,
) => {
const pageSize = 10000;
const traceConf = {
page: 0,
pageSize,
disableStack: false,
enableMemory: true,
};

let traceNextPage = true;
let steps: Step[] = [];
while (traceNextPage) {
const res = await api.call.evmTraceApi.traceExtrinsic(extrinsic, { OpcodeTracer: traceConf });

if (!res.isOk) {
throw new Error(`traceVM: trace failed. Err: ${res.asErr.toString()}`);
}

const okRes = res.asOk;
if (!okRes.isSteps) {
throw new Error('traceVM: invalid outcome');
}

const curSteps = okRes.asSteps.toJSON() as StepRaw[];

Check failure on line 221 in packages/eth-providers/src/utils/trace.ts

View workflow job for this annotation

GitHub Actions / tests

Conversion of type 'string | number | boolean | AnyJson[] | { [index: string]: AnyJson; }' to type 'StepRaw[]' may be a mistake because neither type sufficiently overlaps with the other. If this was intentional, convert the expression to 'unknown' first.

steps = steps.concat(
curSteps.map(step => ({
...step,
op: opName(step.op),
// transform memory to 64 bytes chunks
memory: step.memory
? step.memory.map((chunk, idx) => {
// remove 0x prefix
const slice = chunk.slice(2);
// make sure each chunk is 64 bytes
return slice.length < 64 && idx + 1 < step.memory!.length
? slice.padStart(64, '0')
: slice;
})
: null,
})),
);

traceConf.page++;
traceNextPage = curSteps.length == pageSize;
}

return steps;
};

export const traceCall = async (
api: ApiPromise,
extrinsic: Extrinsic | IExtrinsic | string | Uint8Array,
): Promise<CallTrace[]> => {
const traceConf = { CallTracer: null };
const res = await api.call.evmTraceApi.traceExtrinsic(extrinsic, traceConf);
if (!res.isOk) {
throw new Error(`traceCall: trace failed. Err: ${res.asErr.toString()}`);
}

const okRes = res.asOk;
if (!okRes.isCalls) {
throw new Error('traceVM: invalid outcome');
}

return okRes.asCalls.toJSON() as CallTrace[];
};
11 changes: 6 additions & 5 deletions packages/eth-rpc-adapter/src/eip1193-bridge.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,12 +32,8 @@ export class Eip1193Bridge extends EventEmitter {
return this.send(request.method, request.params || []);
}

isMethodValid(method: string): boolean {
return method.startsWith('eth_') || method.startsWith('net_') || method.startsWith('web3_') || method.startsWith('txpool_');
}

isMethodImplemented(method: string): method is keyof Eip1193BridgeImpl {
return this.isMethodValid(method) && method in this.#impl;
return method in this.#impl;
}

async send(method: string, params: any[] = [], ws?: WebSocket): Promise<any> {
Expand Down Expand Up @@ -501,4 +497,9 @@ class Eip1193BridgeImpl {
validate([], params);
return this.#provider.txpoolContent();
}

async debug_traceTransaction(params: any[]): Promise<any> {
validate([{ type: 'trasactionHash' }, { type: '?' }], params);
return this.#provider.traceTx(params[0], params[1]);
}
}
7 changes: 6 additions & 1 deletion packages/eth-rpc-adapter/src/validate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,8 @@ export type Schema = {
| 'message'
| 'hexNumber'
| 'eventName'
| 'substrateGasParams?';
| 'substrateGasParams?'
| '?';
}[];

export const validateEventName = (value: any) => {
Expand Down Expand Up @@ -201,6 +202,10 @@ export const validate = (schema: Schema, data: unknown[]) => {
data[i] && validateSubstrateGasParams(data[i] as any);
break;
}
case '?': {
// TODO: refactor the param validating process
break;
}
default:
break;
}
Expand Down
Loading

0 comments on commit 0629d48

Please sign in to comment.