Skip to content
This repository has been archived by the owner on Feb 26, 2024. It is now read-only.

Commit

Permalink
Extend trace to include CREATE, CREATE2, JUMP, JUMPI ops (#4444)
Browse files Browse the repository at this point in the history
  • Loading branch information
jeffsmale90 committed Jun 16, 2023
1 parent c31569f commit 8352876
Show file tree
Hide file tree
Showing 2 changed files with 135 additions and 98 deletions.
17 changes: 13 additions & 4 deletions src/chains/ethereum/ethereum/src/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -124,14 +124,23 @@ export type GasBreakdown<Estimate extends boolean> = {
export type TraceEntry = {
opcode: Data;
name: string;
from: Address;
to: Address;
value: Quantity;
pc: number;
data: Data;
signature?: string;
} & (CALLTraceEntry | JUMPTraceEntry | {}); // {} because CREATE and CREATE2 materialize as just the base TraceEntry

type CALLTraceEntry = {
from?: Address;
to?: Address;
value?: Quantity;
data?: Data;
args?: { type: string; value: Quantity | Data }[];
};

type JUMPTraceEntry = {
destination: Quantity;
condition?: Quantity;
};

type TransactionSimulationResult<Estimate extends boolean> = {
returnValue: Data;
gas: GasBreakdown<Estimate>;
Expand Down
216 changes: 122 additions & 94 deletions src/chains/ethereum/ethereum/src/blockchain.ts
Original file line number Diff line number Diff line change
Expand Up @@ -101,13 +101,17 @@ const opcode = {
CALLCODE: 0xf2,
DELEGATECALL: 0xf4,
STATICCALL: 0xfa,
CREATE: 0xf0,
CREATE2: 0xf5,
0x55: "SSTORE",
0x56: "JUMP",
0x57: "JUMPI",
0xf1: "CALL",
0xf2: "CALLCODE",
0xf4: "DELEGATECALL",
0xfa: "STATICCALL"
0xfa: "STATICCALL",
0xf0: "CREATE",
0xf5: "CREATE2"
};

export enum Status {
Expand Down Expand Up @@ -1251,7 +1255,7 @@ export default class Blockchain extends Emittery<BlockchainTypedEvents> {

const stepHandler = async (interpreter: Interpreter) => {
const runState = (interpreter as any)._runState as RunState;
const { opCode, stack, env, programCounter } = runState;
const { opCode, stack, env } = runState;
const codeAddress = env.codeAddress;

if (opCode === opcode.SSTORE) {
Expand All @@ -1271,99 +1275,11 @@ export default class Blockchain extends Emittery<BlockchainTypedEvents> {
keyBigInt,
valueBigInt
];
} else if (
includeTrace &&
(opCode === opcode.CALL ||
opCode === opcode.CALLCODE ||
opCode === opcode.DELEGATECALL ||
opCode === opcode.STATICCALL)
) {
// It'd be nice to show call heirarchy, either with nested calls or similar

let inLength: bigint,
inOffset: bigint,
value: bigint,
toAddr: bigint;
if (opCode === opcode.CALL || opCode === opcode.CALLCODE) {
[inLength, inOffset, value, toAddr] = stack._store.slice(-5, -1);
} else {
[inLength, inOffset, toAddr] = stack._store.slice(-4, -1);
} else if (includeTrace) {
const traceElement = this.makeTraceElement(runState);
if (traceElement) {
trace.push(traceElement);
}
const dataLength = Number(inLength);
const data =
dataLength === 0 ? BUFFER_EMPTY : Buffer.allocUnsafe(dataLength);
if (dataLength > 0) {
const dataOffset = Number(inOffset);

runState.memory._store.copy(
data,
0,
dataOffset,
dataLength + dataOffset
);
}
const to = bigIntToBuffer(toAddr);
const functionSelector =
data.length >= 4 ? data.readUIntBE(0, 4) : 0;
const signature = fourBytes.get(functionSelector);

let args: { type: string; value: Quantity | Data }[];
if (signature) {
const parameters = signature
.slice(signature.indexOf("(") + 1, signature.length - 1)
.split(",");
if (parameters.length > 0 && parameters[0] !== "") {
try {
const decoded = rawDecode(parameters, data.subarray(4));
args = Array(parameters.length) as any;
for (let i = 0; i < parameters.length; i++) {
const type = parameters[i];
const rawValue = decoded[i];
let value: Data | Quantity;
if (Buffer.isBuffer(rawValue)) {
value = Data.from(rawValue);
} else {
switch (typeof rawValue) {
case "string":
value = Data.from(Buffer.from(rawValue, "hex"));
break;
case "bigint":
value = Quantity.from(bigIntToBuffer(rawValue));
break;
default:
value = Data.from(
Buffer.from(rawValue.toString(16), "hex")
);
break;
}
}

args[i] = {
type,
value
};
}
} catch (er) {
console.error(
er,
parameters,
Data.from(data.subarray(4)),
typeof value
);
}
}
}
trace.push({
opcode: Data.from(Buffer.from([opCode])),
name: opcode[opCode],
from: Address.from(codeAddress.buf),
to: Address.from(to),
signature,
value: value === undefined ? undefined : Quantity.from(value),
data: Data.from(data),
args,
pc: programCounter
});
}
};

Expand Down Expand Up @@ -1519,6 +1435,118 @@ export default class Blockchain extends Emittery<BlockchainTypedEvents> {
return i < transactions.length ? results.slice(0, i) : results;
}

private makeTraceElement(runState: RunState): TraceEntry | undefined {
// It'd be nice to show call heirarchy, either with nested calls or similar
const { opCode, env, programCounter, stack } = runState;
switch (opCode) {
case opcode.CALL:
case opcode.STATICCALL:
case opcode.CALLCODE:
case opcode.DELEGATECALL:
let inLength: bigint, inOffset: bigint, value: bigint, toAddr: bigint;
if (opCode === opcode.CALL || opCode === opcode.CALLCODE) {
[inLength, inOffset, value, toAddr] = stack._store.slice(-5, -1);
} else {
[inLength, inOffset, toAddr] = stack._store.slice(-4, -1);
}
const dataLength = Number(inLength);
const data =
dataLength === 0 ? BUFFER_EMPTY : Buffer.allocUnsafe(dataLength);
if (dataLength > 0) {
const dataOffset = Number(inOffset);

runState.memory._store.copy(
data,
0,
dataOffset,
dataLength + dataOffset
);
}
const to = bigIntToBuffer(toAddr);
const functionSelector = data.length >= 4 ? data.readUIntBE(0, 4) : 0;
const signature = fourBytes.get(functionSelector);

let args: { type: string; value: Quantity | Data }[];
if (signature) {
const parameters = signature
.slice(signature.indexOf("(") + 1, signature.length - 1)
.split(",");
if (parameters.length > 0 && parameters[0] !== "") {
try {
const decoded = rawDecode(parameters, data.subarray(4));
args = Array(parameters.length) as any;
for (let i = 0; i < parameters.length; i++) {
const type = parameters[i];
const rawValue = decoded[i];
let value: Data | Quantity;
if (Buffer.isBuffer(rawValue)) {
value = Data.from(rawValue);
} else {
switch (typeof rawValue) {
case "string":
value = Data.from(Buffer.from(rawValue, "hex"));
break;
case "bigint":
value = Quantity.from(bigIntToBuffer(rawValue));
break;
default:
value = Data.from(
Buffer.from(rawValue.toString(16), "hex")
);
break;
}
}

args[i] = {
type,
value
};
}
} catch (er) {
console.error(
er,
parameters,
Data.from(data.subarray(4)),
typeof value
);
}
}
}
return {
opcode: Data.from(Buffer.from([opCode])),
name: opcode[opCode],
from: Address.from(env.codeAddress.buf),
to: Address.from(to),
signature,
value: value === undefined ? undefined : Quantity.from(value),
data: Data.from(data),
args,
pc: programCounter
};
case opcode.CREATE:
case opcode.CREATE2:
return {
opcode: Data.from(Buffer.from([opCode])),
name: opcode[opCode],
pc: programCounter
};
case opcode.JUMP:
case opcode.JUMPI:
const destination = Quantity.from(stack._store[stack.length - 1]);
const condition =
opCode === opcode.JUMPI
? Quantity.from(stack._store[stack.length - 1])
: undefined;
return {
opcode: Data.from(Buffer.from([opCode])),
name: opcode[opCode],
destination,
condition,
pc: programCounter
};
}
}

/**
* Creates a new VM with it's internal state set to that of the given `block`,
* up to, but _not_ including, the transaction at the given
Expand Down

0 comments on commit 8352876

Please sign in to comment.