aere-research/research/specs/spec-parallel-execution.md
Aere Network 4a0b48588c Initial public release
Aere Network public source. Everything here can be checked against the live
chain (chain id 2800, https://rpc.aere.network).

Scope note, stated up front rather than buried: consensus on chain 2800 is
classical secp256k1 ECDSA QBFT. The post-quantum work in this repository is at
the signature, precompile, account and transport layers. Nothing here makes the
consensus post-quantum, and no document in it should be read as claiming so.
2026-07-20 01:02:30 +03:00

26 KiB

AERE Parallel Execution: Block-STM Executor and On-Chain Hint Registry

Technical specification. Chain 2800 (AERE Network mainnet).

Grounding: parallel-executor/ (Rust), rollup-sequencer/src/blockstm/ (TypeScript), contracts/contracts/parallel/AereBlockSTMRegistry.sol, contracts/contracts/parallel/AereRollupValidity.sol, contracts/deployments/block-stm-registry.json, and sdk-js/src/addresses.ts.


1. Executive summary and honest scope

AERE's parallel-execution work has three distinct pieces at three different levels of maturity. Stating them plainly up front, because the boundary between "shipped" and "roadmap" is the whole point of this document:

  1. A real, self-verifying Block-STM parallel executor (shipped, benchmarked). aere-block-stm is a from-scratch Rust implementation of the Block-STM optimistic-concurrency algorithm (Gelashvili et al., arXiv:2203.06871, 2022, the same design behind Aptos and the family Sui and Monad build on). It executes a batch of transactions across many CPU cores and commits a single keccak256 state root. Every run also executes the batch sequentially and refuses to emit a root unless the two results match, so a concurrency bug cannot silently produce a bad commitment. Correctness is proven over 6000 randomized adversarial comparisons with zero mismatches, and measured speedup on a 16-core box is roughly 8.6x to 9.3x on low and medium-conflict batches.

  2. An on-chain advisory registry (shipped, live). AereBlockSTMRegistry at 0x98E2C3e615841919d173D8FF642514c5902E8A28 is an opt-in registry where a contract publishes the storage-slot read/write hints for its own function selectors. It is advisory metadata for a scheduler, holds no funds, changes no execution semantics, and has no admin over anyone else.

  3. Moving parallelism into base consensus (roadmap, not built). AERE's Layer-1 base layer runs Hyperledger Besu and executes transactions sequentially. Nothing here changes that. Making Besu execute in parallel is a fork of a Java execution client, a multi-month effort, and is explicitly out of scope for what exists today. The registry is metadata for that eventual fork; the concrete parallel path that runs today lives in the rollup layer, off-chain in the sequencer.

The honest one-line framing, taken verbatim from the executor README, is: "Real Block-STM parallel executor, wired into the AERE rollup sequencer; the L1 base layer stays Besu-sequential."

Everything in Section 2 through Section 5 is code that exists and runs. Section 6 (base-consensus promotion) and the fraud-proof / full-EVM items in Section 7 are roadmap and are labeled as such.


2. The parallel executor (aere-block-stm)

The executor is a standalone Rust crate under parallel-executor/ with zero external dependencies: the scheduler, multi-version memory, PRNG and keccak256 are all built on std. The build is fully offline and reproducible.

2.1 State and transaction model

The state model is intentionally EVM-shaped but minimal (src/types.rs):

  • Key::Balance(account): the native-token balance of an account.
  • Key::Storage(contract, slot): a single key/value contract storage slot.

Values are u128; an absent key reads as 0. A transaction version is (txn_index, incarnation), where incarnation counts how many times a transaction has been re-executed after an abort.

The VM understands four transaction kinds (TxKind), each chosen because its control flow or its written values depend on what it reads, so a stale read is silently wrong unless caught:

  • Transfer {from, to, amount}: move amount if from can afford it. Control flow depends on reading the sender balance.
  • Sweep {from, to}: move the entire balance of from. The written values depend directly on the read balance, a strong serializability probe.
  • Increment {contract, slot}: storage[slot] += 1. When many transactions hit the same slot this is the classic all-conflict counter: the sequential result is start + count, and a correct parallel executor must reproduce exactly that despite reordering.
  • AmmSwap {contract, x_slot, y_slot, dx}: a constant-product swap against a two-slot pool, modelling a hot DeFi pool that conflicts across concurrent swaps.

Each Txn also carries a gas field, a simulated per-transaction CPU cost measured in keccak rounds. It does not affect the resulting state (correctness is independent of gas); it exists so benchmarks charge each transaction a realistic amount of interpreter-like work instead of measuring only scheduling overhead. This is discussed honestly in Section 4.

2.2 One transaction semantics, two drivers

The single most important design decision is in src/vm.rs: there is exactly one implementation of what a transaction does, execute_txn, and it runs against an abstract VmView trait with two methods, read(key) and write(key, value). All arithmetic is saturating so both paths behave identically and never panic on overflow.

  • The sequential oracle (src/sequential.rs) drives execute_txn with a view backed directly by a live HashMap state, applying transactions in strict index order. This is the definition of the correct answer.
  • The parallel executor (src/parallel.rs) drives the same execute_txn with a view backed by multi-version memory that records the read set and can signal an abort.

Because both paths share the exact same semantics function, the parallel result can differ from the sequential result only if the concurrency control is wrong. That is precisely the property the correctness harness (Section 3) exercises.

2.3 Multi-version memory (MVMemory)

src/mvmemory.rs keeps, for every Key, an ordered BTreeMap<txn_index, cell>, where a cell is either a concrete Value(incarnation, value) written by a transaction, or an Estimate placeholder (a marker that a lower-indexed transaction previously wrote here but is currently being re-executed, so its value is not trustworthy).

A read by transaction i for key k returns the entry of the highest transaction j < i that wrote k (versions.range(..txn_idx).next_back()). That is what makes speculative parallel execution serial-equivalent: transaction i always observes the writes of all lower-indexed transactions that have run, and nothing from higher ones. If the highest lower entry is an Estimate, the read returns Blocked(j) and the reader must abort and park on j.

Storage is sharded across 256 independent mutexes keyed by an FNV-1a hash of the Key, so non-conflicting transactions touch disjoint locks and run without contention. Hot keys (the shared counter, the AMM pool) serialize on one shard, which is the honest source of the high-conflict slowdown, not an implementation defect.

Three operations matter:

  • apply_write_set installs a completed incarnation's writes, deletes stale entries the previous incarnation wrote but this one did not, and returns whether the set of written keys changed (Block-STM calls this "wrote a new path" and uses it to trigger suffix revalidation).
  • mark_estimates converts an aborted transaction's writes into Estimate placeholders the instant it is validation-aborted, so any concurrent higher transaction that reads one of those keys blocks on it instead of consuming a soon-to-be-wrong value.
  • snapshot_final materializes the committed state once the block is fully executed and validated: base overlaid with, for each key, the value written by the highest transaction index that wrote a concrete value.

2.4 The collaborative scheduler

src/scheduler.rs implements the Block-STM collaborative scheduler (Algorithm 3 of the paper). It hands worker threads two kinds of task:

  • Execution tasks: run incarnation k of transaction i.
  • Validation tasks: re-check that an already-executed transaction's recorded read set is still valid against current MVMemory.

Two atomic cursors, execution_idx and validation_idx, drive it. Validation of a lower index always takes priority over execution of a higher one. When a transaction aborts (validation fails) or writes a new path, the relevant cursor is decreased so the affected range is revisited, and a decrease_cnt counter is bumped. The block is done only when both cursors have passed the end, no task is in flight, and no decrease happened during the check.

Dependencies are handled without condvars: when execution of i reads an Estimate from j, i is parked on j's dependency list and its task dropped; when j finishes it re-arms i (back to ReadyToExecute) and decreases execution_idx so a worker re-picks it. add_dependency holds the blocking transaction's status lock across the whole check-then-park critical section, so j cannot transition to Executed and drain its dependency list between the check and the push (which would lose the wakeup). try_validation_abort uses a compare-and-set on status so only one validator can abort a given incarnation.

The src/parallel.rs driver spawns num_threads workers, each looping next_task -> run -> feed result back -> repeat, until the scheduler reports Done. It tracks aggregate stats: total_executions (>= num_txns; the excess is re-executions), total_validations, and total_aborts.

2.5 keccak256 state root

src/keccak.rs is a from-scratch keccak256 (standard Keccak-f[1600], rate 1088 bits, original Keccak 0x01 .. 0x80 padding, i.e. keccak256 not SHA3-256), verified in tests against the known vectors for "" and "abc". state_root folds a materialized state into one 32-byte root by sorting non-zero entries into a canonical order (balances tagged 0x00, storage tagged 0x01, big-endian fields) and hashing. The canonical sort makes the root deterministic and independent of hashmap iteration order, which is exactly what a rollup state commitment requires, and it is EVM-compatible with zero external crates.


3. Correctness guarantee (proven, not asserted)

The whole point of Block-STM is that the parallel result must equal sequential execution. The harness (aere-block-stm harness, in src/main.rs) proves it empirically.

It runs 6 workload profiles x 200 randomized batches x {1,2,4,8,16} threads = 6000 comparisons (1200 distinct randomized batches, each compared at five thread counts) and asserts the Block-STM committed state equals the sequential oracle for every single case. The profiles include deliberately adversarial fully-conflicting cases: all-hot (400 transactions, 200 hot accounts, conflict probability 1.0) and brutal-single-counter (300 transactions on 64 accounts, one contract, one slot, conflict probability 0.9). Reported result:

  total (batch x threads) comparisons: 6000
  mismatches            : 0
  RESULT: PASS -- parallel Block-STM == sequential on ALL cases

Workloads are generated by a seedable SplitMix64 PRNG (src/workload.rs) with no external RNG crate, so any failing case can be replayed exactly. The default batch mix is 60% transfers, 15% sweeps, 15% shared-counter increments, and 10% AMM swaps.

cargo test --release adds targeted unit checks:

  • shared_counter_is_serialized: 500 transactions all incrementing one slot yield exactly 500 at 1, 2, 4, 8, and 16 threads (naive parallelism would lose updates).
  • parallel_equals_sequential_random: 40 seeds x 4 thread counts.
  • keccak_known_answer: keccak256 of "" and "abc" match the standard vectors.

4. Measured speedup (honest numbers)

Measured on the AERE infra box: 16 physical cores, Linux 6.8, Rust 1.96. 20,000 transactions per batch, each carrying a simulated EVM execution cost of gas = 80 keccak-rounds. Speedup is versus the single-threaded sequential oracle, median of 7 runs. Reproduce with cargo run --release -- bench.

Workload 2 cores 4 cores 8 cores 16 cores abort % @16
Low-conflict (~2% hot-touch) 1.78x 3.22x 5.71x 8.59x 0.3%
Medium-conflict (~15%) 1.87x 3.54x 6.24x 9.31x 1.9%
High-conflict (single hot counter, 15% of txns) 1.70x 3.04x 4.87x 4.62x 24%
Pathological (every tx hits ONE slot) 0.85x 0.85x 0.82x 0.73x 19%

The "8 to 10x" headline is the low and medium-conflict range (8.59x and 9.31x at 16 cores). The honest reading, including the cases that do not look good, is stated in the README and repeated here because it is a credibility asset:

  • Low and medium-conflict batches scale nearly linearly.
  • The high-conflict batch still reaches roughly 4.6x because only about 15% of its transactions touch the single hot slot; the rest are independent transfers.
  • The pathological batch (every transaction increments the same slot) has zero available parallelism. Block-STM correctly serializes it and is actually slower than sequential (0.73x) because it pays for aborted speculation. This is the expected worst case: no correct parallel executor can beat sequential when there is nothing to parallelize.
  • There is also a floor case: if transactions do near-zero compute (gas = 0), concurrency-control overhead dominates and the parallel executor is slower than sequential. Real EVM transactions are compute-heavy (opcode interpretation, keccak, ecrecover), which is the regime Block-STM targets, which is why the benchmark charges a realistic per-transaction cost. Run bench 0 to see the floor and bench 200 for heavier transactions.

These figures are single-machine microbenchmarks of the executor. They are not a network throughput claim, not a TPS figure, and say nothing about AERE L1, whose base layer remains Besu-sequential.


5. Integration into the rollup sequencer

The concrete place this executor runs today is AERE's rollup layer, off-chain in a sequencer.

5.1 The wrapper

rollup-sequencer/src/blockstm/BlockSTMExecutor.ts exposes:

executeBatch(base: Map<StateKey, bigint>, txns: RollupTx[], opts?) : BlockSTMResult

It has no npm dependencies (only Node's built-in child_process). It serializes the prior L2 state slice and the ordered batch into the executor's line protocol and shells out to the aere-block-stm execute subcommand, which runs the batch across cores, computes the new state, and returns a keccak256 state root as JSON. The execute path runs the batch both in parallel and sequentially and sets parallelEqualsSequential; the binary exits non-zero and the wrapper throws if they diverge, so a bad root can never reach the chain. BlockSTMResult carries { stateRoot, numTxns, threads, executions, validations, aborts, parallelEqualsSequential, state }.

The execute stdin wire protocol (src/main.rs) is one record per line: THREADS n, GAS n, BASE <key> <value> (key is B:<acct> or S:<contract>:<slot>), then TX TRANSFER|SWEEP|INC|AMM .... Output is a single JSON object with the committed root and self-check.

5.2 The on-chain commitment flow

The rollup layer is AERE's Rollup-as-a-Service stack. The canonical factory is AereRaaSFactoryV2 at 0xB7F8c754AC3155197d76f01857172bBd5a5F39Ab (bond token WAERE 0x7e84d7d66d5da4cfE46Da67CDEeB05B323e1f5e8, sink AereSink 0x69581B86A48161b067Ff4E01544780625B231676), which deploys a fixed AereRollupSettlementV2 instance per registered rollup. That per-rollup settlement contract, not a single global address, is where a sequencer proposes roots. Its call is proposeStateRoot(uint256 epoch, bytes32 root, bytes32 l2BlockHash), callable only by the rollup's sequencer, with epoch == latestEpoch + 1, followed by an optimistic challenge window.

The flow is therefore:

batch of L2 txns -> BlockSTMExecutor.executeBatch() -> { stateRoot, ... }
                                                          |
                                                          v
                    settlement.proposeStateRoot(epoch, stateRoot, l2BlockHash)

Because the executor guarantees the parallel result equals sequential execution, the committed root is deterministic and reproducible, which is what makes it safe as an optimistic-rollup commitment: a Phase-2 fraud proof would re-derive it by re-running the same deterministic executor over the disputed batch. See rollup-sequencer/src/blockstm/example.ts for an end-to-end run.

5.3 Integration status (honest)

  • Executor: real Block-STM, correctness-proven, benchmarked. Shipped.
  • Integration: the execute JSON boundary is live and tested; the TypeScript wrapper drives it and returns a root ready for proposeStateRoot. Shipped as the batch-execution core.
  • Not yet built: the wiring from a live L2 mempool into executeBatch (this module is the batch-execution core the full sequencer loop plugs into, not a running production sequencer), and an on-chain optimistic fraud-proof verifier (settlement is optimistic Phase 1 today). These are roadmap.

6. The on-chain hint registry: AereBlockSTMRegistry

6.1 Purpose

For Block-STM to schedule efficiently it benefits from hints about which storage slots a transaction is likely to read or write; a good hint reduces the abort rate. Solidity does not expose this metadata. AereBlockSTMRegistry (contracts/contracts/parallel/AereBlockSTMRegistry.sol) is an opt-in registry where a contract publishes its likely read/write set at the function-selector grain, so an eventual scheduler can consult it. It is advisory only: hints cannot change execution semantics, and a scheduler always re-validates the actual read/write set after execution. A wrong hint costs at most one extra abort; it can never make a transaction succeed when it should fail.

6.2 Data model and API

  • SlotHint { bytes32 slotPrefix; Mode mode; } where Mode is READ, WRITE, or READWRITE.
  • HintSet { address publisher; SlotHint[] hints; uint64 publishedAt; }, stored per (target, selector) key keccak256(abi.encodePacked(target, selector)).

Functions:

  • publish(address target, bytes4 selector, SlotHint[] hints): publish or replace the hint set. It caps hints at 64 (TooManyHints above that) and replaces rather than appends.
  • clear(address target, bytes4 selector): clear the set; publisher only.
  • hintsFor(target, selector) -> (publisher, hints, publishedAt): the view a scheduler reads.
  • publisherOf(target, selector) -> address.

6.3 Access control (audit fix HIGH #27)

The first publisher for a (target, selector) must be the target itself or the target's Ownable owner, checked best-effort via a staticcall to owner() (_tryOwner). This closes a front-run squatting vector: without it, an attacker could publish poisoned hints for a victim contract's selector before the real owner did. After the first publisher claims a key, only that publisher can update or clear it (NotPublisher otherwise). There is no admin override and no Foundation gate: the contract author knows its own access pattern best. The registry test (contracts/test/block-stm-registry.test.js) exercises the claim rule, the replace-not-append behavior, the 64-hint cap, and publisher-only clear.

6.4 Deployment facts (live)

  • Address: AereBlockSTMRegistry 0x98E2C3e615841919d173D8FF642514c5902E8A28 (roadmap item #33).
  • Deploy tx: 0x5646445f5a6c8a3d9d06db7a6becd95f6227608b64291319fd19d7d928beee72, block 8877969.
  • Deployer: 0xbeB33D20dFBBD49eC7AC1F617667f1f02dfd6465. Code size 1753 bytes. Deployed 2026-07-10.
  • Read-verified at deploy: hintsFor(unpublished) returns (0x0, [], 0) and publisherOf(unpublished) returns the zero address.
  • No owner, no admin, holds no funds.

6.5 What the registry does NOT do (honest scope)

The AERE L1 (Besu) executes sequentially today. The registry is metadata for the eventual Block-STM Besu fork; it does not make L1 parallel, and publishing hints has no effect on how any transaction executes on mainnet right now. Its hintsFor(target, selector) view is intentionally shaped to be the interface a future consensus precompile (planned at 0x103 in the Phase-2 Besu fork) would read. That precompile and fork do not exist. The contract is deliberately inert-but-ready: real, correct metadata storage today, waiting for a consumer that is roadmap.


There is a second, stronger settlement path already anchored on-chain for the exact same executor. AereRollupValidity at 0x38772063572DF94E90351e44ccbBEefD5F497fbd verifies SP1 zkVM Groth16 validity proofs that AERE's deterministic rollup executor transitioned a prior state root to a new one by applying a specific batch. Its PROGRAM_VKEY binds the SP1 verification key of the guest ELF that ports execute_txn, keccak256 and state-root folding verbatim from parallel-executor/src/{vm,keccak}.rs, so the proven computation is byte-identical to what the production aere-block-stm binary self-checks and what BlockSTMExecutor commits. Public values are exactly 160 bytes: abi.encode(chainId, prevRoot, postRoot, batchHash, numTxns). Submission is permissionless; an epoch is final the instant its proof verifies (no challenge window), gated by a prevRoot == latestRoot continuity check. It routes through the SP1 gateway SP1VerifierGateway 0x9ca479C8c52C0EbB4599319a36a5a017BCC70628.

Per the SDK registry, epoch 0 has already been recorded from a real proof (submitEpoch 0xd4f8d858...12a4b0), advancing latestRoot to 0x256fa277...25e3ac with epochCount = 1, under an immutable vkey 0x00c39737...84ad (these three values are cited in the abbreviated form they appear in sdk-js/src/addresses.ts).

Honest scope, stated in the contract itself: this proves the state transition of AERE's current rollup executor, whose VM implements a bounded set of transaction kinds (Transfer, Sweep, Increment, AmmSwap) over a balance/storage map. It is a real validity proof of that executor. It is not a proof of arbitrary EVM bytecode. The path to a full validity rollup is replacing the executor's VM with a real EVM (revm) inside the zkVM, a separate multi-quarter effort. No claim built on this contract should imply it proves a full EVM.


8. Roadmap: moving parallelism into base consensus

What exists today is a proven off-chain executor plus a live on-chain hint registry. Promoting Block-STM into AERE's base consensus is the roadmap, and it needs work that is not done:

  1. The Besu execution-client fork (the hard blocker). AERE L1 is Hyperledger Besu, a Java client that executes block transactions sequentially. Running Block-STM at L1 requires forking Besu's transaction-processing pipeline to execute speculatively with multi-version memory, validation, and abort/retry, then reconcile with QBFT block production. This is a multi-month Java engineering effort and is not mainnet-active. A first prototype now exists and is benchmarked: a parallel state-root commit was added to the forked Besu client's Bonsai world-state (aerenew/parallel/, parallel_commit.patch), self-checked to produce a commit root bit-identical to the sequential-commit root across independent-transfer, commit-heavy SSTORE, deletion, and mixed workloads. The honest finding is a boundary, not a win: the commit phase is allocation and memory-bandwidth bound, so adding commit worker threads (widths 2 to 16) is slower than a single worker, not faster. The improvement that does hold is a single-threaded commit rewrite that is roughly 2x cheaper than the stock parallel-trie commit (for example, on medium tries about 316.98 ms stock versus 142.38 ms at width 1); end-to-end, Block-STM execution plus the width-1 commit gives about 1.27x to 1.42x on commit-heavy load versus stock sequential exec plus sequential commit, and the real execution-side speedup remains the ~8.6x to 9.3x of Section 4. A network throughput win is gated on real execution-heavy load. This is a research/benchmark control surface (a process-global flag), not a mainnet-active feature; until the full pipeline lands, the registry's 0x103 precompile consumer does not exist and hints have no runtime effect on L1.

  2. Fraud proofs for the optimistic rollup path. The per-rollup AereRollupSettlementV2 settlement is optimistic Phase 1: propose plus challenge window, no on-chain fraud-proof verifier yet. The deterministic executor makes a fraud proof constructible (re-run the same binary over the disputed batch), but the on-chain verifier is not built.

  3. Full-EVM validity. AereRollupValidity proves the bounded four-kind VM, not arbitrary EVM. A full validity rollup needs a real EVM inside the zkVM.

  4. Production sequencer loop. The live-mempool -> executeBatch -> proposeStateRoot loop is not wired end-to-end; today the module is the batch-execution core, not a running sequencer.


9. Honest limitations summary

  • The AERE L1 mainnet executes sequentially on Besu. No parallel execution runs at L1 today, and the hint registry, while live, has no L1 consumer yet.
  • The parallel executor's VM is a bounded four-kind model (Transfer, Sweep, Increment, AmmSwap) over a balance/storage map, not a full EVM. Its correctness proofs and validity proofs are proofs of that executor.
  • The measured speedup figures are single-machine microbenchmarks (16 cores, gas=80), not a network throughput or TPS claim.
  • The rollup settlement path is optimistic Phase 1 with no on-chain fraud proof yet; the validity-proof anchor covers only the bounded VM.
  • Broader chain caveats apply and are not hidden: AERE runs seven validators under one operator, a single client, and has had no external audit; usage is thin. The self-verifying executor and the advisory registry are real and proven, but they sit on that foundation.

10. Address reference (verbatim, chain 2800)

Contract / entity Address
AereBlockSTMRegistry 0x98E2C3e615841919d173D8FF642514c5902E8A28
AereRollupValidity 0x38772063572DF94E90351e44ccbBEefD5F497fbd
AereRaaSFactoryV2 0xB7F8c754AC3155197d76f01857172bBd5a5F39Ab
AereSink 0x69581B86A48161b067Ff4E01544780625B231676
WAERE (RaaS bond token) 0x7e84d7d66d5da4cfE46Da67CDEeB05B323e1f5e8
SP1VerifierGateway 0x9ca479C8c52C0EbB4599319a36a5a017BCC70628
Foundation (owner of Ownable contracts) 0x0243A4f47D44b40b65D33f20329dE20D00c6f3C3
Deployer (registry / validity) 0xbeB33D20dFBBD49eC7AC1F617667f1f02dfd6465

AereRollupSettlementV2 instances are deployed per rollup by AereRaaSFactoryV2, so no single global settlement address exists; the sequencer commits to the per-rollup instance's proposeStateRoot.