Phase 4: Cross-Shard Transactions & Provisions

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.

⏱️ ~2–3 hours 📊 In-depth 🎯 Hyperscale-rs
Tx flow phases
In scope
  1. Declared reads/writes
  2. Multi-shard mempool
  3. Waves + provision_tx_roots
  4. Provisions @ height H
  5. ProvisionedSnapshot
  6. EC → FinalizedWave

How to read this module

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.

#PartYou will understand…
0Start hereThe one-sentence model + reference USDC scenario
1End-to-end flow (20 steps)Submit → commit → provisions → execute → finalize
2Provisions deep diveBundle shape, inbound vs outbound, shadow base, snapshot timing
3Block fields mapWhich header/body field does which job
4Why Merkle / JMTBusiness + security logic of each commitment
5Q&A (37 items)Sharp details — provision_tx_roots, lying, staleness, conflicts
6One-page mental modelWhole pipeline on one screen
Big flow stepThis module
6 Tx to each touched shardPart 1 · steps 1–5
7–11 BFT per shardPart 1 · steps 7–12
12 Execution (cross-shard)Part 1 · steps 16–18; Part 2
13–14 Coordination & compositionParts 2–4; no 2PC

Part 0 — The core idea (read this first)

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:

Reference scenario (keep this open while reading)

Setup: Account A + component Ca live on Shard 1. Account B + component Cb live on Shard 2.

Manifest (one transaction):

  1. Take 50 USDC from account A (vault on Shard 1)
  2. Call Cb-Method-1 on component Cb (Shard 2) — balance check on A, move 25 USDC into worktop bucket BUCKET_1
  3. Deposit BUCKET_1 into account B’s vault (buckets cannot dangle — funds must land in a vault)
  4. Deposit 10 USDC from worktop to B
  5. Call Cb-Method-2 — deposit to component Ca (Shard 1)
  6. Deposit LP tokens to account B
Two clocks — don’t mix them:
Manifest order = Radix runs lines 1→6 inside one engine session (step 2 can read A after step 1 wrote A in-memory).
Protocol order = gossip tx to both shards → both commit → exchange pre-tx provisions at height H → then run the whole manifest once per shard.

Trace in sim: cross-shard E2E lab · JMT keys: JMT deep dive

ValidatorId ↔ ShardId? Not 1:1 globally. Many validators serve each shard committee; each 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.

Part 1 — End-to-end flow (20 steps)

Follow these in order. They are the full pipeline from user click to mempool Completed.

A — Submit & routing (steps 1–6)

  1. Submit. User posts signed bytes via RPC → 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.
  2. Declared sets are static. 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.”
  3. Multi-shard hosting. One 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).
  4. Admission fan-out. If the node hosts both shards, source shard 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.
  5. Mempool is per-validator, per-shard. Gossip/fetch gets tx bodies onto remote shard mempools separately from provisions; cross-shard txs may sit in quiesce until remote in_flight eases (mempool/coordinator.rs). Plain English: getting the tx into a block is separate from getting state proofs later.
  6. Every touched shard executes the whole tx. 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.

B — Proposal & first commitments (steps 7–12)

  1. Proposal (pre-execution). Proposer builds header from tx list; compute_waves groups txs by identical remote-shard dependency sets (wave/computation.rs).
  2. Wave identity. WaveId { local_shard, block_height, remote_shards } — Shard 1 wave {2} and Shard 2 wave {1} are different ids (wave/id.rs).
  3. First provision commitment — 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, …}.”
  4. Why “roots” (plural map, one Merkle root each)? The header field is a map 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.”
  5. Incoming provisions in block — 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).
  6. BFT commit. Validators verify roots (waves, provision_tx_roots, provision_root, state_root, …) then vote; QC attests header including state_root at height H.

C — State at H & building provisions (steps 13–15)

  1. “Committed state at H”. RocksDB substates + versioned JMT; 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.”
  2. Outbound provisions (post-commit, proposer only). build_provision_requestsFetchAndBroadcastProvisions; build_provisions reads JMT at source height H, ships SubstateEntry bytes + multiproof (provisions/coordinator.rs, build.rs, execution/lookups.rs).
  3. Inbound verification (where 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_rootcomputed_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.

How the target uses provision_tx_roots

Only 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).

D — Execution & finish (steps 16–20)

  1. Wave setup on commit. After block H commits on a shard: (1) 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).
  2. Anchor at H. ExecuteCrossShardTransactions uses pending_chain.view_at(wave_start_block_hash, H) — not chain tip (action_handlers.rs, wave_state.rs, pending_chain.rs).
  3. Overlay execution. ProvisionedSnapshot — provision map shadows local DB per key; Radix VM sees unified state (provisioned_snapshot.rs, executor.rs).
  4. Votes → EC → wave certificate. Wave leader aggregates BLS execution votes; collect ECs from all participating shards → FinalizedWave → later block certificates (wave_state.rs, coordinator.rs).
  5. Terminal. Mempool Completed when FinalizedWave commits; node locks released after wave finalization (mempool/lock_tracker.rs).

Part 2 — Provisions deep dive

What a Provisions bundle is

Provisions
├── 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)

What is a JMT 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.

Concrete example — USDC scenario (Shard 1 → Shard 2 @ H=42)

Assume our reference tx has hash TxUSDC = 0x7f3a…c901. After Shard 1 commits block 42, the proposer builds outbound provisions toward Shard 2:

FieldExample value (teaching)
source_shardShardId::leaf(1, 0)
target_shardShardId::leaf(2, 0)
block_heightBlockHeight(42)
proofJMT MultiProof vs header(42).state_root on Shard 1

One ProvisionEntry for TxUSDC:

Sub-fieldExample
tx_hash0x7f3a…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_nodesB, 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.

Inbound vs outbound

DirectionWho buildsWhenBlock fieldWire path
OutboundSource shard proposer after commitPost-commit HSource header provision_tx_roots[target] commits tx listGossip / fetch (FetchAndBroadcastProvisions)
InboundRemote shard (embedded in their block or absorbed after verify)Same or later blockprovision_root + Block::Live.provisionsCommitted 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.”

Retention clock: 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).

Shadow base (one line)

Lookup rule: provision key → use shipped bytes; else → local view@H. Remote snapshot wins for those keys only.

Snapshot vs execution time

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);

Part 3 — Block fields map (cross-shard)

FieldLayerBusiness purpose
transaction_rootHeaderCommits tx bodies in block
state_rootHeaderCommits entire shard JMT after cert deltas in this block
certificate_root / local_receipt_rootHeaderCommits finished prior waves’ execution results applied here
provision_rootHeader (receiver)Inbound: Merkle of provision bundle content hashes in this block’s body.provisions
wavesHeaderDeclares which cross-shard wave shapes exist (remote dependency sets)
provision_tx_rootsHeader (source)Outbound: per target, Merkle of tx hashes that must get state bundles
in_flightHeaderBackpressure signal for cross-shard congestion
transactionsBodyFull tx bytes (sorted by hash)
certificatesBodyFinalizedWave objects (cross-shard execution done)
provisionsBody (Live)Full inbound Provisions payloads + proofs

Deeper field tour: Block fields deep dive

Part 4 — Why Merkle / JMT (logic + business)

MechanismWhat it compressesWhat dishonesty it prevents
Tx-hash Merkle (provision_tx_roots)List of tx hashes per targetProposer dropping txs on gossip while claiming full block
Provision hash Merkle (provision_root)List of inbound bundle hashesOmitting or swapping provision bodies in committed block
JMT multiproof (per bundle)Many substates under one state_rootFabricating balances/vault bytes not in committed tree
QC on state_rootEntire shard state at HSingle validator lying about global state (needs 2f+1 on source shard)
Global receipt root (execution votes)Per-tx outcomes in a waveShard 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.

Part 5 — Q&A (sharp details)

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.

Payload, proof, “state at H”

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

Inbound / outbound

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.

Lying, voting, trust

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

Overlay, anchor H, staleness

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.

Scenario: Cb-Method-1 balance check

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

Waves, conflicts, failure modes

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

Validator / mempool misc

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.

Part 6 — One-page mental model

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:

Together they let Shard 2 run Cb-Method-1 on A’s real USDC at H without trusting a single peer’s gossip bytes.

Code anchors for cross-links

Checklist before you leave Phase 4

Quiz — Phase 4

Pass threshold 70%. Covers Parts 0–6.