-
Notifications
You must be signed in to change notification settings - Fork 175
REVM example: digest-first EVM execution with QMDB persistence #2495
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
REVM example: digest-first EVM execution with QMDB persistence #2495
Conversation
|
Haven't read into this deeply just yet, but awesome to see someone doing this on a lower-level than communicating with ETH ELs over the engine API :) Would be really neat to add a |
This would be :100000000000: |
3c3680e to
20ed6d2
Compare
Thanks for the quick feedback. I really appreciate the suggestion. Agree that a QMDB-backed backend would be a great “Commonware-native” next step. One caveat I’ve been thinking about: revm::Database is a synchronous, hot-path interface, while commonware_storage::qmdb is async (and REVM’s built-in async→sync adapter is Tokio-based). I don’t want to accidentally introduce blocking in the EVM execution loop or pull Tokio in outside runtime. A direction that seems workable is a layered backend: keep a CacheDB/InMemoryDB overlay for sync reads during execution, and add explicit async load_from_qmdb / flush_to_qmdb at finalized block boundaries (so QMDB provides persistence, but the EVM still reads from an in-memory cache). Does that match what you had in mind, or is there a better pattern you’d prefer for bridging the async/sync boundary here? |
|
I'm sorry for the big PR. I know it's not ideal and hard to review. I split it into many small commits to make it more manageable. Please let me know if I should break it into smaller PRs. |
Add a minimal execution layer (EthEvmBuilder + transact_raw) that converts REVM EvmState diffs into deterministic StateChanges, updates the rolling StateRoot, and commits via DatabaseCommit. Includes a single ETH transfer unit test using InMemoryDB.
Introduce a minimal application/mailbox that implements consensus::simplex Automaton/Relay/Reporter over opaque Sha256 digests, builds placeholder blocks, and reuses the alloy-evm execution path when data is present. Also add a shared EVM env helper (chain_id + block env fields).
Teach the simplex mailbox to broadcast full encoded blocks out-of-band via a p2p Sender when a proposal digest is accepted. The application now ingests received block bytes, queues verify requests until the block arrives, and verifies proposals by re-executing against the parent state and checking state_root.
Wire the simulated network to run N threshold-simplex nodes with out-of-band block broadcast and re-execution verification. Add a deterministic smoke test that finalizes a few blocks and checks all nodes converge on the same head, state_root, and transfer balances.
Persist a 32-byte seed hash per finalized/notarized digest, use it as the next block's prevrandao, and assert seed convergence in the deterministic sim.
Install a stateful precompile at 0x…00ff that returns block.prevrandao, and add unit tests for direct calls and contract-based access.
README now documents the prevrandao/seed flow and the seed precompile address; the CLI prints the finalized head, state root, seed and balances.
Refactor the EVM execution tests with explicit prepare/execute/assert structure and small helpers for funding and nonce retrieval.
Split the large event loop into a small dispatcher and concept-level handlers (genesis/propose/verify/broadcast/report + control queries), keeping behavior unchanged.
Extract helpers for channel registration, application startup, block forwarding, and simplex engine config to make node bootstrap easier to read.
Rename the stream-select enum from Event::{Ingress,Control} to Input::{Consensus,Control} to better reflect the source of messages.
Rename consensus mailbox messages to ConsensusRequest and application handle messages to ApplicationRequest, and update the application actor wiring for clearer concepts.
Please let me know what you think about the current solution when you have time |
Thanks a ton for continuing on this work. I'll make sure to give this a review by the end of the week! |
Thanks! I tried to make this easy to review with a clean history, docs/diagrams, and clear run/test instructions. If you’d prefer this split into smaller PRs or reviewed in a specific order, happy to adjust. |
There are a number of external teams now following this PR lol. Many are very keen to see how this goes 👀 |
Noted, I appreciate the heads‑up. I'll try to keep things moving cleanly so everyone watching can track progress. On a separate note, I’m seeing a reproducible stack overflow in the revm example after merging main. It hits on a tokio worker thread during persist_snapshot -> QmdbState::commit_changes -> open_stores, and the backtrace shows the crash inside commonware_runtime::storage::validate_partition_name while initializing QMDB stores. I captured an lldb trace confirming the call chain (marshal -> finalized reporter -> persist_snapshot -> QMDB init). The workaround/fix for now is running the persistence path on the blocking pool (context.shared(true)), which eliminates the overflow and the test passes. I’m still investigating a minimal repro on main (so far I can’t trigger it with QMDB init alone). Will follow up if I isolate it to WrapDatabaseAsync or another specific path. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Cursor Bugbot has reviewed your changes and found 1 potential issue.
Bugbot Autofix is OFF. To automatically fix reported issues with Cloud Agents, enable Autofix in the Cursor dashboard.
|
I dug into the stack overflow issue, and I believe the root cause is a nested block_in_place + block_on call stack on a Tokio worker thread. Context:
Why this crashes:
Fix:
So I believe this is a wiring issue in the example, not a bug in WrapDatabaseAsync. The workaround is the correct fix given the sync REVM + async QMDB bridge. |
|
Not sure if you've played around with QMDB's batch interface (haven't looked at your changes lately) but I suspect it'll be a huge performance boost. |
Summary
This PR provides the REVM example chain: a minimal, digest-first EVM execution pipeline driven by
threshold-simplex and backed by QMDB for authenticated persistence. It is designed as a reference
implementation for how Commonware primitives compose end-to-end.
Mental model (3 planes)
StateRoot, persists change sets.A block is accepted only if re-execution yields the advertised
StateRoot.Snapshots are cached so finalization can persist without re-exec when possible.
How it works (short)
Key design choices
ConsensusDigest = sha256(BlockId)rather than full blocks.StateRootis derived from QMDB partition roots (pre-commit), not an Ethereum trie.Where to look
examples/revm/src/application/examples/revm/src/application/ledger/examples/revm/src/application/execution.rsexamples/revm/src/qmdb/examples/revm/src/simulation/examples/revm/docs/How to run
Tests
just fix-fmtjust clippyjust test -p commonware-revmNon-goals
This example intentionally omits signatures, fee markets, Ethereum MPT state roots, and other
production Ethereum client features. The focus is on composition and correctness of the pipeline.