feat(evm): read through asynchronous databases - #373
Conversation
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: ebd44425c0
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
commit: |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: f735c4b782
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| return attempt(evm, () => { | ||
| const result = evm['~engine'].callTx({ | ||
| envelope: envelope(transaction, evm['~chainId']), | ||
| signer: transaction.from, | ||
| }) |
There was a problem hiding this comment.
Snapshot transaction before queuing async retries
For async EVMs, attempt queues this closure on a promise and re-runs it after awaited reads; because the envelope and signer are read inside the closure, a caller can mutate transaction.from or a serialized Uint8Array immediately after Evm.callTx(...) (or between retries) and execute different bytes/signer than the call was made with. The sync path reads these during the call, so compute the envelope and signer once before entering the retry closure; the same pattern in transact needs the same treatment.
Useful? React with 👍 / 👎.
| // loudly rather than spinning. Real transactions read far fewer values. | ||
| type Outcome = { kind: 'pending' } | { kind: 'value'; value: result } | ||
|
|
||
| for (let reads = 0; reads < maxReads; reads++) { |
There was a problem hiding this comment.
Allow valid async executions past 100k reads
When an async transaction legitimately touches 100,000 or more uncached slots/accounts/code hashes, this loop throws StalledError even though every settle() made progress; that can happen in custom/high-gas EVM experiments because Evm.create defaults the block gas limit to u64 max and a small loop can perform that many distinct SLOADs. This makes the async path diverge from sync/evm2 for otherwise valid executions, so the retry loop should continue while reads are progressing rather than fail solely on the count.
AGENTS.md reference: AGENTS.md:L49-L49
Useful? React with 👍 / 👎.
| accounts.set( | ||
| outstanding.address.toLowerCase(), | ||
| await source.getAccount(outstanding.address), | ||
| ) |
There was a problem hiding this comment.
Clone mutable async state before caching
When an async source returns account objects or Uint8Array code backed by mutable storage, this stores the same reference in a cache that survives later awaits and later operations. A retry can cache the account, await another miss, and then execute with whatever mutations happened to the returned object/code in the meantime, unlike the sync path where the value is encoded immediately; copy the account fields and byte arrays before caching them.
Useful? React with 👍 / 👎.
Adds reading through an asynchronous database, so an EVM can execute against a
node instead of only against state seeded up front.
evm2's own asynchronous form runs on
corosenseifiber stacks, which WebAssemblyhas no equivalent for, so its
asyncfeature stays off. Instead a read thedriver's cache cannot serve abandons the attempt with a dedicated status, before
any state is accepted, and the operation repeats once the value is cached.
Execution is deterministic, so repeating with more state converges.
Database.fromAsyncmarks a source asynchronous andDatabase.fromRpcreadsover a JSON-RPC endpoint, batching an account's balance, nonce, and code into one
request rather than three round trips.
Evmcarries whether its reads are asynchronous, socallTx,transact, andreadAccountInforeturn a promise exactly when theymust; resolution stays synchronous because
commit,discard, anddetachreadnothing. Marking is explicit rather than sniffed: whether a read returns a
promise is otherwise only knowable by performing one, and
fromMemorythrows foran unseeded block hash.
The generated corpus replays identically through an asynchronous source across
all 96 cases, including the 21 validation rejections. Disabling the driver fails
102 tests, so the asynchronous path is not silently falling through to the
synchronous one.
The divergence the plan pre-recorded, a source seeing the same read more than
once per transaction, does not apply to this design: the cache answers repeats,
so each distinct read reaches the source once per EVM. The ledger records what is
actually true, including that a transaction touching many values executes several
times because only reads are cached.
RPC replay is covered:
fork.record.tsreplays the first transaction of mainnetblock 19868020 against archive state at its parent, and
fork.test.tsreplaysthe recorded exchange offline, matching the chain's own receipt exactly (gas,
status, all five logs) plus the post-state it touched. Bending the recorded gas
by one unit fails it.
Targets #366.