Skip to content

Commit 9b8cfe6

Browse files
committed
bugc: lay storage arrays out as Solidity does
1 parent 6c81c10 commit 9b8cfe6

15 files changed

Lines changed: 576 additions & 395 deletions

File tree

‎packages/bugc/CHANGELOG.md‎

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,17 @@ support. Changes to the specification itself are tracked in the root
2828

2929
### Changed
3030

31+
- Storage arrays now have Solidity's layout. A fixed-size array is
32+
inline: its elements start at its own slot, so `[0] a: array<uint256,
33+
3>` takes slots 0 to 2, and a struct member that is a fixed-size array
34+
takes all its slots. A dynamic array still keeps its length in its
35+
slot and its elements from `keccak256(slot)`. In either, elements
36+
narrower than 16 bytes now share a slot, as many as fit, from its
37+
low-order end: an `array<uint8, 40>` takes two slots. Each storage
38+
variable's pointer describes the same layout. Before, every array's
39+
elements started at `keccak256(slot)`, one element to a slot, while
40+
the pointer of a fixed-size array described it inline, and the pointer
41+
of an array of narrow elements described them packed ([#364]).
3142
- An integer literal operand of an arithmetic or comparison operator now
3243
takes the type of the other operand when its value fits, so with
3344
`x: int8`, `x < 0`, `x == 1` and `-1 < x` compare as `int8`. A literal
@@ -290,3 +301,4 @@ First publication.
290301
[#349]: https://github.com/ethdebug/format/pull/349
291302
[#351]: https://github.com/ethdebug/format/pull/351
292303
[#352]: https://github.com/ethdebug/format/pull/352
304+
[#364]: https://github.com/ethdebug/format/pull/364

‎packages/bugc/examples/intermediate/arrays.bug‎

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,11 @@ name ArraysAndLoops;
22

33
storage {
44
[0] numbers: array<uint256, 10>;
5-
[1] sum: uint256;
6-
[2] max: uint256;
7-
[3] count: uint256;
8-
[4] found: bool;
9-
[5] searchValue: uint256;
5+
[10] sum: uint256;
6+
[11] max: uint256;
7+
[12] count: uint256;
8+
[13] found: bool;
9+
[14] searchValue: uint256;
1010
}
1111

1212
code {
@@ -21,7 +21,7 @@ code {
2121
after: deploy
2222
variables:
2323
sum:
24-
pointer: { location: storage, slot: 1 }
24+
pointer: { location: storage, slot: 10 }
2525
value: 0
2626
*/
2727
for (let i = 0; i < 10; i = i + 1) {
@@ -78,21 +78,21 @@ code {
7878
fails: stack overflow during call - needs investigation
7979
variables:
8080
sum:
81-
pointer: { location: storage, slot: 1 }
81+
pointer: { location: storage, slot: 10 }
8282
value: 285
8383
*/
8484
/*@test final-max
8585
fails: stack overflow during call - needs investigation
8686
variables:
8787
max:
88-
pointer: { location: storage, slot: 2 }
88+
pointer: { location: storage, slot: 11 }
8989
value: 81
9090
*/
9191
/*@test final-count
9292
fails: stack overflow during call - needs investigation
9393
variables:
9494
count:
95-
pointer: { location: storage, slot: 3 }
95+
pointer: { location: storage, slot: 12 }
9696
value: 8
9797
*/
9898

‎packages/bugc/src/evmgen/behavioral.test.ts‎

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1843,7 +1843,8 @@ code {
18431843
o5 = m128[6];
18441844
}`;
18451845

1846-
// bugc gives each array element its own slot
1846+
// A fixed-size array is inline, and its narrow elements share a
1847+
// slot, from the low-order end (a[1] is its bytes 2 and 3)
18471848
const arrays = `name NarrowArrays;
18481849
storage { [0] a: array<int16, 3>; [1] n: int256; [10] o0: int256; }
18491850
create { n = -2 as int256; }
@@ -1882,7 +1883,7 @@ code { a[1] = n as int16; o0 = a[1]; }`;
18821883
optimizationLevel: level,
18831884
});
18841885
expect(result.callSuccess).toBe(true);
1885-
expect(await result.getStorage(hash(pad(0n)) + 1n)).toBe(0xfffen);
1886+
expect(await result.getStorage(0n)).toBe(0xfffe0000n);
18861887
expect(await result.getStorage(10n)).toBe(neg(2n));
18871888
});
18881889
}

‎packages/bugc/src/evmgen/constructor-array.test.ts‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -51,11 +51,11 @@ code {}
5151
const createFunc = module.create!;
5252
const entry = createFunc.blocks.get("entry")!;
5353

54-
// Check that we have compute_slot instructions for array access
54+
// A fixed-size array is inline: no keccak256 of its slot
5555
const computeSlotInstructions = entry.instructions.filter(
5656
(i) => i.kind === "compute_slot" && i.slotKind === "array",
5757
);
58-
expect(computeSlotInstructions.length).toBe(3);
58+
expect(computeSlotInstructions.length).toBe(0);
5959

6060
// Instructions are verified by checking write instructions below
6161

‎packages/bugc/src/evmgen/generation/instructions/storage.ts‎

Lines changed: 31 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -108,17 +108,42 @@ function signExtend<S extends Stack>(
108108
);
109109
}
110110

111+
/** Whether a storage offset is the constant 0 (or absent) */
112+
function isZero(offset: Ir.Value | undefined): boolean {
113+
return !offset || (offset.kind === "const" && BigInt(offset.value) === 0n);
114+
}
115+
116+
/**
117+
* Push a storage offset in bits: `offset * 8`. A storage array's
118+
* element that shares its slot has an offset known only at run time.
119+
*/
120+
function bitOffset<S extends Stack>(
121+
offset: Ir.Value | undefined,
122+
debug: Ir.Instruction.Debug,
123+
): Transition<S, readonly ["shift", ...S]> {
124+
if (!offset || offset.kind === "const") {
125+
const bytes = offset ? BigInt(offset.value as bigint) : 0n;
126+
return pipe<S>()
127+
.then(PUSHn(bytes * 8n, { debug }), { as: "shift" })
128+
.done();
129+
}
130+
return pipe<S>()
131+
.then(loadValue(offset, { debug }), { as: "value" })
132+
.then(PUSHn(3n, { debug }), { as: "shift" })
133+
.then(SHL({ debug }), { as: "shift" })
134+
.done();
135+
}
136+
111137
/**
112138
* Storage read: SLOAD with optional partial-slot extraction
113139
*/
114140
function generateStorageRead<S extends Stack>(
115141
inst: Ir.Instruction.Read,
116142
debug: Ir.Instruction.Debug,
117143
): Transition<S, readonly ["value", ...S]> {
118-
const offset = inst.offset?.kind === "const" ? inst.offset.value : 0n;
119144
const length = inst.length?.kind === "const" ? inst.length.value : 32n;
120145

121-
if (offset === 0n && length === 32n) {
146+
if (isZero(inst.offset) && length === 32n) {
122147
// Full slot read - simple SLOAD
123148
return pipe<S>()
124149
.then(loadValue(inst.slot!, { debug }), { as: "key" })
@@ -136,7 +161,7 @@ function generateStorageRead<S extends Stack>(
136161

137162
// Shift right by offset * 8 bits: storage writes count `offset`
138163
// from the low-order end of the slot
139-
.then(PUSHn(BigInt(offset) * 8n, { debug }), { as: "shift" })
164+
.then(bitOffset(inst.offset, debug), { as: "shift" })
140165
.then(SHR({ debug }), { as: "shiftedValue" })
141166
.then(PUSHn(1n, { debug }), { as: "b" })
142167

@@ -402,10 +427,9 @@ function generateStorageWrite<S extends Stack>(
402427
inst: Ir.Instruction.Write,
403428
debug: Ir.Instruction.Debug,
404429
): Transition<S, S> {
405-
const offset = inst.offset?.kind === "const" ? inst.offset.value : 0n;
406430
const length = inst.length?.kind === "const" ? inst.length.value : 32n;
407431

408-
if (offset === 0n && length === 32n) {
432+
if (isZero(inst.offset) && length === 32n) {
409433
// Full slot write - simple SSTORE
410434
return pipe<S>()
411435
.then(loadValue(inst.value!, { debug }), { as: "value" })
@@ -430,9 +454,7 @@ function generateStorageWrite<S extends Stack>(
430454
.then(SUB({ debug }), { as: "lengthMask" })
431455

432456
// Shift mask to offset position
433-
.then(PUSHn(BigInt(offset) * 8n, { debug }), {
434-
as: "bitOffset",
435-
})
457+
.then(bitOffset(inst.offset, debug), { as: "bitOffset" })
436458
.then(
437459
rebrand<"bitOffset", "shift", "lengthMask", "value">({
438460
1: "shift",
@@ -460,7 +482,7 @@ function generateStorageWrite<S extends Stack>(
460482
as: "a",
461483
})
462484
.then(AND({ debug }), { as: "value" })
463-
.then(PUSHn(BigInt(offset) * 8n, { debug }), { as: "shift" })
485+
.then(bitOffset(inst.offset, debug), { as: "shift" })
464486
.then(SHL({ debug }), { as: "shiftedValue" })
465487

466488
.then(
Lines changed: 180 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,180 @@
1+
/**
2+
* Storage arrays have Solidity's layout. A fixed-size array is inline:
3+
* its elements start at its own slot. A dynamic array keeps its length
4+
* in its slot and its elements from keccak256(slot). Either way, an
5+
* element narrower than 16 bytes shares a slot with its neighbors, as
6+
* many as fit, from the low-order end; any other element starts a slot
7+
* of its own and takes as many slots as its type needs. An array, as a
8+
* struct member, starts a slot, and the next member starts the slot
9+
* after it. Each storage variable's pointer must read the values the
10+
* program wrote.
11+
*/
12+
import { describe, it, expect } from "vitest";
13+
import { keccak256 } from "ethereum-cryptography/keccak";
14+
import { bytesToHex } from "ethereum-cryptography/utils";
15+
import { createMachineState } from "@ethdebug/evm";
16+
import { dereference } from "@ethdebug/pointers";
17+
import type * as Format from "@ethdebug/format";
18+
19+
import { executeProgram } from "#test/evm/behavioral";
20+
import { traceLocals } from "#test/evm/locals";
21+
22+
const levels = [0, 1, 2, 3] as const;
23+
24+
/** keccak256 of a slot number, as a dynamic array's first slot */
25+
function hashed(slot: bigint): bigint {
26+
const word = new Uint8Array(32);
27+
for (let i = 0; i < 32; i++) {
28+
word[31 - i] = Number((slot >> BigInt(8 * i)) & 0xffn);
29+
}
30+
return BigInt("0x" + bytesToHex(keccak256(word)));
31+
}
32+
33+
interface Case {
34+
source: string;
35+
/** Expected words, by slot */
36+
slots: [bigint, bigint][];
37+
/** Expected values, in order, of each variable's regions, by name */
38+
pointers?: Record<string, Record<string, bigint[]>>;
39+
}
40+
41+
const cases: Record<string, Case> = {
42+
"a fixed-size array is inline": {
43+
source: `storage {
44+
[0] f: array<uint256, 3>;
45+
[3] r: uint256;
46+
}
47+
code { f[0] = 10; f[2] = 30; r = f[0] + f[2]; }`,
48+
slots: [
49+
[0n, 10n],
50+
[1n, 0n],
51+
[2n, 30n],
52+
[3n, 40n],
53+
],
54+
pointers: { f: { element: [10n, 0n, 30n] } },
55+
},
56+
"narrow elements share a slot, from the low-order end": {
57+
source: `storage {
58+
[0] a: array<uint8, 40>;
59+
[2] r: uint256;
60+
}
61+
code { a[0] = 1; a[1] = 2; a[33] = 7; r = (a[1] as uint256) + (a[33] as uint256); }`,
62+
slots: [
63+
[0n, 0x0201n],
64+
[1n, 0x0700n],
65+
[2n, 9n],
66+
],
67+
pointers: {
68+
a: {
69+
element: Array.from({ length: 40 }, (_, i) =>
70+
i === 0 ? 1n : i === 1 ? 2n : i === 33 ? 7n : 0n,
71+
),
72+
},
73+
},
74+
},
75+
"a fixed-size array of fixed-size arrays": {
76+
source: `storage {
77+
[0] m: array<array<uint256, 3>, 2>;
78+
[6] r: uint256;
79+
}
80+
code { m[0][1] = 4; m[1][2] = 5; r = m[1][2]; }`,
81+
slots: [
82+
[1n, 4n],
83+
[5n, 5n],
84+
[6n, 5n],
85+
],
86+
pointers: { m: { element: [0n, 4n, 0n, 0n, 0n, 5n] } },
87+
},
88+
"an array in a struct takes its slots": {
89+
source: `define {
90+
struct S { x: uint8; arr: array<uint256, 2>; y: uint8; };
91+
}
92+
storage {
93+
[0] s: S;
94+
[4] r: uint256;
95+
}
96+
code { s.x = 1; s.arr[1] = 2; s.y = 3; r = s.arr[1] + (s.y as uint256); }`,
97+
slots: [
98+
[0n, 1n],
99+
[1n, 0n],
100+
[2n, 2n],
101+
[3n, 3n],
102+
[4n, 5n],
103+
],
104+
pointers: { s: { x: [1n], "arr-element": [0n, 2n], y: [3n] } },
105+
},
106+
"a fixed-size array of structs": {
107+
source: `define {
108+
struct P { a: uint256; b: uint256; };
109+
}
110+
storage {
111+
[0] ps: array<P, 2>;
112+
[4] r: uint256;
113+
}
114+
code { ps[1].a = 6; ps[1].b = 7; ps[0].b = 8; r = ps[1].b; }`,
115+
slots: [
116+
[1n, 8n],
117+
[2n, 6n],
118+
[3n, 7n],
119+
[4n, 7n],
120+
],
121+
pointers: { ps: { a: [0n, 6n], b: [8n, 7n] } },
122+
},
123+
"a dynamic array's narrow elements share a slot": {
124+
source: `storage {
125+
[0] d: array<uint16>;
126+
[1] r: uint256;
127+
}
128+
code { d[0] = 3; d[1] = 5; r = d[1] as uint256; }`,
129+
slots: [
130+
[hashed(0n), 0x00050003n],
131+
[1n, 5n],
132+
],
133+
},
134+
};
135+
136+
describe("storage array layout", () => {
137+
for (const [name, { source, slots }] of Object.entries(cases)) {
138+
for (const level of levels) {
139+
it(`${name} (level ${level})`, async () => {
140+
const result = await executeProgram(`name Layout;\n${source}`, {
141+
calldata: "",
142+
optimizationLevel: level,
143+
});
144+
expect(result.callSuccess).toBe(true);
145+
for (const [slot, value] of slots) {
146+
expect(await result.getStorage(slot), `slot ${slot}`).toBe(value);
147+
}
148+
});
149+
}
150+
}
151+
});
152+
153+
describe("storage array pointers", () => {
154+
for (const [name, { source, pointers }] of Object.entries(cases)) {
155+
if (!pointers) continue;
156+
it(name, async () => {
157+
const { program, executor, steps } = await traceLocals(
158+
`name Layout;\n${source}`,
159+
);
160+
const state = createMachineState(executor, {
161+
traceStep: steps[steps.length - 1],
162+
});
163+
for (const [identifier, regions] of Object.entries(pointers)) {
164+
const variable = (
165+
program.context as Format.Program.Context.Variables
166+
).variables.find((v) => v.identifier === identifier)!;
167+
const cursor = await dereference(variable.pointer!, { state });
168+
const view = await cursor.view(state);
169+
for (const [regionName, values] of Object.entries(regions)) {
170+
const read = await Promise.all(
171+
view.regions
172+
.filter((region) => region.name === regionName)
173+
.map(async (region) => (await view.read(region)).asUint()),
174+
);
175+
expect(read, `${identifier}: ${regionName}`).toEqual(values);
176+
}
177+
}
178+
});
179+
}
180+
});

0 commit comments

Comments
 (0)