Skip to content

feat(evm): read through asynchronous databases - #373

Merged
jxom merged 5 commits into
jxom/evm2-wasmfrom
jxom/evm2-async
Aug 5, 2026
Merged

feat(evm): read through asynchronous databases#373
jxom merged 5 commits into
jxom/evm2-wasmfrom
jxom/evm2-async

Conversation

@jxom

@jxom jxom commented Aug 5, 2026

Copy link
Copy Markdown
Member

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 corosensei fiber stacks, which WebAssembly
has no equivalent for, so its async feature stays off. Instead a read the
driver'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.fromAsync marks a source asynchronous and Database.fromRpc reads
over a JSON-RPC endpoint, batching an account's balance, nonce, and code into one
request rather than three round trips. Evm carries whether its reads are asynchronous, so
callTx, transact, and readAccountInfo return a promise exactly when they
must; resolution stays synchronous because commit, discard, and detach read
nothing. Marking is explicit rather than sniffed: whether a read returns a
promise is otherwise only knowable by performing one, and fromMemory throws for
an 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.ts replays the first transaction of mainnet
block 19868020 against archive state at its parent, and fork.test.ts replays
the 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.

@vercel

vercel Bot commented Aug 5, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
ox Error Error Aug 5, 2026 1:54am

Request Review

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 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".

Comment thread src/evm/Database.ts Outdated
Comment thread src/evm/Evm.ts Outdated
Comment thread src/evm/Evm.ts Outdated
@jxom
jxom force-pushed the jxom/evm2-async branch from ebd4442 to b70fc62 Compare August 5, 2026 00:43
@pkg-pr-new

pkg-pr-new Bot commented Aug 5, 2026

Copy link
Copy Markdown

Open in StackBlitz

npm i https://pkg.pr.new/ox@373

commit: f735c4b

@jxom
jxom merged commit 04727a8 into jxom/evm2-wasm Aug 5, 2026
14 of 15 checks passed
@jxom
jxom deleted the jxom/evm2-async branch August 5, 2026 01:57

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 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".

Comment thread src/evm/Evm.ts
Comment on lines +338 to +342
return attempt(evm, () => {
const result = evm['~engine'].callTx({
envelope: envelope(transaction, evm['~chainId']),
signer: transaction.from,
})

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge 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 👍 / 👎.

Comment thread src/evm/internal/async.ts
// 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++) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge 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 👍 / 👎.

Comment thread src/evm/internal/async.ts
Comment on lines +123 to +126
accounts.set(
outstanding.address.toLowerCase(),
await source.getAccount(outstanding.address),
)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge 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 👍 / 👎.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant