Skip to content

Conversation

@diegomrsantos
Copy link
Contributor

@diegomrsantos diegomrsantos commented Dec 13, 2025

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)

  1. Consensus plane (Simplex): orders 32-byte digests only.
  2. Data plane (Marshal + broadcast + resolver): moves full blocks and backfills ancestors.
  3. State plane (REVM + QMDB): re-executes blocks, computes 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)

  • Propose: build tx batch from mempool, execute with REVM, compute QMDB root, return block.
  • Verify: re-execute on parent snapshot, recompute root, accept only if it matches.
  • Finalize: persist QMDB changes for the finalized digest; prune mempool; emit events.

Key design choices

  • Digest-first: consensus orders ConsensusDigest = sha256(BlockId) rather than full blocks.
  • State root semantics: StateRoot is derived from QMDB partition roots (pre-commit), not an Ethereum trie.
  • Persistence: QMDB changes are merged in ancestor order and committed on finalization.
  • Runtime: tokio is required to bridge async QMDB into REVM’s sync database interface.

Where to look

  • Application / execution: examples/revm/src/application/
  • Ledger state & snapshots: examples/revm/src/application/ledger/
  • REVM integration: examples/revm/src/application/execution.rs
  • QMDB adapter/persistence: examples/revm/src/qmdb/
  • Simulation harness: examples/revm/src/simulation/
  • Docs + diagrams: examples/revm/docs/

How to run

cargo run -p commonware-revm --release -- --nodes 4 --blocks 5 --seed 1

Tests

  • just fix-fmt
  • just clippy
  • just test -p commonware-revm

Non-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.

@clabby
Copy link
Collaborator

clabby commented Dec 13, 2025

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 revm::Database implementation that's backed by commonware_storage::qmdb.

@patrick-ogrady
Copy link
Contributor

Would be really neat to add a revm::Database implementation that's backed by commonware_storage::qmdb.

This would be :100000000000:

@diegomrsantos diegomrsantos force-pushed the feat/revm-chain-example branch from 3c3680e to 20ed6d2 Compare December 14, 2025 23:07
@diegomrsantos diegomrsantos marked this pull request as draft December 14, 2025 23:22
@diegomrsantos
Copy link
Contributor Author

diegomrsantos commented Dec 15, 2025

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 revm::Database implementation that's backed by commonware_storage::qmdb.

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?

@diegomrsantos
Copy link
Contributor Author

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.

@diegomrsantos diegomrsantos marked this pull request as ready for review December 15, 2025 18:37
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.
@diegomrsantos diegomrsantos changed the title [examples/revm] Add REVM example on threshold-simplex REVM example: digest-first EVM execution with QMDB persistence Jan 20, 2026
@diegomrsantos diegomrsantos marked this pull request as ready for review January 20, 2026 19:51
@diegomrsantos
Copy link
Contributor Author

Given the goal of external state sync/proofs, I propose switching the example to an authenticated QMDB using the ordered current variant (so we can provide inclusion + exclusion proofs and complete range proofs).

I think that would be the most powerful example, although I've always believed you could get away with any: https://commonware.xyz/blogs/adb-any
In any case, I think it'll be easy to swap out the QMDB as needed (for different variants).

The benefit to using a simpler one at first is that we already have state sync done. current is more complex (and will come later).

Please let me know what you think about the current solution when you have time

@clabby
Copy link
Collaborator

clabby commented Jan 21, 2026

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 revm::Database implementation that's backed by commonware_storage::qmdb.

Thanks a ton for continuing on this work. I'll make sure to give this a review by the end of the week!

@diegomrsantos
Copy link
Contributor Author

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 revm::Database implementation that's backed by commonware_storage::qmdb.

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.

@patrick-ogrady
Copy link
Contributor

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 revm::Database implementation that's backed by commonware_storage::qmdb.

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 👀

@diegomrsantos
Copy link
Contributor Author

diegomrsantos commented Jan 21, 2026

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 revm::Database implementation that's backed by commonware_storage::qmdb.

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.

Copy link

@cursor cursor bot left a 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.

@diegomrsantos
Copy link
Contributor Author

diegomrsantos commented Jan 22, 2026

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:

  • REVM uses WrapDatabaseAsync to bridge async QMDB reads into a sync DatabaseRef.
  • WrapDatabaseAsync does block_in_place(|| handle.block_on(async_db_call)) on the current Tokio worker thread.
  • Our finalized path was running persist_snapshot on that same worker thread.

Why this crashes:

  • While block_on is active, Tokio keeps the runtime loop alive on that thread.
  • That means other ready tasks can be polled inside that block_on call.
  • If the finalize task runs there, persist_snapshot -> QMDB open_stores -> storage open ends up nested under the WrapDatabaseAsync stack.
  • The call stack gets very deep and can overflow (the crash shows up at validate_partition_name, but it’s just where the stack blows).

Fix:

  • Running persistence on the blocking pool (context.shared(true)) moves it to a dedicated blocking thread.
  • This isolates the heavy QMDB commit path from worker threads that might already be inside WrapDatabaseAsync.
  • With that change, the stack overflow disappears.

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.

@patrick-ogrady
Copy link
Contributor

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.

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.

3 participants