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.
This commit is contained in:
parent
4a0b48588c
commit
37f55e4f71
27
README.md
27
README.md
@ -7,11 +7,36 @@ This repository is the "prove it yourself" half of the Aere Network verify-yours
|
||||
a third party re-run the machine-checked proofs, re-derive the cryptographic reference vectors, and
|
||||
read the research that underpins the protocol, without trusting any claim on faith.
|
||||
|
||||
## Scope, stated up front
|
||||
|
||||
Aere Network runs post-quantum signature verification natively on mainnet: Falcon-512 (`0x0AE1`),
|
||||
Falcon-1024 (`0x0AE2`), ML-DSA-44 (`0x0AE3`), SLH-DSA-128s (`0x0AE4`) and SHAKE256 (`0x0AE5`)
|
||||
have been live as precompiles since block 9,189,161. You can call them yourself against
|
||||
`https://rpc.aere.network` without asking us for anything.
|
||||
|
||||
**Consensus on chain 2800 is classical secp256k1 ECDSA QBFT.** The post-quantum work lives at the
|
||||
signature, precompile, account and transport layers. Nothing in this repository makes the consensus
|
||||
post-quantum, and nothing in it should be read as claiming so. Where you see post-quantum consensus
|
||||
discussed, it is research about what a future activation could look like, not a description of the
|
||||
running chain.
|
||||
|
||||
Two further limits worth knowing before you judge anything else here:
|
||||
|
||||
- `0x0AE6` (ML-KEM-768) and `0x0AE7` (Falcon HashToPoint) are **testnet only**. They are not
|
||||
active on mainnet.
|
||||
- The on-chain zero-knowledge verifiers are classical BN254. They are broken by Shor's algorithm
|
||||
like any other elliptic-curve construction, and we do not describe them as quantum-safe.
|
||||
|
||||
The network is operated by seven Foundation-run validators, so its Nakamoto coefficient is
|
||||
effectively one today. That is a real limitation, it is on the roadmap, and it is not fixed by any
|
||||
code in this repository.
|
||||
|
||||
## Layout
|
||||
|
||||
- `formal-consensus/` Z3 / SMT models of consensus and contract safety properties (30 Python
|
||||
models plus a Quint spec `FalconQuorum.qnt`), driven by `run_consensus_verification.py`. These
|
||||
cover QBFT safety and liveness, hybrid dual-quorum post-quantum activation, and the money-contract
|
||||
cover QBFT safety and liveness, the money-contract invariants, and a PROPOSED hybrid
|
||||
dual-quorum activation that is modelled research and is NOT running on chain 2800, and the
|
||||
invariants. `CONSENSUS-VERIFICATION-2026-07-12.md` documents the results.
|
||||
- `pq-stark/` reference implementations and self-tests for the STARK verifier port: BabyBear field,
|
||||
Poseidon2, MMCS, FRI, the Fiat-Shamir challenger, and AIR quotient logic. Each component ships a
|
||||
|
||||
@ -220,7 +220,13 @@ def _kpke_encrypt(ek, m, r):
|
||||
t_hat = [_byte_decode(ek[384 * i:384 * (i + 1)], 12) for i in range(K)]
|
||||
rho = ek[384 * K:384 * K + 32]
|
||||
|
||||
a_hat = [[_sample_ntt(_XOF(rho, i, j)) for j in range(K)] for i in range(K)]
|
||||
# FIPS 203 absorbs rho || j || i to produce A_hat[i][j], so the array built
|
||||
# here from _XOF(rho, i, j) is already A_hat TRANSPOSED. Encrypt wants
|
||||
# A_hat^T . y, so it is indexed [i][j] directly below with no second
|
||||
# transpose. Getting this backwards still yields a correct shared secret K
|
||||
# and a correct v, and corrupts only c1, which is why the shared-secret
|
||||
# check alone is not sufficient evidence.
|
||||
a_hat_t = [[_sample_ntt(_XOF(rho, i, j)) for j in range(K)] for i in range(K)]
|
||||
|
||||
y = [_ntt(_sample_poly_cbd(_PRF(ETA1, r, i), ETA1)) for i in range(K)]
|
||||
e1 = [_sample_poly_cbd(_PRF(ETA2, r, K + i), ETA2) for i in range(K)]
|
||||
@ -231,7 +237,7 @@ def _kpke_encrypt(ek, m, r):
|
||||
for i in range(K):
|
||||
acc = [0] * N
|
||||
for j in range(K):
|
||||
acc = _add(acc, _multiply_ntts(a_hat[j][i], y[j]))
|
||||
acc = _add(acc, _multiply_ntts(a_hat_t[i][j], y[j]))
|
||||
u.append(_add(_ntt_inv(acc), e1[i]))
|
||||
|
||||
mu = _decompress(_byte_decode(m, 1), 1)
|
||||
|
||||
325
parallel-executor/BLOCKSTM-ENDTOEND-THROUGHPUT-2026-07-13.md
Normal file
325
parallel-executor/BLOCKSTM-ENDTOEND-THROUGHPUT-2026-07-13.md
Normal file
@ -0,0 +1,325 @@
|
||||
# Block-STM end-to-end block-production throughput (measured)
|
||||
|
||||
**Date:** 2026-07-13
|
||||
**Module:** `parallel-executor/` (the from-scratch Rust Block-STM executor)
|
||||
**Scope:** ISOLATED benchmark on a throwaway cloud box. Nothing here touches live
|
||||
chain 2800, its validators, or any production node.
|
||||
This does **not** make Block-STM mainnet-active; that remains a founder-supervised
|
||||
fork activation. The site's honest framing ("testnet demonstration, not
|
||||
mainnet-active; execution-phase win") stays intact. This report only **adds a
|
||||
real, measured end-to-end number** to that framing.
|
||||
|
||||
---
|
||||
|
||||
## 0. Why this exists (the gap being closed)
|
||||
|
||||
The existing `bench` command times **only** the Block-STM execution phase and
|
||||
reports the documented near-linear execution-phase scaling
|
||||
(~1.94x / 3.61x / 6.19x / 9.38x at 2/4/8/16 workers). That is an honest
|
||||
execution-phase number, but it is **not** an end-to-end block-throughput number.
|
||||
A block is not finished when execution finishes; it is finished when the state is
|
||||
**committed** and a **state root** is produced. Those tail phases are serial, so
|
||||
by Amdahl's law the end-to-end speedup must be **lower** than the execution-phase
|
||||
speedup. The site states exactly this gap:
|
||||
|
||||
> "this is an execution-phase win; an end-to-end throughput win still needs real
|
||||
> execution-heavy load plus a parallel commit."
|
||||
|
||||
This report closes that gap with a real measurement: the **full pipeline**
|
||||
(parallel execution + commit + state-root), parallel vs sequential, at 1/2/4/8/16
|
||||
workers, across conflict ratios, with serial-equivalence checked on every run.
|
||||
|
||||
New reproducible harness: `aere-block-stm endtoend [gas] [ntx] [reps]`
|
||||
(added to `src/main.rs`; phase timing exposed via the additive
|
||||
`execute_block_parallel_split` in `src/parallel.rs`).
|
||||
|
||||
---
|
||||
|
||||
## 1. Environment (honest)
|
||||
|
||||
| | |
|
||||
|---|---|
|
||||
| Box | Throwaway cloud VM, 16 vCPU, 32 GB, Ubuntu 24.04, US region |
|
||||
| CPU | AMD EPYC-Rome, 16 cores, **Thread(s)/core = 1** (no in-guest SMT), 2.0 GHz |
|
||||
| Kernel / toolchain | Linux 6.8.0, rustc 1.97.0, release build (`lto=true`, `codegen-units=1`) |
|
||||
| Firewall | dedicated scratch firewall, SSH restricted to operator addresses |
|
||||
| Lifecycle | provisioned for this benchmark, **DELETED at end** (see §9) |
|
||||
|
||||
**Caveat, stated up front:** this is a **shared-vCPU** cloud box, not the
|
||||
dedicated-core infra machine the original 9.38x figure was taken on. Shared vCPU
|
||||
adds run-to-run variance to **absolute** timings. Mitigations: 16 single-threaded
|
||||
vCPUs (so worker count maps to real cores with no hyperthread muddying), median of
|
||||
7 runs, and the fact that the **headline deliverable is a ratio** (end-to-end vs
|
||||
execution-phase speedup, and the serial fraction) in which shared-vCPU noise
|
||||
affects numerator and denominator together and largely cancels.
|
||||
|
||||
---
|
||||
|
||||
## 2. Methodology
|
||||
|
||||
**Pipeline, timed in three phases** (see `endtoend()` in `src/main.rs`):
|
||||
|
||||
1. **exec** - Block-STM parallel execution + validation + re-execution. PARALLEL;
|
||||
this is the part that scales with worker count. Timed inside
|
||||
`execute_block_parallel_split` between thread spawn and join.
|
||||
2. **commit** - `MvMemory::snapshot_final`: materialize the multi-version memory
|
||||
into the flat committed `StateMap`. SERIAL (single-threaded, after join).
|
||||
3. **root** - `keccak::state_root`: keccak256 over the sorted committed state, the
|
||||
on-chain rollup commitment. SERIAL.
|
||||
|
||||
`end-to-end = exec + commit + root`. The **sequential baseline** is
|
||||
`execute_block_sequential` (which produces the live map directly, so it has no
|
||||
separate commit step) `+ state_root`. The state-root cost is identical serial work
|
||||
in both numerator and denominator, i.e. a pure Amdahl serial tail.
|
||||
|
||||
**Workload (execution-heavy, not empty blocks).** The deterministic SplitMix64
|
||||
generator emits a realistic EVM-style mix per batch: **60% ERC-20 transfers**
|
||||
(two balance touches), **15% sweeps** (value-dependent), **15% shared-counter
|
||||
increments** (the serializability stressor), **10% AMM swaps** (two-slot pool
|
||||
conflict). Each transaction also pays a deterministic simulated EVM cost of `gas`
|
||||
keccak-rounds (a stand-in for opcode/keccak/ecrecover interpreter time), so the
|
||||
executor is timed doing real per-tx work rather than pure scheduling overhead.
|
||||
|
||||
**Conflict ratios.** `conflict_ppm` tunes how often a transaction touches the
|
||||
shared hot set. Four points span the spectrum: **0%** (disjoint accounts, 64
|
||||
contracts), **10%**, **50%**, and a **pathological ~100%** single-hot-slot floor.
|
||||
The labelled % is the *designed* hot-touch rate; the honest *realized* conflict
|
||||
indicator is the **measured abort%** in each table.
|
||||
|
||||
**Defaults:** `gas=80`, `ntx=20000`, median of 7 (chosen to match the regime the
|
||||
documented ~9.38x execution-phase figure was taken in, so the two are directly
|
||||
comparable). A gas sweep (§6) shows sensitivity.
|
||||
|
||||
**Serial-equivalence, checked every run.** On every single parallel run the harness
|
||||
asserts the committed state equals the sequential oracle **and** the two keccak
|
||||
state roots are bit-identical. A single mismatch aborts the whole run. This is the
|
||||
property that makes the parallel root a valid commitment.
|
||||
|
||||
---
|
||||
|
||||
## 3. Serial-equivalence confirmation (the non-negotiable gate)
|
||||
|
||||
| Check | Result |
|
||||
|---|---|
|
||||
| `cargo test --release` (7 tests incl. shared-counter serialization, 40-seed random equivalence, keccak KAT) | **7/7 PASS** |
|
||||
| Correctness harness: 6 profiles x 200 batches x {1,2,4,8,16} threads | **6000 comparisons, 0 mismatches** |
|
||||
| End-to-end harness inline check (state == oracle AND roots identical), every run | **140 checks, 0 mismatches** |
|
||||
|
||||
`RESULT: PASS -- every parallel run committed the exact sequential state + root.`
|
||||
The `04-endtoend.stderr` log is empty (zero mismatch lines). Raw logs in
|
||||
`endtoend-logs-2026-07-13/`.
|
||||
|
||||
---
|
||||
|
||||
## 4. Execution-phase baseline on THIS box (for comparison)
|
||||
|
||||
Running the existing `bench` (execution-phase only) on the same box reproduces the
|
||||
documented range, confirming the box is a faithful stand-in:
|
||||
|
||||
| Workload | 2 | 4 | 8 | **16** (vs sequential) |
|
||||
|---|---|---|---|---|
|
||||
| Low-conflict (~2%) | 1.74x | 3.24x | 5.68x | **8.30x** |
|
||||
| Medium-conflict (~15%) | 1.82x | 3.48x | 6.18x | **9.62x** |
|
||||
| High-conflict (~80%) | 1.74x | 3.10x | 4.96x | **5.13x** |
|
||||
| Pathological (all one slot) | 0.93x | 0.86x | 0.82x | **0.76x** |
|
||||
|
||||
(Medium-conflict vs-1-thread: 1.94x / 3.71x / 6.59x / 10.26x, matching the
|
||||
documented 1.94x/3.61x/6.19x/9.38x.) These are **execution-phase** numbers.
|
||||
|
||||
---
|
||||
|
||||
## 5. END-TO-END RESULTS (the deliverable)
|
||||
|
||||
`gas=80`, `ntx=20000`, median of 7. `execSpd` = execution-phase speedup;
|
||||
`e2eSpd` = **end-to-end** speedup (exec+commit+root) vs the sequential end-to-end
|
||||
baseline. TPS is real transactions/second of the full pipeline. All rows PASS
|
||||
serial-equivalence.
|
||||
|
||||
### 0% designed conflict - 49,637 committed state keys (largest state)
|
||||
Sequential end-to-end: 672.84 ms (root-share 1.4%), 29,725 TPS.
|
||||
|
||||
| W | exec ms | commit ms | root ms | e2e ms | execSpd | **e2eSpd** | **TPS** | abort% | (commit+root)/e2e |
|
||||
|--:|--:|--:|--:|--:|--:|--:|--:|--:|--:|
|
||||
| 1 | 705.51 | 9.01 | 12.52 | 727.04 | 0.94x | 0.93x | 27,509 | 0.0% | 3.0% |
|
||||
| 2 | 354.89 | 9.56 | 11.41 | 375.85 | 1.87x | 1.79x | 53,212 | 0.0% | 5.6% |
|
||||
| 4 | 184.02 | 9.72 | 12.37 | 206.11 | 3.60x | 3.26x | 97,037 | 0.1% | 10.7% |
|
||||
| 8 | 97.99 | 8.52 | 8.31 | 114.82 | 6.77x | 5.86x | 174,191 | 0.1% | 14.7% |
|
||||
| **16** | 64.23 | 8.08 | 8.51 | 80.83 | **10.32x** | **8.32x** | **247,444** | 0.3% | 20.5% |
|
||||
|
||||
Amdahl serial tail (commit+root) = 15.33 ms; e2e ceiling 8.27x @16w, 43.88x @inf cores.
|
||||
|
||||
### 10% designed conflict - 18,342 keys
|
||||
Sequential end-to-end: 672.71 ms, 29,731 TPS.
|
||||
|
||||
| W | exec ms | commit ms | root ms | e2e ms | execSpd | **e2eSpd** | **TPS** | abort% | tail% |
|
||||
|--:|--:|--:|--:|--:|--:|--:|--:|--:|--:|
|
||||
| 1 | 699.73 | 5.15 | 4.77 | 709.65 | 0.96x | 0.95x | 28,183 | 0.0% | 1.4% |
|
||||
| 2 | 366.73 | 5.31 | 4.38 | 376.42 | 1.83x | 1.79x | 53,133 | 0.1% | 2.6% |
|
||||
| 4 | 187.09 | 5.15 | 3.88 | 196.13 | 3.58x | 3.43x | 101,975 | 0.3% | 4.6% |
|
||||
| 8 | 96.64 | 5.06 | 4.29 | 105.99 | 6.93x | 6.35x | 188,699 | 0.7% | 8.8% |
|
||||
| **16** | 66.57 | 4.37 | 3.20 | 74.14 | **10.06x** | **9.07x** | **269,750** | 1.2% | 10.2% |
|
||||
|
||||
Amdahl serial tail = 6.68 ms; e2e ceiling 9.64x @16w, 100.71x @inf.
|
||||
|
||||
### 50% designed conflict - 9,006 keys
|
||||
Sequential end-to-end: 699.97 ms, 28,573 TPS.
|
||||
|
||||
| W | exec ms | commit ms | root ms | e2e ms | execSpd | **e2eSpd** | **TPS** | abort% | tail% |
|
||||
|--:|--:|--:|--:|--:|--:|--:|--:|--:|--:|
|
||||
| 1 | 712.46 | 2.63 | 1.96 | 717.06 | 0.98x | 0.98x | 27,892 | 0.0% | 0.6% |
|
||||
| 2 | 367.61 | 2.46 | 2.01 | 372.07 | 1.90x | 1.88x | 53,753 | 2.1% | 1.2% |
|
||||
| 4 | 195.00 | 2.58 | 1.98 | 199.56 | 3.58x | 3.51x | 100,222 | 5.7% | 2.3% |
|
||||
| 8 | 113.28 | 2.50 | 1.72 | 117.51 | 6.17x | 5.96x | 170,202 | 10.7% | 3.6% |
|
||||
| **16** | 74.60 | 2.35 | 1.59 | 78.53 | **9.36x** | **8.91x** | **254,686** | 16.6% | 5.0% |
|
||||
|
||||
Amdahl serial tail = 4.15 ms; e2e ceiling 8.73x @16w, 168.85x @inf.
|
||||
|
||||
### Pathological ~100% conflict - 1,996 keys (honest floor)
|
||||
Sequential end-to-end: 664.65 ms, 30,091 TPS.
|
||||
|
||||
| W | exec ms | commit ms | root ms | e2e ms | execSpd | **e2eSpd** | **TPS** | abort% |
|
||||
|--:|--:|--:|--:|--:|--:|--:|--:|--:|
|
||||
| 1 | 682.90 | 0.06 | 0.31 | 683.28 | 0.97x | 0.97x | 29,271 | 0.0% |
|
||||
| 2 | 378.02 | 0.04 | 0.37 | 378.42 | 1.76x | 1.76x | 52,851 | 6.5% |
|
||||
| 4 | 213.51 | 0.04 | 0.38 | 213.93 | 3.11x | 3.11x | 93,488 | 14.5% |
|
||||
| 8 | 133.61 | 0.04 | 0.30 | 133.95 | 4.96x | 4.96x | 149,314 | 21.2% |
|
||||
| **16** | 135.50 | 0.04 | 0.40 | 135.94 | **4.90x** | **4.89x** | **147,125** | 25.0% |
|
||||
|
||||
(Here the state is tiny, so commit+root is negligible and e2e ~= exec; the limit is
|
||||
conflict, not the serial tail. 16 workers do not beat 8 under 25% aborts.)
|
||||
|
||||
---
|
||||
|
||||
## 6. The honest Amdahl boundary
|
||||
|
||||
**End-to-end speedup is consistently lower than execution-phase speedup.** At 16
|
||||
workers, on the realistic 0-50% conflict range:
|
||||
|
||||
| Conflict | execSpd @16 | **e2eSpd @16** | gap |
|
||||
|---|--:|--:|--:|
|
||||
| 0% | 10.32x | **8.32x** | -1.9x |
|
||||
| 10% | 10.06x | **9.07x** | -1.0x |
|
||||
| 50% | 9.36x | **8.91x** | -0.45x |
|
||||
|
||||
The gap is **largest at 0% conflict** and this is the important, non-obvious
|
||||
finding: the workload with the *most* execution parallelism (10.32x) has the
|
||||
*least* end-to-end benefit (8.32x), because it touches the *most* state (49,637
|
||||
keys), which makes the serial commit+root the largest fraction of the pipeline
|
||||
(20.5% of end-to-end time at 16 workers). More parallelizable execution buys a
|
||||
bigger, not smaller, serial-tail penalty, exactly the Amdahl trade.
|
||||
|
||||
**Serial fraction and ceiling.** With the commit+root tail fixed and only exec
|
||||
scaling, the end-to-end speedup saturates at `T_seq / (serial_tail + exec_floor)`.
|
||||
Measured 16-worker ceilings: **8.27x (0%), 9.64x (10%), 8.73x (50%)**. Even with
|
||||
infinite cores (exec -> 0) the flat-keccak tail alone caps end-to-end at
|
||||
44x / 101x / 169x respectively; the real cap is far lower because exec has its own
|
||||
floor (residual serialization + thread overhead) and, in the real client, the
|
||||
commit is much heavier (§7).
|
||||
|
||||
**Bottom line:** the honest end-to-end block-production speedup of this Rust
|
||||
Block-STM executor at 16 workers is roughly **8.3x to 9.1x** on realistic
|
||||
0-50%-conflict execution-heavy batches, versus the ~9.4-10.3x execution-phase
|
||||
figure on the same box. The end-to-end win is real and large, but it is
|
||||
**lower** than the execution-phase headline, precisely as Amdahl predicts.
|
||||
|
||||
---
|
||||
|
||||
## 7. Real end-to-end TPS, and why it does NOT reach or imply the 273k ceiling
|
||||
|
||||
The measured end-to-end TPS at `gas=80` (~247k-270k on realistic conflict) lands
|
||||
**coincidentally** near the documented 273k architectural ceiling. It must not be read
|
||||
as reaching or validating it. Two independent reasons:
|
||||
|
||||
**(a) TPS is entirely a function of the assumed per-tx cost.** The gas sweep at 16
|
||||
workers proves the absolute number is not a fixed capability:
|
||||
|
||||
| per-tx cost | e2eSpd @16 (0%/10%/50%) | **end-to-end TPS @16 (0%/10%/50%)** |
|
||||
|---|---|---|
|
||||
| gas=20 (light) | 3.67x / 4.36x / 3.97x | **416k / 502k / 477k** |
|
||||
| gas=80 (default) | 8.32x / 9.07x / 8.91x | **247k / 270k / 255k** |
|
||||
| gas=200 (heavy) | 11.97x / 11.91x / 9.55x | **138k / 143k / 115k** |
|
||||
|
||||
Change the simulated tx weight and the harness reports TPS from ~115k to ~500k on
|
||||
the same box. The `gas=80` proximity to 273k is a coincidence of one chosen
|
||||
per-tx cost, not a capability ceiling. (Note the honest inverse: heavier txs give
|
||||
*higher* speedup, because compute amortizes the serial tail and scheduler
|
||||
overhead, but *lower* TPS; lighter txs give lower speedup but higher TPS.)
|
||||
|
||||
**(b) This is the rollup executor with a light commit proxy, not L1, not a real
|
||||
MPT.** The state model is balances + kv slots (not full EVM), and `state_root` is
|
||||
a flat keccak fold, a *lighter* commit than a real Merkle Patricia Trie. The
|
||||
273k figure is a separate documented-conditions ceiling. These are different
|
||||
measurements under different conditions and this report neither reaches nor
|
||||
implies it.
|
||||
|
||||
The invariant, honest deliverable is the **speedup curve and its Amdahl bound**,
|
||||
not the absolute TPS.
|
||||
|
||||
---
|
||||
|
||||
## 8. Corroboration: the real client's commit is heavier and anti-scales
|
||||
|
||||
The Rust `state_root` is a light proxy for the commit. The real forked client's
|
||||
commit was measured separately (`aerenew/parallel/`, Besu Bonsai world-state, a
|
||||
throwaway box, DELETED). Findings there, which make the end-to-end picture in the
|
||||
*real* client even more conservative than this Rust harness:
|
||||
|
||||
- Bit-identical state roots proven four independent ways (in-JVM harness, faithful
|
||||
storage-phase test, genesis-root equality, and re-import + validation of 253 real
|
||||
exported blocks with zero root mismatch).
|
||||
- The multi-threaded **parallel-commit** path **anti-scales**: storage-commit
|
||||
scaling vs w=1 was 1.00x / ~0.8x / ~0.6x / ~0.4x / ~0.4x at w=1/2/4/8/16. More
|
||||
commit threads made it **slower**. The shipped optimization is therefore a
|
||||
**sequential** storage trie (faster than the fork/join parallel one), not a
|
||||
parallel commit.
|
||||
|
||||
So in the real client the commit is not merely a fixed serial tail; parallelizing
|
||||
it is counterproductive. The real-client end-to-end ceiling is thus **tighter**
|
||||
than the Rust flat-keccak numbers here, not looser. Honest direction of the error
|
||||
bar: down.
|
||||
|
||||
---
|
||||
|
||||
## 9. Box lifecycle
|
||||
|
||||
- Provisioned: a dedicated single-purpose scratch VM behind its own scratch firewall,
|
||||
created solely for this benchmark.
|
||||
- Live chain 2800, its validators, and all production nodes were **not** touched.
|
||||
No benchmark compute ran on production infrastructure.
|
||||
- **Destroyed at end of run; destruction confirmed** (see
|
||||
`endtoend-logs-2026-07-13/`). The scratch firewall was also removed.
|
||||
|
||||
---
|
||||
|
||||
## 10. Reproduce
|
||||
|
||||
```
|
||||
cd aerenew/parallel-executor
|
||||
cargo test --release # 7 serial-equivalence tests
|
||||
cargo run --release -- harness # 6000-comparison serializability gate
|
||||
cargo run --release -- bench # execution-phase speedup (the ~9.4x)
|
||||
cargo run --release -- endtoend # END-TO-END pipeline (this report), gas=80 ntx=20000
|
||||
cargo run --release -- endtoend 200 20000 5 # heavier tx
|
||||
cargo run --release -- endtoend 20 20000 5 # lighter tx
|
||||
```
|
||||
|
||||
Raw logs: `endtoend-logs-2026-07-13/` (env, cargo-test, harness, bench,
|
||||
endtoend at gas 80/200/20, empty stderr = zero mismatches).
|
||||
|
||||
---
|
||||
|
||||
## 11. One-paragraph honest summary
|
||||
|
||||
The AERE Block-STM executor's ~9.4x execution-phase scaling was, until now, an
|
||||
execution-phase-only measurement. Measured end-to-end (parallel execution +
|
||||
serial commit + serial keccak state root) on a 16-vCPU box, under an
|
||||
execution-heavy realistic transaction mix, at 0/10/50% conflict, the real
|
||||
end-to-end block-production speedup at 16 workers is **~8.3x to 9.1x** - clearly
|
||||
lower than the execution-phase figure, exactly by the Amdahl serial tail (commit +
|
||||
state-root), which is 5-20% of pipeline time and grows with state footprint.
|
||||
Serial-equivalence (parallel state and keccak root identical to sequential) held
|
||||
on all 6,000 + 140 checks. The measured end-to-end TPS (~250k at gas=80) is a real
|
||||
rollup-batch number for this harness and this box, swings from ~115k to ~500k as
|
||||
the assumed per-tx cost changes, and is **not** the 273k documented-conditions
|
||||
architectural ceiling and does not reach or imply it. This remains a testnet-grade
|
||||
executor measurement; it does not make Block-STM mainnet-active on chain 2800.
|
||||
7
parallel-executor/Cargo.lock
generated
Normal file
7
parallel-executor/Cargo.lock
generated
Normal file
@ -0,0 +1,7 @@
|
||||
# This file is automatically @generated by Cargo.
|
||||
# It is not intended for manual editing.
|
||||
version = 4
|
||||
|
||||
[[package]]
|
||||
name = "aere-block-stm"
|
||||
version = "0.1.0"
|
||||
22
parallel-executor/Cargo.toml
Normal file
22
parallel-executor/Cargo.toml
Normal file
@ -0,0 +1,22 @@
|
||||
[package]
|
||||
name = "aere-block-stm"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
description = "Real Block-STM optimistic-concurrency parallel EVM-transaction executor for AERE rollup batches. L1 base stays Besu-sequential (honest scope)."
|
||||
license = "MIT"
|
||||
|
||||
# ZERO external dependencies. The scheduler, multi-version memory, PRNG and
|
||||
# keccak256 are all implemented from scratch on top of std. This keeps the
|
||||
# build reproducible offline ($0), and makes the "genuine, from-scratch
|
||||
# Block-STM" claim literally true.
|
||||
[dependencies]
|
||||
|
||||
[[bin]]
|
||||
name = "aere-block-stm"
|
||||
path = "src/main.rs"
|
||||
|
||||
[profile.release]
|
||||
opt-level = 3
|
||||
lto = true
|
||||
codegen-units = 1
|
||||
panic = "abort"
|
||||
177
parallel-executor/README.md
Normal file
177
parallel-executor/README.md
Normal file
@ -0,0 +1,177 @@
|
||||
# aere-block-stm
|
||||
|
||||
A **real Block-STM optimistic-concurrency parallel EVM-transaction executor** for
|
||||
AERE, implemented from scratch in Rust with zero external dependencies.
|
||||
|
||||
It executes a batch of transactions across many CPU cores and commits a single
|
||||
state root that is **provably identical to sequential execution** (deterministic
|
||||
serializability), which is exactly what makes the resulting root a valid rollup
|
||||
commitment.
|
||||
|
||||
Reference algorithm: Gelashvili, Spiegelman, Xiang, Danezis, Li, Malkhi, Xia,
|
||||
Zhou, *"Block-STM: Scaling Blockchain Execution by Turning Ordering Curse to a
|
||||
Performance Blessing"* (arXiv:2203.06871, 2022) - the same design that powers
|
||||
Aptos, and the family Sui and Monad build on.
|
||||
|
||||
---
|
||||
|
||||
## Honest scope (read this first)
|
||||
|
||||
**What this is NOT:** this does **not** make AERE's Layer-1 base execution
|
||||
parallel. AERE's L1 runs Hyperledger Besu, which executes transactions
|
||||
**sequentially**. Parallelizing Besu itself would be a fork of a Java execution
|
||||
client, a multi-month effort, and is explicitly out of scope here. Nothing in
|
||||
this module changes how the AERE L1 mainnet executes blocks. The L1 stays
|
||||
Besu-sequential.
|
||||
|
||||
**What this IS:** a standalone, correct, benchmarked parallel executor that
|
||||
AERE's **rollup layer** can use. AERE already ships a Rollup-as-a-Service stack
|
||||
(`contracts/contracts/raas/AereRaaSFactory.sol` +
|
||||
`AereRollupSettlement.sol`). A rollup sequencer executes a batch of L2
|
||||
transactions off-chain, computes the resulting state root, and commits it via
|
||||
`AereRollupSettlement.proposeStateRoot(epoch, root, l2BlockHash)`. That batch
|
||||
execution is a pure, well-defined, embarrassingly-parallelizable workload, and
|
||||
it is the concrete, real path to parallel execution on AERE. This module is that
|
||||
executor, wired into the sequencer (see `../rollup-sequencer/`).
|
||||
|
||||
So the honest one-line framing is:
|
||||
|
||||
> Real Block-STM parallel executor, wired into the AERE rollup sequencer; the L1
|
||||
> base layer stays Besu-sequential.
|
||||
|
||||
---
|
||||
|
||||
## What it implements (the real algorithm)
|
||||
|
||||
- **Multi-version memory (MVMemory)** - `src/mvmemory.rs`. For every state key,
|
||||
an ordered `txn_index -> cell` map. A read by txn `i` returns the write of the
|
||||
highest txn `j < i`, giving each speculative execution a serial-equivalent
|
||||
view. Sharded across 256 mutexes so non-conflicting txns never contend.
|
||||
- **Read-set / write-set tracking** - `src/parallel.rs`. Each execution records
|
||||
exactly what it read (and the version it read) and what it wrote.
|
||||
- **Dynamic dependency detection** - a read that hits an `ESTIMATE` placeholder
|
||||
(left by a lower txn that is currently being re-executed) aborts the reader and
|
||||
parks it on the blocking txn; it is re-armed when the blocker finishes. No
|
||||
static dependency analysis, no condvars.
|
||||
- **Abort + re-execution** - `src/scheduler.rs`. Collaborative validation:
|
||||
validating txn `i` re-checks its read set against current MVMemory; on mismatch
|
||||
it is aborted, its incarnation bumped, its writes turned into ESTIMATEs, and it
|
||||
is rescheduled. Cursors are decreased so the affected suffix is revalidated.
|
||||
- **Serial-equivalent commit** - the block is done only when every txn has been
|
||||
validated against the final memory. The committed state provably equals
|
||||
sequential execution.
|
||||
- **State model** - account balances + a key/value contract store
|
||||
(`src/types.rs`), with genuine EVM-style transactions whose control flow and
|
||||
written *values* depend on reads: `Transfer`, `Sweep` (value-dependent),
|
||||
`Increment` (the canonical shared-counter serializability probe), and
|
||||
`AmmSwap` (two-slot DeFi pool conflict).
|
||||
- **keccak256 state root** - `src/keccak.rs`, a from-scratch keccak256 (verified
|
||||
against known-answer vectors) folds the final state into a real,
|
||||
EVM-compatible 32-byte root for on-chain commitment.
|
||||
|
||||
Zero external crates: the scheduler, MVMemory, PRNG and keccak are all built on
|
||||
`std`. Build is fully offline and reproducible ($0).
|
||||
|
||||
---
|
||||
|
||||
## Correctness guarantee (the hard part, proven)
|
||||
|
||||
The whole point of Block-STM is that the parallel result **must** equal
|
||||
sequential execution. Both paths call the exact same transaction-semantics
|
||||
function (`src/vm.rs::execute_txn`), so any divergence can only come from a
|
||||
concurrency-control bug. The harness proves there are none:
|
||||
|
||||
```
|
||||
cargo run --release -- harness
|
||||
```
|
||||
|
||||
runs 6 workload profiles x 200 randomized batches x {1,2,4,8,16} threads =
|
||||
**6000 comparisons** (1200 distinct randomized batches), and asserts the
|
||||
Block-STM committed state equals the sequential oracle for every one. Profiles
|
||||
include adversarial 100%-conflict cases (`all-hot`, `brutal-single-counter`).
|
||||
Result:
|
||||
|
||||
```
|
||||
total (batch x threads) comparisons: 6000
|
||||
mismatches : 0
|
||||
RESULT: PASS -- parallel Block-STM == sequential on ALL cases
|
||||
```
|
||||
|
||||
`cargo test --release` additionally checks:
|
||||
- `shared_counter_is_serialized`: 500 txns all incrementing one slot yield
|
||||
exactly 500 at 1/2/4/8/16 threads (naive parallelism would lose updates).
|
||||
- `parallel_equals_sequential_random`: 40 seeds x 4 thread counts.
|
||||
- `keccak_known_answer`: keccak256("") and keccak256("abc") match the standard
|
||||
vectors.
|
||||
|
||||
---
|
||||
|
||||
## 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 realistic simulated EVM execution
|
||||
cost (`gas = 80` keccak-rounds, a stand-in for the microseconds a real EVM tx
|
||||
spends in the interpreter). Speedup is versus the single-threaded **sequential**
|
||||
oracle. Median of 7 runs.
|
||||
|
||||
| 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% |
|
||||
|
||||
Honest reading of these numbers:
|
||||
|
||||
- Low/medium-conflict batches scale nearly linearly: **~8.6-9.3x on 16 cores**.
|
||||
- The high-conflict batch still gets ~4.6x because only ~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, honest worst case: parallelism collapses
|
||||
under total conflict, and no correct parallel executor can beat sequential
|
||||
when there is nothing to parallelize.
|
||||
|
||||
There is also an honest floor case worth stating: if transactions do
|
||||
near-**zero** compute (`gas = 0`), the concurrency-control overhead dominates and
|
||||
the parallel executor is slower than sequential. That is why the benchmark
|
||||
charges each transaction a realistic execution cost - real EVM transactions are
|
||||
compute-heavy (opcode interpretation, keccak, ecrecover), which is precisely the
|
||||
regime Block-STM is designed for. Run `bench 0` to see the floor, `bench 200`
|
||||
for heavier txs.
|
||||
|
||||
Reproduce:
|
||||
|
||||
```
|
||||
cargo run --release -- bench # gas=80, 20k txns (default)
|
||||
cargo run --release -- bench 200 # heavier txs
|
||||
cargo run --release -- bench 0 # trivial txs: scheduler-overhead floor
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Sequencer integration
|
||||
|
||||
The rollup sequencer calls the `execute` subcommand (a JSON-out CLI) with a batch
|
||||
and gets back a self-checked keccak256 state root to commit on-chain. See
|
||||
`../rollup-sequencer/README.md` and `../rollup-sequencer/src/blockstm/` for the
|
||||
TypeScript wrapper and the exact `proposeStateRoot` flow. The library API
|
||||
(`execute_block_parallel`, `execute_block_sequential`, `state_root`) is also
|
||||
usable directly from Rust.
|
||||
|
||||
Every `execute` run computes the batch **both** sequentially and in parallel and
|
||||
refuses to emit a root unless they match (`parallelEqualsSequential: true`), so a
|
||||
concurrency bug can never silently produce a bad rollup commitment.
|
||||
|
||||
---
|
||||
|
||||
## Build
|
||||
|
||||
```
|
||||
source ~/.cargo/env # on the AERE infra box
|
||||
cargo build --release
|
||||
cargo test --release
|
||||
cargo run --release -- harness
|
||||
cargo run --release -- bench
|
||||
```
|
||||
9
parallel-executor/endtoend-logs-2026-07-13/00-env.txt
Normal file
9
parallel-executor/endtoend-logs-2026-07-13/00-env.txt
Normal file
@ -0,0 +1,9 @@
|
||||
==== ENV ====
|
||||
Linux aere-blockstm-bench 6.8.0-117-generic #117-Ubuntu SMP PREEMPT_DYNAMIC Tue May 5 19:26:24 UTC 2026 x86_64 x86_64 x86_64 GNU/Linux
|
||||
16
|
||||
Model name: AMD EPYC-Rome Processor
|
||||
BIOS Model name: NotSpecified CPU @ 2.0GHz
|
||||
Thread(s) per core: 1
|
||||
Core(s) per socket: 16
|
||||
Socket(s): 1
|
||||
rustc 1.97.0 (2d8144b78 2026-07-07)
|
||||
20
parallel-executor/endtoend-logs-2026-07-13/01-cargo-test.txt
Normal file
20
parallel-executor/endtoend-logs-2026-07-13/01-cargo-test.txt
Normal file
@ -0,0 +1,20 @@
|
||||
|
||||
test result: ok. 2 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
|
||||
|
||||
Running unittests src/main.rs (target/release/deps/aere_block_stm-0c381095ca5068c5)
|
||||
|
||||
running 5 tests
|
||||
test tests::keccak_known_answer ... ok
|
||||
test tests::shared_counter_is_serialized ... ok
|
||||
test tests::prediction_eliminates_aborts_on_hot_slot ... ok
|
||||
test tests::parallel_equals_sequential_random ... ok
|
||||
test tests::predicted_equals_sequential_random ... ok
|
||||
|
||||
test result: ok. 5 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.28s
|
||||
|
||||
Doc-tests aere_block_stm
|
||||
|
||||
running 0 tests
|
||||
|
||||
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
|
||||
|
||||
@ -0,0 +1,9 @@
|
||||
=======================================================
|
||||
CORRECTNESS HARNESS
|
||||
profiles : 6
|
||||
batches per profile : 200
|
||||
thread counts : [1, 2, 4, 8, 16]
|
||||
total (batch x threads) comparisons: 6000
|
||||
mismatches : 0
|
||||
RESULT: PASS -- parallel Block-STM == sequential on ALL cases
|
||||
=======================================================
|
||||
@ -0,0 +1,51 @@
|
||||
=======================================================
|
||||
BLOCK-STM SPEEDUP BENCHMARK
|
||||
machine threads available: 16
|
||||
txns per batch : 20000
|
||||
simulated EVM gas per tx : 80 keccak-rounds
|
||||
timing : median of 7 runs (ms)
|
||||
=======================================================
|
||||
|
||||
### LOW-CONFLICT (50k accounts, few hot, ~2% hot-touch)
|
||||
parallel==sequential check: PASS
|
||||
sequential baseline : 663.24 ms
|
||||
threads time(ms) speedup vs-1thread aborts abort%
|
||||
1 725.04 0.91x 1.00x 0 0.0%
|
||||
2 380.42 1.74x 1.91x 3 0.0%
|
||||
4 204.86 3.24x 3.54x 13 0.1%
|
||||
8 116.83 5.68x 6.21x 36 0.2%
|
||||
16 79.90 8.30x 9.07x 61 0.3%
|
||||
|
||||
### MEDIUM-CONFLICT (5k accounts, ~15% hot-touch)
|
||||
parallel==sequential check: PASS
|
||||
sequential baseline : 661.01 ms
|
||||
threads time(ms) speedup vs-1thread aborts abort%
|
||||
1 705.12 0.94x 1.00x 0 0.0%
|
||||
2 363.28 1.82x 1.94x 37 0.2%
|
||||
4 189.85 3.48x 3.71x 90 0.4%
|
||||
8 107.00 6.18x 6.59x 198 1.0%
|
||||
16 68.74 9.62x 10.26x 423 2.1%
|
||||
|
||||
### HIGH-CONFLICT (single hot counter, ~80% hot-touch)
|
||||
parallel==sequential check: PASS
|
||||
sequential baseline : 669.68 ms
|
||||
threads time(ms) speedup vs-1thread aborts abort%
|
||||
1 694.53 0.96x 1.00x 0 0.0%
|
||||
2 385.06 1.74x 1.80x 1335 6.2%
|
||||
4 216.35 3.10x 3.21x 3425 13.5%
|
||||
8 134.93 4.96x 5.15x 6940 19.8%
|
||||
16 130.67 5.13x 5.32x 13314 23.8%
|
||||
|
||||
### PATHOLOGICAL (every tx increments the SAME slot: no parallelism)
|
||||
parallel==sequential check: PASS
|
||||
sequential baseline : 676.98 ms
|
||||
threads time(ms) speedup vs-1thread aborts abort%
|
||||
1 694.65 0.97x 1.00x 0 0.0%
|
||||
2 727.81 0.93x 0.95x 13410 1.6%
|
||||
4 783.44 0.86x 0.89x 22510 5.8%
|
||||
8 824.31 0.82x 0.84x 27712 12.4%
|
||||
16 884.96 0.76x 0.78x 30654 18.4%
|
||||
|
||||
Note: speedup is vs the sequential oracle. High-conflict batches
|
||||
serialize on the hot slot and are expected to show little or no
|
||||
speedup -- that is the honest, correct behavior of Block-STM.
|
||||
73
parallel-executor/endtoend-logs-2026-07-13/04-endtoend.txt
Normal file
73
parallel-executor/endtoend-logs-2026-07-13/04-endtoend.txt
Normal file
@ -0,0 +1,73 @@
|
||||
=======================================================
|
||||
BLOCK-STM END-TO-END BLOCK-PRODUCTION THROUGHPUT
|
||||
machine threads available : 16
|
||||
txns per batch : 20000
|
||||
simulated EVM cost per tx : 80 keccak-rounds
|
||||
timing : median of 7 runs (ms)
|
||||
pipeline : exec (parallel) + commit (serial) + state-root (serial)
|
||||
serial-equivalence : par state == seq state AND keccak roots identical, EVERY run
|
||||
=======================================================
|
||||
|
||||
### 0% designed conflict (disjoint accounts, 64 contracts spread)
|
||||
committed state keys : 49637
|
||||
sequential exec 663.15 ms | root 9.69 ms | END-TO-END 672.84 ms | root-share 1.4%
|
||||
sequential end-to-end TPS: 29725
|
||||
W | exec commit root e2e_ms | execSpd e2eSpd | TPS abort% | root%e2e | equiv
|
||||
1 | 705.51 9.01 12.52 727.04 | 0.94x 0.93x | 27509 0.0% | 3.0% | PASS
|
||||
2 | 354.89 9.56 11.41 375.85 | 1.87x 1.79x | 53212 0.0% | 5.6% | PASS
|
||||
4 | 184.02 9.72 12.37 206.11 | 3.60x 3.26x | 97037 0.1% | 10.7% | PASS
|
||||
8 | 97.99 8.52 8.31 114.82 | 6.77x 5.86x | 174191 0.1% | 14.7% | PASS
|
||||
16 | 64.23 8.08 8.51 80.83 | 10.32x 8.32x | 247444 0.3% | 20.5% | PASS
|
||||
Amdahl: serial tail (commit+root) = 15.33 ms; end-to-end speedup ceiling @16w = 8.27x, @infinite cores = 43.88x
|
||||
|
||||
### 10% designed conflict (100 hot accts, 16 contracts)
|
||||
committed state keys : 18342
|
||||
sequential exec 669.73 ms | root 2.98 ms | END-TO-END 672.71 ms | root-share 0.4%
|
||||
sequential end-to-end TPS: 29731
|
||||
W | exec commit root e2e_ms | execSpd e2eSpd | TPS abort% | root%e2e | equiv
|
||||
1 | 699.73 5.15 4.77 709.65 | 0.96x 0.95x | 28183 0.0% | 1.4% | PASS
|
||||
2 | 366.73 5.31 4.38 376.42 | 1.83x 1.79x | 53133 0.1% | 2.6% | PASS
|
||||
4 | 187.09 5.15 3.88 196.13 | 3.58x 3.43x | 101975 0.3% | 4.6% | PASS
|
||||
8 | 96.64 5.06 4.29 105.99 | 6.93x 6.35x | 188699 0.7% | 8.8% | PASS
|
||||
16 | 66.57 4.37 3.20 74.14 | 10.06x 9.07x | 269750 1.2% | 10.2% | PASS
|
||||
Amdahl: serial tail (commit+root) = 6.68 ms; end-to-end speedup ceiling @16w = 9.64x, @infinite cores = 100.71x
|
||||
|
||||
### 50% designed conflict (20 hot accts, 4 contracts)
|
||||
committed state keys : 9006
|
||||
sequential exec 698.55 ms | root 1.42 ms | END-TO-END 699.97 ms | root-share 0.2%
|
||||
sequential end-to-end TPS: 28573
|
||||
W | exec commit root e2e_ms | execSpd e2eSpd | TPS abort% | root%e2e | equiv
|
||||
1 | 712.46 2.63 1.96 717.06 | 0.98x 0.98x | 27892 0.0% | 0.6% | PASS
|
||||
2 | 367.61 2.46 2.01 372.07 | 1.90x 1.88x | 53753 2.1% | 1.2% | PASS
|
||||
4 | 195.00 2.58 1.98 199.56 | 3.58x 3.51x | 100222 5.7% | 2.3% | PASS
|
||||
8 | 113.28 2.50 1.72 117.51 | 6.17x 5.96x | 170202 10.7% | 3.6% | PASS
|
||||
16 | 74.60 2.35 1.59 78.53 | 9.36x 8.91x | 254686 16.6% | 5.0% | PASS
|
||||
Amdahl: serial tail (commit+root) = 4.15 ms; end-to-end speedup ceiling @16w = 8.73x, @infinite cores = 168.85x
|
||||
|
||||
### PATHOLOGICAL ~100% (single hot slot, no parallelism)
|
||||
committed state keys : 1996
|
||||
sequential exec 664.35 ms | root 0.30 ms | END-TO-END 664.65 ms | root-share 0.0%
|
||||
sequential end-to-end TPS: 30091
|
||||
W | exec commit root e2e_ms | execSpd e2eSpd | TPS abort% | root%e2e | equiv
|
||||
1 | 682.90 0.06 0.31 683.28 | 0.97x 0.97x | 29271 0.0% | 0.1% | PASS
|
||||
2 | 378.02 0.04 0.37 378.42 | 1.76x 1.76x | 52851 6.5% | 0.1% | PASS
|
||||
4 | 213.51 0.04 0.38 213.93 | 3.11x 3.11x | 93488 14.5% | 0.2% | PASS
|
||||
8 | 133.61 0.04 0.30 133.95 | 4.97x 4.96x | 149314 21.2% | 0.2% | PASS
|
||||
16 | 135.50 0.04 0.40 135.94 | 4.90x 4.89x | 147125 25.0% | 0.3% | PASS
|
||||
Amdahl: serial tail (commit+root) = 0.56 ms; end-to-end speedup ceiling @16w = 5.37x, @infinite cores = 1183.26x
|
||||
|
||||
=======================================================
|
||||
SERIAL-EQUIVALENCE: 140 checks, 0 mismatches
|
||||
RESULT: PASS -- every parallel run committed the exact sequential state + root
|
||||
=======================================================
|
||||
|
||||
HONEST READING:
|
||||
- execSpd is the EXECUTION-PHASE speedup (comparable to the documented ~9.4x).
|
||||
- e2eSpd is the END-TO-END block-production speedup. It is LOWER, because the
|
||||
commit (state materialization) and state-root (keccak) phases are serial and
|
||||
do not shrink with more workers -- the Amdahl serial tail.
|
||||
- TPS is the real end-to-end transactions/second of THIS pipeline on THIS box
|
||||
(shared-vCPU cloud). It is a measured rollup-batch number, NOT the 273k TPS
|
||||
documented-conditions marketing ceiling and does not reach or imply it.
|
||||
- Under total conflict (pathological) there is no parallelism; end-to-end can be
|
||||
<= 1x. That is the correct, honest behavior of Block-STM.
|
||||
@ -0,0 +1,73 @@
|
||||
=======================================================
|
||||
BLOCK-STM END-TO-END BLOCK-PRODUCTION THROUGHPUT
|
||||
machine threads available : 16
|
||||
txns per batch : 20000
|
||||
simulated EVM cost per tx : 200 keccak-rounds
|
||||
timing : median of 5 runs (ms)
|
||||
pipeline : exec (parallel) + commit (serial) + state-root (serial)
|
||||
serial-equivalence : par state == seq state AND keccak roots identical, EVERY run
|
||||
=======================================================
|
||||
|
||||
### 0% designed conflict (disjoint accounts, 64 contracts spread)
|
||||
committed state keys : 49637
|
||||
sequential exec 1724.47 ms | root 9.46 ms | END-TO-END 1733.93 ms | root-share 0.5%
|
||||
sequential end-to-end TPS: 11535
|
||||
W | exec commit root e2e_ms | execSpd e2eSpd | TPS abort% | root%e2e | equiv
|
||||
1 | 1732.47 8.95 9.31 1750.73 | 1.00x 0.99x | 11424 0.0% | 1.0% | PASS
|
||||
2 | 872.83 9.82 10.15 892.80 | 1.98x 1.94x | 22402 0.0% | 2.2% | PASS
|
||||
4 | 443.14 8.61 8.70 460.44 | 3.89x 3.77x | 43436 0.1% | 3.8% | PASS
|
||||
8 | 224.20 9.49 10.65 244.34 | 7.69x 7.10x | 81854 0.2% | 8.2% | PASS
|
||||
16 | 127.82 8.57 8.52 144.91 | 13.49x 11.97x | 138021 0.3% | 11.8% | PASS
|
||||
Amdahl: serial tail (commit+root) = 17.72 ms; end-to-end speedup ceiling @16w = 11.54x, @infinite cores = 97.86x
|
||||
|
||||
### 10% designed conflict (100 hot accts, 16 contracts)
|
||||
committed state keys : 18342
|
||||
sequential exec 1660.34 ms | root 3.02 ms | END-TO-END 1663.36 ms | root-share 0.2%
|
||||
sequential end-to-end TPS: 12024
|
||||
W | exec commit root e2e_ms | execSpd e2eSpd | TPS abort% | root%e2e | equiv
|
||||
1 | 1763.89 4.87 4.27 1773.02 | 0.94x 0.94x | 11280 0.0% | 0.5% | PASS
|
||||
2 | 867.29 4.93 3.98 876.21 | 1.91x 1.90x | 22826 0.1% | 1.0% | PASS
|
||||
4 | 428.26 5.18 4.03 437.47 | 3.88x 3.80x | 45717 0.3% | 2.1% | PASS
|
||||
8 | 218.20 5.32 3.80 227.31 | 7.61x 7.32x | 87986 0.7% | 4.0% | PASS
|
||||
16 | 131.28 5.16 3.23 139.68 | 12.65x 11.91x | 143189 1.3% | 6.0% | PASS
|
||||
Amdahl: serial tail (commit+root) = 8.43 ms; end-to-end speedup ceiling @16w = 12.04x, @infinite cores = 197.38x
|
||||
|
||||
### 50% designed conflict (20 hot accts, 4 contracts)
|
||||
committed state keys : 9006
|
||||
sequential exec 1663.44 ms | root 1.41 ms | END-TO-END 1664.84 ms | root-share 0.1%
|
||||
sequential end-to-end TPS: 12013
|
||||
W | exec commit root e2e_ms | execSpd e2eSpd | TPS abort% | root%e2e | equiv
|
||||
1 | 1696.76 2.52 1.54 1700.82 | 0.98x 0.98x | 11759 0.0% | 0.2% | PASS
|
||||
2 | 872.94 2.49 1.74 877.18 | 1.91x 1.90x | 22800 2.2% | 0.5% | PASS
|
||||
4 | 460.75 2.57 1.59 464.91 | 3.61x 3.58x | 43019 5.8% | 0.9% | PASS
|
||||
8 | 256.91 2.32 1.57 260.79 | 6.47x 6.38x | 76690 11.1% | 1.5% | PASS
|
||||
16 | 170.84 1.96 1.48 174.29 | 9.74x 9.55x | 114753 18.2% | 2.0% | PASS
|
||||
Amdahl: serial tail (commit+root) = 3.51 ms; end-to-end speedup ceiling @16w = 9.21x, @infinite cores = 474.53x
|
||||
|
||||
### PATHOLOGICAL ~100% (single hot slot, no parallelism)
|
||||
committed state keys : 1996
|
||||
sequential exec 1624.41 ms | root 0.29 ms | END-TO-END 1624.71 ms | root-share 0.0%
|
||||
sequential end-to-end TPS: 12310
|
||||
W | exec commit root e2e_ms | execSpd e2eSpd | TPS abort% | root%e2e | equiv
|
||||
1 | 1670.23 0.06 0.38 1670.67 | 0.97x 0.97x | 11971 0.0% | 0.0% | PASS
|
||||
2 | 896.96 0.05 0.37 897.37 | 1.81x 1.81x | 22287 6.6% | 0.0% | PASS
|
||||
4 | 503.18 0.05 0.68 503.91 | 3.23x 3.22x | 39689 14.5% | 0.1% | PASS
|
||||
8 | 311.47 0.07 0.57 312.11 | 5.22x 5.21x | 64081 21.3% | 0.2% | PASS
|
||||
16 | 294.20 0.04 0.36 294.60 | 5.52x 5.51x | 67888 24.1% | 0.1% | PASS
|
||||
Amdahl: serial tail (commit+root) = 0.38 ms; end-to-end speedup ceiling @16w = 5.93x, @infinite cores = 4288.05x
|
||||
|
||||
=======================================================
|
||||
SERIAL-EQUIVALENCE: 100 checks, 0 mismatches
|
||||
RESULT: PASS -- every parallel run committed the exact sequential state + root
|
||||
=======================================================
|
||||
|
||||
HONEST READING:
|
||||
- execSpd is the EXECUTION-PHASE speedup (comparable to the documented ~9.4x).
|
||||
- e2eSpd is the END-TO-END block-production speedup. It is LOWER, because the
|
||||
commit (state materialization) and state-root (keccak) phases are serial and
|
||||
do not shrink with more workers -- the Amdahl serial tail.
|
||||
- TPS is the real end-to-end transactions/second of THIS pipeline on THIS box
|
||||
(shared-vCPU cloud). It is a measured rollup-batch number, NOT the 273k TPS
|
||||
documented-conditions marketing ceiling and does not reach or imply it.
|
||||
- Under total conflict (pathological) there is no parallelism; end-to-end can be
|
||||
<= 1x. That is the correct, honest behavior of Block-STM.
|
||||
@ -0,0 +1,73 @@
|
||||
=======================================================
|
||||
BLOCK-STM END-TO-END BLOCK-PRODUCTION THROUGHPUT
|
||||
machine threads available : 16
|
||||
txns per batch : 20000
|
||||
simulated EVM cost per tx : 20 keccak-rounds
|
||||
timing : median of 5 runs (ms)
|
||||
pipeline : exec (parallel) + commit (serial) + state-root (serial)
|
||||
serial-equivalence : par state == seq state AND keccak roots identical, EVERY run
|
||||
=======================================================
|
||||
|
||||
### 0% designed conflict (disjoint accounts, 64 contracts spread)
|
||||
committed state keys : 49637
|
||||
sequential exec 166.95 ms | root 9.45 ms | END-TO-END 176.40 ms | root-share 5.4%
|
||||
sequential end-to-end TPS: 113378
|
||||
W | exec commit root e2e_ms | execSpd e2eSpd | TPS abort% | root%e2e | equiv
|
||||
1 | 192.12 8.69 9.24 210.04 | 0.87x 0.84x | 95220 0.0% | 8.5% | PASS
|
||||
2 | 108.62 8.72 10.23 127.57 | 1.54x 1.38x | 156775 0.0% | 14.9% | PASS
|
||||
4 | 71.23 8.45 8.97 88.65 | 2.34x 1.99x | 225609 0.0% | 19.7% | PASS
|
||||
8 | 38.84 7.82 8.10 54.76 | 4.30x 3.22x | 365254 0.1% | 29.1% | PASS
|
||||
16 | 32.50 7.33 8.23 48.06 | 5.14x 3.67x | 416134 0.2% | 32.4% | PASS
|
||||
Amdahl: serial tail (commit+root) = 16.54 ms; end-to-end speedup ceiling @16w = 3.65x, @infinite cores = 10.67x
|
||||
|
||||
### 10% designed conflict (100 hot accts, 16 contracts)
|
||||
committed state keys : 18342
|
||||
sequential exec 170.92 ms | root 2.87 ms | END-TO-END 173.79 ms | root-share 1.7%
|
||||
sequential end-to-end TPS: 115081
|
||||
W | exec commit root e2e_ms | execSpd e2eSpd | TPS abort% | root%e2e | equiv
|
||||
1 | 208.57 4.91 3.13 216.61 | 0.82x 0.80x | 92331 0.0% | 3.7% | PASS
|
||||
2 | 111.66 5.00 3.57 120.24 | 1.53x 1.45x | 166336 0.1% | 7.1% | PASS
|
||||
4 | 68.63 5.13 3.34 77.11 | 2.49x 2.25x | 259374 0.2% | 11.0% | PASS
|
||||
8 | 41.14 3.93 3.27 48.33 | 4.15x 3.60x | 413814 0.5% | 14.9% | PASS
|
||||
16 | 31.59 5.02 3.26 39.87 | 5.41x 4.36x | 501666 0.8% | 20.8% | PASS
|
||||
Amdahl: serial tail (commit+root) = 9.54 ms; end-to-end speedup ceiling @16w = 4.20x, @infinite cores = 18.21x
|
||||
|
||||
### 50% designed conflict (20 hot accts, 4 contracts)
|
||||
committed state keys : 9006
|
||||
sequential exec 164.90 ms | root 1.52 ms | END-TO-END 166.41 ms | root-share 0.9%
|
||||
sequential end-to-end TPS: 120182
|
||||
W | exec commit root e2e_ms | execSpd e2eSpd | TPS abort% | root%e2e | equiv
|
||||
1 | 194.05 2.72 1.57 198.34 | 0.85x 0.84x | 100839 0.0% | 2.2% | PASS
|
||||
2 | 111.70 2.63 1.73 116.05 | 1.48x 1.43x | 172332 2.0% | 3.8% | PASS
|
||||
4 | 64.83 2.23 1.40 68.46 | 2.54x 2.43x | 292161 5.2% | 5.3% | PASS
|
||||
8 | 44.69 2.25 1.57 48.51 | 3.69x 3.43x | 412270 9.1% | 7.9% | PASS
|
||||
16 | 38.28 2.03 1.59 41.90 | 4.31x 3.97x | 477368 13.5% | 8.6% | PASS
|
||||
Amdahl: serial tail (commit+root) = 3.19 ms; end-to-end speedup ceiling @16w = 3.92x, @infinite cores = 52.13x
|
||||
|
||||
### PATHOLOGICAL ~100% (single hot slot, no parallelism)
|
||||
committed state keys : 1996
|
||||
sequential exec 164.73 ms | root 0.29 ms | END-TO-END 165.03 ms | root-share 0.2%
|
||||
sequential end-to-end TPS: 121191
|
||||
W | exec commit root e2e_ms | execSpd e2eSpd | TPS abort% | root%e2e | equiv
|
||||
1 | 191.33 0.06 0.35 191.74 | 0.86x 0.86x | 104306 0.0% | 0.2% | PASS
|
||||
2 | 115.17 0.05 0.30 115.51 | 1.43x 1.43x | 173140 6.4% | 0.3% | PASS
|
||||
4 | 71.82 0.07 0.35 72.23 | 2.29x 2.28x | 276876 14.0% | 0.6% | PASS
|
||||
8 | 57.49 0.04 0.30 57.83 | 2.87x 2.85x | 345848 20.6% | 0.6% | PASS
|
||||
16 | 61.98 0.04 0.41 62.44 | 2.66x 2.64x | 320331 24.2% | 0.7% | PASS
|
||||
Amdahl: serial tail (commit+root) = 0.39 ms; end-to-end speedup ceiling @16w = 2.66x, @infinite cores = 422.21x
|
||||
|
||||
=======================================================
|
||||
SERIAL-EQUIVALENCE: 100 checks, 0 mismatches
|
||||
RESULT: PASS -- every parallel run committed the exact sequential state + root
|
||||
=======================================================
|
||||
|
||||
HONEST READING:
|
||||
- execSpd is the EXECUTION-PHASE speedup (comparable to the documented ~9.4x).
|
||||
- e2eSpd is the END-TO-END block-production speedup. It is LOWER, because the
|
||||
commit (state materialization) and state-root (keccak) phases are serial and
|
||||
do not shrink with more workers -- the Amdahl serial tail.
|
||||
- TPS is the real end-to-end transactions/second of THIS pipeline on THIS box
|
||||
(shared-vCPU cloud). It is a measured rollup-batch number, NOT the 273k TPS
|
||||
documented-conditions marketing ceiling and does not reach or imply it.
|
||||
- Under total conflict (pathological) there is no parallelism; end-to-end can be
|
||||
<= 1x. That is the correct, honest behavior of Block-STM.
|
||||
3
parallel-executor/endtoend-logs-2026-07-13/run.status
Normal file
3
parallel-executor/endtoend-logs-2026-07-13/run.status
Normal file
@ -0,0 +1,3 @@
|
||||
START Mon Jul 13 08:53:05 AM UTC 2026
|
||||
BENCH_DONE Mon Jul 13 08:54:27 AM UTC 2026
|
||||
E2E_DONE Mon Jul 13 08:55:33 AM UTC 2026
|
||||
3
parallel-executor/endtoend-logs-2026-07-13/sweep.status
Normal file
3
parallel-executor/endtoend-logs-2026-07-13/sweep.status
Normal file
@ -0,0 +1,3 @@
|
||||
SWEEP_START Mon Jul 13 08:56:38 AM UTC 2026
|
||||
GAS200_DONE
|
||||
GAS20_DONE
|
||||
160
parallel-executor/src/keccak.rs
Normal file
160
parallel-executor/src/keccak.rs
Normal file
@ -0,0 +1,160 @@
|
||||
//! A compact, from-scratch Keccak-256 (the exact hash Ethereum/EVM uses). Used
|
||||
//! to fold the final committed state into a single 32-byte state root that the
|
||||
//! AERE rollup sequencer commits on-chain via
|
||||
//! `AereRollupSettlement.proposeStateRoot`. Keeping it dependency-free means the
|
||||
//! executor produces a real EVM-compatible root with zero external crates.
|
||||
//!
|
||||
//! This is standard Keccak-f[1600] with rate 1088 bits (136 bytes) and the
|
||||
//! original Keccak padding (0x01 .. 0x80), i.e. keccak256, NOT SHA3-256.
|
||||
|
||||
const RC: [u64; 24] = [
|
||||
0x0000000000000001,
|
||||
0x0000000000008082,
|
||||
0x800000000000808a,
|
||||
0x8000000080008000,
|
||||
0x000000000000808b,
|
||||
0x0000000080000001,
|
||||
0x8000000080008081,
|
||||
0x8000000000008009,
|
||||
0x000000000000008a,
|
||||
0x0000000000000088,
|
||||
0x0000000080008009,
|
||||
0x000000008000000a,
|
||||
0x000000008000808b,
|
||||
0x800000000000008b,
|
||||
0x8000000000008089,
|
||||
0x8000000000008003,
|
||||
0x8000000000008002,
|
||||
0x8000000000000080,
|
||||
0x000000000000800a,
|
||||
0x800000008000000a,
|
||||
0x8000000080008081,
|
||||
0x8000000000008080,
|
||||
0x0000000080000001,
|
||||
0x8000000080008008,
|
||||
];
|
||||
|
||||
const ROTC: [u32; 24] = [
|
||||
1, 3, 6, 10, 15, 21, 28, 36, 45, 55, 2, 14, 27, 41, 56, 8, 25, 43, 62, 18, 39, 61, 20, 44,
|
||||
];
|
||||
|
||||
const PILN: [usize; 24] = [
|
||||
10, 7, 11, 17, 18, 3, 5, 16, 8, 21, 24, 4, 15, 23, 19, 13, 12, 2, 20, 14, 22, 9, 6, 1,
|
||||
];
|
||||
|
||||
fn keccak_f(st: &mut [u64; 25]) {
|
||||
for round in 0..24 {
|
||||
// theta
|
||||
let mut bc = [0u64; 5];
|
||||
for i in 0..5 {
|
||||
bc[i] = st[i] ^ st[i + 5] ^ st[i + 10] ^ st[i + 15] ^ st[i + 20];
|
||||
}
|
||||
for i in 0..5 {
|
||||
let t = bc[(i + 4) % 5] ^ bc[(i + 1) % 5].rotate_left(1);
|
||||
for j in (0..25).step_by(5) {
|
||||
st[j + i] ^= t;
|
||||
}
|
||||
}
|
||||
// rho + pi
|
||||
let mut t = st[1];
|
||||
for i in 0..24 {
|
||||
let j = PILN[i];
|
||||
let tmp = st[j];
|
||||
st[j] = t.rotate_left(ROTC[i]);
|
||||
t = tmp;
|
||||
}
|
||||
// chi
|
||||
for j in (0..25).step_by(5) {
|
||||
let mut bc2 = [0u64; 5];
|
||||
for i in 0..5 {
|
||||
bc2[i] = st[j + i];
|
||||
}
|
||||
for i in 0..5 {
|
||||
st[j + i] ^= (!bc2[(i + 1) % 5]) & bc2[(i + 2) % 5];
|
||||
}
|
||||
}
|
||||
// iota
|
||||
st[0] ^= RC[round];
|
||||
}
|
||||
}
|
||||
|
||||
/// keccak256 of `input`.
|
||||
pub fn keccak256(input: &[u8]) -> [u8; 32] {
|
||||
let mut st = [0u64; 25];
|
||||
const RATE: usize = 136; // 1088 bits
|
||||
let mut offset = 0;
|
||||
let len = input.len();
|
||||
|
||||
// absorb full blocks
|
||||
while len - offset >= RATE {
|
||||
for i in 0..RATE / 8 {
|
||||
let mut lane = 0u64;
|
||||
for b in 0..8 {
|
||||
lane |= (input[offset + i * 8 + b] as u64) << (8 * b);
|
||||
}
|
||||
st[i] ^= lane;
|
||||
}
|
||||
keccak_f(&mut st);
|
||||
offset += RATE;
|
||||
}
|
||||
|
||||
// last block with padding
|
||||
let mut block = [0u8; RATE];
|
||||
let rem = len - offset;
|
||||
block[..rem].copy_from_slice(&input[offset..]);
|
||||
block[rem] ^= 0x01; // keccak padding start
|
||||
block[RATE - 1] ^= 0x80; // padding end
|
||||
for i in 0..RATE / 8 {
|
||||
let mut lane = 0u64;
|
||||
for b in 0..8 {
|
||||
lane |= (block[i * 8 + b] as u64) << (8 * b);
|
||||
}
|
||||
st[i] ^= lane;
|
||||
}
|
||||
keccak_f(&mut st);
|
||||
|
||||
let mut out = [0u8; 32];
|
||||
for i in 0..4 {
|
||||
let lane = st[i].to_le_bytes();
|
||||
out[i * 8..i * 8 + 8].copy_from_slice(&lane);
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
/// Fold a materialized state into a single keccak256 root. Keys are serialized
|
||||
/// in a canonical, sorted order so the root is deterministic and independent of
|
||||
/// hashmap iteration order -- exactly what a rollup state commitment needs.
|
||||
pub fn state_root(state: &crate::types::StateMap) -> [u8; 32] {
|
||||
use crate::types::Key;
|
||||
let mut entries: Vec<(Key, u128)> = state
|
||||
.iter()
|
||||
.filter(|(_, v)| **v != 0) // absent == 0; omit zeros for a canonical form
|
||||
.map(|(k, v)| (*k, *v))
|
||||
.collect();
|
||||
entries.sort();
|
||||
let mut buf: Vec<u8> = Vec::with_capacity(entries.len() * 40);
|
||||
for (k, v) in entries {
|
||||
match k {
|
||||
Key::Balance(a) => {
|
||||
buf.push(0x00);
|
||||
buf.extend_from_slice(&a.to_be_bytes());
|
||||
}
|
||||
Key::Storage(c, s) => {
|
||||
buf.push(0x01);
|
||||
buf.extend_from_slice(&c.to_be_bytes());
|
||||
buf.extend_from_slice(&s.to_be_bytes());
|
||||
}
|
||||
}
|
||||
buf.extend_from_slice(&v.to_be_bytes());
|
||||
}
|
||||
keccak256(&buf)
|
||||
}
|
||||
|
||||
pub fn hex32(b: &[u8; 32]) -> String {
|
||||
let mut s = String::with_capacity(66);
|
||||
s.push_str("0x");
|
||||
for byte in b {
|
||||
s.push_str(&format!("{:02x}", byte));
|
||||
}
|
||||
s
|
||||
}
|
||||
40
parallel-executor/src/lib.rs
Normal file
40
parallel-executor/src/lib.rs
Normal file
@ -0,0 +1,40 @@
|
||||
//! aere-block-stm: a real Block-STM optimistic-concurrency parallel executor for
|
||||
//! AERE rollup batches.
|
||||
//!
|
||||
//! HONEST SCOPE
|
||||
//! ------------
|
||||
//! This does NOT make AERE's Layer-1 base execution parallel. AERE's L1 is
|
||||
//! Hyperledger Besu, which executes transactions sequentially; parallelizing
|
||||
//! Besu itself is a Java execution-client fork and is out of scope here.
|
||||
//!
|
||||
//! What this IS: a standalone, correct, benchmarked parallel EVM-style
|
||||
//! transaction executor that AERE's ROLLUP layer (AereRaaSFactory /
|
||||
//! AereRollupSettlement / the rollup sequencer) can call to execute a batch of
|
||||
//! rollup transactions across many CPU cores and commit a single state root.
|
||||
//! The committed result is provably identical to sequential execution
|
||||
//! (serializability), which is exactly what makes the resulting state root a
|
||||
//! valid rollup commitment.
|
||||
//!
|
||||
//! Public API
|
||||
//! ----------
|
||||
//! - [`sequential::execute_block_sequential`] -- the reference/oracle.
|
||||
//! - [`parallel::execute_block_parallel`] -- the Block-STM executor.
|
||||
//! - [`keccak::state_root`] -- keccak256 commitment over a final state.
|
||||
|
||||
pub mod keccak;
|
||||
pub mod mvmemory;
|
||||
pub mod parallel;
|
||||
pub mod predict;
|
||||
pub mod scheduler;
|
||||
pub mod sequential;
|
||||
pub mod types;
|
||||
pub mod vm;
|
||||
pub mod workload;
|
||||
|
||||
pub use parallel::{
|
||||
execute_block_parallel, execute_block_parallel_predicted, execute_block_parallel_split,
|
||||
ExecStats,
|
||||
};
|
||||
pub use predict::compute_predicted_preds;
|
||||
pub use sequential::execute_block_sequential;
|
||||
pub use types::{Key, StateMap, TxKind, Txn, Value};
|
||||
986
parallel-executor/src/main.rs
Normal file
986
parallel-executor/src/main.rs
Normal file
@ -0,0 +1,986 @@
|
||||
//! CLI for the AERE Block-STM executor.
|
||||
//!
|
||||
//! aere-block-stm harness run the correctness harness (parallel == sequential)
|
||||
//! aere-block-stm bench run the speedup benchmark
|
||||
//! aere-block-stm execute read a batch on stdin, emit committed state root (JSON)
|
||||
//!
|
||||
//! `execute` is the integration entry point the rollup sequencer shells out to.
|
||||
|
||||
use std::io::{Read, Write};
|
||||
use std::time::Instant;
|
||||
|
||||
use aere_block_stm::keccak::{hex32, state_root};
|
||||
use aere_block_stm::parallel::{
|
||||
execute_block_parallel, execute_block_parallel_predicted, execute_block_parallel_split,
|
||||
};
|
||||
use aere_block_stm::predict::compute_predicted_preds;
|
||||
use aere_block_stm::sequential::execute_block_sequential;
|
||||
use aere_block_stm::types::{Key, StateMap, TxKind, Txn};
|
||||
use aere_block_stm::workload::{generate_batch, make_base_state, WorkloadConfig};
|
||||
|
||||
fn main() {
|
||||
let args: Vec<String> = std::env::args().collect();
|
||||
let cmd = args.get(1).map(|s| s.as_str()).unwrap_or("harness");
|
||||
match cmd {
|
||||
"harness" => harness(),
|
||||
"bench" => bench(),
|
||||
"ab" => ab_experiment(),
|
||||
"endtoend" | "e2e" => endtoend(),
|
||||
"execute" => execute_stdin(),
|
||||
other => {
|
||||
eprintln!("unknown command: {other}");
|
||||
eprintln!("usage: aere-block-stm [harness|bench|ab|endtoend|execute]");
|
||||
std::process::exit(2);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// ------------------------------------------------------------------ harness
|
||||
/// Prove serializability: over a large matrix of randomized, conflicting
|
||||
/// batches, the Block-STM committed state MUST equal the sequential oracle for
|
||||
/// EVERY batch and EVERY thread count.
|
||||
fn harness() {
|
||||
let thread_counts = [1usize, 2, 4, 8, 16];
|
||||
// (num_txns, num_accounts, hot_accounts, contracts, hot_slots, conflict_ppm, label)
|
||||
let profiles: &[(usize, u64, u64, u64, u64, u64, &str)] = &[
|
||||
(200, 500, 50, 4, 4, 200_000, "mixed-moderate"),
|
||||
(500, 1000, 20, 2, 2, 500_000, "high-conflict"),
|
||||
(1000, 5000, 200, 8, 8, 100_000, "low-conflict"),
|
||||
(300, 64, 8, 1, 1, 900_000, "brutal-single-counter"),
|
||||
(400, 200, 200, 1, 1, 1_000_000, "all-hot"),
|
||||
(50, 20, 4, 1, 1, 1_000_000, "tiny-dense"),
|
||||
];
|
||||
|
||||
let mut total_cases = 0usize;
|
||||
let mut mismatches = 0usize;
|
||||
let batches_per_profile = 200u64;
|
||||
|
||||
for (ntx, nacc, hot, ncon, hslots, conf, label) in profiles.iter().copied() {
|
||||
for seed in 0..batches_per_profile {
|
||||
let cfg = WorkloadConfig {
|
||||
num_txns: ntx,
|
||||
num_accounts: nacc,
|
||||
num_hot_accounts: hot,
|
||||
num_contracts: ncon,
|
||||
num_hot_slots: hslots,
|
||||
conflict_ppm: conf,
|
||||
gas: 4, // small; correctness is independent of gas
|
||||
seed: 0xAE_5E_0000 ^ (seed.wrapping_mul(0x9E3779B9)),
|
||||
};
|
||||
let base = make_base_state(&cfg);
|
||||
let txns = generate_batch(&cfg);
|
||||
|
||||
let seq = execute_block_sequential(&base, &txns);
|
||||
for &threads in &thread_counts {
|
||||
let (par, _stats) = execute_block_parallel(&base, &txns, threads);
|
||||
total_cases += 1;
|
||||
if par != seq {
|
||||
mismatches += 1;
|
||||
eprintln!(
|
||||
"MISMATCH profile={label} seed={seed} threads={threads}: parallel != sequential"
|
||||
);
|
||||
// show a couple of differing keys for debugging
|
||||
let mut shown = 0;
|
||||
for (k, v) in &seq {
|
||||
let pv = par.get(k).copied().unwrap_or(0);
|
||||
if pv != *v {
|
||||
eprintln!(" key {:?}: seq={} par={}", k, v, pv);
|
||||
shown += 1;
|
||||
if shown >= 5 {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
eprintln!(" profile '{label}' done");
|
||||
}
|
||||
|
||||
println!("=======================================================");
|
||||
println!("CORRECTNESS HARNESS");
|
||||
println!(" profiles : {}", profiles.len());
|
||||
println!(" batches per profile : {batches_per_profile}");
|
||||
println!(" thread counts : {:?}", thread_counts);
|
||||
println!(
|
||||
" total (batch x threads) comparisons: {total_cases}"
|
||||
);
|
||||
println!(" mismatches : {mismatches}");
|
||||
if mismatches == 0 {
|
||||
println!(" RESULT: PASS -- parallel Block-STM == sequential on ALL cases");
|
||||
} else {
|
||||
println!(" RESULT: FAIL -- serializability violated");
|
||||
std::process::exit(1);
|
||||
}
|
||||
println!("=======================================================");
|
||||
}
|
||||
|
||||
/// -------------------------------------------------------------------- bench
|
||||
fn median(mut v: Vec<f64>) -> f64 {
|
||||
v.sort_by(|a, b| a.partial_cmp(b).unwrap());
|
||||
v[v.len() / 2]
|
||||
}
|
||||
|
||||
fn time_parallel(base: &StateMap, txns: &[Txn], threads: usize, reps: usize) -> (f64, aere_block_stm::ExecStats) {
|
||||
let mut times = Vec::new();
|
||||
let mut last = None;
|
||||
for _ in 0..reps {
|
||||
let t0 = Instant::now();
|
||||
let (_state, stats) = execute_block_parallel(base, txns, threads);
|
||||
times.push(t0.elapsed().as_secs_f64() * 1000.0);
|
||||
last = Some(stats);
|
||||
}
|
||||
(median(times), last.unwrap())
|
||||
}
|
||||
|
||||
fn time_sequential(base: &StateMap, txns: &[Txn], reps: usize) -> f64 {
|
||||
let mut times = Vec::new();
|
||||
for _ in 0..reps {
|
||||
let t0 = Instant::now();
|
||||
let _ = execute_block_sequential(base, txns);
|
||||
times.push(t0.elapsed().as_secs_f64() * 1000.0);
|
||||
}
|
||||
median(times)
|
||||
}
|
||||
|
||||
fn report_scenario(
|
||||
label: &str,
|
||||
base: &StateMap,
|
||||
txns: &[Txn],
|
||||
thread_counts: &[usize],
|
||||
reps: usize,
|
||||
) {
|
||||
// sanity: this exact batch is serializable (spot-check once at max threads)
|
||||
let seq_state = execute_block_sequential(base, txns);
|
||||
let (par_state, _) = execute_block_parallel(base, txns, 16);
|
||||
let ok = seq_state == par_state;
|
||||
|
||||
let seq_ms = time_sequential(base, txns, reps);
|
||||
|
||||
println!();
|
||||
println!("### {label}");
|
||||
println!(" parallel==sequential check: {}", if ok { "PASS" } else { "FAIL" });
|
||||
println!(" sequential baseline : {seq_ms:.2} ms");
|
||||
println!(
|
||||
" {:<8} {:>10} {:>10} {:>12} {:>10} {:>10}",
|
||||
"threads", "time(ms)", "speedup", "vs-1thread", "aborts", "abort%"
|
||||
);
|
||||
let mut t1 = None;
|
||||
for &threads in thread_counts {
|
||||
let (ms, stats) = time_parallel(base, txns, threads, reps);
|
||||
if threads == 1 {
|
||||
t1 = Some(ms);
|
||||
}
|
||||
let speedup = seq_ms / ms;
|
||||
let vs1 = t1.map(|a| a / ms).unwrap_or(1.0);
|
||||
let abort_pct =
|
||||
100.0 * stats.total_aborts as f64 / stats.total_executions.max(1) as f64;
|
||||
println!(
|
||||
" {:<8} {:>10.2} {:>9.2}x {:>11.2}x {:>10} {:>9.1}%",
|
||||
threads, ms, speedup, vs1, stats.total_aborts, abort_pct
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
fn bench() {
|
||||
// optional args: aere-block-stm bench [gas] [ntx]
|
||||
let argv: Vec<String> = std::env::args().collect();
|
||||
let gas: u32 = argv.get(2).and_then(|s| s.parse().ok()).unwrap_or(80);
|
||||
let ntx: usize = argv.get(3).and_then(|s| s.parse().ok()).unwrap_or(20_000);
|
||||
|
||||
let thread_counts = [1usize, 2, 4, 8, 16];
|
||||
let reps = 7usize;
|
||||
|
||||
let scenarios: &[(&str, WorkloadConfig)] = &[
|
||||
(
|
||||
"LOW-CONFLICT (50k accounts, few hot, ~2% hot-touch)",
|
||||
WorkloadConfig {
|
||||
num_txns: ntx,
|
||||
num_accounts: 50_000,
|
||||
num_hot_accounts: 100,
|
||||
num_contracts: 64,
|
||||
num_hot_slots: 64,
|
||||
conflict_ppm: 20_000, // 2%
|
||||
gas,
|
||||
seed: 12345,
|
||||
},
|
||||
),
|
||||
(
|
||||
"MEDIUM-CONFLICT (5k accounts, ~15% hot-touch)",
|
||||
WorkloadConfig {
|
||||
num_txns: ntx,
|
||||
num_accounts: 5_000,
|
||||
num_hot_accounts: 50,
|
||||
num_contracts: 16,
|
||||
num_hot_slots: 16,
|
||||
conflict_ppm: 150_000, // 15%
|
||||
gas,
|
||||
seed: 999,
|
||||
},
|
||||
),
|
||||
(
|
||||
"HIGH-CONFLICT (single hot counter, ~80% hot-touch)",
|
||||
WorkloadConfig {
|
||||
num_txns: ntx,
|
||||
num_accounts: 2_000,
|
||||
num_hot_accounts: 20,
|
||||
num_contracts: 1,
|
||||
num_hot_slots: 1,
|
||||
conflict_ppm: 800_000, // 80%
|
||||
gas,
|
||||
seed: 7,
|
||||
},
|
||||
),
|
||||
];
|
||||
|
||||
println!("=======================================================");
|
||||
println!("BLOCK-STM SPEEDUP BENCHMARK");
|
||||
println!(" machine threads available: {}", std::thread::available_parallelism().map(|n| n.get()).unwrap_or(0));
|
||||
println!(" txns per batch : {ntx}");
|
||||
println!(" simulated EVM gas per tx : {gas} keccak-rounds");
|
||||
println!(" timing : median of {reps} runs (ms)");
|
||||
println!("=======================================================");
|
||||
|
||||
for (label, cfg) in scenarios {
|
||||
let base = make_base_state(cfg);
|
||||
let txns = generate_batch(cfg);
|
||||
report_scenario(label, &base, &txns, &thread_counts, reps);
|
||||
}
|
||||
|
||||
// PATHOLOGICAL fully-serial case: every single transaction increments the
|
||||
// SAME slot. There is zero available parallelism; Block-STM must serialize
|
||||
// all of them and pays extra for aborted speculation. This is the honest
|
||||
// worst case -- speedup should be ~1x or below.
|
||||
{
|
||||
let mut base = StateMap::new();
|
||||
base.insert(Key::Storage(0, 0), 0);
|
||||
let txns: Vec<Txn> = (0..ntx)
|
||||
.map(|_| Txn {
|
||||
kind: TxKind::Increment { contract: 0, slot: 0 },
|
||||
gas,
|
||||
})
|
||||
.collect();
|
||||
report_scenario(
|
||||
"PATHOLOGICAL (every tx increments the SAME slot: no parallelism)",
|
||||
&base,
|
||||
&txns,
|
||||
&thread_counts,
|
||||
reps,
|
||||
);
|
||||
}
|
||||
|
||||
println!();
|
||||
println!("Note: speedup is vs the sequential oracle. High-conflict batches");
|
||||
println!("serialize on the hot slot and are expected to show little or no");
|
||||
println!("speedup -- that is the honest, correct behavior of Block-STM.");
|
||||
}
|
||||
|
||||
/// ------------------------------------------------------------------ ab
|
||||
/// The predictive-scheduler CEILING experiment.
|
||||
///
|
||||
/// For each workload profile x thread count we run Block-STM TWICE on the SAME
|
||||
/// batch: once as-is (WITHOUT prediction) and once WITH advisory pre-parking
|
||||
/// driven by PERFECT read/write sets (the exact conflicts of the real serial
|
||||
/// execution). Perfect prediction is the best any predictive scheduler could
|
||||
/// ever do, so the gap between the two arms is the theoretical UPPER BOUND on
|
||||
/// what prediction can buy. We report the abort-rate delta (the direct metric)
|
||||
/// and the wall-time delta, NOT throughput multipliers.
|
||||
fn median_f64(mut v: Vec<f64>) -> f64 {
|
||||
v.sort_by(|a, b| a.partial_cmp(b).unwrap());
|
||||
v[v.len() / 2]
|
||||
}
|
||||
fn median_usize(mut v: Vec<usize>) -> usize {
|
||||
v.sort_unstable();
|
||||
v[v.len() / 2]
|
||||
}
|
||||
|
||||
/// Summary of one arm at one thread count: median wall-time (ms), median abort
|
||||
/// count, median executions, median predicted-parks.
|
||||
struct ArmSummary {
|
||||
ms: f64,
|
||||
aborts: usize,
|
||||
executions: usize,
|
||||
parks: usize,
|
||||
}
|
||||
|
||||
fn run_arm(
|
||||
base: &StateMap,
|
||||
txns: &[Txn],
|
||||
threads: usize,
|
||||
reps: usize,
|
||||
preds: Option<&Vec<Vec<usize>>>,
|
||||
) -> ArmSummary {
|
||||
let mut times = Vec::with_capacity(reps);
|
||||
let mut aborts = Vec::with_capacity(reps);
|
||||
let mut execs = Vec::with_capacity(reps);
|
||||
let mut parks = Vec::with_capacity(reps);
|
||||
for _ in 0..reps {
|
||||
let t0 = Instant::now();
|
||||
let (_state, stats) = match preds {
|
||||
None => execute_block_parallel(base, txns, threads),
|
||||
Some(p) => execute_block_parallel_predicted(base, txns, threads, p.clone()),
|
||||
};
|
||||
times.push(t0.elapsed().as_secs_f64() * 1000.0);
|
||||
aborts.push(stats.total_aborts);
|
||||
execs.push(stats.total_executions);
|
||||
parks.push(stats.total_predicted_parks);
|
||||
}
|
||||
ArmSummary {
|
||||
ms: median_f64(times),
|
||||
aborts: median_usize(aborts),
|
||||
executions: median_usize(execs),
|
||||
parks: median_usize(parks),
|
||||
}
|
||||
}
|
||||
|
||||
fn ab_experiment() {
|
||||
// optional args: aere-block-stm ab [gas] [ntx] [reps]
|
||||
let argv: Vec<String> = std::env::args().collect();
|
||||
let gas: u32 = argv.get(2).and_then(|s| s.parse().ok()).unwrap_or(60);
|
||||
let ntx: usize = argv.get(3).and_then(|s| s.parse().ok()).unwrap_or(6_000);
|
||||
let reps: usize = argv.get(4).and_then(|s| s.parse().ok()).unwrap_or(5);
|
||||
let thread_counts = [1usize, 2, 4, 8, 16];
|
||||
|
||||
// Six workload profiles spanning the conflict spectrum. All are
|
||||
// parameterizations of the same deterministic SplitMix64 generator; the
|
||||
// labels describe the conflict structure each one produces, honestly.
|
||||
let profiles: &[(&str, bool, WorkloadConfig)] = &[
|
||||
// (label, is_low_or_medium_traffic (counts toward the headroom verdict), cfg)
|
||||
(
|
||||
"uniform (no designed hot set, ~0% hot-touch)",
|
||||
true,
|
||||
WorkloadConfig {
|
||||
num_txns: ntx, num_accounts: 50_000, num_hot_accounts: 0,
|
||||
num_contracts: 64, num_hot_slots: 64, conflict_ppm: 0, gas, seed: 101,
|
||||
},
|
||||
),
|
||||
(
|
||||
"low-conflict (~2% hot-touch)",
|
||||
true,
|
||||
WorkloadConfig {
|
||||
num_txns: ntx, num_accounts: 50_000, num_hot_accounts: 100,
|
||||
num_contracts: 64, num_hot_slots: 64, conflict_ppm: 20_000, gas, seed: 202,
|
||||
},
|
||||
),
|
||||
(
|
||||
"medium-conflict (~15% hot-touch)",
|
||||
true,
|
||||
WorkloadConfig {
|
||||
num_txns: ntx, num_accounts: 5_000, num_hot_accounts: 50,
|
||||
num_contracts: 16, num_hot_slots: 16, conflict_ppm: 150_000, gas, seed: 303,
|
||||
},
|
||||
),
|
||||
(
|
||||
"disjoint-hot-groups (32 contracts x 32 slots, ~40% hot-touch)",
|
||||
false,
|
||||
WorkloadConfig {
|
||||
num_txns: ntx, num_accounts: 5_000, num_hot_accounts: 200,
|
||||
num_contracts: 32, num_hot_slots: 32, conflict_ppm: 400_000, gas, seed: 404,
|
||||
},
|
||||
),
|
||||
(
|
||||
"single-hot-slot (1 contract, 1 slot, ~70% hot-touch)",
|
||||
false,
|
||||
WorkloadConfig {
|
||||
num_txns: ntx, num_accounts: 2_000, num_hot_accounts: 20,
|
||||
num_contracts: 1, num_hot_slots: 1, conflict_ppm: 700_000, gas, seed: 505,
|
||||
},
|
||||
),
|
||||
(
|
||||
"high-conflict (2 contracts, 2 slots, ~80% hot-touch)",
|
||||
false,
|
||||
WorkloadConfig {
|
||||
num_txns: ntx, num_accounts: 2_000, num_hot_accounts: 20,
|
||||
num_contracts: 2, num_hot_slots: 2, conflict_ppm: 800_000, gas, seed: 606,
|
||||
},
|
||||
),
|
||||
];
|
||||
|
||||
println!("=======================================================");
|
||||
println!("PREDICTIVE-SCHEDULER CEILING (perfect-prediction A/B)");
|
||||
println!(" machine threads available : {}", std::thread::available_parallelism().map(|n| n.get()).unwrap_or(0));
|
||||
println!(" txns per batch : {ntx}");
|
||||
println!(" simulated EVM gas per tx : {gas} keccak-rounds");
|
||||
println!(" timing : median of {reps} runs (ms)");
|
||||
println!(" prediction : PERFECT exact-RW-set pre-parking (upper bound)");
|
||||
println!(" metric of record : TOTAL_ABORTS + wall-time, WITH vs WITHOUT");
|
||||
println!("=======================================================");
|
||||
|
||||
// Accumulators for the verdict: wall-time deltas on low/medium/uniform
|
||||
// ("most real traffic"), and abort-rate deltas everywhere.
|
||||
let mut lowmed_wall_deltas: Vec<f64> = Vec::new();
|
||||
let mut lowmed_abort_pp: Vec<f64> = Vec::new();
|
||||
let mut worst_case_wall_help = f64::NEG_INFINITY;
|
||||
let mut worst_case_label = String::new();
|
||||
|
||||
for (label, is_lowmed, cfg) in profiles.iter() {
|
||||
let base = make_base_state(cfg);
|
||||
let txns = generate_batch(cfg);
|
||||
|
||||
// PERFECT predicted soft-dependencies for this exact batch.
|
||||
let preds = compute_predicted_preds(&base, &txns);
|
||||
|
||||
// Correctness: both arms must equal the sequential oracle (spot-check @16).
|
||||
let seq = execute_block_sequential(&base, &txns);
|
||||
let (par_no, _) = execute_block_parallel(&base, &txns, 16);
|
||||
let (par_yes, _) =
|
||||
execute_block_parallel_predicted(&base, &txns, 16, preds.clone());
|
||||
let ok = par_no == seq && par_yes == seq;
|
||||
|
||||
// Average predicted predecessors per txn (structure of the profile).
|
||||
let avg_deps: f64 =
|
||||
preds.iter().map(|p| p.len()).sum::<usize>() as f64 / preds.len() as f64;
|
||||
|
||||
println!();
|
||||
println!("### {label}");
|
||||
println!(" correctness (both arms == sequential oracle): {}", if ok { "PASS" } else { "FAIL" });
|
||||
println!(" avg predicted predecessors / txn : {avg_deps:.3}");
|
||||
println!(
|
||||
" {:<8} {:>10} {:>10} {:>9} | {:>9} {:>9} {:>9} | {:>8}",
|
||||
"threads", "no_ms", "pred_ms", "wallD%", "no_ab%", "pred_ab%", "abD_pp", "parks"
|
||||
);
|
||||
if !ok {
|
||||
println!(" !! correctness FAILED -- numbers below are meaningless, aborting");
|
||||
std::process::exit(1);
|
||||
}
|
||||
|
||||
for &threads in &thread_counts {
|
||||
let no = run_arm(&base, &txns, threads, reps, None);
|
||||
let yes = run_arm(&base, &txns, threads, reps, Some(&preds));
|
||||
|
||||
let no_ab_pct = 100.0 * no.aborts as f64 / no.executions.max(1) as f64;
|
||||
let yes_ab_pct = 100.0 * yes.aborts as f64 / yes.executions.max(1) as f64;
|
||||
let abort_delta_pp = no_ab_pct - yes_ab_pct; // positive = prediction removed aborts
|
||||
// wall delta: positive % = prediction was FASTER (saved wall time)
|
||||
let wall_delta = 100.0 * (no.ms - yes.ms) / no.ms;
|
||||
|
||||
println!(
|
||||
" {:<8} {:>10.2} {:>10.2} {:>8.1}% | {:>8.2}% {:>8.2}% {:>9.2} | {:>8}",
|
||||
threads, no.ms, yes.ms, wall_delta, no_ab_pct, yes_ab_pct, abort_delta_pp, yes.parks
|
||||
);
|
||||
|
||||
if *is_lowmed && threads > 1 {
|
||||
lowmed_wall_deltas.push(wall_delta);
|
||||
lowmed_abort_pp.push(abort_delta_pp);
|
||||
}
|
||||
if threads > 1 && wall_delta > worst_case_wall_help {
|
||||
worst_case_wall_help = wall_delta;
|
||||
worst_case_label = format!("{label} @ {threads}t");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let mean_lowmed_wall = if lowmed_wall_deltas.is_empty() {
|
||||
0.0
|
||||
} else {
|
||||
lowmed_wall_deltas.iter().sum::<f64>() / lowmed_wall_deltas.len() as f64
|
||||
};
|
||||
let max_lowmed_wall = lowmed_wall_deltas
|
||||
.iter()
|
||||
.cloned()
|
||||
.fold(f64::NEG_INFINITY, f64::max);
|
||||
let mean_lowmed_abort_pp = if lowmed_abort_pp.is_empty() {
|
||||
0.0
|
||||
} else {
|
||||
lowmed_abort_pp.iter().sum::<f64>() / lowmed_abort_pp.len() as f64
|
||||
};
|
||||
|
||||
println!();
|
||||
println!("=======================================================");
|
||||
println!("VERDICT (perfect-prediction ceiling)");
|
||||
println!(" low/medium/uniform traffic (>1 thread):");
|
||||
println!(" mean wall-time saved by PERFECT prediction : {mean_lowmed_wall:+.2}%");
|
||||
println!(" max wall-time saved by PERFECT prediction : {max_lowmed_wall:+.2}%");
|
||||
println!(" mean abort-rate reduction : {mean_lowmed_abort_pp:+.2} pp");
|
||||
println!(" best single wall-time help anywhere (>1t) : {worst_case_wall_help:+.2}% [{worst_case_label}]");
|
||||
println!("=======================================================");
|
||||
println!();
|
||||
println!("READ THIS: The numbers above are an UPPER BOUND -- the predictor is fed the");
|
||||
println!("exact conflicts of the real serial run, which no deployable predictor can match.");
|
||||
println!("On low/medium-conflict traffic (the bulk of real blocks) Block-STM already");
|
||||
println!("discovers dependencies dynamically and its abort rate is low, so even a PERFECT");
|
||||
println!("predictor removes only a small residue of aborts and the wall-time it saves is");
|
||||
println!("within noise. Prediction only visibly helps on pathological high-conflict batches,");
|
||||
println!("where it converts speculative-then-aborted work into ordered waits. Because the");
|
||||
println!("predictions are ADVISORY (they change only WHEN a tx is attempted, never the");
|
||||
println!("committed state -- validation stays the sole authority), a wrong or approximate");
|
||||
println!("prediction can never corrupt the state root; at worst it fails to help. Conclusion:");
|
||||
println!("a predictive pre-parking lever is safe to add but is NOT a throughput win for");
|
||||
println!("mainstream traffic; keep it advisory and off the correctness path.");
|
||||
}
|
||||
|
||||
/// --------------------------------------------------------------- endtoend
|
||||
/// END-TO-END block-production throughput.
|
||||
///
|
||||
/// The `bench` command times ONLY the parallel Block-STM execution phase (the
|
||||
/// part that scales) and reports the well-known ~9.4x-at-16-cores figure. That
|
||||
/// is an honest EXECUTION-PHASE number, but it is NOT an end-to-end block
|
||||
/// throughput number: a real block is not finished when execution finishes, it
|
||||
/// is finished when the resulting state is committed and a state root is
|
||||
/// produced. Those tail phases are (largely) SERIAL, so by Amdahl's law the
|
||||
/// end-to-end speedup must be LOWER than the execution-phase speedup.
|
||||
///
|
||||
/// This command measures the FULL pipeline, parallel vs sequential, at 1/2/4/8/16
|
||||
/// workers and across conflict ratios, timing three phases:
|
||||
/// 1. exec -- Block-STM parallel execution + validation + re-execution (PARALLEL)
|
||||
/// 2. commit -- MvMemory::snapshot_final: materialize the flat state (SERIAL)
|
||||
/// 3. root -- keccak256 state root over the committed state (SERIAL)
|
||||
/// end-to-end = exec + commit + root. The sequential baseline is
|
||||
/// execute_block_sequential (produces the live map directly) + root.
|
||||
///
|
||||
/// Serial-equivalence is CHECKED on every single run: the parallel committed
|
||||
/// state must equal the sequential oracle AND the two keccak state roots must be
|
||||
/// bit-identical. A single mismatch aborts the whole run.
|
||||
fn med(mut v: Vec<f64>) -> f64 {
|
||||
v.sort_by(|a, b| a.partial_cmp(b).unwrap());
|
||||
v[v.len() / 2]
|
||||
}
|
||||
|
||||
fn endtoend() {
|
||||
// args: aere-block-stm endtoend [gas] [ntx] [reps]
|
||||
// gas=80, ntx=20000 match the documented execution-phase regime so the
|
||||
// end-to-end numbers are directly comparable to the ~9.4x figure.
|
||||
let argv: Vec<String> = std::env::args().collect();
|
||||
let gas: u32 = argv.get(2).and_then(|s| s.parse().ok()).unwrap_or(80);
|
||||
let ntx: usize = argv.get(3).and_then(|s| s.parse().ok()).unwrap_or(20_000);
|
||||
let reps: usize = argv.get(4).and_then(|s| s.parse().ok()).unwrap_or(7);
|
||||
let thread_counts = [1usize, 2, 4, 8, 16];
|
||||
|
||||
// Conflict-ratio spectrum. `conflict_ppm` tunes how often a tx touches the
|
||||
// shared hot set; with few contracts/slots the increments/AMM swaps also
|
||||
// concentrate, so the labelled % is the DESIGNED hot-touch rate. The honest
|
||||
// realized-conflict indicator is the measured abort% printed per row.
|
||||
let scenarios: &[(&str, WorkloadConfig)] = &[
|
||||
(
|
||||
"0% designed conflict (disjoint accounts, 64 contracts spread)",
|
||||
WorkloadConfig {
|
||||
num_txns: ntx, num_accounts: 50_000, num_hot_accounts: 0,
|
||||
num_contracts: 64, num_hot_slots: 64, conflict_ppm: 0, gas, seed: 101,
|
||||
},
|
||||
),
|
||||
(
|
||||
"10% designed conflict (100 hot accts, 16 contracts)",
|
||||
WorkloadConfig {
|
||||
num_txns: ntx, num_accounts: 20_000, num_hot_accounts: 100,
|
||||
num_contracts: 16, num_hot_slots: 16, conflict_ppm: 100_000, gas, seed: 202,
|
||||
},
|
||||
),
|
||||
(
|
||||
"50% designed conflict (20 hot accts, 4 contracts)",
|
||||
WorkloadConfig {
|
||||
num_txns: ntx, num_accounts: 10_000, num_hot_accounts: 20,
|
||||
num_contracts: 4, num_hot_slots: 4, conflict_ppm: 500_000, gas, seed: 303,
|
||||
},
|
||||
),
|
||||
(
|
||||
"PATHOLOGICAL ~100% (single hot slot, no parallelism)",
|
||||
WorkloadConfig {
|
||||
num_txns: ntx, num_accounts: 2_000, num_hot_accounts: 20,
|
||||
num_contracts: 1, num_hot_slots: 1, conflict_ppm: 1_000_000, gas, seed: 404,
|
||||
},
|
||||
),
|
||||
];
|
||||
|
||||
println!("=======================================================");
|
||||
println!("BLOCK-STM END-TO-END BLOCK-PRODUCTION THROUGHPUT");
|
||||
println!(" machine threads available : {}", std::thread::available_parallelism().map(|n| n.get()).unwrap_or(0));
|
||||
println!(" txns per batch : {ntx}");
|
||||
println!(" simulated EVM cost per tx : {gas} keccak-rounds");
|
||||
println!(" timing : median of {reps} runs (ms)");
|
||||
println!(" pipeline : exec (parallel) + commit (serial) + state-root (serial)");
|
||||
println!(" serial-equivalence : par state == seq state AND keccak roots identical, EVERY run");
|
||||
println!("=======================================================");
|
||||
|
||||
let mut total_equiv_checks = 0usize;
|
||||
let mut total_mismatches = 0usize;
|
||||
|
||||
for (label, cfg) in scenarios {
|
||||
let base = make_base_state(cfg);
|
||||
let txns = generate_batch(cfg);
|
||||
|
||||
// ---- sequential oracle + baseline timing ----
|
||||
let seq_state = execute_block_sequential(&base, &txns);
|
||||
let seq_root = state_root(&seq_state);
|
||||
let seq_root_hex = hex32(&seq_root);
|
||||
let state_keys = seq_state.iter().filter(|(_, v)| **v != 0).count();
|
||||
|
||||
let mut seq_exec_v = Vec::with_capacity(reps);
|
||||
let mut seq_root_v = Vec::with_capacity(reps);
|
||||
for _ in 0..reps {
|
||||
let t0 = Instant::now();
|
||||
let s = execute_block_sequential(&base, &txns);
|
||||
let t1 = Instant::now();
|
||||
let r = state_root(&s);
|
||||
let t2 = Instant::now();
|
||||
std::hint::black_box(&r);
|
||||
seq_exec_v.push(t1.duration_since(t0).as_secs_f64() * 1000.0);
|
||||
seq_root_v.push(t2.duration_since(t1).as_secs_f64() * 1000.0);
|
||||
}
|
||||
let seq_exec_ms = med(seq_exec_v);
|
||||
let seq_root_ms = med(seq_root_v);
|
||||
let seq_e2e_ms = seq_exec_ms + seq_root_ms;
|
||||
|
||||
println!();
|
||||
println!("### {label}");
|
||||
println!(" committed state keys : {state_keys}");
|
||||
println!(" sequential exec {:.2} ms | root {:.2} ms | END-TO-END {:.2} ms | root-share {:.1}%",
|
||||
seq_exec_ms, seq_root_ms, seq_e2e_ms, 100.0 * seq_root_ms / seq_e2e_ms);
|
||||
println!(" sequential end-to-end TPS: {:.0}", ntx as f64 / seq_e2e_ms * 1000.0);
|
||||
println!(
|
||||
" {:<4} | {:>8} {:>8} {:>7} {:>9} | {:>9} {:>9} | {:>7} {:>7} | {:>9} | {:>6}",
|
||||
"W", "exec", "commit", "root", "e2e_ms", "execSpd", "e2eSpd", "TPS", "abort%", "root%e2e", "equiv"
|
||||
);
|
||||
|
||||
for &w in &thread_counts {
|
||||
let mut ex_v = Vec::with_capacity(reps);
|
||||
let mut cm_v = Vec::with_capacity(reps);
|
||||
let mut rt_v = Vec::with_capacity(reps);
|
||||
let mut abort_v = Vec::with_capacity(reps);
|
||||
let mut equiv_ok = true;
|
||||
for _ in 0..reps {
|
||||
let (state, stats, exec_dur, commit_dur) =
|
||||
execute_block_parallel_split(&base, &txns, w);
|
||||
let tr0 = Instant::now();
|
||||
let root = state_root(&state);
|
||||
let root_ms = tr0.elapsed().as_secs_f64() * 1000.0;
|
||||
|
||||
// serial-equivalence check, EVERY run
|
||||
total_equiv_checks += 1;
|
||||
let ok = state == seq_state && hex32(&root) == seq_root_hex;
|
||||
if !ok {
|
||||
equiv_ok = false;
|
||||
total_mismatches += 1;
|
||||
eprintln!(" !! SERIAL-EQUIVALENCE MISMATCH scenario='{label}' W={w}");
|
||||
}
|
||||
ex_v.push(exec_dur.as_secs_f64() * 1000.0);
|
||||
cm_v.push(commit_dur.as_secs_f64() * 1000.0);
|
||||
rt_v.push(root_ms);
|
||||
abort_v.push(
|
||||
100.0 * stats.total_aborts as f64 / stats.total_executions.max(1) as f64,
|
||||
);
|
||||
}
|
||||
let exec_ms = med(ex_v);
|
||||
let commit_ms = med(cm_v);
|
||||
let root_ms = med(rt_v);
|
||||
let e2e_ms = exec_ms + commit_ms + root_ms;
|
||||
let abort_pct = med(abort_v);
|
||||
let exec_spd = seq_exec_ms / exec_ms;
|
||||
let e2e_spd = seq_e2e_ms / e2e_ms;
|
||||
let tps = ntx as f64 / e2e_ms * 1000.0;
|
||||
let root_share = 100.0 * (commit_ms + root_ms) / e2e_ms;
|
||||
println!(
|
||||
" {:<4} | {:>8.2} {:>8.2} {:>7.2} {:>9.2} | {:>8.2}x {:>8.2}x | {:>7.0} {:>6.1}% | {:>8.1}% | {:>6}",
|
||||
w, exec_ms, commit_ms, root_ms, e2e_ms, exec_spd, e2e_spd, tps, abort_pct, root_share,
|
||||
if equiv_ok { "PASS" } else { "FAIL" }
|
||||
);
|
||||
}
|
||||
|
||||
// Amdahl ceiling for END-TO-END speedup from the measured serial tail.
|
||||
// Serial tail (commit + root) is fixed regardless of workers; only exec
|
||||
// scales. Ceiling = seq_e2e / (serial_tail + exec_floor). We use the
|
||||
// measured 16-worker commit+root as the serial tail and the measured
|
||||
// 16-worker exec as the (non-zero) exec floor.
|
||||
let (_st16, _ss16, ex16, cm16) = execute_block_parallel_split(&base, &txns, 16);
|
||||
let rt16 = {
|
||||
let s = execute_block_parallel_split(&base, &txns, 16).0;
|
||||
let t = Instant::now();
|
||||
let _ = state_root(&s);
|
||||
t.elapsed().as_secs_f64() * 1000.0
|
||||
};
|
||||
let ex16ms = ex16.as_secs_f64() * 1000.0;
|
||||
let cm16ms = cm16.as_secs_f64() * 1000.0;
|
||||
let serial_tail = cm16ms + rt16;
|
||||
let ceiling_with_floor = seq_e2e_ms / (serial_tail + ex16ms);
|
||||
let ceiling_infinite_cores = seq_e2e_ms / serial_tail;
|
||||
println!(
|
||||
" Amdahl: serial tail (commit+root) = {:.2} ms; end-to-end speedup ceiling @16w = {:.2}x, @infinite cores = {:.2}x",
|
||||
serial_tail, ceiling_with_floor, ceiling_infinite_cores
|
||||
);
|
||||
}
|
||||
|
||||
println!();
|
||||
println!("=======================================================");
|
||||
println!("SERIAL-EQUIVALENCE: {total_equiv_checks} checks, {total_mismatches} mismatches");
|
||||
if total_mismatches == 0 {
|
||||
println!(" RESULT: PASS -- every parallel run committed the exact sequential state + root");
|
||||
} else {
|
||||
println!(" RESULT: FAIL -- serializability violated; numbers are meaningless");
|
||||
std::process::exit(1);
|
||||
}
|
||||
println!("=======================================================");
|
||||
println!();
|
||||
println!("HONEST READING:");
|
||||
println!(" - execSpd is the EXECUTION-PHASE speedup (comparable to the documented ~9.4x).");
|
||||
println!(" - e2eSpd is the END-TO-END block-production speedup. It is LOWER, because the");
|
||||
println!(" commit (state materialization) and state-root (keccak) phases are serial and");
|
||||
println!(" do not shrink with more workers -- the Amdahl serial tail.");
|
||||
println!(" - TPS is the real end-to-end transactions/second of THIS pipeline on THIS box");
|
||||
println!(" (shared-vCPU cloud). It is a measured rollup-batch number, NOT the 273k TPS");
|
||||
println!(" documented-conditions architectural ceiling and does not reach or imply it.");
|
||||
println!(" - Under total conflict (pathological) there is no parallelism; end-to-end can be");
|
||||
println!(" <= 1x. That is the correct, honest behavior of Block-STM.");
|
||||
}
|
||||
|
||||
/// ----------------------------------------------------------------- execute
|
||||
/// Wire protocol (stdin), one record per line:
|
||||
/// THREADS <n>
|
||||
/// BASE <key> <value>
|
||||
/// TX TRANSFER <from> <to> <amount>
|
||||
/// TX SWEEP <from> <to>
|
||||
/// TX INC <contract> <slot>
|
||||
/// TX AMM <contract> <xslot> <yslot> <dx>
|
||||
/// where <key> is "B:<acct>" or "S:<contract>:<slot>".
|
||||
///
|
||||
/// Emits a JSON object on stdout with the committed keccak256 state root and a
|
||||
/// self-check that parallel execution equaled sequential execution.
|
||||
fn parse_key(s: &str) -> Option<Key> {
|
||||
if let Some(rest) = s.strip_prefix("B:") {
|
||||
Some(Key::Balance(rest.parse().ok()?))
|
||||
} else if let Some(rest) = s.strip_prefix("S:") {
|
||||
let mut it = rest.split(':');
|
||||
let c = it.next()?.parse().ok()?;
|
||||
let slot = it.next()?.parse().ok()?;
|
||||
Some(Key::Storage(c, slot))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
fn key_to_wire(k: &Key) -> String {
|
||||
match k {
|
||||
Key::Balance(a) => format!("B:{a}"),
|
||||
Key::Storage(c, s) => format!("S:{c}:{s}"),
|
||||
}
|
||||
}
|
||||
|
||||
fn execute_stdin() {
|
||||
let mut input = String::new();
|
||||
std::io::stdin().read_to_string(&mut input).unwrap();
|
||||
|
||||
let mut threads = std::thread::available_parallelism()
|
||||
.map(|n| n.get())
|
||||
.unwrap_or(4);
|
||||
let mut gas: u32 = 0;
|
||||
let mut base = StateMap::new();
|
||||
let mut txns: Vec<Txn> = Vec::new();
|
||||
|
||||
for line in input.lines() {
|
||||
let line = line.trim();
|
||||
if line.is_empty() {
|
||||
continue;
|
||||
}
|
||||
let f: Vec<&str> = line.split_whitespace().collect();
|
||||
match f[0] {
|
||||
"THREADS" => threads = f[1].parse().unwrap_or(threads),
|
||||
"GAS" => gas = f[1].parse().unwrap_or(0),
|
||||
"BASE" => {
|
||||
if let Some(k) = parse_key(f[1]) {
|
||||
base.insert(k, f[2].parse().unwrap_or(0));
|
||||
}
|
||||
}
|
||||
"TX" => {
|
||||
let kind = match f[1] {
|
||||
"TRANSFER" => TxKind::Transfer {
|
||||
from: f[2].parse().unwrap(),
|
||||
to: f[3].parse().unwrap(),
|
||||
amount: f[4].parse().unwrap(),
|
||||
},
|
||||
"SWEEP" => TxKind::Sweep {
|
||||
from: f[2].parse().unwrap(),
|
||||
to: f[3].parse().unwrap(),
|
||||
},
|
||||
"INC" => TxKind::Increment {
|
||||
contract: f[2].parse().unwrap(),
|
||||
slot: f[3].parse().unwrap(),
|
||||
},
|
||||
"AMM" => TxKind::AmmSwap {
|
||||
contract: f[2].parse().unwrap(),
|
||||
x_slot: f[3].parse().unwrap(),
|
||||
y_slot: f[4].parse().unwrap(),
|
||||
dx: f[5].parse().unwrap(),
|
||||
},
|
||||
_ => continue,
|
||||
};
|
||||
txns.push(Txn { kind, gas });
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
let seq = execute_block_sequential(&base, &txns);
|
||||
let (par, stats) = execute_block_parallel(&base, &txns, threads);
|
||||
let equal = seq == par;
|
||||
let root = state_root(&par);
|
||||
|
||||
// emit state entries (sorted) as JSON
|
||||
let mut entries: Vec<(Key, u128)> =
|
||||
par.iter().filter(|(_, v)| **v != 0).map(|(k, v)| (*k, *v)).collect();
|
||||
entries.sort();
|
||||
let mut state_json = String::from("[");
|
||||
for (i, (k, v)) in entries.iter().enumerate() {
|
||||
if i > 0 {
|
||||
state_json.push(',');
|
||||
}
|
||||
state_json.push_str(&format!("[\"{}\",\"{}\"]", key_to_wire(k), v));
|
||||
}
|
||||
state_json.push(']');
|
||||
|
||||
let out = format!(
|
||||
"{{\"stateRoot\":\"{}\",\"numTxns\":{},\"threads\":{},\"executions\":{},\"validations\":{},\"aborts\":{},\"parallelEqualsSequential\":{},\"state\":{}}}",
|
||||
hex32(&root),
|
||||
stats.num_txns,
|
||||
stats.num_threads,
|
||||
stats.total_executions,
|
||||
stats.total_validations,
|
||||
stats.total_aborts,
|
||||
equal,
|
||||
state_json,
|
||||
);
|
||||
let mut stdout = std::io::stdout();
|
||||
stdout.write_all(out.as_bytes()).unwrap();
|
||||
stdout.write_all(b"\n").unwrap();
|
||||
if !equal {
|
||||
eprintln!("FATAL: parallel result diverged from sequential -- root not trustworthy");
|
||||
std::process::exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
/// ------------------------------------------------------------------- tests
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
/// The canonical serializability probe: N transactions all increment the
|
||||
/// SAME counter slot. Sequential result is exactly N. Block-STM, despite
|
||||
/// running them in parallel and reordering, MUST also produce exactly N.
|
||||
#[test]
|
||||
fn shared_counter_is_serialized() {
|
||||
let contract = 0u64;
|
||||
let slot = 0u64;
|
||||
let n = 500usize;
|
||||
let mut base = StateMap::new();
|
||||
base.insert(Key::Storage(contract, slot), 0);
|
||||
let txns: Vec<Txn> = (0..n)
|
||||
.map(|_| Txn {
|
||||
kind: TxKind::Increment { contract, slot },
|
||||
gas: 0,
|
||||
})
|
||||
.collect();
|
||||
|
||||
for threads in [1usize, 2, 4, 8, 16] {
|
||||
let (par, _) = execute_block_parallel(&base, &txns, threads);
|
||||
assert_eq!(
|
||||
par.get(&Key::Storage(contract, slot)).copied().unwrap_or(0),
|
||||
n as u128,
|
||||
"counter not serialized at {threads} threads"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parallel_equals_sequential_random() {
|
||||
for seed in 0..40u64 {
|
||||
let cfg = WorkloadConfig {
|
||||
num_txns: 300,
|
||||
num_accounts: 400,
|
||||
num_hot_accounts: 16,
|
||||
num_contracts: 2,
|
||||
num_hot_slots: 2,
|
||||
conflict_ppm: 400_000,
|
||||
gas: 2,
|
||||
seed,
|
||||
};
|
||||
let base = make_base_state(&cfg);
|
||||
let txns = generate_batch(&cfg);
|
||||
let seq = execute_block_sequential(&base, &txns);
|
||||
for threads in [1usize, 3, 8, 16] {
|
||||
let (par, _) = execute_block_parallel(&base, &txns, threads);
|
||||
assert_eq!(par, seq, "seed {seed} threads {threads}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Advisory perfect prediction must NEVER change the committed state: the
|
||||
/// predicted arm has to equal the sequential oracle on every profile/thread
|
||||
/// count, exactly like the plain arm. This proves prediction is truly
|
||||
/// advisory (it reorders attempts, not results).
|
||||
#[test]
|
||||
fn predicted_equals_sequential_random() {
|
||||
for seed in 0..40u64 {
|
||||
let cfg = WorkloadConfig {
|
||||
num_txns: 300,
|
||||
num_accounts: 400,
|
||||
num_hot_accounts: 16,
|
||||
num_contracts: 2,
|
||||
num_hot_slots: 2,
|
||||
conflict_ppm: 400_000,
|
||||
gas: 2,
|
||||
seed,
|
||||
};
|
||||
let base = make_base_state(&cfg);
|
||||
let txns = generate_batch(&cfg);
|
||||
let seq = execute_block_sequential(&base, &txns);
|
||||
let preds = compute_predicted_preds(&base, &txns);
|
||||
for threads in [1usize, 3, 8, 16] {
|
||||
let (par, _) =
|
||||
execute_block_parallel_predicted(&base, &txns, threads, preds.clone());
|
||||
assert_eq!(par, seq, "predicted seed {seed} threads {threads}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// On the pathological all-same-slot batch, perfect prediction should drive
|
||||
/// aborts to zero (every increment is pre-parked behind its predecessor),
|
||||
/// while remaining serializable. The plain arm may abort; the predicted arm
|
||||
/// must both be correct AND have aborts <= the plain arm.
|
||||
#[test]
|
||||
fn prediction_eliminates_aborts_on_hot_slot() {
|
||||
let contract = 0u64;
|
||||
let slot = 0u64;
|
||||
let n = 400usize;
|
||||
let mut base = StateMap::new();
|
||||
base.insert(Key::Storage(contract, slot), 0);
|
||||
let txns: Vec<Txn> = (0..n)
|
||||
.map(|_| Txn {
|
||||
kind: TxKind::Increment { contract, slot },
|
||||
gas: 0,
|
||||
})
|
||||
.collect();
|
||||
let preds = compute_predicted_preds(&base, &txns);
|
||||
for threads in [2usize, 4, 8, 16] {
|
||||
let (par_no, s_no) = execute_block_parallel(&base, &txns, threads);
|
||||
let (par_yes, s_yes) =
|
||||
execute_block_parallel_predicted(&base, &txns, threads, preds.clone());
|
||||
// correctness: both equal exactly n
|
||||
assert_eq!(par_no.get(&Key::Storage(contract, slot)).copied().unwrap_or(0), n as u128);
|
||||
assert_eq!(par_yes.get(&Key::Storage(contract, slot)).copied().unwrap_or(0), n as u128);
|
||||
// perfect prediction must not INCREASE aborts, and here should zero them
|
||||
assert!(
|
||||
s_yes.total_aborts <= s_no.total_aborts,
|
||||
"prediction increased aborts at {threads}t: {} > {}",
|
||||
s_yes.total_aborts, s_no.total_aborts
|
||||
);
|
||||
assert_eq!(
|
||||
s_yes.total_aborts, 0,
|
||||
"perfect prediction should eliminate aborts on a pure chain at {threads}t (got {})",
|
||||
s_yes.total_aborts
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn keccak_known_answer() {
|
||||
// keccak256("") = c5d2460186f7233c927e7db2dcc703c0e500b653ca82273b7bfad8045d85a470
|
||||
let h = aere_block_stm::keccak::keccak256(b"");
|
||||
assert_eq!(
|
||||
hex32(&h),
|
||||
"0xc5d2460186f7233c927e7db2dcc703c0e500b653ca82273b7bfad8045d85a470"
|
||||
);
|
||||
// keccak256("abc") = 4e03657aea45a94fc7d47ba826c8d667c0d1e6e33a64a036ec44f58fa12d6c45
|
||||
let h2 = aere_block_stm::keccak::keccak256(b"abc");
|
||||
assert_eq!(
|
||||
hex32(&h2),
|
||||
"0x4e03657aea45a94fc7d47ba826c8d667c0d1e6e33a64a036ec44f58fa12d6c45"
|
||||
);
|
||||
}
|
||||
}
|
||||
182
parallel-executor/src/mvmemory.rs
Normal file
182
parallel-executor/src/mvmemory.rs
Normal file
@ -0,0 +1,182 @@
|
||||
//! Multi-version shared memory (MVMemory).
|
||||
//!
|
||||
//! For every `Key`, MVMemory keeps an ordered map `txn_index -> cell`, where a
|
||||
//! cell is either a concrete `Value` written by that transaction's latest
|
||||
//! incarnation, or an `Estimate` placeholder (a marker that a lower 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`. 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.
|
||||
//!
|
||||
//! Storage is sharded across many independent mutexes keyed by a hash of the
|
||||
//! `Key`, so non-conflicting transactions touch disjoint locks and run without
|
||||
//! contention. Hot keys (the counter, the AMM pool) serialize on one shard,
|
||||
//! which is exactly the honest source of the high-conflict slowdown.
|
||||
|
||||
use std::collections::BTreeMap;
|
||||
use std::collections::HashMap;
|
||||
use std::sync::Mutex;
|
||||
|
||||
use crate::types::{Incarnation, Key, TxnIndex, Value};
|
||||
|
||||
const SHARDS: usize = 256;
|
||||
|
||||
/// One stored cell for a (key, txn_index) pair.
|
||||
#[derive(Clone, Debug)]
|
||||
enum Cell {
|
||||
/// A concrete value written by this txn's incarnation.
|
||||
Value(Incarnation, Value),
|
||||
/// Placeholder: this txn wrote here before but is being re-executed.
|
||||
Estimate,
|
||||
}
|
||||
|
||||
/// What a read observed. This is recorded into the reader's read set so that
|
||||
/// validation can later re-check that the read would still return the same
|
||||
/// thing.
|
||||
#[derive(Clone, PartialEq, Eq, Debug)]
|
||||
pub enum ReadDescriptor {
|
||||
/// Nothing below the reader wrote this key: it came from base state.
|
||||
Base,
|
||||
/// It was written by `(txn_index, incarnation)`.
|
||||
Version(TxnIndex, Incarnation),
|
||||
}
|
||||
|
||||
/// The result of a live read against MVMemory.
|
||||
pub enum ReadResult {
|
||||
/// Read resolved to a concrete value with the given provenance.
|
||||
Ok(ReadDescriptor, Value),
|
||||
/// Read hit an ESTIMATE written by `blocking_txn`; the reader must abort and
|
||||
/// register a dependency on `blocking_txn`.
|
||||
Blocked(TxnIndex),
|
||||
}
|
||||
|
||||
pub struct MvMemory {
|
||||
shards: Vec<Mutex<HashMap<Key, BTreeMap<TxnIndex, Cell>>>>,
|
||||
}
|
||||
|
||||
fn shard_of(key: &Key) -> usize {
|
||||
// Cheap, deterministic spread. FNV-1a over the key's discriminant + fields.
|
||||
let mut h: u64 = 0xcbf29ce484222325;
|
||||
let bytes: [u64; 3] = match key {
|
||||
Key::Balance(a) => [1, *a, 0],
|
||||
Key::Storage(c, s) => [2, *c, *s],
|
||||
};
|
||||
for b in bytes {
|
||||
h ^= b;
|
||||
h = h.wrapping_mul(0x100000001b3);
|
||||
}
|
||||
(h as usize) % SHARDS
|
||||
}
|
||||
|
||||
impl MvMemory {
|
||||
pub fn new() -> Self {
|
||||
let mut shards = Vec::with_capacity(SHARDS);
|
||||
for _ in 0..SHARDS {
|
||||
shards.push(Mutex::new(HashMap::new()));
|
||||
}
|
||||
MvMemory { shards }
|
||||
}
|
||||
|
||||
/// Read the value visible to `txn_idx`: the highest entry strictly below it.
|
||||
pub fn read(&self, key: &Key, txn_idx: TxnIndex) -> ReadResult {
|
||||
let shard = &self.shards[shard_of(key)];
|
||||
let guard = shard.lock().unwrap();
|
||||
if let Some(versions) = guard.get(key) {
|
||||
// highest entry with index < txn_idx
|
||||
if let Some((&j, cell)) = versions.range(..txn_idx).next_back() {
|
||||
return match cell {
|
||||
Cell::Value(inc, v) => ReadResult::Ok(ReadDescriptor::Version(j, *inc), *v),
|
||||
Cell::Estimate => ReadResult::Blocked(j),
|
||||
};
|
||||
}
|
||||
}
|
||||
ReadResult::Ok(ReadDescriptor::Base, 0)
|
||||
}
|
||||
|
||||
/// Apply `txn_idx`'s new write set (incarnation `inc`). `prev_keys` is the
|
||||
/// set of keys the previous incarnation wrote, so we can (a) delete entries
|
||||
/// this incarnation no longer writes and (b) decide whether the *set* of
|
||||
/// written keys changed, which downstream validation depends on.
|
||||
///
|
||||
/// Returns `true` if the written key-set differs from the previous one
|
||||
/// (a key was added or removed) -- Block-STM calls this "wrote a new path"
|
||||
/// and uses it to trigger revalidation of the suffix.
|
||||
pub fn apply_write_set(
|
||||
&self,
|
||||
txn_idx: TxnIndex,
|
||||
inc: Incarnation,
|
||||
writes: &HashMap<Key, Value>,
|
||||
prev_keys: &std::collections::HashSet<Key>,
|
||||
) -> bool {
|
||||
// Install / overwrite current writes.
|
||||
for (k, v) in writes {
|
||||
let shard = &self.shards[shard_of(k)];
|
||||
let mut guard = shard.lock().unwrap();
|
||||
guard
|
||||
.entry(*k)
|
||||
.or_insert_with(BTreeMap::new)
|
||||
.insert(txn_idx, Cell::Value(inc, *v));
|
||||
}
|
||||
// Remove stale entries: keys written previously but not now.
|
||||
let mut removed_any = false;
|
||||
for k in prev_keys {
|
||||
if !writes.contains_key(k) {
|
||||
let shard = &self.shards[shard_of(k)];
|
||||
let mut guard = shard.lock().unwrap();
|
||||
if let Some(versions) = guard.get_mut(k) {
|
||||
versions.remove(&txn_idx);
|
||||
}
|
||||
removed_any = true;
|
||||
}
|
||||
}
|
||||
// Added a key not previously written?
|
||||
let mut added_any = false;
|
||||
for k in writes.keys() {
|
||||
if !prev_keys.contains(k) {
|
||||
added_any = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
added_any || removed_any
|
||||
}
|
||||
|
||||
/// Convert all of `txn_idx`'s current writes (over `keys`) into ESTIMATE
|
||||
/// placeholders. Called the moment a transaction is validation-aborted, so
|
||||
/// that any concurrent higher transaction that reads one of these keys will
|
||||
/// block on `txn_idx` instead of consuming a soon-to-be-wrong value. This is
|
||||
/// what protects the suffix during a re-execution window.
|
||||
pub fn mark_estimates(&self, txn_idx: TxnIndex, keys: &std::collections::HashSet<Key>) {
|
||||
for k in keys {
|
||||
let shard = &self.shards[shard_of(k)];
|
||||
let mut guard = shard.lock().unwrap();
|
||||
if let Some(versions) = guard.get_mut(k) {
|
||||
if let Some(cell) = versions.get_mut(&txn_idx) {
|
||||
*cell = Cell::Estimate;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Materialize the final committed state: base overlaid with, for each key,
|
||||
/// the value written by the HIGHEST transaction index that wrote a concrete
|
||||
/// value to it. Called once after the block is fully executed & validated.
|
||||
pub fn snapshot_final(&self, base: &crate::types::StateMap) -> crate::types::StateMap {
|
||||
let mut out = base.clone();
|
||||
for shard in &self.shards {
|
||||
let guard = shard.lock().unwrap();
|
||||
for (k, versions) in guard.iter() {
|
||||
// highest concrete value wins
|
||||
for (_, cell) in versions.iter().rev() {
|
||||
if let Cell::Value(_, v) = cell {
|
||||
out.insert(*k, *v);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
out
|
||||
}
|
||||
}
|
||||
354
parallel-executor/src/parallel.rs
Normal file
354
parallel-executor/src/parallel.rs
Normal file
@ -0,0 +1,354 @@
|
||||
//! The parallel Block-STM executor driver.
|
||||
//!
|
||||
//! Spawns `num_threads` workers. Each worker loops: ask the scheduler for a
|
||||
//! task, run it (execute or validate), feed the result back to the scheduler,
|
||||
//! repeat, until the scheduler says Done. Shared state lives in the MVMemory and
|
||||
//! the per-txn read-set / written-key-set tables.
|
||||
|
||||
use std::collections::{HashMap, HashSet};
|
||||
use std::sync::{Arc, Mutex};
|
||||
use std::thread;
|
||||
|
||||
use crate::mvmemory::{MvMemory, ReadDescriptor, ReadResult};
|
||||
use crate::scheduler::{Scheduler, Task};
|
||||
use crate::types::{Key, StateMap, TxnIndex, Txn, Value};
|
||||
use crate::vm::{execute_txn, VmView};
|
||||
|
||||
/// Aggregate stats returned alongside the final state.
|
||||
#[derive(Clone, Debug, Default)]
|
||||
pub struct ExecStats {
|
||||
pub num_txns: usize,
|
||||
pub num_threads: usize,
|
||||
/// Total incarnations executed (executions >= num_txns; excess = re-executions).
|
||||
pub total_executions: usize,
|
||||
/// Total validation tasks run.
|
||||
pub total_validations: usize,
|
||||
/// Total aborts (validation failures that triggered re-execution).
|
||||
pub total_aborts: usize,
|
||||
/// Number of times a transaction's first/retry execution attempt was
|
||||
/// pre-parked behind a predicted conflicting predecessor. 0 when prediction
|
||||
/// is disabled. This is advisory bookkeeping only.
|
||||
pub total_predicted_parks: usize,
|
||||
}
|
||||
|
||||
/// A capturing view over MVMemory for one transaction execution.
|
||||
struct MvView<'a> {
|
||||
txn_idx: TxnIndex,
|
||||
mv: &'a MvMemory,
|
||||
/// immutable base state; a read that resolves to `Base` returns from here
|
||||
base: &'a StateMap,
|
||||
/// buffered writes for read-your-writes and later commit
|
||||
writes: HashMap<Key, Value>,
|
||||
/// recorded read set: (key, what-we-observed)
|
||||
reads: Vec<(Key, ReadDescriptor)>,
|
||||
/// set when a read hit an ESTIMATE; execution aborts and we park on this txn
|
||||
blocked_on: Option<TxnIndex>,
|
||||
}
|
||||
|
||||
impl<'a> VmView for MvView<'a> {
|
||||
fn read(&mut self, key: Key) -> Option<Value> {
|
||||
// read-your-writes first (not recorded as a dependency read)
|
||||
if let Some(v) = self.writes.get(&key) {
|
||||
return Some(*v);
|
||||
}
|
||||
match self.mv.read(&key, self.txn_idx) {
|
||||
ReadResult::Ok(ReadDescriptor::Base, _) => {
|
||||
// no lower txn wrote this key: value comes from immutable base state
|
||||
self.reads.push((key, ReadDescriptor::Base));
|
||||
Some(*self.base.get(&key).unwrap_or(&0))
|
||||
}
|
||||
ReadResult::Ok(desc, v) => {
|
||||
self.reads.push((key, desc));
|
||||
Some(v)
|
||||
}
|
||||
ReadResult::Blocked(blocking) => {
|
||||
self.blocked_on = Some(blocking);
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
fn write(&mut self, key: Key, value: Value) {
|
||||
self.writes.insert(key, value);
|
||||
}
|
||||
}
|
||||
|
||||
struct Shared {
|
||||
scheduler: Scheduler,
|
||||
mv: MvMemory,
|
||||
txns: Vec<Txn>,
|
||||
base: StateMap,
|
||||
/// last recorded read set per txn (for validation)
|
||||
last_reads: Vec<Mutex<Vec<(Key, ReadDescriptor)>>>,
|
||||
/// keys written by the last incarnation per txn (for stale-entry cleanup)
|
||||
last_write_keys: Vec<Mutex<HashSet<Key>>>,
|
||||
/// PERFECT predicted soft-dependencies, `Some` only when prediction is on.
|
||||
/// `predicted_preds[i]` = ascending lower-index transactions that conflict
|
||||
/// with `i` (highest prior writer per key `i` reads). Advisory only; never
|
||||
/// gates correctness. See `predict.rs`.
|
||||
predicted_preds: Option<Vec<Vec<TxnIndex>>>,
|
||||
// counters
|
||||
executions: std::sync::atomic::AtomicUsize,
|
||||
validations: std::sync::atomic::AtomicUsize,
|
||||
aborts: std::sync::atomic::AtomicUsize,
|
||||
predicted_parks: std::sync::atomic::AtomicUsize,
|
||||
}
|
||||
|
||||
/// Execute one incarnation of `idx`. Returns Some(follow-up task) or None
|
||||
/// (worker should ask the scheduler for the next task). Loops internally to
|
||||
/// re-run immediately when a dependency was already resolved.
|
||||
fn try_execute(shared: &Shared, mut idx: TxnIndex, mut inc: usize) -> Task {
|
||||
loop {
|
||||
// ADVISORY predictive pre-parking (perfect-prediction upper bound).
|
||||
// Before running the VM, if we have predicted conflicting predecessors
|
||||
// for `idx`, park behind the HIGHEST one that has not yet reached
|
||||
// `Executed`. Re-checked on every (re)attempt, so by the time `idx`
|
||||
// actually executes all of its conflicting predecessors are committed and
|
||||
// its optimistic reads see final values -> the abort is avoided instead of
|
||||
// paid-for-then-undone. This only changes *when* `idx` runs; the ordinary
|
||||
// read-set validation below is unchanged and remains the sole authority on
|
||||
// the committed state, so a wrong/approximate prediction can never corrupt
|
||||
// the result -- at worst it fails to prevent an abort.
|
||||
if let Some(preds) = &shared.predicted_preds {
|
||||
let mut blocker = None;
|
||||
for &j in preds[idx].iter().rev() {
|
||||
if !shared.scheduler.is_executed(j) {
|
||||
blocker = Some(j);
|
||||
break;
|
||||
}
|
||||
}
|
||||
if let Some(b) = blocker {
|
||||
if shared.scheduler.add_dependency(idx, b) {
|
||||
shared
|
||||
.predicted_parks
|
||||
.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
|
||||
return Task::NoTask; // parked; task dropped, worker asks for next
|
||||
}
|
||||
// `b` reached Executed between our check and the park attempt;
|
||||
// re-scan for the next still-pending predecessor (or none).
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
let mut view = MvView {
|
||||
txn_idx: idx,
|
||||
mv: &shared.mv,
|
||||
base: &shared.base,
|
||||
writes: HashMap::new(),
|
||||
reads: Vec::new(),
|
||||
blocked_on: None,
|
||||
};
|
||||
shared
|
||||
.executions
|
||||
.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
|
||||
let completed = execute_txn(&shared.txns[idx], &mut view);
|
||||
|
||||
if completed.is_none() {
|
||||
// hit an ESTIMATE -> either park, or (if already resolved) retry
|
||||
let blocking = view.blocked_on.expect("blocked without a blocker");
|
||||
if shared.scheduler.add_dependency(idx, blocking) {
|
||||
return Task::NoTask; // parked; task dropped
|
||||
} else {
|
||||
continue; // dependency resolved meanwhile; re-execute same version
|
||||
}
|
||||
}
|
||||
|
||||
// Completed. Commit read set + apply write set to MVMemory.
|
||||
let prev_keys: HashSet<Key> = {
|
||||
let g = shared.last_write_keys[idx].lock().unwrap();
|
||||
g.clone()
|
||||
};
|
||||
let new_keys: HashSet<Key> = view.writes.keys().cloned().collect();
|
||||
|
||||
let wrote_new_path =
|
||||
shared
|
||||
.mv
|
||||
.apply_write_set(idx, inc, &view.writes, &prev_keys);
|
||||
|
||||
*shared.last_reads[idx].lock().unwrap() = view.reads;
|
||||
*shared.last_write_keys[idx].lock().unwrap() = new_keys;
|
||||
|
||||
let t = shared
|
||||
.scheduler
|
||||
.finish_execution(idx, inc, wrote_new_path);
|
||||
// finish_execution may hand us a Validation task for the same idx; if it
|
||||
// hands an Execution task that never happens (finish_execution only
|
||||
// returns Validation/NoTask), so just return it.
|
||||
let _ = (&mut idx, &mut inc);
|
||||
return t;
|
||||
}
|
||||
}
|
||||
|
||||
/// Validate `idx`'s recorded read set against current MVMemory. Returns a
|
||||
/// follow-up task (a re-execution) or NoTask.
|
||||
fn try_validate(shared: &Shared, idx: TxnIndex, inc: usize) -> Task {
|
||||
shared
|
||||
.validations
|
||||
.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
|
||||
let reads = { shared.last_reads[idx].lock().unwrap().clone() };
|
||||
let mut valid = true;
|
||||
for (key, observed) in &reads {
|
||||
let now = shared.mv.read(key, idx);
|
||||
let matches = match (observed, now) {
|
||||
(ReadDescriptor::Base, ReadResult::Ok(ReadDescriptor::Base, _)) => true,
|
||||
(
|
||||
ReadDescriptor::Version(j, k),
|
||||
ReadResult::Ok(ReadDescriptor::Version(j2, k2), _),
|
||||
) => *j == j2 && *k == k2,
|
||||
// Estimate now, or provenance changed -> invalid
|
||||
_ => false,
|
||||
};
|
||||
if !matches {
|
||||
valid = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
let aborted = !valid && shared.scheduler.try_validation_abort(idx, inc);
|
||||
if aborted {
|
||||
shared
|
||||
.aborts
|
||||
.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
|
||||
// mark this txn's writes as ESTIMATE so concurrent readers block on it
|
||||
let keys = { shared.last_write_keys[idx].lock().unwrap().clone() };
|
||||
shared.mv.mark_estimates(idx, &keys);
|
||||
}
|
||||
shared.scheduler.finish_validation(idx, aborted)
|
||||
}
|
||||
|
||||
fn worker(shared: &Shared) {
|
||||
let mut task = shared.scheduler.next_task();
|
||||
loop {
|
||||
task = match task {
|
||||
Task::Done => break,
|
||||
Task::NoTask => shared.scheduler.next_task(),
|
||||
Task::Execution(idx, inc) => try_execute(shared, idx, inc),
|
||||
Task::Validation(idx, inc) => try_validate(shared, idx, inc),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/// Execute `txns` starting from `base` using Block-STM on `num_threads` threads.
|
||||
/// Returns the final committed state plus execution statistics.
|
||||
///
|
||||
/// This is the original, unchanged public entry point: it runs pure Block-STM
|
||||
/// with NO prediction (the `WITHOUT` arm of the A/B experiment).
|
||||
pub fn execute_block_parallel(
|
||||
base: &StateMap,
|
||||
txns: &[Txn],
|
||||
num_threads: usize,
|
||||
) -> (StateMap, ExecStats) {
|
||||
let (s, st, _e, _c) = execute_block_parallel_inner(base, txns, num_threads, None);
|
||||
(s, st)
|
||||
}
|
||||
|
||||
/// Same as [`execute_block_parallel`] but ALSO returns a phase-timing split for
|
||||
/// honest end-to-end pipeline measurement:
|
||||
/// - `exec` = wall time of the PARALLEL Block-STM phase (workers running:
|
||||
/// execution + validation + re-execution). This is the
|
||||
/// parallelizable part; it is what scales with `num_threads`.
|
||||
/// - `commit` = wall time of `MvMemory::snapshot_final`, the SERIAL
|
||||
/// materialization of the multi-version memory into the flat
|
||||
/// committed `StateMap`. This does NOT parallelize.
|
||||
///
|
||||
/// The caller adds the (also serial) `keccak::state_root` cost on top to obtain
|
||||
/// the full block-production pipeline time. Splitting the phases lets us report
|
||||
/// the real Amdahl serial fraction rather than an execution-phase-only speedup.
|
||||
pub fn execute_block_parallel_split(
|
||||
base: &StateMap,
|
||||
txns: &[Txn],
|
||||
num_threads: usize,
|
||||
) -> (StateMap, ExecStats, std::time::Duration, std::time::Duration) {
|
||||
execute_block_parallel_inner(base, txns, num_threads, None)
|
||||
}
|
||||
|
||||
/// Same as [`execute_block_parallel`] but with advisory predictive pre-parking
|
||||
/// enabled: `predicted_preds[i]` lists the lower-index transactions `i`
|
||||
/// conflicts with (see [`crate::predict::compute_predicted_preds`]). This is the
|
||||
/// `WITH` arm of the A/B experiment. The predictions are advisory only and never
|
||||
/// change the committed state; they only reduce optimistic aborts.
|
||||
pub fn execute_block_parallel_predicted(
|
||||
base: &StateMap,
|
||||
txns: &[Txn],
|
||||
num_threads: usize,
|
||||
predicted_preds: Vec<Vec<TxnIndex>>,
|
||||
) -> (StateMap, ExecStats) {
|
||||
let (s, st, _e, _c) =
|
||||
execute_block_parallel_inner(base, txns, num_threads, Some(predicted_preds));
|
||||
(s, st)
|
||||
}
|
||||
|
||||
fn execute_block_parallel_inner(
|
||||
base: &StateMap,
|
||||
txns: &[Txn],
|
||||
num_threads: usize,
|
||||
predicted_preds: Option<Vec<Vec<TxnIndex>>>,
|
||||
) -> (StateMap, ExecStats, std::time::Duration, std::time::Duration) {
|
||||
let n = txns.len();
|
||||
if n == 0 {
|
||||
return (
|
||||
base.clone(),
|
||||
ExecStats {
|
||||
num_threads,
|
||||
..Default::default()
|
||||
},
|
||||
std::time::Duration::ZERO,
|
||||
std::time::Duration::ZERO,
|
||||
);
|
||||
}
|
||||
debug_assert!(
|
||||
predicted_preds.as_ref().map(|p| p.len()).unwrap_or(n) == n,
|
||||
"predicted_preds must have one entry per transaction"
|
||||
);
|
||||
|
||||
let mut last_reads = Vec::with_capacity(n);
|
||||
let mut last_write_keys = Vec::with_capacity(n);
|
||||
for _ in 0..n {
|
||||
last_reads.push(Mutex::new(Vec::new()));
|
||||
last_write_keys.push(Mutex::new(HashSet::new()));
|
||||
}
|
||||
|
||||
let shared = Arc::new(Shared {
|
||||
scheduler: Scheduler::new(n),
|
||||
mv: MvMemory::new(),
|
||||
txns: txns.to_vec(),
|
||||
base: base.clone(),
|
||||
last_reads,
|
||||
last_write_keys,
|
||||
predicted_preds,
|
||||
executions: std::sync::atomic::AtomicUsize::new(0),
|
||||
validations: std::sync::atomic::AtomicUsize::new(0),
|
||||
aborts: std::sync::atomic::AtomicUsize::new(0),
|
||||
predicted_parks: std::sync::atomic::AtomicUsize::new(0),
|
||||
});
|
||||
|
||||
let threads = num_threads.max(1);
|
||||
let exec_t0 = std::time::Instant::now();
|
||||
let mut handles = Vec::with_capacity(threads);
|
||||
for _ in 0..threads {
|
||||
let s = Arc::clone(&shared);
|
||||
handles.push(thread::spawn(move || worker(&s)));
|
||||
}
|
||||
for h in handles {
|
||||
h.join().unwrap();
|
||||
}
|
||||
let exec_dur = exec_t0.elapsed();
|
||||
|
||||
let commit_t0 = std::time::Instant::now();
|
||||
let final_state = shared.mv.snapshot_final(&shared.base);
|
||||
let commit_dur = commit_t0.elapsed();
|
||||
let stats = ExecStats {
|
||||
num_txns: n,
|
||||
num_threads: threads,
|
||||
total_executions: shared
|
||||
.executions
|
||||
.load(std::sync::atomic::Ordering::Relaxed),
|
||||
total_validations: shared
|
||||
.validations
|
||||
.load(std::sync::atomic::Ordering::Relaxed),
|
||||
total_aborts: shared.aborts.load(std::sync::atomic::Ordering::Relaxed),
|
||||
total_predicted_parks: shared
|
||||
.predicted_parks
|
||||
.load(std::sync::atomic::Ordering::Relaxed),
|
||||
};
|
||||
(final_state, stats, exec_dur, commit_dur)
|
||||
}
|
||||
162
parallel-executor/src/predict.rs
Normal file
162
parallel-executor/src/predict.rs
Normal file
@ -0,0 +1,162 @@
|
||||
//! PERFECT predicted soft-dependencies (theoretical upper bound of a predictive
|
||||
//! scheduler).
|
||||
//!
|
||||
//! MOTIVATION
|
||||
//! ----------
|
||||
//! Block-STM discovers conflicts *dynamically*: a transaction executes
|
||||
//! optimistically, and only when validation catches a stale read does it abort
|
||||
//! and re-execute. Aborts are wasted work. A "predictive scheduler" tries to
|
||||
//! avoid that waste by predicting conflicts up front and pre-ordering / parking
|
||||
//! conflicting transactions so their first execution already sees the right
|
||||
//! values.
|
||||
//!
|
||||
//! The open question is how much headroom such a predictor could ever buy. To
|
||||
//! answer it we cheat in the predictor's favor: we compute the EXACT read/write
|
||||
//! sets from the real serial execution of the batch, so the prediction is
|
||||
//! PERFECT. Any real predictor (static analysis, access-list hints, a learned
|
||||
//! model) is strictly worse than this oracle, so the abort-rate reduction and
|
||||
//! wall-time delta measured here are an UPPER BOUND on what prediction can do.
|
||||
//!
|
||||
//! ADVISORY-ONLY (correctness invariant)
|
||||
//! -------------------------------------
|
||||
//! The predicted predecessors are used ONLY to decide *when* to (re-)attempt a
|
||||
//! transaction's first execution -- i.e. to pre-park it behind conflicting
|
||||
//! lower-index transactions. They never gate the committed result: the ordinary
|
||||
//! Block-STM read-set validation still runs and is the sole authority on
|
||||
//! correctness. If a prediction is wrong (or, in a real deployment, merely
|
||||
//! approximate) the worst that can happen is a transaction executes too early
|
||||
//! and aborts exactly as it would have without any prediction. Predictions can
|
||||
//! therefore NEVER produce an incorrect state root; at worst they fail to help.
|
||||
//!
|
||||
//! WHY THE HIGHEST PRIOR WRITER PER READ KEY IS SUFFICIENT
|
||||
//! ------------------------------------------------------
|
||||
//! A Block-STM validation abort is caused by a READ whose provenance changed:
|
||||
//! the transaction read some key `k` and the value it observed was later
|
||||
//! superseded by a lower-index write. In the true serial order the value of `k`
|
||||
//! that transaction `i` must observe is written by the HIGHEST-index transaction
|
||||
//! `j < i` that writes `k`. If every such `j` (over all keys `i` reads) has
|
||||
//! already reached `Executed` before `i` first runs, then `i`'s reads all resolve
|
||||
//! to their final serial values and `i` cannot be validation-aborted. So the
|
||||
//! minimal perfect predecessor set for `i` is exactly
|
||||
//! { last_writer(k) : k in reads(i) }.
|
||||
//! Parking `i` behind the maximum still-pending member of that set, re-checking
|
||||
//! each time it is resumed, guarantees all of them are `Executed` before `i`
|
||||
//! runs. This is what [`compute_predicted_preds`] returns.
|
||||
|
||||
use std::collections::{HashMap, HashSet};
|
||||
|
||||
use crate::types::{Key, StateMap, Txn, Value};
|
||||
use crate::vm::{execute_txn, VmView};
|
||||
|
||||
/// A `VmView` that executes against a live state map (exactly like the
|
||||
/// sequential oracle) while additionally recording the set of keys read and the
|
||||
/// set of keys written. Because it writes through to `state`, later transactions
|
||||
/// observe committed values, so the recorded sets are the true serial-order
|
||||
/// read/write sets.
|
||||
struct RecordingView<'a> {
|
||||
state: &'a mut StateMap,
|
||||
reads: HashSet<Key>,
|
||||
writes: HashSet<Key>,
|
||||
}
|
||||
|
||||
impl<'a> VmView for RecordingView<'a> {
|
||||
fn read(&mut self, key: Key) -> Option<Value> {
|
||||
self.reads.insert(key);
|
||||
Some(*self.state.get(&key).unwrap_or(&0))
|
||||
}
|
||||
fn write(&mut self, key: Key, value: Value) {
|
||||
self.writes.insert(key);
|
||||
self.state.insert(key, value);
|
||||
}
|
||||
}
|
||||
|
||||
/// Compute, for every transaction, its PERFECT soft-dependency predecessors: the
|
||||
/// ascending, de-duplicated list of lower-index transactions that wrote a key
|
||||
/// this transaction reads (keeping, per read key, the highest such writer).
|
||||
///
|
||||
/// This is a single extra sequential pass over the batch (same cost class as the
|
||||
/// sequential oracle plus O(reads) bookkeeping). It is only ever used to drive
|
||||
/// advisory pre-parking; see the module docs for the correctness argument.
|
||||
pub fn compute_predicted_preds(base: &StateMap, txns: &[Txn]) -> Vec<Vec<usize>> {
|
||||
let mut state = base.clone();
|
||||
// key -> highest txn index seen so far that wrote it
|
||||
let mut last_writer: HashMap<Key, usize> = HashMap::new();
|
||||
let mut preds: Vec<Vec<usize>> = Vec::with_capacity(txns.len());
|
||||
|
||||
for (i, txn) in txns.iter().enumerate() {
|
||||
let mut view = RecordingView {
|
||||
state: &mut state,
|
||||
reads: HashSet::new(),
|
||||
writes: HashSet::new(),
|
||||
};
|
||||
// Sequential execution never aborts (the recording view never returns
|
||||
// None), so this always completes.
|
||||
let _ = execute_txn(txn, &mut view);
|
||||
|
||||
// Dependency = highest prior writer of each key we READ. `last_writer`
|
||||
// only holds entries for j < i because we update it *after* this txn, so
|
||||
// a transaction never lists itself (read-your-own-write is not a
|
||||
// cross-transaction dependency).
|
||||
let mut dep_set: HashSet<usize> = HashSet::new();
|
||||
for k in &view.reads {
|
||||
if let Some(&j) = last_writer.get(k) {
|
||||
dep_set.insert(j);
|
||||
}
|
||||
}
|
||||
let mut deps: Vec<usize> = dep_set.into_iter().collect();
|
||||
deps.sort_unstable();
|
||||
preds.push(deps);
|
||||
|
||||
// Record this transaction as the newest writer of each key it wrote.
|
||||
for k in &view.writes {
|
||||
last_writer.insert(*k, i);
|
||||
}
|
||||
}
|
||||
|
||||
preds
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::types::{Key, StateMap, TxKind, Txn};
|
||||
|
||||
#[test]
|
||||
fn shared_counter_forms_a_chain() {
|
||||
// N txns incrementing the same slot: perfect prediction should make each
|
||||
// txn depend on exactly the immediately preceding one (a linear chain).
|
||||
let mut base = StateMap::new();
|
||||
base.insert(Key::Storage(0, 0), 0);
|
||||
let n = 8usize;
|
||||
let txns: Vec<Txn> = (0..n)
|
||||
.map(|_| Txn {
|
||||
kind: TxKind::Increment { contract: 0, slot: 0 },
|
||||
gas: 0,
|
||||
})
|
||||
.collect();
|
||||
let preds = compute_predicted_preds(&base, &txns);
|
||||
assert_eq!(preds[0], Vec::<usize>::new());
|
||||
for i in 1..n {
|
||||
assert_eq!(preds[i], vec![i - 1], "txn {i} should depend on {}", i - 1);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn disjoint_slots_have_no_dependencies() {
|
||||
// Increments of DISTINCT slots never conflict, so no predecessors.
|
||||
let mut base = StateMap::new();
|
||||
for s in 0..6u64 {
|
||||
base.insert(Key::Storage(0, s), 0);
|
||||
}
|
||||
let txns: Vec<Txn> = (0..6u64)
|
||||
.map(|s| Txn {
|
||||
kind: TxKind::Increment { contract: 0, slot: s },
|
||||
gas: 0,
|
||||
})
|
||||
.collect();
|
||||
let preds = compute_predicted_preds(&base, &txns);
|
||||
for (i, p) in preds.iter().enumerate() {
|
||||
assert!(p.is_empty(), "txn {i} should have no predecessors, got {p:?}");
|
||||
}
|
||||
}
|
||||
}
|
||||
293
parallel-executor/src/scheduler.rs
Normal file
293
parallel-executor/src/scheduler.rs
Normal file
@ -0,0 +1,293 @@
|
||||
//! The Block-STM collaborative scheduler (Gelashvili et al. 2022, Algorithm 3).
|
||||
//!
|
||||
//! It hands out two kinds of task to a pool of worker threads:
|
||||
//! - EXECUTION tasks: run incarnation `k` of transaction `i`.
|
||||
//! - VALIDATION tasks: re-check that an already-executed transaction's read
|
||||
//! set is still valid against current MVMemory.
|
||||
//!
|
||||
//! Two monotone-ish cursors drive it: `execution_idx` and `validation_idx`.
|
||||
//! 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. The block
|
||||
//! is done when both cursors have passed the end AND 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 is
|
||||
//! dropped; when `j` finishes it re-arms `i` (ReadyToExecute) and decreases
|
||||
//! `execution_idx` so a worker re-picks it.
|
||||
//!
|
||||
//! Correctness of this module is not asserted by inspection -- it is proven
|
||||
//! empirically by the harness, which runs thousands of adversarial, conflicting
|
||||
//! batches and checks that the committed state equals the sequential oracle.
|
||||
|
||||
use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering};
|
||||
use std::sync::Mutex;
|
||||
|
||||
use crate::types::{Incarnation, TxnIndex};
|
||||
|
||||
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
|
||||
enum Status {
|
||||
ReadyToExecute(Incarnation),
|
||||
Executing(Incarnation),
|
||||
Suspended(Incarnation),
|
||||
Executed(Incarnation),
|
||||
Aborting(Incarnation),
|
||||
}
|
||||
|
||||
/// A unit of work for a worker, or a control signal.
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
pub enum Task {
|
||||
Execution(TxnIndex, Incarnation),
|
||||
Validation(TxnIndex, Incarnation),
|
||||
NoTask,
|
||||
Done,
|
||||
}
|
||||
|
||||
pub struct Scheduler {
|
||||
num_txns: usize,
|
||||
execution_idx: AtomicUsize,
|
||||
validation_idx: AtomicUsize,
|
||||
decrease_cnt: AtomicUsize,
|
||||
num_active_tasks: AtomicUsize,
|
||||
done: AtomicBool,
|
||||
status: Vec<Mutex<Status>>,
|
||||
dependencies: Vec<Mutex<Vec<TxnIndex>>>,
|
||||
}
|
||||
|
||||
impl Scheduler {
|
||||
pub fn new(num_txns: usize) -> Self {
|
||||
let mut status = Vec::with_capacity(num_txns);
|
||||
let mut dependencies = Vec::with_capacity(num_txns);
|
||||
for _ in 0..num_txns {
|
||||
status.push(Mutex::new(Status::ReadyToExecute(0)));
|
||||
dependencies.push(Mutex::new(Vec::new()));
|
||||
}
|
||||
Scheduler {
|
||||
num_txns,
|
||||
execution_idx: AtomicUsize::new(0),
|
||||
validation_idx: AtomicUsize::new(0),
|
||||
decrease_cnt: AtomicUsize::new(0),
|
||||
num_active_tasks: AtomicUsize::new(0),
|
||||
done: AtomicBool::new(false),
|
||||
status,
|
||||
dependencies,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn done(&self) -> bool {
|
||||
self.done.load(Ordering::SeqCst)
|
||||
}
|
||||
|
||||
/// True iff `idx` is currently in the `Executed` state. Used ONLY by the
|
||||
/// advisory predictive pre-parking hook (see `predict.rs`): before a
|
||||
/// transaction's first execution we check whether each predicted conflicting
|
||||
/// predecessor has already reached `Executed`, and if not we park on it. This
|
||||
/// never affects the committed result -- it only reorders *when* work is
|
||||
/// attempted. A predecessor that has aborted and is re-executing reports
|
||||
/// `false` here, which correctly causes the dependent to wait for it again.
|
||||
pub fn is_executed(&self, idx: TxnIndex) -> bool {
|
||||
matches!(*self.status[idx].lock().unwrap(), Status::Executed(_))
|
||||
}
|
||||
|
||||
fn decrease_execution_idx(&self, target: TxnIndex) {
|
||||
self.execution_idx.fetch_min(target, Ordering::SeqCst);
|
||||
self.decrease_cnt.fetch_add(1, Ordering::SeqCst);
|
||||
}
|
||||
|
||||
fn decrease_validation_idx(&self, target: TxnIndex) {
|
||||
self.validation_idx.fetch_min(target, Ordering::SeqCst);
|
||||
self.decrease_cnt.fetch_add(1, Ordering::SeqCst);
|
||||
}
|
||||
|
||||
fn check_done(&self) {
|
||||
let observed = self.decrease_cnt.load(Ordering::SeqCst);
|
||||
if self.execution_idx.load(Ordering::SeqCst) >= self.num_txns
|
||||
&& self.validation_idx.load(Ordering::SeqCst) >= self.num_txns
|
||||
&& self.num_active_tasks.load(Ordering::SeqCst) == 0
|
||||
&& self.decrease_cnt.load(Ordering::SeqCst) == observed
|
||||
{
|
||||
self.done.store(true, Ordering::SeqCst);
|
||||
}
|
||||
}
|
||||
|
||||
/// Try to move `idx` from ReadyToExecute -> Executing. On success returns the
|
||||
/// execution version (keeping the active-task unit). On failure it releases
|
||||
/// the active-task unit and returns None.
|
||||
fn try_incarnate(&self, idx: TxnIndex) -> Option<Task> {
|
||||
if idx < self.num_txns {
|
||||
let mut st = self.status[idx].lock().unwrap();
|
||||
if let Status::ReadyToExecute(inc) = *st {
|
||||
*st = Status::Executing(inc);
|
||||
return Some(Task::Execution(idx, inc));
|
||||
}
|
||||
}
|
||||
self.num_active_tasks.fetch_sub(1, Ordering::SeqCst);
|
||||
None
|
||||
}
|
||||
|
||||
fn next_version_to_execute(&self) -> Option<Task> {
|
||||
if self.execution_idx.load(Ordering::SeqCst) >= self.num_txns {
|
||||
self.check_done();
|
||||
return None;
|
||||
}
|
||||
self.num_active_tasks.fetch_add(1, Ordering::SeqCst);
|
||||
let idx = self.execution_idx.fetch_add(1, Ordering::SeqCst);
|
||||
self.try_incarnate(idx)
|
||||
}
|
||||
|
||||
fn next_version_to_validate(&self) -> Option<Task> {
|
||||
if self.validation_idx.load(Ordering::SeqCst) >= self.num_txns {
|
||||
self.check_done();
|
||||
return None;
|
||||
}
|
||||
self.num_active_tasks.fetch_add(1, Ordering::SeqCst);
|
||||
let idx = self.validation_idx.fetch_add(1, Ordering::SeqCst);
|
||||
if idx < self.num_txns {
|
||||
let st = *self.status[idx].lock().unwrap();
|
||||
if let Status::Executed(inc) = st {
|
||||
return Some(Task::Validation(idx, inc));
|
||||
}
|
||||
}
|
||||
self.num_active_tasks.fetch_sub(1, Ordering::SeqCst);
|
||||
None
|
||||
}
|
||||
|
||||
/// Main entry point workers spin on. Returns a task, NoTask (retry), or Done.
|
||||
pub fn next_task(&self) -> Task {
|
||||
loop {
|
||||
if self.done.load(Ordering::SeqCst) {
|
||||
return Task::Done;
|
||||
}
|
||||
let v = self.validation_idx.load(Ordering::SeqCst);
|
||||
let e = self.execution_idx.load(Ordering::SeqCst);
|
||||
let task = if v < e {
|
||||
self.next_version_to_validate()
|
||||
} else {
|
||||
self.next_version_to_execute()
|
||||
};
|
||||
match task {
|
||||
Some(t) => return t,
|
||||
None => {
|
||||
// both cursors may be exhausted; loop re-checks done marker
|
||||
if self.done.load(Ordering::SeqCst) {
|
||||
return Task::Done;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Register that `idx` depends on `blocking` (it read `blocking`'s ESTIMATE).
|
||||
/// Returns true if `idx` was parked (caller drops the task), false if
|
||||
/// `blocking` already finished (caller should just re-execute `idx`).
|
||||
pub fn add_dependency(&self, idx: TxnIndex, blocking: TxnIndex) -> bool {
|
||||
// Hold the blocking txn's status lock across the whole critical section so
|
||||
// it cannot transition to Executed and drain its dependency list between
|
||||
// our check and our push (which would lose the wakeup).
|
||||
let bstatus = self.status[blocking].lock().unwrap();
|
||||
if let Status::Executed(_) = *bstatus {
|
||||
return false; // resolved already; re-execute instead of parking
|
||||
}
|
||||
self.dependencies[blocking].lock().unwrap().push(idx);
|
||||
// park idx (it was Executing at its current incarnation)
|
||||
{
|
||||
let mut st = self.status[idx].lock().unwrap();
|
||||
if let Status::Executing(inc) = *st {
|
||||
*st = Status::Suspended(inc);
|
||||
}
|
||||
}
|
||||
drop(bstatus);
|
||||
self.num_active_tasks.fetch_sub(1, Ordering::SeqCst);
|
||||
true
|
||||
}
|
||||
|
||||
fn resume(&self, idx: TxnIndex) {
|
||||
let mut st = self.status[idx].lock().unwrap();
|
||||
if let Status::Suspended(inc) = *st {
|
||||
*st = Status::ReadyToExecute(inc);
|
||||
}
|
||||
}
|
||||
|
||||
fn set_ready_after_abort(&self, idx: TxnIndex) {
|
||||
let mut st = self.status[idx].lock().unwrap();
|
||||
if let Status::Aborting(inc) = *st {
|
||||
*st = Status::ReadyToExecute(inc + 1);
|
||||
}
|
||||
}
|
||||
|
||||
fn set_executed(&self, idx: TxnIndex, inc: Incarnation) {
|
||||
let mut st = self.status[idx].lock().unwrap();
|
||||
*st = Status::Executed(inc);
|
||||
}
|
||||
|
||||
/// Called after an execution task completes. `wrote_new_path` = the set of
|
||||
/// written keys changed vs the previous incarnation. Returns a follow-up
|
||||
/// task (inheriting the active-task unit) or NoTask (releasing it).
|
||||
pub fn finish_execution(
|
||||
&self,
|
||||
idx: TxnIndex,
|
||||
inc: Incarnation,
|
||||
wrote_new_path: bool,
|
||||
) -> Task {
|
||||
self.set_executed(idx, inc);
|
||||
|
||||
// Resume any transactions parked on this one.
|
||||
let deps: Vec<TxnIndex> = {
|
||||
let mut d = self.dependencies[idx].lock().unwrap();
|
||||
std::mem::take(&mut *d)
|
||||
};
|
||||
if !deps.is_empty() {
|
||||
let min_dep = *deps.iter().min().unwrap();
|
||||
for d in &deps {
|
||||
self.resume(*d);
|
||||
}
|
||||
self.decrease_execution_idx(min_dep);
|
||||
}
|
||||
|
||||
if self.validation_idx.load(Ordering::SeqCst) > idx {
|
||||
if wrote_new_path {
|
||||
// new/removed keys can invalidate the suffix -> revalidate from idx
|
||||
self.decrease_validation_idx(idx);
|
||||
self.num_active_tasks.fetch_sub(1, Ordering::SeqCst);
|
||||
Task::NoTask
|
||||
} else {
|
||||
// only this txn needs (re)validation right now
|
||||
Task::Validation(idx, inc)
|
||||
}
|
||||
} else {
|
||||
self.num_active_tasks.fetch_sub(1, Ordering::SeqCst);
|
||||
Task::NoTask
|
||||
}
|
||||
}
|
||||
|
||||
/// Attempt to win the right to abort `idx` at `inc`. Returns true iff this
|
||||
/// caller flipped Executed(inc) -> Aborting(inc). Used to ensure only one
|
||||
/// validator aborts a given incarnation.
|
||||
pub fn try_validation_abort(&self, idx: TxnIndex, inc: Incarnation) -> bool {
|
||||
let mut st = self.status[idx].lock().unwrap();
|
||||
if *st == Status::Executed(inc) {
|
||||
*st = Status::Aborting(inc);
|
||||
true
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
/// Called after a validation task. `aborted` = the validation failed AND we
|
||||
/// won `try_validation_abort`. Returns a follow-up execution task (inheriting
|
||||
/// the active-task unit) or NoTask (releasing it).
|
||||
pub fn finish_validation(&self, idx: TxnIndex, aborted: bool) -> Task {
|
||||
if aborted {
|
||||
self.set_ready_after_abort(idx); // bump incarnation
|
||||
self.decrease_validation_idx(idx + 1);
|
||||
if self.execution_idx.load(Ordering::SeqCst) > idx {
|
||||
// re-execute idx now (keeps the active-task unit if incarnate succeeds)
|
||||
return self.try_incarnate(idx).unwrap_or(Task::NoTask);
|
||||
}
|
||||
}
|
||||
self.num_active_tasks.fetch_sub(1, Ordering::SeqCst);
|
||||
Task::NoTask
|
||||
}
|
||||
}
|
||||
30
parallel-executor/src/sequential.rs
Normal file
30
parallel-executor/src/sequential.rs
Normal file
@ -0,0 +1,30 @@
|
||||
//! The sequential reference executor. This is the ORACLE: the definition of the
|
||||
//! correct answer. Block-STM's output must equal this, byte for byte, for every
|
||||
//! batch. It simply applies transactions in index order against a live map.
|
||||
|
||||
use crate::types::{Key, StateMap, Txn, Value};
|
||||
use crate::vm::{execute_txn, VmView};
|
||||
|
||||
struct SeqView<'a> {
|
||||
state: &'a mut StateMap,
|
||||
}
|
||||
|
||||
impl<'a> VmView for SeqView<'a> {
|
||||
fn read(&mut self, key: Key) -> Option<Value> {
|
||||
Some(*self.state.get(&key).unwrap_or(&0))
|
||||
}
|
||||
fn write(&mut self, key: Key, value: Value) {
|
||||
self.state.insert(key, value);
|
||||
}
|
||||
}
|
||||
|
||||
/// Execute `txns` in order starting from `base`, returning the final state.
|
||||
pub fn execute_block_sequential(base: &StateMap, txns: &[Txn]) -> StateMap {
|
||||
let mut state = base.clone();
|
||||
for txn in txns {
|
||||
let mut view = SeqView { state: &mut state };
|
||||
// Sequential execution never aborts (no ESTIMATE), so this is always Some.
|
||||
let _ = execute_txn(txn, &mut view);
|
||||
}
|
||||
state
|
||||
}
|
||||
72
parallel-executor/src/types.rs
Normal file
72
parallel-executor/src/types.rs
Normal file
@ -0,0 +1,72 @@
|
||||
//! Core value types shared across the executor.
|
||||
//!
|
||||
//! State model (intentionally EVM-shaped but minimal):
|
||||
//! - `Key::Balance(account)` -> the native-token balance of an account
|
||||
//! - `Key::Storage(contract, slot)` -> a single key/value contract storage slot
|
||||
//!
|
||||
//! Values are `u128`. This is enough to model realistic EVM-style workloads:
|
||||
//! plain transfers (two balance touches), value-dependent sweeps, shared
|
||||
//! counters (the canonical serializability stress test), and constant-product
|
||||
//! AMM swaps (two-slot conflicting DeFi writes).
|
||||
|
||||
use std::collections::HashMap;
|
||||
|
||||
/// A single addressable state location.
|
||||
#[derive(Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord, Debug)]
|
||||
pub enum Key {
|
||||
/// Native-token balance of `account`.
|
||||
Balance(u64),
|
||||
/// Contract storage: `(contract_id, slot)`.
|
||||
Storage(u64, u64),
|
||||
}
|
||||
|
||||
/// Stored value type. Absent key == 0.
|
||||
pub type Value = u128;
|
||||
|
||||
/// A transaction version: `(txn_index, incarnation)`.
|
||||
///
|
||||
/// `txn_index` is the position of the transaction in the block's preferred
|
||||
/// serialization order (0..n). `incarnation` counts how many times this
|
||||
/// transaction has been (re-)executed; it starts at 0 and is bumped on abort.
|
||||
pub type TxnIndex = usize;
|
||||
pub type Incarnation = usize;
|
||||
pub type Version = (TxnIndex, Incarnation);
|
||||
|
||||
/// The kinds of transaction the toy VM understands. Each one has genuine EVM
|
||||
/// characteristics: conditional control flow that depends on reads, and writes
|
||||
/// whose *values* depend on reads (so a stale read produces a wrong result and
|
||||
/// MUST be caught by validation + re-execution).
|
||||
#[derive(Clone, Debug)]
|
||||
pub enum TxKind {
|
||||
/// Move `amount` from `from` to `to` if `from` can afford it.
|
||||
/// Control flow depends on reading `from`'s balance.
|
||||
Transfer { from: u64, to: u64, amount: Value },
|
||||
/// Move the ENTIRE balance of `from` into `to`. The written values depend
|
||||
/// directly on the read balance, so a stale read is silently wrong unless
|
||||
/// caught (a strong serializability probe).
|
||||
Sweep { from: u64, to: u64 },
|
||||
/// `storage[contract][slot] += 1`. When many txns hit the same slot this is
|
||||
/// the classic all-conflict counter: sequential result is start + count, and
|
||||
/// the parallel executor MUST reproduce exactly that despite reordering.
|
||||
Increment { contract: u64, slot: u64 },
|
||||
/// Constant-product-style swap against a two-slot pool. Reads and writes two
|
||||
/// slots; models a hot DeFi pool that conflicts across concurrent swaps.
|
||||
AmmSwap { contract: u64, x_slot: u64, y_slot: u64, dx: Value },
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct Txn {
|
||||
pub kind: TxKind,
|
||||
/// Simulated EVM execution cost in "compute units". Real EVM transactions
|
||||
/// spend microseconds in the interpreter (opcode dispatch, keccak, ecrecover,
|
||||
/// SLOAD/SSTORE bookkeeping). A pure state-transition toy tx costs only
|
||||
/// nanoseconds, so a parallel executor over it measures nothing but its own
|
||||
/// scheduling overhead. `gas` makes each tx pay a realistic, deterministic
|
||||
/// amount of CPU (identical in the sequential and parallel paths), so the
|
||||
/// benchmark measures the executor doing real work. It does NOT affect the
|
||||
/// resulting state, so correctness is independent of `gas`.
|
||||
pub gas: u32,
|
||||
}
|
||||
|
||||
/// A plain materialized state snapshot: `Key -> Value` (absent == 0).
|
||||
pub type StateMap = HashMap<Key, Value>;
|
||||
113
parallel-executor/src/vm.rs
Normal file
113
parallel-executor/src/vm.rs
Normal file
@ -0,0 +1,113 @@
|
||||
//! The transaction execution semantics.
|
||||
//!
|
||||
//! CRITICAL DESIGN POINT: there is exactly ONE implementation of what a
|
||||
//! transaction *does*, `execute_txn`. It runs against an abstract `VmView`.
|
||||
//!
|
||||
//! - The SEQUENTIAL oracle drives it with a view backed directly by the live
|
||||
//! state map.
|
||||
//! - The PARALLEL Block-STM executor drives it with a view backed by the
|
||||
//! multi-version memory (which records the read set and can signal that the
|
||||
//! execution must abort because it read an ESTIMATE placeholder).
|
||||
//!
|
||||
//! Because both paths share this exact function, the parallel result can only
|
||||
//! ever differ from the sequential result if the *concurrency control* is
|
||||
//! wrong. That is precisely the property the correctness harness proves.
|
||||
|
||||
use crate::types::{Key, TxKind, Txn, Value};
|
||||
|
||||
/// Simulate the CPU cost of executing an EVM transaction: `gas` rounds of real
|
||||
/// keccak256 seeded by `seed`. This is deterministic and input-dependent so the
|
||||
/// optimizer cannot elide it, and it is pure (does not touch shared state), so
|
||||
/// it models interpreter/hashing cost without adding artificial conflicts. Both
|
||||
/// the sequential oracle and the parallel executor run this identically.
|
||||
#[inline(never)]
|
||||
fn evm_work(gas: u32, seed: u128) -> u128 {
|
||||
let mut acc = seed ^ 0x9E3779B97F4A7C15_9E3779B97F4A7C15u128;
|
||||
let mut buf = [0u8; 16];
|
||||
for _ in 0..gas {
|
||||
buf.copy_from_slice(&acc.to_le_bytes());
|
||||
let h = crate::keccak::keccak256(&buf);
|
||||
acc = u128::from_le_bytes(h[0..16].try_into().unwrap());
|
||||
}
|
||||
acc
|
||||
}
|
||||
|
||||
fn work_seed(kind: &TxKind) -> u128 {
|
||||
match kind {
|
||||
TxKind::Transfer { from, to, amount } => {
|
||||
(*from as u128) << 96 ^ (*to as u128) << 32 ^ *amount
|
||||
}
|
||||
TxKind::Sweep { from, to } => (*from as u128) << 64 ^ (*to as u128),
|
||||
TxKind::Increment { contract, slot } => (*contract as u128) << 64 ^ (*slot as u128),
|
||||
TxKind::AmmSwap { contract, dx, .. } => (*contract as u128) << 96 ^ *dx,
|
||||
}
|
||||
}
|
||||
|
||||
/// Abstract view over state that a transaction reads/writes.
|
||||
pub trait VmView {
|
||||
/// Read the current value of `key`.
|
||||
///
|
||||
/// Returns `None` to mean "this execution must abort right now" (the
|
||||
/// parallel view returns `None` when it reads an ESTIMATE written by a
|
||||
/// not-yet-final lower transaction). The sequential view never returns
|
||||
/// `None`. Callers propagate `None` with `?`.
|
||||
fn read(&mut self, key: Key) -> Option<Value>;
|
||||
|
||||
/// Buffer a write to `key`. Writes are visible to later reads *within the
|
||||
/// same transaction* (read-your-writes).
|
||||
fn write(&mut self, key: Key, value: Value);
|
||||
}
|
||||
|
||||
/// Execute one transaction against `view`.
|
||||
///
|
||||
/// Returns `Some(())` on a completed execution and `None` if the execution was
|
||||
/// aborted mid-way because a read hit an ESTIMATE (parallel path only). All
|
||||
/// arithmetic is saturating so that both paths behave identically and never
|
||||
/// panic on overflow.
|
||||
pub fn execute_txn<V: VmView>(txn: &Txn, view: &mut V) -> Option<()> {
|
||||
match &txn.kind {
|
||||
TxKind::Transfer { from, to, amount } => {
|
||||
let from_bal = view.read(Key::Balance(*from))?;
|
||||
if from_bal >= *amount {
|
||||
view.write(Key::Balance(*from), from_bal - *amount);
|
||||
let to_bal = view.read(Key::Balance(*to))?;
|
||||
view.write(Key::Balance(*to), to_bal.saturating_add(*amount));
|
||||
}
|
||||
// insufficient funds -> no-op (still a valid, committed no-op)
|
||||
}
|
||||
TxKind::Sweep { from, to } => {
|
||||
let from_bal = view.read(Key::Balance(*from))?;
|
||||
if from_bal > 0 {
|
||||
view.write(Key::Balance(*from), 0);
|
||||
let to_bal = view.read(Key::Balance(*to))?;
|
||||
view.write(Key::Balance(*to), to_bal.saturating_add(from_bal));
|
||||
}
|
||||
}
|
||||
TxKind::Increment { contract, slot } => {
|
||||
let cur = view.read(Key::Storage(*contract, *slot))?;
|
||||
view.write(Key::Storage(*contract, *slot), cur.saturating_add(1));
|
||||
}
|
||||
TxKind::AmmSwap {
|
||||
contract,
|
||||
x_slot,
|
||||
y_slot,
|
||||
dx,
|
||||
} => {
|
||||
let x = view.read(Key::Storage(*contract, *x_slot))?;
|
||||
let y = view.read(Key::Storage(*contract, *y_slot))?;
|
||||
let new_x = x.saturating_add(*dx);
|
||||
if new_x > 0 {
|
||||
// constant-product: dy = y * dx / (x + dx)
|
||||
let dy = (y.saturating_mul(*dx)) / new_x;
|
||||
view.write(Key::Storage(*contract, *x_slot), new_x);
|
||||
let dy = if dy > y { y } else { dy };
|
||||
view.write(Key::Storage(*contract, *y_slot), y - dy);
|
||||
}
|
||||
}
|
||||
}
|
||||
// Pay the simulated EVM execution cost. Pure CPU; does not affect state.
|
||||
if txn.gas > 0 {
|
||||
std::hint::black_box(evm_work(txn.gas, work_seed(&txn.kind)));
|
||||
}
|
||||
Some(())
|
||||
}
|
||||
123
parallel-executor/src/workload.rs
Normal file
123
parallel-executor/src/workload.rs
Normal file
@ -0,0 +1,123 @@
|
||||
//! Deterministic, seedable workload generation. No external RNG crate: a small
|
||||
//! SplitMix64 gives reproducible batches so a failing case can be replayed
|
||||
//! exactly. `conflict` in [0,1] tunes how often a transaction touches the small
|
||||
//! "hot" set (shared accounts / the shared counter / the shared AMM pool),
|
||||
//! which is what drives the abort rate and destroys parallelism when high.
|
||||
|
||||
use crate::types::{Key, StateMap, TxKind, Txn, Value};
|
||||
|
||||
pub struct SplitMix64 {
|
||||
state: u64,
|
||||
}
|
||||
|
||||
impl SplitMix64 {
|
||||
pub fn new(seed: u64) -> Self {
|
||||
SplitMix64 { state: seed }
|
||||
}
|
||||
pub fn next_u64(&mut self) -> u64 {
|
||||
self.state = self.state.wrapping_add(0x9E3779B97F4A7C15);
|
||||
let mut z = self.state;
|
||||
z = (z ^ (z >> 30)).wrapping_mul(0xBF58476D1CE4E5B9);
|
||||
z = (z ^ (z >> 27)).wrapping_mul(0x94D049BB133111EB);
|
||||
z ^ (z >> 31)
|
||||
}
|
||||
pub fn below(&mut self, n: u64) -> u64 {
|
||||
if n == 0 {
|
||||
0
|
||||
} else {
|
||||
self.next_u64() % n
|
||||
}
|
||||
}
|
||||
/// probability p in [0,1] scaled to 1e6.
|
||||
pub fn chance(&mut self, p_ppm: u64) -> bool {
|
||||
self.below(1_000_000) < p_ppm
|
||||
}
|
||||
}
|
||||
|
||||
pub struct WorkloadConfig {
|
||||
pub num_txns: usize,
|
||||
pub num_accounts: u64,
|
||||
pub num_hot_accounts: u64,
|
||||
pub num_contracts: u64,
|
||||
pub num_hot_slots: u64,
|
||||
/// probability (0..1e6 ppm) that a given account/slot pick is from the hot set
|
||||
pub conflict_ppm: u64,
|
||||
/// simulated EVM execution cost per tx (keccak rounds). 0 = trivial tx.
|
||||
pub gas: u32,
|
||||
pub seed: u64,
|
||||
}
|
||||
|
||||
/// Build the base state: every account funded, hot slots seeded, AMM pools seeded.
|
||||
pub fn make_base_state(cfg: &WorkloadConfig) -> StateMap {
|
||||
let mut s = StateMap::new();
|
||||
for a in 0..cfg.num_accounts {
|
||||
s.insert(Key::Balance(a), 1_000_000u128);
|
||||
}
|
||||
for c in 0..cfg.num_contracts {
|
||||
// slot layout: 0..num_hot_slots = counters; then two AMM slots 1000,1001
|
||||
for slot in 0..cfg.num_hot_slots {
|
||||
s.insert(Key::Storage(c, slot), 0);
|
||||
}
|
||||
s.insert(Key::Storage(c, 1000), 1_000_000u128); // AMM reserve x
|
||||
s.insert(Key::Storage(c, 1001), 1_000_000u128); // AMM reserve y
|
||||
}
|
||||
s
|
||||
}
|
||||
|
||||
impl WorkloadConfig {
|
||||
fn pick_account(&self, rng: &mut SplitMix64) -> u64 {
|
||||
if self.num_hot_accounts > 0 && rng.chance(self.conflict_ppm) {
|
||||
rng.below(self.num_hot_accounts)
|
||||
} else {
|
||||
self.num_hot_accounts + rng.below(self.num_accounts - self.num_hot_accounts)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Generate a batch. Mix: transfers, sweeps, increments, AMM swaps.
|
||||
pub fn generate_batch(cfg: &WorkloadConfig) -> Vec<Txn> {
|
||||
let mut rng = SplitMix64::new(cfg.seed);
|
||||
let mut txns = Vec::with_capacity(cfg.num_txns);
|
||||
for _ in 0..cfg.num_txns {
|
||||
let roll = rng.below(100);
|
||||
let kind = if roll < 60 {
|
||||
// 60% transfers
|
||||
let from = cfg.pick_account(&mut rng);
|
||||
let mut to = cfg.pick_account(&mut rng);
|
||||
if to == from {
|
||||
to = (to + 1) % cfg.num_accounts;
|
||||
}
|
||||
let amount = 1 + rng.below(1000) as Value;
|
||||
TxKind::Transfer { from, to, amount }
|
||||
} else if roll < 75 {
|
||||
// 15% sweeps
|
||||
let from = cfg.pick_account(&mut rng);
|
||||
let mut to = cfg.pick_account(&mut rng);
|
||||
if to == from {
|
||||
to = (to + 1) % cfg.num_accounts;
|
||||
}
|
||||
TxKind::Sweep { from, to }
|
||||
} else if roll < 90 {
|
||||
// 15% shared-counter increments (the serializability stressor)
|
||||
let contract = rng.below(cfg.num_contracts.max(1));
|
||||
let slot = if cfg.num_hot_slots > 0 {
|
||||
rng.below(cfg.num_hot_slots)
|
||||
} else {
|
||||
0
|
||||
};
|
||||
TxKind::Increment { contract, slot }
|
||||
} else {
|
||||
// 10% AMM swaps against a shared pool
|
||||
let contract = rng.below(cfg.num_contracts.max(1));
|
||||
let dx = 1 + rng.below(10_000) as Value;
|
||||
TxKind::AmmSwap {
|
||||
contract,
|
||||
x_slot: 1000,
|
||||
y_slot: 1001,
|
||||
dx,
|
||||
}
|
||||
};
|
||||
txns.push(Txn { kind, gas: cfg.gas });
|
||||
}
|
||||
txns
|
||||
}
|
||||
146
results/PROVEN-RESULTS-2026-07-11.md
Normal file
146
results/PROVEN-RESULTS-2026-07-11.md
Normal file
@ -0,0 +1,146 @@
|
||||
# AERE Network: Proven Results (status as of 2026-07-11)
|
||||
|
||||
> **UPDATE (2026-07-16): two facts in this 2026-07-11 snapshot have since changed on
|
||||
> mainnet and are corrected inline below.** (1) The validator set has grown from 3 to
|
||||
> **7 QBFT validators** (f=2, quorum 5-of-7), confirmed live via
|
||||
> `qbft_getValidatorsByBlockNumber`; still one operator, one client (Besu). (2) The
|
||||
> native PQC precompiles (band `0x0AE1`-`0x0AE5`) and the extended EIP-2935 block-hash
|
||||
> lookback are **now LIVE on mainnet**, activated at block 9,189,161. Where the body
|
||||
> below says "3 validators" or "precompiles inactive on mainnet," read the corrected
|
||||
> value. Everything else in this snapshot still holds. Consensus remains classical
|
||||
> secp256k1 QBFT (not post-quantum); no external audit yet.
|
||||
|
||||
**Chain:** AERE Network mainnet, chain ID 2800 (Hyperledger Besu QBFT, ~0.5s blocks, 7 validators / f=2 as of 2026-07-16 [was 3 at this snapshot], single operator, one client).
|
||||
**Source of truth for canonical addresses:** `sdk-js/src/addresses.ts`.
|
||||
**Purpose:** one honest reference of what is actually real, at what scope, with the on-chain evidence and the explicit testnet-versus-mainnet, staged, and proof-of-approach boundaries. Nothing here is a marketing figure. Every measured number is from a repo artifact, an on-chain transaction, or a captured session log. Where a capability is demonstrated on an isolated testnet or scratch fork rather than on mainnet chain 2800, that is stated in the same breath as the result.
|
||||
|
||||
This document does not claim anything that mainnet chain 2800 does not do today, except where a result is explicitly labeled TESTNET, SCRATCH-FORK, or STAGED.
|
||||
|
||||
---
|
||||
|
||||
## 0. The honesty baseline that applies to everything below
|
||||
|
||||
AERE runs seven QBFT validators (f=2, quorum 5-of-7; three at the 2026-07-11 snapshot) under a single operator, one execution client (Hyperledger Besu), and has had no external security audit of its AERE-authored contracts or client changes. Real usage is thin. Consensus on mainnet is classical secp256k1 QBFT and is not post-quantum. Every result below sits on that foundation and is scoped against it. The strong results are real; the boundaries are real too.
|
||||
|
||||
---
|
||||
|
||||
## 1. Consensus post-quantum quorum certificate (TESTNET demonstration)
|
||||
|
||||
**What is proven.** A real gossiped `>= 2f+1` Falcon-512 quorum certificate, embedded in every block, verified by every node on import, and **blocking after a fork block**, demonstrated on an isolated N=4 QBFT testnet. Each validator holds a fresh Falcon-512 keypair and Falcon-signs the same commit hash the ECDSA committed seal signs, piggybacking the seal on the QBFT commit message; the block creator re-verifies, de-duplicates, and embeds the set of distinct valid seals as the block's certificate. ECDSA committed seals remain the decisive safety and liveness seal throughout.
|
||||
|
||||
**Evidence (isolated N=4 testnet, fresh keys, isolated chain, 2s blocks, Falcon fork block 20).**
|
||||
- Full consensus unit suite green after the change (`consensus:common`, `consensus:qbft-core`, `consensus:qbft`).
|
||||
- Block-hash preservation: the certificate is excluded from every signed pre-image, so a header is byte-identical to upstream with or without the certificate. Proven three ways (committed-seal and block-hash pre-images identical, `FalconAudit` on real headers, byte-identical stored encoding when empty).
|
||||
- Healthy run #1: 229 blocks, all 4 nodes agreed at every height, blocks 20 onward post-fork BLOCKING each with a valid 3-of-4 certificate, 0 rejects, exact 2s cadence. Run #2: reproduced from fresh genesis.
|
||||
- `FalconAudit` on real post-fork headers: the certificate validator set equals the ECDSA committer set on each block (proving genuinely gossiped, per-block-varying seals, not a static self-seal); genuine 9/9 ACCEPT, tampered-signature 9/9 REJECT, tampered-commit-hash 9/9 REJECT.
|
||||
- Negative tests: (a) tamper one Falcon seal rejects it; (c) one validator with a wrong Falcon key, the chain still finalizes via the honest quorum with transient single-block retry, no halt; (b) two bad signers (more than f), the post-fork chain correctly HALTS at block 353 because the Falcon quorum cannot form, while an ECDSA quorum still existed. That halt is the blocking rule working as designed.
|
||||
|
||||
**Honest scope.** This is a TESTNET demonstration only. Mainnet chain 2800 consensus is classical secp256k1 QBFT and is NOT post-quantum; this work does not change what mainnet validators sign. Making the Falcon quorum blocking raises the liveness requirement to `2f+1` Falcon-valid validators and is most acute at small N (at N=4 a single faulty Falcon signer forces all three honest validators to commit each block). The recommendation is to ship the always-on additive certificate (log-only, full gossip and verification) and to defer blocking activation until the validator set has slack over the quorum (N >= 7). Blocking must not be activated on chain 2800 without an explicit founder go.
|
||||
|
||||
**Artifacts:** `consensus-pqc/` (`QUORUM-DESIGN.md`, quorum patch, run logs), held in the internal working repository and not published.
|
||||
|
||||
---
|
||||
|
||||
## 2. Full-EVM validity: real revm-in-SP1 proofs of real chain-2800 blocks
|
||||
|
||||
**What is proven.** A real AERE mainnet block, re-executed with the real `revm` execution stack (SP1-patched `reth` 1.9.3 / `revm`, tag `reth-1.9.3-sp1-6.1.0`, SP1 v6.1.0) inside the SP1 zkVM, reproduces the canonical post-state root, and the resulting Groth16 proof verifies on chain 2800 through the live SP1 gateway. Full opcode set, call frames, `SLOAD`/`SSTORE`, `LOG`, EIP-1559 fee split, and the Cancun/Prague/Osaka system calls all execute and all affect the proven root. This is the real EVM, not a bounded subset.
|
||||
|
||||
**On-chain evidence (mainnet chain 2800).**
|
||||
- Single block, `AereEVMValidity` `0x1f2CB0ebDBb21500e99868DbF1dB3abCcDd7DF26` (canonical in `sdk-js/src/addresses.ts`): block **9073073** recorded via `recordBlock` tx `0xb28a39507f8c03e6ffcfed307f9848814dd9d35268f01983825b658cbc56415c` (submit block 9074454, status 1), gasUsed 460,015, well under the EIP-7825 per-transaction cap of 16,777,216. A second block **9075035** was proven fresh in the same session. Tamper test: flipping one byte of `postStateRoot` makes the on-chain-equivalent verifier reject the proof.
|
||||
- 16-block contiguous batch, `AereEVMValidityBatch` `0x49D30a5999eA5b7f6eFf12196AB006316683Ff3C` (canonical): real chain-2800 blocks **9111008..9111023** proven in one Groth16 (batch program vkey `0x00418c4f40178103cc75ae57b63703f6075cabdce23582fed6043ceae819192e`), each block's recomputed post-state root threaded into the next block's pre-state, recorded via `recordBatch` tx `0x074a4e6c17855b78db4c56808e253d4c00a7b392cf0d141e4aa0e6e299ded9ec` (submit block 9114536, status 1), gasUsed 460,817, under the cap.
|
||||
|
||||
**New this session: dense workload proof (removes the low-tx-density caveat at demonstrated scale).** Historical mainnet blocks are low-density and the live node prunes state (Bonsai), which limited earlier witnessable batches to a handful of transactions. To remove that caveat, a genuinely dense real-EVM workload was proven with the same batch prover and verified read-only against the same live mainnet gateway:
|
||||
- A single ultra-dense block with **155 real transactions and 460 SSTOREs** (52 freshly-created EOAs, 52 fresh ERC-20 holder slots), 29,572,987 guest cycles: local SP1 Groth16 verify PASS, tampered `postStateRoot` REJECTED; the live mainnet SP1 gateway `0x9ca479C8c52C0EbB4599319a36a5a017BCC70628` accepted the real proof read-only and reverted both a tampered-public-values and a tampered-proof call.
|
||||
- A 2-block contiguous, state-continuity-chained dense batch (310 txs, 920 SSTOREs, 27,050,310 gas, 61,366,308 cycles; the guest enforces `post_root(N) == header.stateRoot` and `pre_root(N+1) == post_root(N)`): same live-gateway acceptance read-only, both tampers reverted.
|
||||
- These proofs bind to the same batch-program vkey the mainnet batch anchor uses. No mainnet transaction was sent; verification was `eth_call` only.
|
||||
|
||||
**Honest scope.**
|
||||
- This is a **proof-of-approach on real blocks, not a continuous production rollup cadence.** The mainnet-recorded blocks and batch are real chain-2800 blocks but small; the anchors record isolated proven blocks and ranges rather than a sequenced chain. There is no sequencer, no forced inclusion, no data-availability commitment, and no withdrawal settlement.
|
||||
- The dense workload is an **isolated, engineered** dense chain produced on a throwaway single-validator node running the AERE fork profile (chain-config 2800), in archive mode so heavy state is witnessable. It is NOT a replay of historical mainnet blocks. Its value is to remove the "low tx density" limitation at the demonstrated scale (roughly two full-density blocks / ~61M cycles was the practical ceiling on the box used; a longer dense batch is a pure prover-resource matter, not an approach question). Compatibility was confirmed: the reth-in-SP1 guest reproduced Besu 26.4.0's exact `header.stateRoot` for every dense block, the same agreement already established on real chain-2800 blocks.
|
||||
- **Canonical pre-state binding V2, CORRECTED 2026-07-14: LIVE, not staged, and unused.** This bullet originally said V2 "must not be deployed yet." That was true when written (2026-07-11) and is no longer true: the AerePQC `futureEips` fork activated at mainnet block 9,189,161 (bringing EIP-2935 history storage live), and `AereEVMValidityV2` (`0x4884ad66…768D9B`, deploy block 9,257,558) and `AereEVMValidityBatchV2` (`0xe154B899…9E690aE`, deploy block 9,257,566) were then deployed with the re-proved vkeys wired in, per the sequence this document's own §"Conclusion / go condition" in `rollup-evm-validity/NOTE.md` specified. Verified live on 2026-07-14 via a direct `eth_getCode` read against `https://rpc.aere.network` (non-empty bytecode both addresses) and a `PROGRAM_VKEY()` read-back matching `sdk-js/src/addresses.ts` exactly. The V1 anchors above remain honest about attesting only self-consistent execution; V2 now genuinely binds to the canonical chain. **However, live reads of both V2 contracts on 2026-07-14 show `provenCount() == 0` on each**, no `recordBlock` or `recordBatch` call has actually succeeded against either V2 contract yet, despite `addresses.ts`'s inline comment on `AereEVMValidityV2` claiming "batch path proven, see AereEVMValidityBatchV2." That comment is stale/inaccurate as of this correction; treat the V2 canonical-binding pipeline as deployed but never yet exercised end-to-end on mainnet. See the "2026-07-14 reconciliation" section of the internal `VALIDITY-ROLLUP-SEQUENCER-PLAN.md` (not published) for the full picture, including a separate, still-open gap: no batch-guest source (the program behind the 320-byte `AereEVMValidityBatchV2` layout) is checked into this repo at all, only the single-block guest is.
|
||||
- The crypto is classical (SP1 Groth16 over BN254, classical QBFT keys); this validity path is not itself post-quantum.
|
||||
|
||||
**Artifacts:** `rollup-evm-validity/` (host/guest/spec), `dense-batch-2026-07-11/` (dense proofs + gateway verifications), `NOTE.md` (V2 staging), `contracts/deployments/evm-validity*.json`. These are held in the internal working repository and are not published; the on-chain transactions and addresses cited above are independently checkable against chain 2800 without them.
|
||||
|
||||
---
|
||||
|
||||
## 3. Recursive proof aggregation at scale (LIVE on-chain, n=10)
|
||||
|
||||
**What is proven.** Ten real, heterogeneous inner SP1 proofs, drawn from three distinct real production programs (zkscreen sanctions-screening, over18 age credential, zkml-mnist inference), folded into a single Groth16 proof and verified once on-chain. The aggregator recomputes a composite digest from caller-supplied inner data and requires it to equal the proof's committed output, so a caller cannot lie about which programs were folded.
|
||||
|
||||
**On-chain evidence (mainnet chain 2800).** `AereProofAggregator` `0x6a260238890E740dB12b371E0C5d17a2470F84C5` (canonical, owner Foundation).
|
||||
- n=10 record tx `0xd1fd4d60635fbafe8b269ea156a6bbc65e2b6ff8b886eb7910569a9c9344d978`, block 8930005, gasUsed **393,844**, read-back `count = 10`, `recognized = 10`.
|
||||
- n=3 record tx `0xde4c102ab86567bfb7b384a784c049bf6ded33378e0b71acc0bfe97b7b8d4b39`, block 8811943, gasUsed **387,858**.
|
||||
- On-chain cost is essentially flat in N: raising N by 3.3x raised gas by about 1.5%, because the cost is dominated by the fixed Groth16 verification, not by the number folded. Tamper checks: a tampered proof reverts, and dropping one child (so the recomputed composite no longer matches) reverts.
|
||||
|
||||
**Honest scope.** This is a proof-of-approach attestation aggregator, not a production rollup batcher. The inner statements in the scale run are deterministically derived for the demonstration, not organic traffic from ten distinct users (the ELFs are the real programs, checked byte-for-byte against their on-chain vkeys before folding). Proving is heavy and off-chain (about 1549s wall time for n=10). No run beyond n=10 is recorded on-chain yet (n=16 in progress). Soundness rests on Groth16 over BN254 and the SP1 recursion, which are classical, not post-quantum.
|
||||
|
||||
**Artifacts:** `research/specs/spec-recursive-aggregation-scale.md`, `contracts/deployments/proof-aggregator*.json`.
|
||||
|
||||
---
|
||||
|
||||
## 4. Parallel execution: Block-STM executor and forked-client commit prototype (TESTNET / benchmark)
|
||||
|
||||
**What is proven.**
|
||||
- A real from-scratch Block-STM optimistic-concurrency parallel executor (`aere-block-stm`, Rust, zero external dependencies) that executes a transaction batch across many cores and commits a keccak256 state root **bit-identical to sequential execution**. Correctness is proven over **6000 randomized adversarial comparisons with zero mismatches** (6 workload profiles x 200 batches x {1,2,4,8,16} threads), including fully-conflicting adversarial cases.
|
||||
- Measured execution-bound speedup on a 16-core box is roughly **8.6x to 9.3x** on low and medium-conflict batches (versus the single-threaded sequential oracle), degrading gracefully to about 4.6x under a hot single-counter and correctly falling below 1.0x on a pathological all-conflict batch where no parallelism exists.
|
||||
- New this session: a parallel state-root **commit** prototype in the forked Besu client (Bonsai world-state), self-checked to produce a commit root **bit-identical to the sequential-commit root** across correctness reps (independent transfers, commit-heavy SSTORE, deletions, mixed kitchen-sink).
|
||||
|
||||
**Honest boundary (the important part).** The state-root COMMIT does **not** parallelize for a throughput win: it is measured to be allocation and memory-bandwidth bound, and adding commit worker threads (widths 2 to 16) makes it slower than a single worker, not faster. What does help is a single-threaded commit rewrite: at width 1 the AERE commit is roughly **2x cheaper than the stock parallel-trie commit** (for example, on medium tries about 316.98ms stock versus 142.38ms at width 1). End-to-end, Block-STM execution plus the width-1 commit gives a modest win on commit-heavy load (about 1.27x to 1.42x versus stock sequential exec plus sequential commit), and the real execution-side win is the ~9x above. An end-to-end network throughput win is gated on real execution-heavy load, and none of this runs on mainnet.
|
||||
|
||||
**On-chain adjacent (live).** `AereBlockSTMRegistry` `0x98E2C3e615841919d173D8FF642514c5902E8A28` is a live, advisory, opt-in hint registry (holds no funds, changes no execution semantics, has no L1 consumer yet). The bounded-VM validity anchor `AereRollupValidity` `0x38772063572DF94E90351e44ccbBEefD5F497fbd` has epoch 0 recorded from a real proof of the same deterministic executor.
|
||||
|
||||
**Honest scope.** AERE L1 (Besu) executes **sequentially** on mainnet today; nothing here changes that. The speedups are single-machine microbenchmarks, not TPS or network-throughput claims. Promoting Block-STM into base consensus is a multi-month Java-client effort; the commit prototype and benchmark are a research control surface, not a mainnet-active feature.
|
||||
|
||||
**Artifacts:** `parallel-executor/` (published in this repository, including the end-to-end throughput report and its raw run logs), `research/specs/spec-parallel-execution.md` (published). The forked-client commit patch and its benchmark logs are held in the internal working repository and are not published.
|
||||
|
||||
---
|
||||
|
||||
## 5. Native PQC precompiles and EIP-2935 lookback in the Besu fork (SCRATCH-FORK, staged for mainnet)
|
||||
|
||||
**What is proven.** A fork of Hyperledger Besu 26.4.0 (source tag 26.4.0, commit `d2032017`) adds five native precompiles wrapping the audited Bouncy Castle 1.83 BCPQC verifiers already on the client classpath (Falcon-512, Falcon-1024, ML-DSA-44 FIPS 204 internal, SLH-DSA-SHA2-128s FIPS 205 internal, SHAKE256 FIPS 202), introducing zero new dependencies and zero hand-rolled cryptography, plus a native EIP-2935 8191-block history-storage lookback written by consensus. Every scheme is validated against the same official NIST KAT and ACVP vectors the pure-Solidity verifiers use, positive and negative, including 15/15 ML-DSA-44 ACVP cases and 14/14 SLH-DSA-SHA2-128s ACVP cases (12 negatives each).
|
||||
|
||||
**Measured on the isolated scratch fork (chain 28099), full verify-and-record transactions:** Falcon-512 86,336 gas; Falcon-1024 **145,496** gas (0.87% of the 16,777,216 cap); ML-DSA-44 **351,050** gas (2.09%); SLH-DSA-SHA2-128s 558,276 gas; SHAKE256 21,470 gas. This moves the two schemes that are view-only in pure Solidity on mainnet (Falcon-1024 at roughly 21.7M gas, ML-DSA-44 at roughly 52.9M gas) to record-on-chain with wide margin.
|
||||
|
||||
**Keyless statefork rehearsal against real 2800 state.** On the infra host, a keyless two-axis rehearsal (no real validator keys copied anywhere, verified) confirmed a second non-validating follower running the fork binary validated the real 2800 chain to head under the fork's rules, and an isolated single-validator network proved the five PQC precompiles plus EIP-2935 activate on Osaka-format blocks (futureEips activated at block 182: precompiles inactive before, active after; Falcon-1024 gasUsed 172,720, ML-DSA-44 351,970, records=1; EIP-2935 history read equals the actual block hash; block format identical before and after). The live node was untouched.
|
||||
|
||||
**Honest scope.** These precompiles and the EIP-2935 write path are **STAGED for a supervised mainnet fork (the AerePQC futureEips activation), NOT active on chain 2800 today.** On mainnet the reserved precompile band addresses are empty, Falcon-1024 and ML-DSA-44 remain view-only in pure Solidity, and the EIP-2935 history contract has no code (the deployed anchor falls back to the 256-block `blockhash()` window). Activation is a coordinated, client-only, flag-day hard fork with no re-genesis and no state migration, and must be externally audited before it secures material value. It requires an explicit founder go.
|
||||
|
||||
**Artifacts:** `research/aip-draft-pqc-precompiles.md`, `research/specs/spec-eip2935-lookback.md`, and `statefork-test-results/` (internal working repository, not published).
|
||||
|
||||
---
|
||||
|
||||
## 6. Application and account layer PQC verifier suite (LIVE on-chain)
|
||||
|
||||
**What is proven.** A suite of post-quantum signature verifiers, in pure Solidity on the live chain's own EVM, covering both families NIST is standardizing: the hash-based WOTS+, XMSS-SHA2_10_256 (RFC 8391), and SLH-DSA-SHA2-128s (FIPS 205), and the lattice-based Falcon-512, Falcon-1024 (NIST round-3), and ML-DSA-44 (FIPS 204). Each is validated bit-for-bit against official NIST KAT or ACVP fixtures committed to the repository, in both directions (valid vectors accept, crafted-invalid vectors reject). Four of the six record a full verification on-chain within the EIP-7825 per-transaction gas cap (XMSS 1,561,963 gas; SLH-DSA 1,812,066 gas; Falcon-512 demonstrated inside a full ERC-4337 userOp at 10,278,313 gas and hybrid auth at 10,299,873 gas; WOTS+ well under the cap). Falcon-1024 and ML-DSA-44 are verifiable today as read-only `eth_call` views because a recording transaction would exceed the cap in pure Solidity (this is the exact gap the precompile fork of section 5 closes).
|
||||
|
||||
On top of the verifiers sit a hybrid ECDSA-plus-Falcon-512 authorizer and a Falcon-512-owned ERC-4337 smart account, both recording on-chain.
|
||||
|
||||
**Honest scope.** This is application-layer and account-layer post-quantum security. It does not make consensus post-quantum; mainnet validators still sign classical secp256k1 QBFT. The verifiers are unaudited (their assurance rests on bit-for-bit agreement with the official NIST vectors and independent from-scratch cross-checks, which is strong evidence of correctness but is not an audit). Only the smallest ML-DSA and SLH-DSA parameter sets and only the internal interfaces are implemented. With those caveats stated honestly, and to the best of our present knowledge, we are not aware of any other public chain that verifies all of these on-chain.
|
||||
|
||||
**Canonical addresses (verbatim from `sdk-js/src/addresses.ts`):** AerePQCVerifier `0x1cE2949e8cE3f1A77b178aF767a4455c08ec6F82`, AereXmssVerifier `0x77b14E264D0bb08d304d4e0E527F0fCdFc88B112`, AereSphincsVerifier `0xAfFc9F8d950969b46b54e77758BbFf7e000c87e6`, AereFalcon512Verifier `0x4E8e9682329e646784fB3bd01430aA4bA54D8fFC`, AereFalcon1024Verifier `0xF0aFA59BaB2058e4B6e6B424b7f76750F1F66e36`, AereMLDSA44Verifier `0xf1F7A6Acd82D5DAf9AF3166a2F736EE52C5F85AE`, AereHybridAuth `0xc20390C9656ECe1AE37603c84E395bC898b3FAA1`, AerePQCAccountFactory `0xd5315Ea7caa60d320c4f34b1bEd70dd9cc02CE58`.
|
||||
|
||||
**Artifacts:** `research/pqc-onchain-verification.md`, `aips/AIP-4.md`.
|
||||
|
||||
---
|
||||
|
||||
## 7. Proof of reality: independent adversarial read-only sweep of mainnet 2800
|
||||
|
||||
An independent adversarial read-only sweep of mainnet chain 2800 returned **zero claim violations**. Specifically, and consistent with everything above:
|
||||
|
||||
- The SP1 gateway `0x9ca479C8c52C0EbB4599319a36a5a017BCC70628` verifies real proofs and rejects tampered ones.
|
||||
- Recorded validity batch roots equal the canonical block `stateRoot` values.
|
||||
- Block time is about **0.506s**, there were **3 validators** at this 2026-07-11 snapshot (now **7**, f=2, per the update banner), and the native token supply is exactly **2,800,000,000 AERE**.
|
||||
- At this snapshot the PQC precompiles were **inactive** on mainnet. **This has since changed:** the native PQC precompiles (band `0x0AE1`-`0x0AE5`) and the extended EIP-2935 lookback were activated on mainnet at block 9,189,161 and are now live. The section 5 "staged, not live" wording reflects the 2026-07-11 state, not the current one.
|
||||
|
||||
The sweep confirms the split this document draws throughout: the live-on-mainnet claims (the verifier suite, the aggregator, the validity anchors, the block time, the validator count, the supply) hold on chain 2800, and the then-not-yet-on-mainnet claims are labeled testnet, scratch-fork, or staged (of these, the native precompiles and EIP-2935 lookback have since gone live on mainnet per the update banner; the consensus PQC quorum, canonical-binding V2, and parallel L1 execution remain testnet/scratch-fork/staged).
|
||||
|
||||
---
|
||||
|
||||
## 8. One-line summary of scope
|
||||
|
||||
Live on mainnet chain 2800: the pure-Solidity PQC verifier suite, the recursive proof aggregator at n=10, the single-block and 16-block full-EVM validity anchors (proof-of-approach), the Block-STM hint registry, ~0.506s blocks, 7 validators (f=2; was 3 at this 2026-07-11 snapshot), 2.8B supply, and (since block 9,189,161, added after this snapshot) the native PQC precompiles and the extended EIP-2935 8191-block lookback. Proven on isolated testnet or scratch fork and staged for a supervised mainnet fork, not active today: the gossiped Falcon-512 consensus quorum certificate, canonical-binding validity V2, and the parallel-commit client prototype. Consensus on mainnet remains classical secp256k1 QBFT and is not post-quantum. No external audit yet.
|
||||
|
||||
---
|
||||
|
||||
*Every canonical address is verbatim from `sdk-js/src/addresses.ts`. Testnet, scratch-fork, and staged results are labeled as such and are cited from the named repo artifacts. No gas figure, transaction hash, block number, or measured result in this document is invented; each is drawn from an on-chain transaction or a committed artifact.*
|
||||
212
results/kat-results-mlkem-reference.json
Normal file
212
results/kat-results-mlkem-reference.json
Normal file
@ -0,0 +1,212 @@
|
||||
{
|
||||
"precompile": "ML-KEM-768 (FIPS 203) deterministic encapsulate",
|
||||
"address": "0x0000000000000000000000000000000000000ae6",
|
||||
"liveOnMainnet2800": false,
|
||||
"note": "TESTNET-ONLY precompile. Verified here against an independent pure-Python FIPS 203 oracle (stdlib hashlib only), not Bouncy Castle.",
|
||||
"oracle": "mlkem768_reference.py (this file)",
|
||||
"passed": 25,
|
||||
"failed": 0,
|
||||
"total": 25,
|
||||
"results": [
|
||||
{
|
||||
"tcId": "26",
|
||||
"ekLen": 1184,
|
||||
"ctLen": 1088,
|
||||
"ciphertextMatch": true,
|
||||
"sharedSecretMatch": true,
|
||||
"status": "PASS"
|
||||
},
|
||||
{
|
||||
"tcId": "27",
|
||||
"ekLen": 1184,
|
||||
"ctLen": 1088,
|
||||
"ciphertextMatch": true,
|
||||
"sharedSecretMatch": true,
|
||||
"status": "PASS"
|
||||
},
|
||||
{
|
||||
"tcId": "28",
|
||||
"ekLen": 1184,
|
||||
"ctLen": 1088,
|
||||
"ciphertextMatch": true,
|
||||
"sharedSecretMatch": true,
|
||||
"status": "PASS"
|
||||
},
|
||||
{
|
||||
"tcId": "29",
|
||||
"ekLen": 1184,
|
||||
"ctLen": 1088,
|
||||
"ciphertextMatch": true,
|
||||
"sharedSecretMatch": true,
|
||||
"status": "PASS"
|
||||
},
|
||||
{
|
||||
"tcId": "30",
|
||||
"ekLen": 1184,
|
||||
"ctLen": 1088,
|
||||
"ciphertextMatch": true,
|
||||
"sharedSecretMatch": true,
|
||||
"status": "PASS"
|
||||
},
|
||||
{
|
||||
"tcId": "31",
|
||||
"ekLen": 1184,
|
||||
"ctLen": 1088,
|
||||
"ciphertextMatch": true,
|
||||
"sharedSecretMatch": true,
|
||||
"status": "PASS"
|
||||
},
|
||||
{
|
||||
"tcId": "32",
|
||||
"ekLen": 1184,
|
||||
"ctLen": 1088,
|
||||
"ciphertextMatch": true,
|
||||
"sharedSecretMatch": true,
|
||||
"status": "PASS"
|
||||
},
|
||||
{
|
||||
"tcId": "33",
|
||||
"ekLen": 1184,
|
||||
"ctLen": 1088,
|
||||
"ciphertextMatch": true,
|
||||
"sharedSecretMatch": true,
|
||||
"status": "PASS"
|
||||
},
|
||||
{
|
||||
"tcId": "34",
|
||||
"ekLen": 1184,
|
||||
"ctLen": 1088,
|
||||
"ciphertextMatch": true,
|
||||
"sharedSecretMatch": true,
|
||||
"status": "PASS"
|
||||
},
|
||||
{
|
||||
"tcId": "35",
|
||||
"ekLen": 1184,
|
||||
"ctLen": 1088,
|
||||
"ciphertextMatch": true,
|
||||
"sharedSecretMatch": true,
|
||||
"status": "PASS"
|
||||
},
|
||||
{
|
||||
"tcId": "36",
|
||||
"ekLen": 1184,
|
||||
"ctLen": 1088,
|
||||
"ciphertextMatch": true,
|
||||
"sharedSecretMatch": true,
|
||||
"status": "PASS"
|
||||
},
|
||||
{
|
||||
"tcId": "37",
|
||||
"ekLen": 1184,
|
||||
"ctLen": 1088,
|
||||
"ciphertextMatch": true,
|
||||
"sharedSecretMatch": true,
|
||||
"status": "PASS"
|
||||
},
|
||||
{
|
||||
"tcId": "38",
|
||||
"ekLen": 1184,
|
||||
"ctLen": 1088,
|
||||
"ciphertextMatch": true,
|
||||
"sharedSecretMatch": true,
|
||||
"status": "PASS"
|
||||
},
|
||||
{
|
||||
"tcId": "39",
|
||||
"ekLen": 1184,
|
||||
"ctLen": 1088,
|
||||
"ciphertextMatch": true,
|
||||
"sharedSecretMatch": true,
|
||||
"status": "PASS"
|
||||
},
|
||||
{
|
||||
"tcId": "40",
|
||||
"ekLen": 1184,
|
||||
"ctLen": 1088,
|
||||
"ciphertextMatch": true,
|
||||
"sharedSecretMatch": true,
|
||||
"status": "PASS"
|
||||
},
|
||||
{
|
||||
"tcId": "41",
|
||||
"ekLen": 1184,
|
||||
"ctLen": 1088,
|
||||
"ciphertextMatch": true,
|
||||
"sharedSecretMatch": true,
|
||||
"status": "PASS"
|
||||
},
|
||||
{
|
||||
"tcId": "42",
|
||||
"ekLen": 1184,
|
||||
"ctLen": 1088,
|
||||
"ciphertextMatch": true,
|
||||
"sharedSecretMatch": true,
|
||||
"status": "PASS"
|
||||
},
|
||||
{
|
||||
"tcId": "43",
|
||||
"ekLen": 1184,
|
||||
"ctLen": 1088,
|
||||
"ciphertextMatch": true,
|
||||
"sharedSecretMatch": true,
|
||||
"status": "PASS"
|
||||
},
|
||||
{
|
||||
"tcId": "44",
|
||||
"ekLen": 1184,
|
||||
"ctLen": 1088,
|
||||
"ciphertextMatch": true,
|
||||
"sharedSecretMatch": true,
|
||||
"status": "PASS"
|
||||
},
|
||||
{
|
||||
"tcId": "45",
|
||||
"ekLen": 1184,
|
||||
"ctLen": 1088,
|
||||
"ciphertextMatch": true,
|
||||
"sharedSecretMatch": true,
|
||||
"status": "PASS"
|
||||
},
|
||||
{
|
||||
"tcId": "46",
|
||||
"ekLen": 1184,
|
||||
"ctLen": 1088,
|
||||
"ciphertextMatch": true,
|
||||
"sharedSecretMatch": true,
|
||||
"status": "PASS"
|
||||
},
|
||||
{
|
||||
"tcId": "47",
|
||||
"ekLen": 1184,
|
||||
"ctLen": 1088,
|
||||
"ciphertextMatch": true,
|
||||
"sharedSecretMatch": true,
|
||||
"status": "PASS"
|
||||
},
|
||||
{
|
||||
"tcId": "48",
|
||||
"ekLen": 1184,
|
||||
"ctLen": 1088,
|
||||
"ciphertextMatch": true,
|
||||
"sharedSecretMatch": true,
|
||||
"status": "PASS"
|
||||
},
|
||||
{
|
||||
"tcId": "49",
|
||||
"ekLen": 1184,
|
||||
"ctLen": 1088,
|
||||
"ciphertextMatch": true,
|
||||
"sharedSecretMatch": true,
|
||||
"status": "PASS"
|
||||
},
|
||||
{
|
||||
"tcId": "50",
|
||||
"ekLen": 1184,
|
||||
"ctLen": 1088,
|
||||
"ciphertextMatch": true,
|
||||
"sharedSecretMatch": true,
|
||||
"status": "PASS"
|
||||
}
|
||||
]
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user