Skip to content

Commit

Permalink
Add support for decoding contract state in the middle of a TX
Browse files Browse the repository at this point in the history
  • Loading branch information
cd1m0 committed Oct 4, 2024
1 parent 357e0c2 commit f65b391
Show file tree
Hide file tree
Showing 10 changed files with 15,114 additions and 28 deletions.
41 changes: 40 additions & 1 deletion src/debug/layout.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import { EVMStateManagerInterface } from "@ethereumjs/common";
import { Address } from "@ethereumjs/util";
import {
ArrayType,
ContractDefinition,
Expand All @@ -7,9 +9,10 @@ import {
PointerType,
TypeNode
} from "solc-typed-ast";
import { ContractStates } from "../utils";
import { IArtifactManager } from "./artifact_manager";
import { nextWord, roundLocToType, stor_decodeValue } from "./decoding";
import { MapKeys } from "./tracers";
import { getMapKeys, getStorage, KeccakPreimageMap, MapKeys } from "./tracers";
import { DataLocationKind, Storage, StorageLocation } from "./types";

export interface ContractSolidityState {
Expand All @@ -20,6 +23,42 @@ function isTypeStringStatic32Bytes(t: string): boolean {
return t.endsWith("[]") || t.includes("mapping(");
}

export async function decodeContractStates(
artifactManager: IArtifactManager,
contracts: Iterable<Address>,
state: EVMStateManagerInterface,
preimages: KeccakPreimageMap
): Promise<ContractStates> {
const res: ContractStates = {};
const mapKeys = getMapKeys(preimages);

for (const addr of contracts) {
const code = await state.getContractCode(addr);
const info = artifactManager.getContractFromDeployedBytecode(code);

if (!info || !info.ast) {
continue;
}

const infer = artifactManager.infer(info.artifact.compilerVersion);
const storage = await getStorage(state, addr);

const contractState = decodeContractState(
artifactManager,
infer,
info.ast,
storage,
mapKeys
);

if (contractState) {
res[addr.toString()] = contractState;
}
}

return res;
}

export function decodeContractState(
artifactManager: IArtifactManager,
infer: InferType,
Expand Down
49 changes: 40 additions & 9 deletions src/debug/tracers/base_tracer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,16 @@ export interface FoundryTxResult extends RunTxResult {
*/
const vmToEVMMap = new Map<VM, EVM>();

export abstract class BaseSolTxTracer<State> {
/**
* Base class for all trace processors. You can think of trace processing as a combination
* of a map operation from `InterpreterStep` -> `TraceT`, along with a reduce operation
* from `(TraceT, CtxT) -> CtxT`. All the map functions are stored in `transformers/` to allow
* for reusability between different tracers.
*
* There is yet no formal place for reducers to go to. Check out `storage_decode_tracer.ts` for an example
* of a reducer that accumulates live contracts and keccak preimages at each step.
*/
export abstract class BaseSolTxTracer<TraceT, CtxT> {
artifactManager!: IArtifactManager;
protected readonly strict: boolean;
protected readonly foundryCheatcodes: boolean;
Expand Down Expand Up @@ -131,26 +140,37 @@ export abstract class BaseSolTxTracer<State> {
abstract processRawTraceStep(
vm: VM,
step: InterpreterStep,
trace: State[],
tx: TypedTransaction
): Promise<State>;
trace: TraceT[],
tx: TypedTransaction,
ctx: CtxT
): Promise<[TraceT, CtxT]>;

/**
* Run a TX with the specified "transformers" returning a quadruple including:
*
* 1. An enriched trace
* 2. The TX result (with added info for Foundry TX)
* 3. The StateManager at the end of the TX
* 4. The final (reduced) context `CtxT`
*/
async debugTx(
tx: TypedTransaction,
block: Block | undefined, // TODO: Make block required and add to processRawTraceStep
stateBefore: EVMStateManagerInterface
): Promise<[State[], FoundryTxResult, EVMStateManagerInterface]> {
stateBefore: EVMStateManagerInterface,
ctx: CtxT
): Promise<[TraceT[], FoundryTxResult, EVMStateManagerInterface, CtxT]> {
const vm = await BaseSolTxTracer.createVm(
stateBefore.shallowCopy(true),
this.foundryCheatcodes
);

const trace: State[] = [];
const trace: TraceT[] = [];

assert(vm.evm.events !== undefined, "Unable to access EVM events at this point");

vm.evm.events.on("step", async (step: InterpreterStep, next: any) => {
const curStep = await this.processRawTraceStep(vm, step, trace, tx);
const [curStep, newCtx] = await this.processRawTraceStep(vm, step, trace, tx, ctx);
ctx = newCtx;

trace.push(curStep);

Expand All @@ -177,7 +197,18 @@ export abstract class BaseSolTxTracer<State> {
...txRes,
failCalled: foundryFailCalled
},
stateAfter
stateAfter,
ctx
];
}
}

export abstract class MapOnlyTracer<TraceT> extends BaseSolTxTracer<TraceT, null> {
async debugTx(
tx: TypedTransaction,
block: Block | undefined,
stateBefore: EVMStateManagerInterface
): Promise<[TraceT[], FoundryTxResult, EVMStateManagerInterface, null]> {
return super.debugTx(tx, block, stateBefore, null);
}
}
8 changes: 4 additions & 4 deletions src/debug/tracers/sol_debugger.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { InterpreterStep } from "@ethereumjs/evm";
import { TypedTransaction } from "@ethereumjs/tx";
import { VM } from "@ethereumjs/vm";
import { StepState } from "../types";
import { BaseSolTxTracer } from "./base_tracer";
import { MapOnlyTracer } from "./base_tracer";
import {
addBasicInfo,
addContractLifetimeInfo,
Expand All @@ -14,13 +14,13 @@ import { addExternalFrame } from "./transformers/ext_stack";
import { addInternalFrame } from "./transformers/int_stack";
import { addSource } from "./transformers/source";

export class SolTxDebugger extends BaseSolTxTracer<StepState> {
export class SolTxDebugger extends MapOnlyTracer<StepState> {
async processRawTraceStep(
vm: VM,
step: InterpreterStep,
trace: StepState[],
tx: TypedTransaction
): Promise<StepState> {
): Promise<[StepState, null]> {
const opInfo = addOpInfo(vm, step, {});
const basicInfo = await addBasicInfo(vm, step, opInfo, trace);
const extFrameInfo = await addExternalFrame(
Expand All @@ -47,6 +47,6 @@ export class SolTxDebugger extends BaseSolTxTracer<StepState> {

const keccakPreimages = await addKeccakInvertInfo(vm, step, contractLifetime, trace);

return keccakPreimages;
return [keccakPreimages, null];
}
}
104 changes: 104 additions & 0 deletions src/debug/tracers/storage_decode_tracer.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
import { InterpreterStep } from "@ethereumjs/evm";
import { TypedTransaction } from "@ethereumjs/tx";
import { Address } from "@ethereumjs/util";
import { VM } from "@ethereumjs/vm";
import { ContractStates } from "../../utils";
import { decodeContractStates } from "../layout";
import { BaseSolTxTracer } from "./base_tracer";
import {
addBasicInfo,
addContractLifetimeInfo,
addExternalFrame,
addKeccakInvertInfo,
addOpInfo,
BasicStepInfo,
ContractLifeTimeInfo,
ExternalFrameInfo,
Keccak256InvertInfo,
KeccakPreimageMap
} from "./transformers";

export interface DecodedStorageInfo {
decodedStorage?: ContractStates;
}

export type StorageDecodeTracerInfo = BasicStepInfo &
ExternalFrameInfo &
ContractLifeTimeInfo &
Keccak256InvertInfo &
DecodedStorageInfo;

export interface StorageDecodeTracerCtx {
liveContracts: Set<string>;
preimages: KeccakPreimageMap;
targetSteps: Set<number>;
}

function reducer(
state: ContractLifeTimeInfo & Keccak256InvertInfo,
ctx: StorageDecodeTracerCtx
): void {
if (state.contractCreated) {
ctx.liveContracts.add(state.contractCreated.toString());
}

if (state.contractKilled) {
ctx.liveContracts.delete(state.contractKilled.toString());
}

if (state.keccak) {
ctx.preimages.set(state.keccak.to, state.keccak.from);
}
}
/**
* This tracer computes contract lifetime information and keccak256 pre-images for a TX.
* The information it collects supports the debugging for the main SolTxDebugger tracer.
* It is more-light weight and is ran by the TestRunner for all TXs, even if we are not going to debug them.
*/
export class StorageDecodeTracer extends BaseSolTxTracer<
StorageDecodeTracerInfo,
StorageDecodeTracerCtx
> {
async processRawTraceStep(
vm: VM,
step: InterpreterStep,
trace: StorageDecodeTracerInfo[],
tx: TypedTransaction,
ctx: StorageDecodeTracerCtx
): Promise<[StorageDecodeTracerInfo, StorageDecodeTracerCtx]> {
const opInfo = addOpInfo(vm, step, {});
const basicInfo = await addBasicInfo(vm, step, opInfo, trace);
const extFrameInfo = await addExternalFrame(
vm,
step,
basicInfo,
trace,
this.artifactManager,
tx
);
const contracLifetimeInfo = addContractLifetimeInfo(vm, step, extFrameInfo, trace);
const keccakInfo = addKeccakInvertInfo(vm, step, contracLifetimeInfo, trace);

reducer(keccakInfo, ctx);

if (!ctx.targetSteps.has(trace.length)) {
return [keccakInfo, ctx];
}

const state = vm.stateManager;
const decodedStorage: ContractStates = await decodeContractStates(
this.artifactManager,
[...ctx.liveContracts].map(Address.fromString),
state,
ctx.preimages
);

return [
{
...keccakInfo,
decodedStorage
},
ctx
];
}
}
8 changes: 4 additions & 4 deletions src/debug/tracers/support_tracer.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { InterpreterStep } from "@ethereumjs/evm";
import { TypedTransaction } from "@ethereumjs/tx";
import { VM } from "@ethereumjs/vm";
import { BaseSolTxTracer } from "./base_tracer";
import { MapOnlyTracer } from "./base_tracer";
import {
addBasicInfo,
addContractLifetimeInfo,
Expand All @@ -23,13 +23,13 @@ export type SupportTracerStepInfo = BasicStepInfo &
* The information it collects supports the debugging for the main SolTxDebugger tracer.
* It is more-light weight and is ran by the TestRunner for all TXs, even if we are not going to debug them.
*/
export class SupportTracer extends BaseSolTxTracer<SupportTracerStepInfo> {
export class SupportTracer extends MapOnlyTracer<SupportTracerStepInfo> {
async processRawTraceStep(
vm: VM,
step: InterpreterStep,
trace: SupportTracerStepInfo[],
tx: TypedTransaction
): Promise<SupportTracerStepInfo> {
): Promise<[SupportTracerStepInfo, null]> {
const opInfo = addOpInfo(vm, step, {});
const basicInfo = await addBasicInfo(vm, step, opInfo, trace);
const extFrameInfo = await addExternalFrame(
Expand All @@ -43,6 +43,6 @@ export class SupportTracer extends BaseSolTxTracer<SupportTracerStepInfo> {
const contracLifetimeInfo = addContractLifetimeInfo(vm, step, extFrameInfo, trace);
const keccakInfo = addKeccakInvertInfo(vm, step, contracLifetimeInfo, trace);

return keccakInfo;
return [keccakInfo, null];
}
}
Loading

0 comments on commit f65b391

Please sign in to comment.