Skip to content

Commit 6907563

Browse files
authored
feat(evm): wrap block access lists (#376)
* feat(evm): record executions with an inspector * fix(evm): bound and reset traces correctly * test(evm): assert setters are awaitable on asynchronous evms * feat(evm): wrap block access lists * fix(evm): report uncovered reads from the operations that raise them
1 parent a3fa593 commit 6907563

19 files changed

Lines changed: 4639 additions & 14 deletions

File tree

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
---
2+
"ox": minor
3+
---
4+
5+
Added EIP-7928 block access lists: attach one so reads are gated by what it covers, or build one from executed transactions.
6+
7+
```ts
8+
import { Bal, Evm } from 'ox/evm'
9+
10+
// Validating: a read the list does not cover is refused, not served.
11+
Evm.setBal(evm, bal)
12+
13+
// Building: transaction `i` records at index `i + 1`.
14+
Evm.enableBalBuilder(evm)
15+
Evm.setBalIndex(evm, 1n)
16+
Evm.transact(evm, transaction)
17+
const built = Evm.takeBal(evm)
18+
```

package.json

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -717,6 +717,11 @@
717717
"types": "./dist/evm/index.d.ts",
718718
"default": "./dist/evm/index.js"
719719
},
720+
"./evm/Bal": {
721+
"src": "./src/evm/Bal.ts",
722+
"types": "./dist/evm/Bal.d.ts",
723+
"default": "./dist/evm/Bal.js"
724+
},
720725
"./evm/Database": {
721726
"src": "./src/evm/Database.ts",
722727
"types": "./dist/evm/Database.d.ts",

src/evm/Bal.ts

Lines changed: 145 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,145 @@
1+
import type * as codec from './internal/codec.js'
2+
3+
/**
4+
* A block access list, as [EIP-7928](https://eips.ethereum.org/EIPS/eip-7928)
5+
* defines it.
6+
*
7+
* Enumerates every account and storage slot a block touches, with the value each
8+
* one held after each transaction. Attached to an EVM it replaces the database
9+
* for covered reads; built from executions it is what a block proposer publishes.
10+
*
11+
* Ordering is canonical: accounts sorted by address, entries sorted by key. A
12+
* list read back carries that order whatever order it was written in.
13+
*/
14+
export type Bal = codec.Bal
15+
16+
/**
17+
* One account's entries.
18+
*
19+
* Every list is keyed by block access index, where index `0` is the pre-execution
20+
* state and transaction `i` writes at index `i + 1`. A read is an entry with no
21+
* changes, which is how a list records that a slot was touched without being
22+
* written.
23+
*/
24+
export type Account = codec.BalAccount
25+
26+
/**
27+
* Returns the accounts a list covers, in the order it carries them.
28+
*
29+
* @example
30+
* ```ts twoslash
31+
* // @noErrors
32+
* import { Bal } from 'ox/evm'
33+
*
34+
* Bal.addresses(bal)
35+
* // @log: ['0x…', '0x…']
36+
* ```
37+
*
38+
* @param bal - List to read.
39+
* @returns Each covered address.
40+
*/
41+
export function addresses(bal: Bal): readonly `0x${string}`[] {
42+
return bal.accounts.map((account) => account.address)
43+
}
44+
45+
/**
46+
* Returns whether a list covers a storage slot.
47+
*
48+
* A read that a list does not cover is refused rather than served from the
49+
* database, so this answers ahead of time what an execution would refuse.
50+
*
51+
* @example
52+
* ```ts twoslash
53+
* // @noErrors
54+
* import { Bal } from 'ox/evm'
55+
*
56+
* Bal.covers(bal, { address, slot: 0n })
57+
* // @log: true
58+
* ```
59+
*
60+
* @param bal - List to read.
61+
* @param options - Account, and the slot to check within it.
62+
* @returns Whether the list covers it.
63+
*/
64+
export function covers(bal: Bal, options: covers.Options): boolean {
65+
const address = options.address.toLowerCase()
66+
const account = bal.accounts.find(
67+
(entry) => entry.address.toLowerCase() === address,
68+
)
69+
if (!account) return false
70+
if (options.slot === undefined) return true
71+
return (
72+
account.storageReads.includes(options.slot) ||
73+
account.storageChanges.some((entry) => entry.slot === options.slot)
74+
)
75+
}
76+
77+
export declare namespace covers {
78+
type Options = {
79+
/** Account to look for. */
80+
address: `0x${string}`
81+
/** Slot to look for within the account. Omit to check the account alone. */
82+
slot?: bigint | undefined
83+
}
84+
}
85+
86+
/**
87+
* Returns the value a list holds for a slot at a block access index.
88+
*
89+
* Resolves the same way a covered read does: the most recent write *strictly
90+
* before* the index. Transaction `i` records its post-state at index `i + 1`, so
91+
* reading at `i + 1` sees what preceded that transaction, not its own writes.
92+
* `undefined` when the list carries no applicable write, which is when an
93+
* execution would read through to the database.
94+
*
95+
* @example
96+
* ```ts twoslash
97+
* // @noErrors
98+
* import { Bal } from 'ox/evm'
99+
*
100+
* // The value the second transaction saw.
101+
* Bal.storageAt(bal, { address, index: 2n, slot: 0n })
102+
* ```
103+
*
104+
* @param bal - List to read.
105+
* @param options - Account, slot, and index to resolve at.
106+
* @returns The value, or `undefined` when none applies.
107+
*/
108+
export function storageAt(
109+
bal: Bal,
110+
options: storageAt.Options,
111+
): bigint | undefined {
112+
const address = options.address.toLowerCase()
113+
const account = bal.accounts.find(
114+
(entry) => entry.address.toLowerCase() === address,
115+
)
116+
// A slot listed as a read carries no value even when the list also gives it
117+
// changes: evm2 folds reads in after changes, so the read wins and an
118+
// execution reads through to the database.
119+
if (account?.storageReads.includes(options.slot)) return undefined
120+
121+
const slot = account?.storageChanges.find(
122+
(entry) => entry.slot === options.slot,
123+
)
124+
if (!slot) return undefined
125+
126+
// Entries are ordered by index, and the first at or after the requested one
127+
// is not yet visible, so the value before it is what a read sees.
128+
let value: bigint | undefined
129+
for (const change of slot.changes) {
130+
if (change.index >= options.index) break
131+
value = change.value
132+
}
133+
return value
134+
}
135+
136+
export declare namespace storageAt {
137+
type Options = {
138+
/** Account holding the slot. */
139+
address: `0x${string}`
140+
/** Block access index to resolve at. */
141+
index: bigint
142+
/** Slot to resolve. */
143+
slot: bigint
144+
}
145+
}

0 commit comments

Comments
 (0)