Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
43 changes: 43 additions & 0 deletions packages/bugc/src/irgen/debug/pointers.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import { describe, it, expect } from "vitest";

import { mappingAccess, arrayElementAccess } from "./pointers.js";

/**
* `$keccak256` operands must be width-bearing bytes: the EVM hashes
* key‖slot as two 32-byte words, so a bare integer slot operand would
* hash the wrong number of bytes.
*/
describe("mappingAccess", () => {
it("wordsizes a literal slot operand", () => {
expect(mappingAccess(0, 0x1234)).toEqual({
$keccak256: [{ $wordsized: 0x1234 }, { $wordsized: 0 }],
});
});

it("wordsizes an arithmetic slot operand", () => {
expect(mappingAccess({ $sum: [3, 1] }, 0x1234)).toEqual({
$keccak256: [{ $wordsized: 0x1234 }, { $wordsized: { $sum: [3, 1] } }],
});
});

it("does not re-wrap a nested keccak256 slot operand", () => {
const inner = mappingAccess(1, 0xaaaa);
expect(mappingAccess(inner, 0xbbbb)).toEqual({
$keccak256: [{ $wordsized: 0xbbbb }, inner],
});
});
});

describe("arrayElementAccess", () => {
it("wordsizes the base slot of a dynamic array", () => {
expect(arrayElementAccess(2, "i", true)).toEqual({
$sum: [{ $keccak256: [{ $wordsized: 2 }] }, "i"],
});
});

it("does not hash the base slot of a fixed array", () => {
expect(arrayElementAccess(2, "i", false)).toEqual({
$sum: [2, "i"],
});
});
});
35 changes: 25 additions & 10 deletions packages/bugc/src/irgen/debug/pointers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -121,21 +121,19 @@ export function translateComputeSlotChain(
const inst = step.instruction;

if (inst.slotKind === "mapping") {
// Mapping access: keccak256(wordsized(key), slot)
// Mapping access: keccak256(wordsized(key), wordsized(slot))
// Try to convert key to expression
const keyExpr = valueToExpression(step.key);
if (keyExpr !== null) {
expr = {
$keccak256: [{ $wordsized: keyExpr }, expr],
};
expr = mappingAccess(expr, keyExpr);
}
// If we can't convert the key, skip this step (use current expr)
} else if (inst.slotKind === "array") {
// Array base: keccak256(slot)
// Array base: keccak256(wordsized(slot))
// Note: actual element access is done with binary.add afterward
// which we don't see in the compute_slot chain
expr = {
$keccak256: [expr],
$keccak256: [wordsized(expr)],
};
} else if (inst.slotKind === "field") {
// Struct field: slot + fieldSlotOffset
Expand Down Expand Up @@ -182,17 +180,34 @@ function valueToExpression(
return null;
}

/**
* Give an expression a 32-byte width for use as a `$keccak256` operand.
*
* `$keccak256` operands must be width-bearing bytes; a bare integer
* (literal, `$sum`, ...) is invalid there. A `$keccak256` result is
* already 32 bytes wide, so it is passed through unwrapped.
*/
function wordsized(
expression: Format.Pointer.Expression,
): Format.Pointer.Expression {
if (typeof expression === "object" && "$keccak256" in expression) {
return expression;
}
return { $wordsized: expression };
}

/**
* Helper to create pointer expression for mapping access
*
* Generates: keccak256(concat(key, slot))
* Generates: keccak256(wordsized(key) ++ wordsized(slot)), matching the
* EVM's hash over key‖slot as two 32-byte words
*/
export function mappingAccess(
slot: number | Format.Pointer.Expression,
key: Format.Pointer.Expression,
): Format.Pointer.Expression {
return {
$keccak256: [{ $wordsized: key }, slot],
$keccak256: [{ $wordsized: key }, wordsized(slot)],
};
}

Expand All @@ -208,9 +223,9 @@ export function arrayElementAccess(
isDynamic: boolean,
): Format.Pointer.Expression {
if (isDynamic) {
// Dynamic array: keccak256(slot) + index
// Dynamic array: keccak256(wordsized(slot)) + index
return {
$sum: [{ $keccak256: [baseSlot] }, index],
$sum: [{ $keccak256: [wordsized(baseSlot)] }, index],
};
} else {
// Fixed array: slot + index
Expand Down
21 changes: 13 additions & 8 deletions packages/bugc/src/irgen/debug/storage-analysis.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -426,9 +426,9 @@ describe("storage-analysis", () => {

const pointer = translateComputeSlotChain(chain!);

// Should be: keccak256(wordsized(0x1234), 0)
// Should be: keccak256(wordsized(0x1234), wordsized(0))
expect(pointer).toEqual({
$keccak256: [{ $wordsized: 0x1234 }, 0],
$keccak256: [{ $wordsized: 0x1234 }, { $wordsized: 0 }],
});
});

Expand All @@ -446,12 +446,17 @@ describe("storage-analysis", () => {

const pointer = translateComputeSlotChain(chain!);

// Should be: keccak256(wordsized(0xbbbb), keccak256(wordsized(0xaaaa), 1))
// Should be:
// keccak256(
// wordsized(0xbbbb),
// keccak256(wordsized(0xaaaa), wordsized(1)),
// )
// The inner keccak256 is already 32-byte bytes, so it is not wrapped.
expect(pointer).toEqual({
$keccak256: [
{ $wordsized: 0xbbbb },
{
$keccak256: [{ $wordsized: 0xaaaa }, 1],
$keccak256: [{ $wordsized: 0xaaaa }, { $wordsized: 1 }],
},
],
});
Expand All @@ -470,9 +475,9 @@ describe("storage-analysis", () => {

const pointer = translateComputeSlotChain(chain!);

// Should be: keccak256(2)
// Should be: keccak256(wordsized(2))
expect(pointer).toEqual({
$keccak256: [2],
$keccak256: [{ $wordsized: 2 }],
});
});

Expand Down Expand Up @@ -509,11 +514,11 @@ describe("storage-analysis", () => {

const pointer = translateComputeSlotChain(chain!);

// Should be: sum(keccak256(wordsized(0xaaaa), 4), 2)
// Should be: sum(keccak256(wordsized(0xaaaa), wordsized(4)), 2)
expect(pointer).toEqual({
$sum: [
{
$keccak256: [{ $wordsized: 0xaaaa }, 4],
$keccak256: [{ $wordsized: 0xaaaa }, { $wordsized: 4 }],
},
2,
],
Expand Down
4 changes: 2 additions & 2 deletions packages/pointers/src/dereference/generate.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import type { Pointer } from "@ethdebug/format";
import type { Machine } from "#machine";
import type { Cursor } from "#cursor";
import type { Data } from "#data";
import type { Value } from "#evaluate";

import { Memo } from "./memo.js";
import { processPointer, type ProcessOptions } from "./process.js";
Expand Down Expand Up @@ -129,7 +129,7 @@ async function initializeProcessOptions({
const stackLengthChange = currentStackLength - initialStackLength;

const regions: Record<string, Cursor.Region> = {};
const variables: Record<string, Data> = {};
const variables: Record<string, Value> = {};

return {
templates,
Expand Down
4 changes: 1 addition & 3 deletions packages/pointers/src/dereference/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -135,9 +135,7 @@ describe("dereference", () => {
expect(region).toEqual({
name: "item",
location: "memory",
offset: Data.fromUint(
Data.fromNumber(index).asUint() * 32n,
).padUntilAtLeast(1),
offset: Data.fromUint(BigInt(index) * 32n),
length: Data.fromNumber(32),
});
}
Expand Down
6 changes: 3 additions & 3 deletions packages/pointers/src/dereference/memo.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import type { Pointer } from "@ethdebug/format";
import type { Cursor } from "#cursor";
import type { Data } from "#data";
import type { Value } from "#evaluate";

/**
* A single state transition for processing on a stack
Expand Down Expand Up @@ -62,14 +62,14 @@ export namespace Memo {
*/
export interface SaveVariables {
kind: "save-variables";
variables: Record<string, Data>;
variables: Record<string, Value>;
}

/**
* Initialize a SaveVariables memo
*/
export const saveVariables = (
variables: Record<string, Data>,
variables: Record<string, Value>,
): SaveVariables => ({
kind: "save-variables",
variables,
Expand Down
19 changes: 9 additions & 10 deletions packages/pointers/src/dereference/process.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
import { Pointer } from "@ethdebug/format";
import type { Machine } from "#machine";
import type { Cursor } from "#cursor";
import { Data } from "#data";
import { evaluate } from "#evaluate";
import { evaluate, Value } from "#evaluate";

import { Memo } from "./memo.js";
import { adjustStackLength, evaluateRegion } from "./region.js";
Expand All @@ -15,7 +14,7 @@ export interface ProcessOptions {
state: Machine.State;
stackLengthChange: bigint;
regions: Record<string, Cursor.Region>;
variables: Record<string, Data>;
variables: Record<string, Value>;
}

/**
Expand Down Expand Up @@ -101,13 +100,13 @@ async function* processList(
const { list } = collection;
const { count: countExpression, each, is } = list;

const count = (await evaluate(countExpression, options)).asUint();
const count = Value.toInteger(await evaluate(countExpression, options));

const memos: Memo[] = [];
for (let index = 0n; index < count; index++) {
memos.push(
Memo.saveVariables({
[each]: Data.fromUint(index),
[each]: Value.integer(index),
}),
);

Expand All @@ -123,7 +122,7 @@ async function* processConditional(
): Process {
const { if: ifExpression, then: then_, else: else_ } = collection;

const if_ = (await evaluate(ifExpression, options)).asUint();
const if_ = Value.toInteger(await evaluate(ifExpression, options));

if (if_) {
return [Memo.dereferencePointer(then_)];
Expand All @@ -142,15 +141,15 @@ async function* processScope(
const allVariables = {
...options.variables,
};
const newVariables: { [identifier: string]: Data } = {};
const newVariables: { [identifier: string]: Value } = {};
for (const [identifier, expression] of Object.entries(variableExpressions)) {
const data = await evaluate(expression, {
const value = await evaluate(expression, {
...options,
variables: allVariables,
});

allVariables[identifier] = data;
newVariables[identifier] = data;
allVariables[identifier] = value;
newVariables[identifier] = value;
}

return [Memo.saveVariables(newVariables), Memo.dereferencePointer(in_)];
Expand Down
6 changes: 3 additions & 3 deletions packages/pointers/src/dereference/region.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { Pointer } from "@ethdebug/format";
import type { Cursor } from "#cursor";
import type { Data } from "#data";
import { evaluate, type EvaluateOptions } from "#evaluate";
import { evaluate, Value, type EvaluateOptions } from "#evaluate";

/**
* Evaluate all Pointer.Expression-value properties on a given region
Expand Down Expand Up @@ -55,15 +55,15 @@ export async function evaluateRegion<R extends Pointer.Region>(
const [property, expression] = expressionQueue.shift()!;

try {
const data = await evaluate(expression, {
const value = await evaluate(expression, {
...options,
regions: {
...options.regions,
$this: partialRegion,
},
});

evaluatedProperties[property as keyof R] = data;
evaluatedProperties[property as keyof R] = Value.toData(value);
} catch (error) {
if (
error instanceof Error &&
Expand Down
Loading
Loading