In-depth walk through big-flow step 12 on one shard: what happens after Phase 2 commits a block — execution coordinator, waves, execution certificates, and how FinalizedWave receipts flow back into the next proposal. Human story first; Rust excerpts are evidence.
Committed in the mempool. Phase 3 is where the execution state machine runs manifests, collects outcomes, and produces FinalizedWave certificates that a later proposer can put in a block body.weighted_timestamp from the last committed block, not your laptop clock. Timeouts like WAVE_TIMEOUT and vote-retry windows are measured against that stamp so all honest nodes abort or retry together.0 and can execute immediately at commit; cross-shard txs wait until provisions arrive (Phase 4).Proof snippets are trimmed from the hyperscale-rs tree. Ellipsis (...) hides boilerplate.
When ProtocolEvent::BlockCommitted arrives, the node orchestrator in crates/node/src/state/participation/shard.rs notifies subsystems in a fixed order. Execution is deliberately after mempool and provision bookkeeping on that block, so conflict detection and queue pruning see the committed provision hashes first.
After execution runs, the orchestrator calls shard_coordinator.queue_ready_proposal() again — finalized waves (and freed in-flight counts) may mean the leader should try another block soon.
In-repo proof — commit order: mempool → remote headers → provisions → execution → proposal latch
// crates/node/src/state/participation/shard.rs — on_block_committed
self.shard_coordinator.on_block_committed_verification(block_hash);
actions.extend(self.mempool.on_block_committed(..., certified));
actions.extend(self.remote_headers.on_block_committed(..., certified));
actions.extend(self.provisions.on_block_committed(..., certified));
self.outbound_provisions.on_block_committed(certified.qc().weighted_timestamp());
actions.extend(self.apply_block_to_execution(certified));
self.shard_coordinator.queue_ready_proposal();ExecutionCoordinator::on_block_committedThe execution crate’s coordinator advances committed_height and committed_ts from the certifying QC, then advances the provisioning clock. Even empty blocks run timeout sweeps (execution-cert fallbacks, vote retries, pruning) so lagging remote shards do not stall forever.
For a live block (normal path with full body), it calls setup_waves_and_dispatch: assign txs to waves, create WaveState machines, register cross-shard txs with the conflict detector, and emit ExecuteTransactions / ExecuteCrossShardTransactions when a wave is already fully provisioned.
Block::Sealed sync path: Synced blocks may record wave assignments without re-dispatching Radix work; the certificates in the block body are already the source of truth for those heights.
In-repo proof — chain time + clock advance at commit
// crates/execution/src/coordinator.rs — on_block_committed (start)
if height > self.committed_height {
self.committed_height = height;
self.committed_ts = certified.qc().weighted_timestamp();
}
self.provisioning.advance_clock(self.committed_ts);
actions.extend(self.check_exec_cert_timeouts(topology));
actions.extend(self.check_vote_retry_timeouts(topology));0assign_waves partitions the block’s transactions by which shards they touch. Wave id 0 means single-shard: no remote provision set. Cross-shard txs get non-zero wave ids and start in “waiting for provisions” until Phase 4 data arrives.
At wave creation, single-shard waves mark themselves provisioned immediately; the coordinator can dispatch Radix execution in the same commit tick. Cross-shard waves call absorb_ready_provisions and only dispatch when every required remote shard has supplied verified provision data.
Topology note: wave bucketing uses the classification TopologySnapshot at the block anchor (which shards each tx touches). Committee lookups — wave leader, execution-vote quorum — use TopologySchedule::at(weighted_timestamp) so artifacts signed under an earlier epoch still verify against that epoch’s committee (topology/schedule.rs).
In-repo proof — wave loop: assign → WaveState → dispatch_if_ready
// crates/execution/src/coordinator.rs — setup_waves_and_dispatch
let waves = assign_waves(classification, local_shard, block_height, transactions);
let setup_committee = topology_schedule.at(block_ts);
for (wave_id, txs) in waves {
let is_single_shard = wave_id.is_zero();
let mut wave_state = WaveState::new(wave_id.clone(), block_hash, block_ts, txs, is_single_shard);
if !is_single_shard {
wave_state.absorb_ready_provisions(&self.provisioning, block_ts);
}
if let Some(action) = wave_state.dispatch_if_ready(&self.provisioning) {
dispatch_actions.push(action);
}
self.waves.insert_wave(wave_id.clone(), wave_state);
}Hyperscale-rs orders transactions (BFT), coordinates cross-shard provisions, and drives when execution runs. It does not interpret Radix manifests or run Radix Engine instructions.
Radix Engine (external) takes a transaction (e.g. a Radix manifest), runs instructions (CallMethod, TakeFromWorktop, …), and updates Radix-style state. Hyperscale-rs may analyze a tx for declared_reads / declared_writes (shard routing), then order and coordinate it; the VM runs in the engine, often via a separate worker the node calls.
The execution crate wires committed blocks to that engine: single-shard apply-in-order; cross-shard uses ProvisionCoordinator then ExecutionCoordinator waves (see Phase 4). State views and persistence may live in storage crates; canonical execution transitions are engine-owned.
The coordinator emits Action::ExecuteTransactions; shard-loop workers return ProtocolEvent::ExecutionComplete. Phase 3 is orchestration + agreement on outcomes (waves, ECs, FinalizedWave)—not VM opcode semantics.
When every non-aborted tx in a wave has an outcome (or WAVE_TIMEOUT fires), scan_complete_waves yields CompletionData. The shard_loop signs and gossips execution votes; the wave leader (deterministic function of wave_id and committee) aggregates BLS votes into an execution certificate (EC) when 2f+1 power agrees on the same receipt root.
Cross-shard waves also track expected ECs from remote shards (via block headers and fallback fetch). When all participating shards are covered, the wave can finalize locally.
VOTE_RETRY_TIMEOUT schedules resends so liveness does not depend on one peer’s gossip luck. That is separate from WAVE_TIMEOUT, which aborts the whole wave if provisioning or progress stalls too long.
In-repo proof — wave leader creates VoteTracker; early votes replayed
// crates/execution/src/coordinator.rs — setup_waves_and_dispatch
if let Some(committee) = setup_committee
&& self.me == wave_leader(&wave_id, committee.consensus_committee_for_shard(local_shard))
{
let tracker = VoteTracker::new(wave_id.clone(), block_hash, quorum);
self.waves.insert_tracker(wave_id.clone(), tracker);
votes_to_replay.extend(self.early.drain_votes_for_wave(&wave_id));
}finalize_wave and FinalizedWavesAdmittedfinalize_wave removes the in-flight WaveState, builds a FinalizedWave (execution certificate + receipt bundle), stores it in FinalizedWaveStore, and emits ProtocolEvent::FinalizedWavesAdmitted. That event is the handoff back to BFT: the next try_propose can select those certificates into a block body, which updates the JMT/state root during build_proposal.
Waves fetched from peers go through admit_finalized_wave + BLS verify first; locally finalized waves skip that extra gate because they were built from already-verified ECs.
In-repo proof — finalize emits admission continuation
// crates/execution/src/coordinator.rs — finalize_wave
let finalized_arc = Arc::new(wave.into_finalized());
self.finalized.insert(wave_id.clone(), Arc::clone(&finalized_arc));
vec![Action::Continuation(
ProtocolEvent::FinalizedWavesAdmitted { waves: vec![finalized_arc] },
)]Phase 2 already noted: when a committed block carries certificates(), mempool can move txs to Completed or Aborted in the same on_block_committed pass. Execution drove those certificates in an earlier height’s waves; this block is the ledger record everyone agrees on.
Until that certificate is proposed and committed, execution progress and mempool status are related but not identical instants on your node.
ConflictDetector preview (full story in Phase 4)Cross-shard txs declare read/write node sets. When a remote provision commits, the detector checks overlap with local cross-shard txs (and vice versa). Conflicting txs abort deterministically (lower tx hash loses) so the cluster does not deadlock waiting on incompatible locks.
Phase 4 explains how provisions arrive; Phase 3 shows the detector runs at wave setup on commit.
In-repo proof — bidirectional registration in module docs
// crates/execution/src/conflict.rs (module-level)
/// When provisions commit → checked against registered local txs
/// When local tx registers → checked against stored provision dataWhy does execution wait until after BlockCommitted?
Consensus first agrees on ordering and header fields (including the claimed state_root from the proposer’s build). Execution replays committed txs against that anchor. Running Radix before commit would let validators diverge on speculative state that might never be finalized.
What is a “wave” in one sentence?
A batch of transactions in the same committed block that share the same provision dependency pattern and execute/vote/finalize together under one execution certificate.
Who runs Radix — coordinator or engine?
The ExecutionCoordinator schedules ExecuteTransactions actions; the engine/shard-loop workers run the VM and return results as events. The coordinator stays deterministic and free of signing keys for execution votes (those are handled in the shard-loop).
How do finalized waves get into the chain?
FinalizedWavesAdmitted feeds the BFT/mempool “ready” inputs; the leader’s next try_propose includes selected FinalizedWave entries in the block body. Peers verify receipts against the EC when validating the proposal, and the state root in the header must match recomputation via prepare_block_commit.
BlockCommitted runs mempool, provisions, then execution.on_block_committedfinalize_wave → FinalizedWavesAdmittedcommitted_ts (QC weighted time), not wall clockPass threshold 70%. Assumes Phases 1–2 plus this module.