Unified teaching module for big-flow steps 6, 12–14. One Radix transaction can touch many shards. Each shard commits and runs the whole tx, but only writes substates it owns. Remote data arrives as provisions — proven snapshots from the source shard at a fixed block height — overlaid on local storage for one deterministic execution. No global 2PC coordinator.
Work through the parts in order. Each part builds on the last. Plain-English lines summarize the idea; Technical lines tie to hyperscale-rs code (file names in monospace). Nothing here is optional for exam-style mastery — all bullets and Q&A from the unified syllabus are included.
| # | Part | You will understand… |
|---|---|---|
| 0 | Start here | The one-sentence model + reference USDC scenario |
| 1 | End-to-end flow (20 steps) | Submit → commit → provisions → execute → finalize |
| 2 | Provisions deep dive | Bundle shape, inbound vs outbound, shadow base, snapshot timing |
| 3 | Block fields map | Which header/body field does which job |
| 4 | Why Merkle / JMT | Business + security logic of each commitment |
| 5 | Q&A (37 items) | Sharp details — provision_tx_roots, lying, staleness, conflicts |
| 6 | One-page mental model | Whole pipeline on one screen |
| Big flow step | This module |
|---|---|
| 6 Tx to each touched shard | Part 1 · steps 1–5 |
| 7–11 BFT per shard | Part 1 · steps 7–12 |
| 12 Execution (cross-shard) | Part 1 · steps 16–18; Part 2 |
| 13–14 Coordination & composition | Parts 2–4; no 2PC |
A cross-shard Radix transaction touches multiple shards because its manifest reads or writes accounts/components on different committees. Hyperscale does not ship one shard’s live database to another. Instead:
Setup: Account A + component Ca live on Shard 1. Account B + component Cb live on Shard 2.
Manifest (one transaction):
Trace in sim: cross-shard E2E lab · JMT keys: JMT deep dive
ValidatorId is assigned to one home shard per epoch in TopologySnapshot. One physical node may host several ShardLoops if it runs validators on multiple shards.
Follow these in order. They are the full pipeline from user click to mempool Completed.
SubmitTransaction on a shard loop; async Radix validation extracts declared reads/writes from manifest instructions (manifest_analysis.rs). Plain English: the node reads the manifest like a checklist of every account the tx might touch.RoutableTransaction carries declared_reads / declared_writes as NodeId lists — routing, waves, and provisions all derive from these, not runtime discovery (routable.rs). Plain English: the protocol decides shards before execution, from the signed tx, not from “whatever the VM happens to read.”NodeHost can run multiple ShardLoops (one per hosted shard). Validator ↔ shard: each ValidatorId sits on one shard committee per epoch (TopologySnapshot), but each shard has many validators — so it is not 1 validator : 1 shard globally; it is N validators → 1 shard. Process ↔ shard is still N:M (one machine can host several loops) (host.rs, topology/snapshot.rs).AdmitAndGossipTransaction; passive shards AdmitTransaction only (mempool without submitted_locally / gossip) (host.rs). Plain English: one shard “owns” the submit; siblings still admit the tx locally but don’t re-broadcast it.in_flight eases (mempool/coordinator.rs). Plain English: getting the tx into a block is separate from getting state proofs later.all_shards_for_transaction unions reads ∪ writes; symmetric for routing (topology/snapshot.rs). Plain English: Shard 2 doesn’t run “only line 2” — it runs the entire manifest when dispatched.compute_waves groups txs by identical remote-shard dependency sets (wave/computation.rs).WaveId { local_shard, block_height, remote_shards } — Shard 1 wave {2} and Shard 2 wave {1} are different ids (wave/id.rs).provision_tx_roots. On the source shard’s block header: for each target shard, collect every cross-shard tx in this block that needs outbound state toward that target → list of tx hashes (in strict block order) → compute_merkle_root → one ProvisionTxRoot stored as provision_tx_roots[target] (provision_tx.rs). Plain English: “Shard 1 block H promises Shard 2: you will receive state bundles for txs {TxUSDC, …}.”target_shard → root. Each value is one Merkle root summarizing that target’s tx-hash list. Hyperscale name = provision_tx_roots; mentally: outbound_provision_tx_merkle_for_target. Not mempool — only “which committed txs’ state must ship outbound.”provision_root. On the receiving shard: proposer embeds inbound Provisions bundles in Block::Live.provisions; header provision_root = Merkle root of those bundles’ content hashes (inbound list commitment — separate from outbound provision_tx_roots) (shard/roots/provisions.rs, block.rs).waves, provision_tx_roots, provision_root, state_root, …) then vote; QC attests header including state_root at height H.state_root in header H = tree root after applying block H’s certificates, before executing txs newly committed in H. Plain English: the header’s balance sheet is “after old finished work,” not “after the txs we just added to this block.”build_provision_requests → FetchAndBroadcastProvisions; build_provisions reads JMT at source height H, ships SubstateEntry bytes + multiproof (provisions/coordinator.rs, build.rs, execution/lookups.rs).provision_tx_roots is used). Target shard receives gossip Provisions + source CertifiedBlockHeader(H). build_verify_action (verification.rs): (1) read expected_root = header.provision_tx_roots[local_shard]; (2) hash each ProvisionEntry.tx_hash in the bundle → compute_merkle_root → computed_root; (3) if computed_root ≠ expected_root, drop bundle (incomplete/tampered) and retry fetch; (4) else emit VerifyProvisions → JMT multiproof vs QC’d state_root. This is the only place the target compares against provision_tx_roots.provision_tx_rootsOnly on inbound gossip/fetch — not when building the source block.
In-repo proof — completeness check before JMT verify (verification.rs)
let expected_root = certified_header.header().provision_tx_roots().get(&local_shard)?;
let leaves: Vec<Hash> = provisions.transactions().iter().map(|t| t.tx_hash.into_raw()).collect();
let computed_root = ProvisionTxRoot::from_raw(compute_merkle_root(&leaves));
if computed_root != expected_root { return None; } // drop → fetch retry
Some(Action::VerifyProvisions { provisions, certified_header })provision_root on the receiver’s own block is different: Merkle of inbound bundle hashes this shard committed in body.provisions — voters recompute before voting (Part 3).
assign_waves buckets each tx into exactly one WaveId on that shard (key = remote shard set) — a tx is not in multiple waves on the same shard in the same block; (2) for each cross-shard tx, record_required(tx, remote_shards) records which peer shards must still deliver provision bundles; (3) ConflictDetector.register_tx checks bidirectional NodeId overlap against already-committed remote provisions and may abort the lower-hash tx before execution (lookups.rs, provisioning.rs, conflict.rs).ExecuteCrossShardTransactions uses pending_chain.view_at(wave_start_block_hash, H) — not chain tip (action_handlers.rs, wave_state.rs, pending_chain.rs).ProvisionedSnapshot — provision map shadows local DB per key; Radix VM sees unified state (provisioned_snapshot.rs, executor.rs).FinalizedWave → later block certificates (wave_state.rs, coordinator.rs).Completed when FinalizedWave commits; node locks released after wave finalization (mempool/lock_tracker.rs).Provisions bundle isProvisions
├── source_shard, target_shard, block_height (H)
├── proof: JMT MultiProof → verifies ALL entries under header(H).state_root
└── transactions[]
└── ProvisionEntry (per tx)
├── tx_hash
├── entries[]: SubstateEntry { storage_key, SBOR value | deletion }
├── target_nodes[] (on target shard — conflict detection)
└── owned_nodes[] (vault→account map from source)
MultiProof?A Merkle tree proof normally shows “this one leaf is in the tree.” A multiproof is one compact proof that proves many leaves at once belong to the same state_root — shared internal nodes are included once. The provision bundle carries one multiproof for all SubstateEntry rows in that bundle, so the target can check every balance/field byte without a separate proof per key.
Assume our reference tx has hash TxUSDC = 0x7f3a…c901. After Shard 1 commits block 42, the proposer builds outbound provisions toward Shard 2:
| Field | Example value (teaching) |
|---|---|
source_shard | ShardId::leaf(1, 0) |
target_shard | ShardId::leaf(2, 0) |
block_height | BlockHeight(42) |
proof | JMT MultiProof vs header(42).state_root on Shard 1 |
One ProvisionEntry for TxUSDC:
| Sub-field | Example |
|---|---|
tx_hash | 0x7f3a…c901 |
entries[0] | SubstateEntry — A’s USDC vault: SBOR balance 120 USDC (pre-tx @ H; not 70 after line 1) |
entries[1] | Ca component field bytes Shard 2’s Cb-Method-2 will read via overlay |
owned_nodes | (vault_usdc_A, account_A), (Ca_internal, account_A) |
target_nodes | B, Cb, BUCKET_1 resource node (Shard 2 — conflict detection only) |
Shard 2 verifies: Merkle of [TxUSDC] equals Shard 1 header provision_tx_roots[2]; multiproof shows 120 USDC really lived under Shard 1’s state_root@42. Symmetric bundle Shard 2 → Shard 1 carries B’s vault / Cb fields for the same tx.
| Direction | Who builds | When | Block field | Wire path |
|---|---|---|---|---|
| Outbound | Source shard proposer after commit | Post-commit H | Source header provision_tx_roots[target] commits tx list | Gossip / fetch (FetchAndBroadcastProvisions) |
| Inbound | Remote shard (embedded in their block or absorbed after verify) | Same or later block | provision_root + Block::Live.provisions | Committed in receiver’s Live block or verified then absorbed |
Outbound = “I promise these txs’ state at H.” Inbound = “I received and committed (or verified) those bundles.”
ProvisionCoordinator keys eviction and orphan sweeps on the source block’s weighted timestamp — certified_header.header().parent_qc().weighted_timestamp() — not the receiver’s local commit time. Verified bundles expire at source_block_ts + RETENTION_HORIZON; expectations whose fallback fetch never resolves are dropped in the same orphan sweep and emit AbandonFetch (provisions/coordinator.rs).
Lookup rule: provision key → use shipped bytes; else → local view@H. Remote snapshot wins for those keys only.
Provisions built after block H commit, before wave execution. Balance in provision (e.g. A = 120 USDC) is pre-this-tx at H. Later blocks on source may change live A to 200; execution still uses view@H + provisions(120) — intentional determinism.
In-repo proof — overlay then execute
// crates/engine/src/executor.rs
let provisioned = ProvisionedSnapshot::from_provisions(snapshot, &entry_slices);
let receipt = provisioned.execute(&executable, &vm_modules, &exec_config);| Field | Layer | Business purpose |
|---|---|---|
transaction_root | Header | Commits tx bodies in block |
state_root | Header | Commits entire shard JMT after cert deltas in this block |
certificate_root / local_receipt_root | Header | Commits finished prior waves’ execution results applied here |
provision_root | Header (receiver) | Inbound: Merkle of provision bundle content hashes in this block’s body.provisions |
waves | Header | Declares which cross-shard wave shapes exist (remote dependency sets) |
provision_tx_roots | Header (source) | Outbound: per target, Merkle of tx hashes that must get state bundles |
in_flight | Header | Backpressure signal for cross-shard congestion |
transactions | Body | Full tx bytes (sorted by hash) |
certificates | Body | FinalizedWave objects (cross-shard execution done) |
provisions | Body (Live) | Full inbound Provisions payloads + proofs |
Deeper field tour: Block fields deep dive
| Mechanism | What it compresses | What dishonesty it prevents |
|---|---|---|
Tx-hash Merkle (provision_tx_roots) | List of tx hashes per target | Proposer dropping txs on gossip while claiming full block |
Provision hash Merkle (provision_root) | List of inbound bundle hashes | Omitting or swapping provision bodies in committed block |
| JMT multiproof (per bundle) | Many substates under one state_root | Fabricating balances/vault bytes not in committed tree |
QC on state_root | Entire shard state at H | Single validator lying about global state (needs 2f+1 on source shard) |
| Global receipt root (execution votes) | Per-tx outcomes in a wave | Shard 1 and Shard 2 disagreeing on what the tx actually did |
Business why: Cross-shard atomicity without a single global DB — each shard BFTs its own chain; provisions bridge read-only truth; waves + ECs bridge agreement on what happened when the tx ran everywhere.
All 37 teaching bullets. Use as self-check after Parts 1–4.
provision_tx_roots (outbound tx-list commitment)1. Plain sentence?
A: Source block H says: “For target shard T, I will ship state bundles for txs {hash₁, hash₂, …}” — stored as one Merkle root per T in provision_tx_roots[T].
2. Can we say provision_tx_roots[target] is the outbound tx-list Merkle for that target?
A: Yes — txs live on the source block; the map value is Merkle of their hashes that require outbound state bundles to `target`. Target later checks the received bundle’s tx list against this root.
3. Why Merkle instead of a plain list?
A: Single header field binds membership + order; receiver recomputes from bundle and catches partial drops (verification.rs tx-root check).
4. Why hash-ascending block order?
A: Block txs are strictly sorted by tx hash (validate_transaction_ordering); bucket hashes appended in that order so all nodes agree on Merkle input.
5. Why not “tx lands in remote mempool” via this field?
A: Mempool = tx body admission (gossip/submit/fetch). provision_tx_roots = commitment to state bundles for already-committed txs.
6. When does the target compare against it?
A: On inbound Provisions gossip/fetch — build_verify_action recomputes Merkle of bundle’s tx_hash list vs source_header.provision_tx_roots[local_shard] before JMT verify.
7. What is “committed state at height H”?
A: Versioned JMT + RocksDB at block H; summarized by state_root in header after cert replay — not a separate product type (pending_chain.rs, header docs).
8. How is it communicated?
A: SubstateEntry bytes on wire + one JMT MultiProof + reference to CertifiedBlockHeader(H) for state_root (provisions.rs).
9. Roots only or real balances?
A: Real SBOR values in entries; roots used for verification and header commitments, not as execution input.
10. Does provision include target shard state?
A: No — only source-owned substates; target_nodes / owned_nodes are metadata for conflicts and keying (provisioning/entry.rs).
11. Outbound — who, when?
A: Source shard block proposer immediately after that block commits; build_provision_requests + FetchAndBroadcastProvisions (coordinator.rs on_live_block_committed).
12. Outbound built from execution results?
A: No — from declared nodes on source shard at height H (lookups.rs, build.rs).
13. Inbound — when absorbed?
A: From Block::Live.provisions on commit, or after gossip/fetch + VerifyProvisions (provisioning.rs absorb_provisions).
14. Same-block provisions?
A: Wave setup runs first; then inline provisions applied so waves can become provisioned immediately (on_live_block_committed ordering).
15. Multiple provision calculation times?
A: Yes — (1) proposal: waves + provision_tx_roots declarative; (2) post-commit outbound build from JMT; (3) inbound verify; execution consumes, does not rebuild outbound.
16. Separate provision vote?
A: No — trust chain: QC on source state_root (BFT) + per-bundle JMT verify on receiver (ProvisionsContext, from_committed_block comment).
17. Can one validator fake A’s balance?
A: Receiver rejects BadInclusion unless leaves match QC’d state_root — can't pass verify with wrong bytes.
18. Can source shard committee lie collectively?
A: They could sign wrong state_root and consistent fake provisions; cross-shard safety relies on both shards’ ECs agreeing on global receipt root for the wave.
19. Provisions in Live block — extra check?
A: Voters run provision_root + per-bundle verify before voting (initiate_provision_root_verification).
20. “Shadow base” meaning?
A: Executing shard uses provision values for keys present; else local view@H (provisioned_snapshot.rs get_raw_substate_by_db_key).
21. Why anchor execution at H not tip?
A: Prevents per-validator drift and leaking writes from blocks after wave start (wave_state.rs dispatch comment, view_at height assert).
22. Which code pins H?
A: WaveId.block_height, ExecuteCrossShardTransactions { block_hash, block_height }, view_at(block_hash, block_height) (action_handlers.rs, pending_chain.rs).
23. Stale snapshot example (A = 120 at H, 200 at execution wall-clock)?
A: Correct — execution uses H snapshot; live source head may advance; by design for committee-identical VM input.
24. Is 120 “stale” wrong?
A: Not wrong — it's authoritative for wave at H; also pre-this-tx because H’s txs aren't executed until after commit when provisions are built.
Cb-Method-1 balance check25. Do provisions “decide” the check?
A: No — they supply the balance the VM reads; method logic (== 51 or > 100) runs in Radix on overlaid state.
26. What if A not in declared sets?
A: Static analysis won't ship A’s vault; overlay empty/wrong → check fails or wrong branch — manifest must declare touched nodes (manifest_analysis.rs).
27. Declared sets for scenario (conceptual)?
A: Shard 1: A, Ca; Shard 2: B, Cb, USDC / BUCKET_1 resource nodes; union drives waves {2} / {1} and both provision directions.
28. Can one tx sit in multiple waves on the same shard?
A: No for the same committed block — assign_waves places each tx in exactly one WaveId bucket (keyed by its remote-shard set). Different blocks → different WaveId.block_height.
29. One global wave?
A: No — per local shard WaveId; cross-shard finalization collects EC per participating shard into WaveCertificate.
30. Wave timeout?
A: If provisions never complete, WAVE_TIMEOUT → vote abort path (coordinator.rs scan_complete_waves).
31. Incomplete gossip bundle?
A: Tx-root mismatch vs provision_tx_roots[us] → drop → fetch retry (verification.rs).
32. Bidirectional conflict?
A: Overlap both ways between local tx and remote provision node sets → lower tx hash wins, loser aborted (conflict.rs).
33. build_cross_shard_ownership conflict?
A: Same vault claimed on both shards → fast Failed on all validators (action_handlers.rs).
34. Passive admit?
A: Co-hosted shard puts tx in mempool without owning submitted_locally or originating gossip (SubmitFanout::Admit passive list).
35. Node locks vs provision snapshot?
A: Locks block new mempool txs on declared nodes while Committed; they don't rewrite an already-built provision snapshot at H.
36. Per-shard vs global provision compute?
A: Each shard computes its own header fields and builds outbound to its targets; no single global coordinator clock.
37. Is provision_root the inbound provisions Merkle?
A: Yes — Merkle over hashes of provision bundles committed in this block’s body.provisions (ProvisionsRoot::compute(batch_hashes)). Distinct from per-target provision_tx_roots on the source block.
Submit → validate → mempool (each shard) → propose block H → header: waves, provision_tx_roots, provision_root, state_root → commit (QC) → outbound: source proposer builds Provisions@H → gossip/fetch → inbound: verify (tx-root + JMT) → absorb → when required ⊆ received: Execute@view(H) + overlay → votes → EC per shard → WaveCertificate → FinalizedWave in later block → mempool Completed
Three commitments, three jobs:
provision_tx_roots = “which txs’ state must ship”provision_root = “which bundles we committed”state_root at H”Together they let Shard 2 run Cb-Method-1 on A’s real USDC at H without trusting a single peer’s gossip bytes.
provision_tx_roots obligationProvisions bundle (payload vs proof vs metadata)Pass threshold 70%. Covers Parts 0–6.