E2E harnesses (simulation + production)

⏱️ ~2–2.5 hours📊 Level 4🎯 Hyperscale-rs

Part I — Simulation: SimCluster, EventKey, and single_shard_tx_sim. Part II — Production: per-shard shard-loop, RPC ingress, production E2E (same module—read both before hands-on labs).

Learning objectives

Simulation vs production (same core, different harness)

🧪 Simulation harness
hyperscale-simulation
  • Time: SimCluster::run_until advances a simulated clock (Duration).
  • Network: SimulatedNetwork + SimNetworkAdapter—latency queues, no real sockets.
  • Storage: SimStorage + SyncDispatch (in-process).
  • Threading: single-threaded driver; ShardLoop::step called directly from the runner loop.
  • After each step: flush_all_batches() — drains validation / header batches and related commits immediately, every time (see ShardLoop::step caller protocol; path in study table). Keeps deterministic tests simple: no deferred batch work whose ordering depends on the next simulated clock tick.
Design note SyncDispatch runs validation/execution closures on the driver thread—zero pool nondeterminism, perfect replay. You do not exercise thread-pool ordering or contention that production’s PooledDispatch can surface.
🌐 Production harness
hyperscale-production
  • Time: wall clock on the pinned thread (run_shard_loop sets time each iteration).
  • Network: Libp2pAdapter / Libp2pNetwork—libp2p gossipsub over QUIC between validators (separate from wallet HTTP submit); gossip arrives on crossbeam channels into the pinned loop.
  • Storage: RocksDbStorage + SharedStorage + PooledDispatch.
  • Threading: Tokio runtime for adapter/RPC/metrics; state machine on a pinned std::thread (spawn_shard_loop).
  • After each step: flush_expired_batches(wall_clock_duration()) — only flushes batch pipelines whose deadlines have passed; amasses work until then for throughput and steadier load (details below).
Design note The hot path is a blocking std::thread that must not .await; async I/O and sleeps stay on Tokio and push NodeInput through crossbeam. Pros: clear ownership of state-machine mutation, no async locks inside step. Cons: two concurrency models and channel backpressure to tune.

Shared core: Simulation uses SimCluster + NodeHost with deterministic ShardEvent scheduling; production uses per-shard ShardLoop::step on dedicated threads. Both drive the same NodeStateMachine—so SubmitTransaction, shard consensus, BlockCommitted, and execution hooks match once an event is delivered. The harness decides how events arrive and when time advances.

Pinned thread, run_shard_loop, and CPU affinity

What “pinned thread” means here

Below is the actual production entrypoint: a named std::thread, optional CPU affinity via core_affinity::set_for_current, then run_shard_loop. Read the code first; captions spell out what each part testifies to.

Snippet context: spawn_shard_loop — pinned-thread entry (file in study table).

pub fn spawn_shard_loop(
    shard_loop: ProdShardLoop,
    config: PinnedLoopConfig,
) -> std::thread::JoinHandle<()> {
    std::thread::Builder::new()
        .name("shard-loop".to_string())
        .spawn(move || {
            // Try to pin to core 0
            if let Some(core_ids) = core_affinity::get_core_ids() {
                if let Some(&core_id) = core_ids.first() {
                    if core_affinity::set_for_current(core_id) {
                        info!(?core_id, "Pinned shard-loop thread to core");
                    } else {
                        warn!("Failed to pin shard-loop thread to core 0");
                    }
                }
            }

            run_shard_loop(shard_loop, config);
        })
        .expect("failed to spawn shard-loop thread")
}

Snippet context: Tokio task after spawn_shard_loop — metrics and shutdown select! (runner file in study table).

        // ── 3. Spawn pinned thread ───────────────────────────────────────
        let loop_handle = spawn_shard_loop(shard_loop, pinned_config);

        // ── 4. Metrics + shutdown loop ───────────────────────────────────
        let mut metrics_tick = tokio::time::interval(Duration::from_secs(1));
        metrics_tick.set_missed_tick_behavior(tokio::time::MissedTickBehavior::Skip);
        let mut shutdown_rx = self.shutdown_rx.take().expect("shutdown_rx already taken");

        loop {
            tokio::select! {
                biased;
                _ = &mut shutdown_rx => {
                    info!("Shutdown signal received");
                    break;
                }
                _ = metrics_tick.tick() => {
                    self.collect_metrics();
                }
            }
        }

The same binary keeps a Tokio loop here (metrics, shutdown); libp2p/RPC/timers live on that runtime too. NodeInput for consensus reaches the shard-loop thread through crossbeam channels (timer_rx / callback_rx / consensus_rx in PinnedLoopConfig) read inside run_shard_loop — see the next subsection.

Design note Genesis runs on the Tokio task before spawn_shard_loop so ShardLoop and storage are initialized deterministically on one side, then the loop is handed off already consistent—avoids splitting genesis across threads. Cost: startup blocks the runtime briefly; acceptable before listeners are fully “hot.”

Diagram: logical CPUs, pinned target, and what runs where

Schematic for one validator process on a typical multi-core host. Affinity affects only the shard-loop std::thread; Tokio worker threads are usually unpinned and the OS may move them across logical CPUs.

Pinning does not reserve a core exclusively for this process unless you add stronger isolation (cgroups, isolated CPUs, etc.).

What run_shard_loop actually does

Read in-repo: run_shard_loop (spawned from spawn_shard_loop in the same module) — path in study table.

It is the main engine loop for advancing the ShardLoop while the node is up. The very first thing inside the function (before the big loop) is to apply genesis-time timer ops once:

Design note Real sleeps live on Tokio because the pinned thread cannot block inside an async runtime; firing into timer_rx re-enters the synchronous step world. Trade-off: timer latency includes Tokio scheduling jitter (usually small vs consensus timeouts).

After that, the loop repeats until shutdown:

Design note Timer > callback > consensus on try_recv favors time-driven liveness (proposal / round timers) over inbound work—otherwise a flood of gossip could delay handling a fired timer and stall progress. Risk if mis-tuned: chronic timer traffic could starve lower priorities; the codebase assumes timer volume stays bounded relative to protocol needs.

Why simulation flushes everything; production batches until deadlines

Read in-repo: flush_expired_batches, flush_all_batches, nearest_batch_deadline, and step caller-protocol docs — study table.

The ShardLoop holds batch accumulators (notably validation and committed-header verification) with time-based deadlines. The two harnesses choose different flush policies after step:

flush_all_batches() still exists on ShardLoop for paths that need an immediate drain (e.g. shutdown); production’s steady-state path prefers deadline-driven flushes.

Correcting a common mental model

Not a “test loop” or “user simulator”

run_shard_loop is production architecture, not something that exists only to simulate users. E2E tests such as runner_boots_listens_and_shuts_down_cleanly start and stop the same path briefly; a real validator runs it continuously. RPC or other tasks send work (e.g. SubmitTransaction) into the crossbeam channels this loop reads—they are real inputs, not pretend traffic generated inside the loop.

vs simulation: Deterministic simulation never spawns this thread: a single driver calls ShardLoop::step from SimCluster::run_until with simulated time and an in-memory network. Same step logic, different harness.

For a broader ladder of multitasking / threading terms mapped to hyperscale-rs (processes, async vs blocking, pools, backpressure), see the Basic module State Machines & Event-Driven Architecture (section on threading and concurrency).

1. Simulation E2E: single_shard_tx_sim

Source & story

Test: single_shard_tx_sim in the simulation E2E integration tests — path in study table.

Story: one shard, four validators; build a signed notarized tx, submit on node 0, run simulated time until the tx is executed (or evicted after success).

Step (test / harness) Protocol meaning → code path
SimCluster::new(config, 42)
Construct topology and one ShardLoop per validator with SimStorage, RadixExecutor, SimNetworkAdapter, SyncDispatch, and a per-node crossbeam channel for injected events. Start from SimCluster::new in the simulation runner (study table).
runner.initialize_genesis()
Engine genesis: For each node, executor.run_genesis via GenesisWrapper, then finalize_genesis_jvt (JVT state commitment at version 0). Consensus genesis: finalize_genesis builds Block::genesis per shard with the genesis JVT root, calls state.initialize_genesis, handle_actions, drains IO, schedules StateCommitComplete for height 0. Implemented in initialize_genesis / finalize_genesis on the same runner (study table).
Manifest + sign_and_notarizeRoutableTransaction
Builds a valid Radix-style transaction outside the consensus loop (test helper + Radix types). Not the hyperscale runner; it’s the payload the node will treat like any user tx.
schedule_initial_event(0, ZERO, SubmitTransaction { tx })
Inserts NodeInput::SubmitTransaction into the global deterministic event queue for node 0 at the current sim time. Follow schedule_initial_eventschedule_event on the simulation runner (study table).
runner.run_until(Duration::from_secs(2)) (and later polling)
Simulation driver: Repeatedly picks the next time from the minimum of (next queued event, next simulated network delivery), advances now, optionally prunes gossip dedup caches, flushes gossip/notifications/responses into channels, drains those into new queued events, then pops all due events and runs shard_loops[i].step(event) + flush_all_batches + drain_node_io + timer scheduling from StepOutput. Core logic in run_until and drain_node_io on the simulation runner (study table).
NodeInput::SubmitTransaction handled in ShardLoop::step
Ingress: Computes shard set from declared_reads/declared_writes, broadcast_to_shard for each (simulated gossip). Marks tx as locally submitted; queue_validation(tx) if not already pending. ShardLoop::step match arm SubmitTransaction (study table).
Validation pipeline completes
Valid txs re-enter as NodeInput::TransactionValidated, which becomes ProtocolEvent::TransactionValidated for the state machine (feed_event). Invalid → TransactionValidationsFailed. Same ShardLoop module as submit (validation completion paths). Why reshape: keeps one mempool/BFT entry shape whether the tx arrived from gossip or from the validator pipeline—extra hop, less duplicated consensus logic.
Mempool + BFT + block commit
NodeStateMachine applies protocol events: tx lands in mempool, proposer builds blocks, votes form QCs, two-chain commit rule fires. Committed blocks surface as ProtocolEvent::BlockCommitted (and related paths). Event dispatch in node state; consensus details in the BFT crate (study table).
on_block_committed → execution
After commit, state runs execution.on_block_committed(...) (and mempool status / wave updates). This is where single-shard work runs through the execution engine; cross-shard would add provision coordination (not this particular test’s focus). Follow on_block_committed in node state (study table).
Test assertions: mempool().status / execution().is_executed
Observes observable end state on node 0: mempool may show progression or None if the tx was evicted after completion; execution’s is_executed confirms the engine path ran. Assertions live in the same simulation E2E test file (study table).
Design note The tx is schedule_initial_event’d before the first long run_until so it sits in the pipeline like a “user submitted just as the network wakes”—cheap way to test mempool → propose path. It does not cover every ordering (e.g. submit only after many rounds); other tests vary timing.

Run this test only

cargo test -p hyperscale-simulation --test scenarios single_shard_tx_sim -- --nocapture

2. Production E2E-style: runner_boots_listens_and_shuts_down_cleanly

Source & story

Test crate: production integration E2Es — path in study table.

This test does not submit a transaction; it proves production wiring: RocksDB, ProductionRunner::build, libp2p listening, run() spawning the pinned loop, then graceful shutdown. The same ShardLoop::step path would process SubmitTransaction if an RPC task (or test) sent it on the consensus channel—see ProductionRunner::tx_submission_sender() on the production runner (study table).

Step (test) What it validates → code path
TestFixtures::new(42, 1) + RocksDbStorage::open
Deterministic topology/keys for one validator; persistent storage under a temp directory. Production I/O path, unlike SimStorage. Test fixtures module + RocksDB-backed storage crate (study table). Why real RocksDB here: catches compaction, locking, and path bugs that in-memory stores miss; cost is slower, flaky-disk-sensitive tests if not temp-scoped.
ProductionRunner::builder()...build().await
Wires Prometheus metrics install, PooledDispatch, NodeStateMachine, SharedStorage, Libp2pAdapter::new, Libp2pNetwork, RadixExecutor, TransactionValidation, and ShardLoop::new(...) with production generic params. Crossbeam senders/receivers are created for timer / callback / consensus / shutdown. ProductionRunnerBuilder::build on the production runner (study table).
runner.network(), listen addresses
Confirms a QUIC listen address for the validator libp2p stack (port 0 = OS-assigned)—validator-to-validator transport, not the wallet HTTP path. Network stack is live; gossip would use gossipsub/QUIC, not simulated delivery queues.
tokio::spawn(runner.run())
ProductionRunner::run: calls maybe_initialize_genesis() on the still-local ShardLoop (engine genesis + genesis block + initialize_genesis + StateCommitComplete—parallel idea to simulation’s genesis, but on RocksDB). Then moves ShardLoop into spawn_shard_loop. Main Tokio task then waits on shutdown + metrics ticks. See maybe_initialize_genesis and run on the production runner (study table).
Pinned thread: run_shard_loop
Once at entry: drain initial_timer_ops from genesis setup (maybe_initialize_genesis) through ProdTimerManager — each TimerOp arms a Tokio sleep that later injects timer NodeInput on crossbeam timer_rx. Then loop: shutdown check → set_time(now) → priority try_recv (timer, callback, consensus) → optional blocking crossbeam::select!shard_loop.step(event)flush_expired_batchesperiodic metrics / channel depth export / optional JVT GC (run_jvt_gc). Production analogue of simulation’s run_until + step; events from real adapters and Tokio timers, not a BTreeMap queue. Implemented in run_shard_loop (production event loop — study table).
drop(shutdown) + join handle
Shutdown handle drops → run’s Tokio loop exits → sends on xb_shutdown_tx → pinned loop observes shutdown and exits → test asserts clean completion. Validates lifecycle, not tx finality.
Design note

This integration test is not a full “tx through consensus to finality” production E2E: it does not submit a transaction or assert block heights, QCs, or executed user state (aside from genesis wiring at startup). The pinned loop is real and would handle SubmitTransaction if something sent it on the consensus channel, but proving submit→mempool→BFT→commit→execute with assertions is what the simulation E2E above is for, or a heavier prod/RPC test if the repo adds one next to the existing production tests (study table).

Skipping a full tx here keeps CI fast and failure modes obvious (build/wire/shutdown). Cross-shard / multishard: simulation includes scenarios such as cross_shard_tx_sim alongside the single-shard E2E in the same integration test file (study table). This module does not trace a production multishard integration test; that would mean multiple validators, real networking, and timing—outside the scope of runner_boots_listens_and_shuts_down_cleanly.

Run this test (use one test thread; serial tests)

cargo test -p hyperscale-production --test runner runner_boots_listens_and_shuts_down_cleanly -- --test-threads=1 --nocapture

3. Mental model: one picture

Design note The sim side uses one ordered global event queue so replay is a total order of (time, tie-break)—great for debugging “who ran first.” Production has no single queue: concurrent producers + crossbeam channel priorities mean order is emergent; tests must reason about protocol rules, not one serialized timeline.
Simulation Production │ │ schedule_event / run_until crossbeam + libp2p + Tokio timers │ │ └──────────────┬─────────────────────┘ ▼ ShardLoop::step(NodeInput) │ NodeStateMachine (BFT, mempool, execution, …)

Where to go next

Quiz

Pass threshold: 70%.

Suggested crates / files to study

Paths relative to the hyperscale-rs repo root. Use these when following the traces and code snippets above.

Topic Path
Simulation E2E tests crates/simulation/tests/scenarios.rssingle_shard_tx_sim, cross_shard_tx_sim, …
Simulation runner crates/simulation/src/runner.rsSimCluster::new, genesis, schedule_initial_event, run_until, drain_node_io
Production E2E tests crates/production/tests/runner.rsrunner_boots_listens_and_shuts_down_cleanly
Production test fixtures crates/network-libp2p/src/test_utils.rs
Production runner crates/production/src/runner.rsProductionRunnerBuilder::build, maybe_initialize_genesis, run, tx_submission_sender
Production event loop crates/production/src/runner.rsspawn_shard_loop, run_shard_loop
Node ShardLoop crates/node/src/shard/mod.rsstep, SubmitTransaction, validation pipeline, flush_all_batches / flush_expired_batches
Node state machine crates/node/src/state/mod.rson_block_committed, protocol event dispatch
BFT crates/shard
RocksDB storage crates/storage-rocksdb (package hyperscale_storage_rocksdb)

Part II — Production harness (continued). ↑ Back to simulation section.

Reading module (3/4): what the production harness uses, where user / RPC input begins, and what canonical production E2Es can show today—after the simulation pair.

Where user input begins (production)

IngressWhat happens
POST /api/v1/transactionsTokio RPC → submit_transaction_handlertx_submission_tx.send(NodeInput::SubmitTransaction { tx }).
ProductionRunner::tx_submission_sender()Same crossbeam path as RPC; for tests/tools without HTTP.
run_shard_loopConsumes NodeInput on timer/callback/consensus channels—does not generate user txs.

What runner_boots_listens_and_shuts_down_cleanly shows: RocksDB + libp2p + pinned loop + shutdown—not a full signed tx path (hands-on module next adds your markers).

Learning objectives

Simulation vs production (same core, different harness)

🌐 Production harness
hyperscale-production
  • Time: wall clock on the pinned thread (run_shard_loop sets time each iteration).
  • Network: Libp2pAdapter / Libp2pNetwork—libp2p gossipsub over QUIC between validators (separate from wallet HTTP POST /api/v1/transactions); gossip arrives on crossbeam channels into the pinned loop.
  • Storage: RocksDbStorage + SharedStorage + PooledDispatch.
  • Threading: Tokio runtime for adapter/RPC/metrics; state machine on a pinned std::thread (spawn_shard_loop).
  • After each step: flush_expired_batches(wall_clock_duration()) — only flushes batch pipelines whose deadlines have passed; amasses work until then for throughput and steadier load (details below).
Design note The hot path is a blocking std::thread that must not .await; async I/O and sleeps stay on Tokio and push NodeInput through crossbeam. Pros: clear ownership of state-machine mutation, no async locks inside step. Cons: two concurrency models and channel backpressure to tune.

Shared core: Simulation uses SimCluster + NodeHost with deterministic ShardEvent scheduling; production uses per-shard ShardLoop::step on dedicated threads. Both drive the same NodeStateMachine—so SubmitTransaction, shard consensus, BlockCommitted, and execution hooks match once an event is delivered. The harness decides how events arrive and when time advances.

Pinned thread, run_shard_loop, and CPU affinity

What “pinned thread” means here

Below is the actual production entrypoint: a named std::thread, optional CPU affinity via core_affinity::set_for_current, then run_shard_loop. Read the code first; captions spell out what each part testifies to.

Snippet context: spawn_shard_loop — pinned-thread entry (file in study table).

pub fn spawn_shard_loop(
    shard_loop: ProdShardLoop,
    config: PinnedLoopConfig,
) -> std::thread::JoinHandle<()> {
    std::thread::Builder::new()
        .name("shard-loop".to_string())
        .spawn(move || {
            // Try to pin to core 0
            if let Some(core_ids) = core_affinity::get_core_ids() {
                if let Some(&core_id) = core_ids.first() {
                    if core_affinity::set_for_current(core_id) {
                        info!(?core_id, "Pinned shard-loop thread to core");
                    } else {
                        warn!("Failed to pin shard-loop thread to core 0");
                    }
                }
            }

            run_shard_loop(shard_loop, config);
        })
        .expect("failed to spawn shard-loop thread")
}

Snippet context: Tokio task after spawn_shard_loop — metrics and shutdown select! (runner file in study table).

        // ── 3. Spawn pinned thread ───────────────────────────────────────
        let loop_handle = spawn_shard_loop(shard_loop, pinned_config);

        // ── 4. Metrics + shutdown loop ───────────────────────────────────
        let mut metrics_tick = tokio::time::interval(Duration::from_secs(1));
        metrics_tick.set_missed_tick_behavior(tokio::time::MissedTickBehavior::Skip);
        let mut shutdown_rx = self.shutdown_rx.take().expect("shutdown_rx already taken");

        loop {
            tokio::select! {
                biased;
                _ = &mut shutdown_rx => {
                    info!("Shutdown signal received");
                    break;
                }
                _ = metrics_tick.tick() => {
                    self.collect_metrics();
                }
            }
        }

The same binary keeps a Tokio loop here (metrics, shutdown); libp2p/RPC/timers live on that runtime too. NodeInput for consensus reaches the shard-loop thread through crossbeam channels (timer_rx / callback_rx / consensus_rx in PinnedLoopConfig) read inside run_shard_loop — see the next subsection.

Design note Genesis runs on the Tokio task before spawn_shard_loop so ShardLoop and storage are initialized deterministically on one side, then the loop is handed off already consistent—avoids splitting genesis across threads. Cost: startup blocks the runtime briefly; acceptable before listeners are fully “hot.”

Diagram: logical CPUs, pinned target, and what runs where

Schematic for one validator process on a typical multi-core host. Affinity affects only the shard-loop std::thread; Tokio worker threads are usually unpinned and the OS may move them across logical CPUs.

Pinning does not reserve a core exclusively for this process unless you add stronger isolation (cgroups, isolated CPUs, etc.).

What run_shard_loop actually does

Read in-repo: run_shard_loop (spawned from spawn_shard_loop in the same module) — path in study table.

It is the main engine loop for advancing the ShardLoop while the node is up. The very first thing inside the function (before the big loop) is to apply genesis-time timer ops once:

Design note Real sleeps live on Tokio because the pinned thread cannot block inside an async runtime; firing into timer_rx re-enters the synchronous step world. Trade-off: timer latency includes Tokio scheduling jitter (usually small vs consensus timeouts).

After that, the loop repeats until shutdown:

Design note Timer > callback > consensus on try_recv favors time-driven liveness (proposal / round timers) over inbound work—otherwise a flood of gossip could delay handling a fired timer and stall progress. Risk if mis-tuned: chronic timer traffic could starve lower priorities; the codebase assumes timer volume stays bounded relative to protocol needs.

Why simulation flushes everything; production batches until deadlines

Read in-repo: flush_expired_batches, flush_all_batches, nearest_batch_deadline, and step caller-protocol docs — study table.

The ShardLoop holds batch accumulators (notably validation and committed-header verification) with time-based deadlines. The two harnesses choose different flush policies after step:

flush_all_batches() still exists on ShardLoop for paths that need an immediate drain (e.g. shutdown); production’s steady-state path prefers deadline-driven flushes.

Correcting a common mental model

Not a “test loop” or “user simulator”

run_shard_loop is production architecture, not something that exists only to simulate users. E2E tests such as runner_boots_listens_and_shuts_down_cleanly start and stop the same path briefly; a real validator runs it continuously. RPC or other tasks send work (e.g. SubmitTransaction) into the crossbeam channels this loop reads—they are real inputs, not pretend traffic generated inside the loop.

vs simulation: Deterministic simulation never spawns this thread: a single driver calls ShardLoop::step from SimCluster::run_until with simulated time and an in-memory network. Same step logic, different harness.

For a broader ladder of multitasking / threading terms mapped to hyperscale-rs (processes, async vs blocking, pools, backpressure), see the Basic module State Machines & Event-Driven Architecture (section on threading and concurrency).

Pinned thread, run_shard_loop, and CPU affinity

What “pinned thread” means here

Below is the actual production entrypoint: a named std::thread, optional CPU affinity via core_affinity::set_for_current, then run_shard_loop. Read the code first; captions spell out what each part testifies to.

Snippet context: spawn_shard_loop — pinned-thread entry (file in study table).

pub fn spawn_shard_loop(
    shard_loop: ProdShardLoop,
    config: PinnedLoopConfig,
) -> std::thread::JoinHandle<()> {
    std::thread::Builder::new()
        .name("shard-loop".to_string())
        .spawn(move || {
            // Try to pin to core 0
            if let Some(core_ids) = core_affinity::get_core_ids() {
                if let Some(&core_id) = core_ids.first() {
                    if core_affinity::set_for_current(core_id) {
                        info!(?core_id, "Pinned shard-loop thread to core");
                    } else {
                        warn!("Failed to pin shard-loop thread to core 0");
                    }
                }
            }

            run_shard_loop(shard_loop, config);
        })
        .expect("failed to spawn shard-loop thread")
}

Snippet context: Tokio task after spawn_shard_loop — metrics and shutdown select! (runner file in study table).

        // ── 3. Spawn pinned thread ───────────────────────────────────────
        let loop_handle = spawn_shard_loop(shard_loop, pinned_config);

        // ── 4. Metrics + shutdown loop ───────────────────────────────────
        let mut metrics_tick = tokio::time::interval(Duration::from_secs(1));
        metrics_tick.set_missed_tick_behavior(tokio::time::MissedTickBehavior::Skip);
        let mut shutdown_rx = self.shutdown_rx.take().expect("shutdown_rx already taken");

        loop {
            tokio::select! {
                biased;
                _ = &mut shutdown_rx => {
                    info!("Shutdown signal received");
                    break;
                }
                _ = metrics_tick.tick() => {
                    self.collect_metrics();
                }
            }
        }

The same binary keeps a Tokio loop here (metrics, shutdown); libp2p/RPC/timers live on that runtime too. NodeInput for consensus reaches the shard-loop thread through crossbeam channels (timer_rx / callback_rx / consensus_rx in PinnedLoopConfig) read inside run_shard_loop — see the next subsection.

Design note Genesis runs on the Tokio task before spawn_shard_loop so ShardLoop and storage are initialized deterministically on one side, then the loop is handed off already consistent—avoids splitting genesis across threads. Cost: startup blocks the runtime briefly; acceptable before listeners are fully “hot.”

Diagram: logical CPUs, pinned target, and what runs where

Schematic for one validator process on a typical multi-core host. Affinity affects only the shard-loop std::thread; Tokio worker threads are usually unpinned and the OS may move them across logical CPUs.

Pinning does not reserve a core exclusively for this process unless you add stronger isolation (cgroups, isolated CPUs, etc.).

What run_shard_loop actually does

Read in-repo: run_shard_loop (spawned from spawn_shard_loop in the same module) — path in study table.

It is the main engine loop for advancing the ShardLoop while the node is up. The very first thing inside the function (before the big loop) is to apply genesis-time timer ops once:

Design note Real sleeps live on Tokio because the pinned thread cannot block inside an async runtime; firing into timer_rx re-enters the synchronous step world. Trade-off: timer latency includes Tokio scheduling jitter (usually small vs consensus timeouts).

After that, the loop repeats until shutdown:

Design note Timer > callback > consensus on try_recv favors time-driven liveness (proposal / round timers) over inbound work—otherwise a flood of gossip could delay handling a fired timer and stall progress. Risk if mis-tuned: chronic timer traffic could starve lower priorities; the codebase assumes timer volume stays bounded relative to protocol needs.

Why simulation flushes everything; production batches until deadlines

Read in-repo: flush_expired_batches, flush_all_batches, nearest_batch_deadline, and step caller-protocol docs — study table.

The ShardLoop holds batch accumulators (notably validation and committed-header verification) with time-based deadlines. The two harnesses choose different flush policies after step:

flush_all_batches() still exists on ShardLoop for paths that need an immediate drain (e.g. shutdown); production’s steady-state path prefers deadline-driven flushes.

Correcting a common mental model

Not a “test loop” or “user simulator”

run_shard_loop is production architecture, not something that exists only to simulate users. E2E tests such as runner_boots_listens_and_shuts_down_cleanly start and stop the same path briefly; a real validator runs it continuously. RPC or other tasks send work (e.g. SubmitTransaction) into the crossbeam channels this loop reads—they are real inputs, not pretend traffic generated inside the loop.

vs simulation: Deterministic simulation never spawns this thread: a single driver calls ShardLoop::step from SimCluster::run_until with simulated time and an in-memory network. Same step logic, different harness.

For a broader ladder of multitasking / threading terms mapped to hyperscale-rs (processes, async vs blocking, pools, backpressure), see the Basic module State Machines & Event-Driven Architecture (section on threading and concurrency).

2. Production E2E-style: runner_boots_listens_and_shuts_down_cleanly

Source & story

Test crate: production integration E2Es — path in study table.

This test does not submit a transaction; it proves production wiring: RocksDB, ProductionRunner::build, libp2p listening, run() spawning the pinned loop, then graceful shutdown. The same ShardLoop::step path would process SubmitTransaction if an RPC task (or test) sent it on the consensus channel—see ProductionRunner::tx_submission_sender() on the production runner (study table).

Step (test) What it validates → code path
TestFixtures::new(42, 1) + RocksDbStorage::open
Deterministic topology/keys for one validator; persistent storage under a temp directory. Production I/O path, unlike SimStorage. Test fixtures module + RocksDB-backed storage crate (study table). Why real RocksDB here: catches compaction, locking, and path bugs that in-memory stores miss; cost is slower, flaky-disk-sensitive tests if not temp-scoped.
ProductionRunner::builder()...build().await
Wires Prometheus metrics install, PooledDispatch, NodeStateMachine, SharedStorage, Libp2pAdapter::new, Libp2pNetwork, RadixExecutor, TransactionValidation, and ShardLoop::new(...) with production generic params. Crossbeam senders/receivers are created for timer / callback / consensus / shutdown. ProductionRunnerBuilder::build on the production runner (study table).
runner.network(), listen addresses
Confirms a QUIC listen address for the validator libp2p stack (port 0 = OS-assigned)—validator-to-validator transport, not the wallet HTTP listener. Network stack is live; gossip would use gossipsub/QUIC, not simulated delivery queues.
tokio::spawn(runner.run())
ProductionRunner::run: calls maybe_initialize_genesis() on the still-local ShardLoop (engine genesis + genesis block + initialize_genesis + StateCommitComplete—parallel idea to simulation’s genesis, but on RocksDB). Then moves ShardLoop into spawn_shard_loop. Main Tokio task then waits on shutdown + metrics ticks. See maybe_initialize_genesis and run on the production runner (study table).
Pinned thread: run_shard_loop
Once at entry: drain initial_timer_ops from genesis setup (maybe_initialize_genesis) through ProdTimerManager — each TimerOp arms a Tokio sleep that later injects timer NodeInput on crossbeam timer_rx. Then loop: shutdown check → set_time(now) → priority try_recv (timer, callback, consensus) → optional blocking crossbeam::select!shard_loop.step(event)flush_expired_batchesperiodic metrics / channel depth export / optional JVT GC (run_jvt_gc). Production analogue of simulation’s run_until + step; events from real adapters and Tokio timers, not a BTreeMap queue. Implemented in run_shard_loop (production event loop — study table).
drop(shutdown) + join handle
Shutdown handle drops → run’s Tokio loop exits → sends on xb_shutdown_tx → pinned loop observes shutdown and exits → test asserts clean completion. Validates lifecycle, not tx finality.
Design note

This integration test is not a full “tx through consensus to finality” production E2E: it does not submit a transaction or assert block heights, QCs, or executed user state (aside from genesis wiring at startup). The pinned loop is real and would handle SubmitTransaction if something sent it on the consensus channel, but proving submit→mempool→BFT→commit→execute with assertions is what the simulation E2E above is for, or a heavier prod/RPC test if the repo adds one next to the existing production tests (study table).

Skipping a full tx here keeps CI fast and failure modes obvious (build/wire/shutdown). Cross-shard / multishard: simulation includes scenarios such as cross_shard_tx_sim alongside the single-shard E2E in the same integration test file (study table). This module does not trace a production multishard integration test; that would mean multiple validators, real networking, and timing—outside the scope of runner_boots_listens_and_shuts_down_cleanly.

Run this test (use one test thread; serial tests)

cargo test -p hyperscale-production --test runner runner_boots_listens_and_shuts_down_cleanly -- --test-threads=1 --nocapture

3. Mental model: one picture

Design note The sim side uses one ordered global event queue so replay is a total order of (time, tie-break)—great for debugging “who ran first.” Production has no single queue: concurrent producers + crossbeam channel priorities mean order is emergent; tests must reason about protocol rules, not one serialized timeline.
Simulation Production │ │ schedule_event / run_until crossbeam + libp2p + Tokio timers │ │ └──────────────┬─────────────────────┘ ▼ ShardLoop::step(NodeInput) │ NodeStateMachine (BFT, mempool, execution, …)

Where to go next

Quiz

Pass threshold: 70%.

Suggested crates / files to study

Paths relative to the hyperscale-rs repo root. Use these when following the traces and code snippets above.

Topic Path
Simulation E2E tests crates/simulation/tests/scenarios.rssingle_shard_tx_sim, cross_shard_tx_sim, …
Simulation runner crates/simulation/src/runner.rsSimCluster::new, genesis, schedule_initial_event, run_until, drain_node_io
Production E2E tests crates/production/tests/runner.rsrunner_boots_listens_and_shuts_down_cleanly
Production test fixtures crates/network-libp2p/src/test_utils.rs
Production runner crates/production/src/runner.rsProductionRunnerBuilder::build, maybe_initialize_genesis, run, tx_submission_sender
Production event loop crates/production/src/runner.rsspawn_shard_loop, run_shard_loop
Node ShardLoop crates/node/src/shard/mod.rsstep, SubmitTransaction, validation pipeline, flush_all_batches / flush_expired_batches
Node state machine crates/node/src/state/mod.rson_block_committed, protocol event dispatch
BFT crates/shard
RocksDB storage crates/storage-rocksdb (package hyperscale_storage_rocksdb)