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 @@ -132,6 +132,16 @@ support. Changes to the specification itself are tracked in the root

- `a.length` on a dynamic array in storage now reads the length in the
array's slot. Before, it did not read that slot ([#366]).
- The pointer for a storage `string` or `bytes` value, as a variable, a
struct member or an array element, now reads its data, in the shape of
solc's `t_string_storage` template. A one-byte `length-flag` region (the
slot's low byte) chooses the form: when it is even, a `data` region in
the slot, of length `length-flag / 2`; else a `long-length` region (the
slot) and a `data` region from keccak256(slot), of length
`(long-length - 1) / 2`. A member's regions have names qualified by its
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]).
- 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 @@ -423,3 +433,4 @@ First publication.
[#365]: https://github.com/ethdebug/format/pull/365
[#366]: https://github.com/ethdebug/format/pull/366
[#367]: https://github.com/ethdebug/format/pull/367
[#369]: https://github.com/ethdebug/format/pull/369
137 changes: 137 additions & 0 deletions packages/bugc/src/irgen/debug/variables.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
/**
* A storage string or `bytes` has Solidity's encoding: up to 31 bytes
* in its slot, left-aligned, with length * 2 in the low byte; else
* length * 2 + 1 in the slot and the data from keccak256(slot). Its
* pointer must say so, so that a debugger reads the exact bytes from
* the pointer alone.
*/
import { describe, it, expect } from "vitest";
import { bytesToHex, hexToBytes } from "ethereum-cryptography/utils";
import { createMachineState } from "@ethdebug/evm";
import { dereference } from "@ethdebug/pointers";
import type * as Format from "@ethdebug/format";

import { parse } from "#parser";
import { checkProgram } from "#typechecker";
import { Type } from "#types";
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;

const alphabet =
"abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";
const text = (length: number) =>
Array.from({ length }, (_, i) => alphabet[i % alphabet.length]).join("");

const hex = (length: number) =>
Array.from({ length }, (_, i) => (i + 1).toString(16).padStart(2, "0")).join(
"",
);

const source = (length: number) => `name StringPointers;
define { struct S { id: uint256; name: string; tag: bytes; }; }
storage {
[0] s: string;
[1] b: bytes;
[2] p: S;
[5] a: array<string, 2>;
[7] u: mapping<uint256, S>;
}
code {
let m: string = "${text(length)}";
let n: bytes = 0x${hex(70)};
let c: bytes = ${length === 70 ? "n" : `n[0:${length}]`};
s = m;
b = c;
p.id = 1;
p.name = m;
p.tag = c;
a[1] = m;
u[7].id = 2;
u[7].name = m;
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;
}

/** Read each region named `name`, in order, as hex */
async function readRegions(
pointer: Format.Pointer,
state: ReturnType<typeof createMachineState>,
name: string,
): Promise<string[]> {
const cursor = await dereference(pointer, { state });
const view = await cursor.view(state);
return Promise.all(
view.regions
.filter((region) => region.name === name)
.map(async (region) => bytesToHex(await view.read(region))),
);
}

describe("storage string and bytes pointers", () => {
for (const level of levels) {
for (const length of lengths) {
it(`read ${length} bytes (level ${level})`, async () => {
const program = source(length);
const run = await traceLocals(program, level);
const state = createMachineState(run.executor, {
traceStep: run.steps[run.steps.length - 1],
});
const pointerOf = (identifier: string) =>
(
run.program.context as Format.Program.Context.Variables
).variables.find((v) => v.identifier === identifier)!.pointer!;

const string = bytesToHex(new TextEncoder().encode(text(length)));
const bytes = bytesToHex(hexToBytes(hex(length)));

expect(await readRegions(pointerOf("s"), state, "data")).toEqual([
string,
]);
expect(await readRegions(pointerOf("b"), state, "data")).toEqual([
bytes,
]);
expect(await readRegions(pointerOf("p"), state, "id")).toEqual([
"0".repeat(63) + "1",
]);
expect(await readRegions(pointerOf("p"), state, "name-data")).toEqual([
string,
]);
expect(await readRegions(pointerOf("p"), state, "tag-data")).toEqual([
bytes,
]);
expect(await readRegions(pointerOf("a"), state, "data")).toEqual([
"",
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([
"0".repeat(63) + "2",
]);
expect(await readRegions(entry, state, "name-data")).toEqual([string]);
expect(await readRegions(entry, state, "tag-data")).toEqual([bytes]);
});
}
}
});
68 changes: 64 additions & 4 deletions packages/bugc/src/irgen/debug/variables.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,9 +41,7 @@ function getTypeSize(bugType: Type): number {
case "bool":
return 1;
case "bytes":
return bugType.size || 32; // Dynamic bytes default to full slot
case "string":
return 32; // Dynamic string default to full slot
return bugType.size || 32;
default:
return 32;
}
Expand All @@ -69,7 +67,7 @@ function plus(
* 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`).
*/
function generateStoragePointer(
export function generateStoragePointer(
baseSlot: Format.Pointer.Expression,
bugType: Type,
byteOffset: number = 0,
Expand Down Expand Up @@ -196,6 +194,14 @@ function generateStoragePointer(
return pointer;
}

if (
Type.Elementary.isString(bugType) ||
(Type.Elementary.isBytes(bugType) &&
Type.Elementary.Bytes.isDynamic(bugType))
) {
return generateStorageBytesPointer(baseSlot, prefix);
}

// For elementary types, generate pointer with offset and length
const size = getTypeSize(bugType);
const pointer: Format.Pointer = {
Expand All @@ -217,6 +223,60 @@ function generateStoragePointer(
return pointer;
}

/**
* A pointer for a storage string or dynamic `bytes`, by Solidity's
* encoding, in the shape solc gives its `t_string_storage` template.
* The slot's low byte is even for a short value (up to 31 bytes): the
* slot holds the data, left-aligned, and length * 2 in that byte. Else
* the slot holds length * 2 + 1, and the data starts at
* keccak256(slot), in one region across as many slots as it needs.
*/
function generateStorageBytesPointer(
slot: Format.Pointer.Expression,
prefix: string,
): Format.Pointer {
const flag = `${prefix}length-flag`;
const longLength = `${prefix}long-length`;
const data = `${prefix}data`;
return {
group: [
{
name: flag,
location: "storage",
slot,
offset: { $difference: ["$wordsize", 1] },
length: 1,
},
{
if: { $remainder: [{ $sum: [{ $read: flag }, 1] }, 2] },
then: {
define: { length: { $quotient: [{ $read: flag }, 2] } },
in: { name: data, location: "storage", slot, length: "length" },
},
else: {
group: [
{ name: longLength, location: "storage", slot },
{
define: {
length: {
$quotient: [{ $difference: [{ $read: longLength }, 1] }, 2],
},
start: { $keccak256: [{ $wordsized: slot }] },
},
in: {
name: data,
location: "storage",
slot: "start",
length: "length",
},
},
],
},
},
],
};
}

/**
* Collect all variables with determinable locations from current state
*
Expand Down
Loading