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
11 changes: 11 additions & 0 deletions packages/bugc/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -142,6 +142,16 @@ support. Changes to the specification itself are tracked in the root
name, such as `name-data`. Before, the pointer gave only the slot, so a
debugger had to know Solidity's encoding to read the value
([#369]).
- The pointer for a storage mapping now gives a template for its entry,
in the shape of solc's `t_mapping` templates, around the region at the
mapping's slot. The template expects `slot` (the mapping's slot) and
`key`, and points at keccak256(key . slot), each a word: a value type
there is a region named `value`, and a struct's members are regions
named `value-<member>`, such as `value-score` or `value-name-data`. The
template is named `entry` for a mapping variable, `<name>-entry` for a
mapping member `name`, and `value-entry` for a mapping in an entry.
Before, the pointer gave only the mapping's slot, so a debugger could
not find an entry or its members from it ([#371]).
- At optimization levels 2 and 3, an inlined body's control flow now has
a `transform: ["inline"]` context: the branch of an `if`, the jumps
between the body's blocks, each block's `JUMPDEST`, and the jump that
Expand Down Expand Up @@ -434,3 +444,4 @@ First publication.
[#366]: https://github.com/ethdebug/format/pull/366
[#367]: https://github.com/ethdebug/format/pull/367
[#369]: https://github.com/ethdebug/format/pull/369
[#371]: https://github.com/ethdebug/format/pull/371
11 changes: 10 additions & 1 deletion packages/bugc/examples/intermediate/mappings.bug
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,16 @@ code {
after: deploy
variables:
balances:
pointer: { location: storage, slot: 0 }
pointer:
templates:
entry:
expect: [slot, key]
for:
define:
slot:
$keccak256: [{ $wordsized: key }, { $wordsized: slot }]
in: { location: storage, slot: slot, name: value }
in: { location: storage, slot: 0 }
*/

// Read from mapping
Expand Down
150 changes: 123 additions & 27 deletions packages/bugc/src/irgen/debug/variables.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,18 +7,13 @@
*/
import { describe, it, expect } from "vitest";
import { bytesToHex, hexToBytes } from "ethereum-cryptography/utils";
import { createMachineState } from "@ethdebug/evm";
import { Executor, createMachineState } from "@ethdebug/evm";
import { dereference } from "@ethdebug/pointers";
import type * as Format from "@ethdebug/format";
import * as Format from "@ethdebug/format";

import { parse } from "#parser";
import { checkProgram } from "#typechecker";
import { Type } from "#types";
import { compile } from "#compiler";
import { traceLocals } from "#test/evm/locals";

import { mappingAccess } from "./pointers.js";
import { generateStoragePointer } from "./variables.js";

const lengths = [0, 5, 31, 32, 70];
const levels = [0, 1, 2, 3] as const;

Expand Down Expand Up @@ -56,16 +51,22 @@ code {
u[7].tag = c;
}`;

/** The value type of storage variable `name`'s mapping */
function mappingValueType(source: string, name: string): Type {
const parsed = parse(source);
if (!parsed.success) throw new Error("parse failed");
const checked = checkProgram(parsed.value);
if (!checked.success) throw new Error("typecheck failed");
const declaration = parsed.value.storage!.find((d) => d.name === name)!;
const type = checked.value.types.get(declaration.id)!;
if (!Type.isMapping(type)) throw new Error(`${name} is not a mapping`);
return type.value;
/**
* The pointer to an entry of a mapping, from the entry template that a
* pointer defines (with the mapping's slot and the key)
*/
function entryOf(
pointer: Format.Pointer,
template: string,
define: Record<string, Format.Pointer.Expression>,
): Format.Pointer {
if (!Format.Pointer.Collection.isTemplates(pointer)) {
throw new Error("pointer defines no templates");
}
return {
templates: pointer.templates,
in: { define, in: { template } },
};
}

/** Read each region named `name`, in order, as hex */
Expand Down Expand Up @@ -120,18 +121,113 @@ describe("storage string and bytes pointers", () => {
string,
]);

// A mapping's pointer names only its slot; its entry `u[7]`
// has the pointer of its value type at the entry's slot
const entry = generateStoragePointer(
mappingAccess(7, 7),
mappingValueType(program, "u"),
)!;
expect(await readRegions(entry, state, "id")).toEqual([
// A mapping's entry `u[7]` comes from its entry template
const entry = entryOf(pointerOf("u"), "entry", { slot: 7, key: 7 });
expect(await readRegions(entry, state, "value-id")).toEqual([
"0".repeat(63) + "2",
]);
expect(await readRegions(entry, state, "name-data")).toEqual([string]);
expect(await readRegions(entry, state, "tag-data")).toEqual([bytes]);
expect(await readRegions(entry, state, "value-name-data")).toEqual([
string,
]);
expect(await readRegions(entry, state, "value-tag-data")).toEqual([
bytes,
]);
});
}
}
});

/**
* A mapping's entry is at keccak256(key . slot). Its pointer must give a
* template for the entry, so that a debugger finds `players[k]` and its
* members, for any key, from the pointer alone.
*/
describe("storage mapping pointers", () => {
const program = `name MappingPointers;
define { struct Player { score: uint64; combo: uint32; name: string; }; }
storage {
[0] other: uint256;
[4] players: mapping<address, Player>;
[5] plays: mapping<address, uint256>;
[6] grid: mapping<address, mapping<uint256, uint8>>;
}
code {
let who = msg.data[0:32] as bytes32 as address;
let len = msg.data[32:64] as bytes32 as uint256;
players[who].score = len as uint64;
players[who].combo = 3;
players[who].name = msg.data[64:64 + len] as string;
plays[who] = plays[who] + 1;
grid[who][len] = 7;
}`;

const players = [
{ key: "0x" + "a11ce".padStart(40, "0"), name: "alice" },
{ key: "0x" + "b0b".padStart(40, "0"), name: "bob" },
{
key: "0x" + "ca201".padStart(40, "0"),
name: "carol, the unstoppable combo queen",
},
];

const word = (value: bigint | number) =>
BigInt(value).toString(16).padStart(64, "0");

for (const level of levels) {
it(`reads alice's, bob's and carol's entries (level ${level})`, async () => {
const result = await compile({
to: "bytecode",
source: program,
optimizer: { level },
});
if (!result.success) throw new Error("compile failed");
const { bytecode } = result.value;
const executor = new Executor();
await executor.deploy(bytesToHex(bytecode.create ?? bytecode.runtime));
for (const { key, name } of players) {
const text = new TextEncoder().encode(name);
const data = word(BigInt(key)) + word(text.length) + bytesToHex(text);
const run = await executor.execute({ data });
expect(run.success).toBe(true);
}
const state = createMachineState(executor);
const pointerOf = (identifier: string) =>
(
bytecode.runtimeProgram.context as Format.Program.Context.Variables
).variables.find((v) => v.identifier === identifier)!.pointer!;

for (const { key, name } of players) {
const text = new TextEncoder().encode(name);
const player = entryOf(pointerOf("players"), "entry", {
slot: 4,
key,
});
expect(await readRegions(player, state, "value-score")).toEqual([
word(text.length).slice(-16),
]);
expect(await readRegions(player, state, "value-combo")).toEqual([
"00000003",
]);
expect(await readRegions(player, state, "value-name-data")).toEqual([
bytesToHex(text),
]);

const plays = entryOf(pointerOf("plays"), "entry", { slot: 5, key });
expect(await readRegions(plays, state, "value")).toEqual([word(1)]);

// `grid[key]` is a mapping: its slot is the inner mapping's,
// and the inner template gives `grid[key][len]`
const grid = entryOf(pointerOf("grid"), "entry", { slot: 6, key });
const cursor = await dereference(grid, { state });
const [inner] = (await cursor.view(state)).regions;
expect(inner.name).toEqual("value");
if (!("slot" in inner)) throw new Error("not a storage region");
const cell = entryOf(pointerOf("grid"), "value-entry", {
slot: inner.slot.toHex(),
key: text.length,
});
expect(await readRegions(cell, state, "value")).toEqual(["07"]);
}
});
}
});
80 changes: 70 additions & 10 deletions packages/bugc/src/irgen/debug/variables.ts
Original file line number Diff line number Diff line change
Expand Up @@ -66,14 +66,33 @@ function plus(
* collection (a nested struct or an array) cannot be named as a whole.
* Instead, every region name inside it is qualified by the member's path:
* `prefix` is that path (e.g. "ceo-" for the members of field `ceo`).
*
* A mapping's entries depend on a key, so each mapping in the pointer
* gets a template for its entry (see `generateMappingPointer`). The
* templates go in one `templates` collection around the whole pointer.
*/
export function generateStoragePointer(
baseSlot: Format.Pointer.Expression,
bugType: Type,
byteOffset: number = 0,
prefix: string = "",
depth: number = 0,
entries?: Entries,
): Format.Pointer | undefined {
if (!entries) {
const templates: Format.Pointer.Templates = {};
const pointer = generateStoragePointer(
baseSlot,
bugType,
byteOffset,
prefix,
depth,
{ templates, scope: "" },
);
if (!pointer || Object.keys(templates).length === 0) return pointer;
return { templates, in: pointer };
}

// For structs, generate a group pointer with each field
if (Type.isStruct(bugType)) {
const group: Format.Pointer[] = [];
Expand All @@ -92,6 +111,7 @@ export function generateStoragePointer(
fieldOffset,
`${prefix}${fieldName}-`,
depth,
entries,
);
if (!fieldPointer) continue;

Expand Down Expand Up @@ -149,6 +169,7 @@ export function generateStoragePointer(
0,
prefix,
depth + 1,
entries,
);
if (elementPointer && Format.Pointer.isRegion(elementPointer)) {
elementPointer = { ...elementPointer, name: `${prefix}element` };
Expand Down Expand Up @@ -181,17 +202,8 @@ export function generateStoragePointer(
};
}

// For mappings, we can't represent them without keys
// Just return the base slot pointer with offset/length
if (Type.isMapping(bugType)) {
const pointer: Format.Pointer = {
location: "storage",
slot: baseSlot,
};
if (byteOffset > 0) {
pointer.offset = byteOffset;
}
return pointer;
return generateMappingPointer(baseSlot, bugType, prefix, entries);
}

if (
Expand Down Expand Up @@ -223,6 +235,54 @@ export function generateStoragePointer(
return pointer;
}

/**
* The entry templates of a pointer, and the path of the template being
* built (empty outside any template), which keeps template names unique
*/
interface Entries {
templates: Format.Pointer.Templates;
scope: string;
}

/**
* A pointer for a storage mapping: a region at its slot, plus (in
* `entries`) a template for its entry, in the shape solc gives its
* `t_mapping` templates. The template expects `slot` (the mapping's
* slot) and `key`; the entry is at keccak256(key . slot), each a word.
* A value type there is a region named `value`; a struct's members are
* named `value-<member>`, and so on, as at a variable's own slot.
*
* The template is named `entry` for a mapping variable, else after the
* mapping's region: `<name>-entry` for the mapping member named `name`,
* or `value-entry` for a mapping in an entry (named inside the outer
* entry's template, so `value-value-entry` for a mapping two deep).
*/
function generateMappingPointer(
slot: Format.Pointer.Expression,
mapping: Type.Mapping,
prefix: string,
{ templates, scope }: Entries,
): Format.Pointer {
const value = generateStoragePointer("slot", mapping.value, 0, "value-", 0, {
templates,
scope: `${scope}${prefix}`,
});
if (value) {
templates[`${scope}${prefix}entry`] = {
expect: ["slot", "key"],
for: {
define: {
slot: { $keccak256: [{ $wordsized: "key" }, { $wordsized: "slot" }] },
},
in: Format.Pointer.isRegion(value)
? { ...value, name: "value" }
: value,
},
};
}
return { location: "storage", slot };
}

/**
* A pointer for a storage string or dynamic `bytes`, by Solidity's
* encoding, in the shape solc gives its `t_string_storage` template.
Expand Down
Loading