Cross-Shard Transactions in Hyperscale-rs

⏱️ Duration: 1.5–2 hours 📊 Difficulty: Intermediate 🎯 Hyperscale-rs Specific

Learning Objectives

How atomic composability works (cross-shard)

The diagram in Transaction Flow emphasizes that cross-shard atomicity depends on correct order of communication and when state updates are applied:

  1. Coordinator (one shard or a designated role) sends prepare to every involved shard in a defined order.
  2. Each shard prepares: runs its part of the tx (e.g. reserve/lock), but does not make the state update visible yet. It replies yes or no.
  3. When all have voted yes, coordinator sends commit; otherwise it sends abort to all.
  4. State updates: Each shard then applies updates in an agreed order (e.g. by shard ID, or the order the coordinator specifies). On commit, each applies its state change; on abort, each releases locks. No shard commits while another aborts — the protocol order ensures a single composed outcome.

So shards communicate in this fixed protocol order (prepare phase → decision → apply phase), and state updates happen only after the decision, in the same order everywhere. That is what makes the cross-shard tx atomic and composable.

Two coordinators: 2PC coordinator and ProvisionCoordinator

Cross-shard flow uses two different “coordinator” ideas:

How is the order fixed? "The protocol fixes an order" means a deterministic rule that all nodes follow (e.g. by shard ID, or by the order of NodeIDs in the tx), so everyone agrees on which shard is first, second, third. The 2PC coordinator does not choose that order; it drives the protocol in that pre-agreed order (sends prepare in that order, then commit/abort, then state updates in that order). So: the protocol defines the order; the coordinator executes the protocol in that order.

How is order determined for composite (cross-shard) transactions? In the Radix manifest, instructions are in a fixed order (e.g. withdraw from A → split → put in staking vault → put in LP → return IOUs). The Radix Engine runs those instructions sequentially when executing; data dependencies are implicit (instruction 2 sees the result of instruction 1). For Hyperscale (consensus), the transaction is turned into a RoutableTransaction by instruction analysis (crates/types/src/transaction.rs): the manifest is walked and every NodeId read or written is collected into declared_reads and declared_writes. Those are sets (deduplicated); instruction order is not preserved at the consensus layer. Shard sets are then derived: consensus_shards = unique shards of declared_writes; provisioning_shards = shards of declared_reads that are not write shards. Both are stored in BTreeSets, so the protocol order is by ShardGroupId (numeric). So: we do not derive "Account_A shard first, then Staking_VAULT shard, then LP shard" from the manifest order—we get a deterministic order by shard ID. Prerequisites are enforced by provisions: a shard that needs remote state (reads from another shard) must receive quorum of provisions from that shard before it can complete; which shards are "required" is the set of all other participating shards. Parallelism: each shard runs BFT and execution independently; cross-shard atomicity is then 2PC (prepare all in shard-ID order, then commit/abort) and provisioning (everyone waits for provisions from every other participating shard). Refs: crates/types/src/topology.rs (consensus_shards, provisioning_shards, all_shards_for_transaction), crates/types/src/transaction.rs (analyze_instructions_v1 / analyze_instructions_v2).

See the Transaction Flow diagram for the full step-by-step from user to finality.

Example: complex Radix manifest and Hyperscale provisioning

Consider a composite transaction that splits funds from a user account into staking and liquidity:

# Simplified Radix-style manifest (conceptual)
1. CallMethod(Account_A, "withdraw", XRD, amount)     # → NodeID_Account_A, vault
2. TakeFromWorktop(XRD, amount)
3. SplitBucket(amount1, amount2)                        # worktop
4. CallMethod(StakingVault, "stake", bucket1)          # → NodeID_Staking_VAULT
5. CallMethod(LiquidityPool, "contribute", bucket2)    # → NodeID_LP_COMPONENT
6. CallMethod(Account_A, "deposit", staking_IOU)        # → NodeID_Account_A
7. CallMethod(Account_A, "deposit", lp_IOU)             # → NodeID_Account_A

Radix Engine: Runs these instructions in order. Data flow is implicit (e.g. step 4 uses the bucket from step 3; step 6–7 deposit what step 4–5 returned).

Instruction analysis (Hyperscale): The manifest is walked and every NodeId read or written is collected into declared_reads and declared_writes (crates/types/src/transaction.rs: analyze_instructions_v1 / analyze_instructions_v2). The result is sets (deduplicated); instruction order is not preserved at the consensus layer. Assume:

Shard mapping (topology): Each NodeId maps to a shard via shard_for_node(node_id, num_shards) (crates/types/src/topology.rs). Suppose Account_A → shard 1, Staking_VAULT → shard 2, LP_COMPONENT → shard 3. Then:

Provisioning and coordination:

So: the manifest defines the logical flow (withdraw → split → stake → LP → deposit); the engine runs it sequentially; Hyperscale uses shard ID order for coordination and provisions to enforce “everyone has proof from everyone else” before completion.

Order: manifest vs protocol

As above: instruction order is not preserved at consensus; protocol order is by ShardGroupId. Prerequisites are enforced by provisions; required_shards is the set of all other participating shards (start_cross_shard_execution in crates/execution/src/state.rs).

Concepts in the Flow

Quiz: Cross-shard and provisioning

Answer based on the content above. Pass threshold: 70%.