State Machines & Event-Driven Architecture

⏱️ Duration: 1.5-2 hours πŸ“Š Difficulty: Basic

Learning Objectives

For a first-pass map of message sources and destinations (RPC, gossip, mempool, BFT, execution, timers), transports, typical in-memory stores, how each path is drained, and handlers / callbacks (including the full register_* table), see Transaction flow β†’ Messaging & transports inventory (registry + four sub-tables).

1. What is a State Machine?

Definition

A state machine is a computational model that consists of:

Simple Example

Light Switch State Machine:
  States: {ON, OFF}
  Transitions:
    - If state is OFF and event is "flip" β†’ state becomes ON
    - If state is ON and event is "flip" β†’ state becomes OFF

Why State Machines?

2. The StateMachine Trait

Core Interface

In hyperscale-rs, everything implements the StateMachine trait:

trait StateMachine {
    fn handle(&mut self, event: Event) -> Vec<Action>;
    fn set_time(&mut self, now: Duration);
    fn now(&self) -> Duration;
}

Key Properties

Why No I/O? I/O (networking, storage, timers) is handled by "runners" that execute actions and feed results back as events. This separation makes the consensus logic testable and deterministic.

Exploring the Trait

Open crates/core/src/traits.rs and find the StateMachine trait. Notice:

3. Events: What Happens

What are Events?

Events are passive data structures that describe something that happened. They flow into state machines.

Event Types

Open crates/core/src/protocol_event.rs to see all event types. Examples:

enum ProtocolEvent {
    // Timers (examples)
    ViewChangeTimer,
    CleanupTimer,

    // Consensus traffic
    BlockHeaderReceived { .. },
    BlockVoteReceived { vote: BlockVote },

    // Pipeline callbacks
    ProposalBuilt { .. },
    QuorumCertificateFormed { .. },

    // ... see crates/core/src/protocol_event.rs
}

Event Sources

Event Priority

Events at the same timestamp are processed by priority:

  1. Internal (0) - Consequences of prior processing
  2. Timer (1) - Scheduled timers
  3. Network (2) - External messages
  4. Client (3) - User submissions
Why Priority? Preserves causality β€” internal events (like QC formation; see Module 1.2 for steps (i)–(v) and hyperscale-rs crate/line refs) must be processed before new external inputs arrive.

4. Actions: What to Do

What are Actions?

Actions are commands that describe what the state machine wants to do. They flow out of state machines and are executed by runners.

Action Types

Open crates/core/src/action.rs to see all action types. Examples:

enum Action {
    // Network
    BroadcastToShard { shard: ShardId, message: OutboundMessage },
    BroadcastStateVote { shard: ShardId, vote: StateVoteBlock },

    // Timers
    SetTimer { id: TimerId, duration: Duration },

    // Internal
    EnqueueInternal { event: Event },

    // Storage
    CommitBlock { block: Block, qc: QuorumCertificate },

    // ... many more
}

Action Execution

Actions are executed by runners:

Key Insight: The same state machine code runs in both simulation and production. This means bugs found in simulation will also exist in production (and vice versa).

5. Event-Driven Flow

The Flow

1. Runner receives network message
2. Runner creates Event::BlockHeaderReceived
3. Runner calls state_machine.handle(event)
4. State machine processes event, updates state
5. State machine returns Vec<Action>
6. Runner executes actions (send network, store, etc.)
7. Actions may generate new events
8. Loop continues...

Example: Block Proposal Flow

  1. New txs/QCs/provisions arrive β†’ node latches try_propose; separately ViewChange timer fires β†’ ProtocolEvent::ViewChangeTimer for round timeout
  2. State machine handles β†’ Checks if this node is proposer
  3. If proposer β†’ Builds block, returns Action::BroadcastToShard
  4. Runner executes β†’ Sends block header to network
  5. Other nodes receive β†’ Create Event::BlockHeaderReceived
  6. They vote β†’ Return Action::BroadcastToShard (vote)
  7. Votes collected β†’ Event::QuorumCertificateFormed
  8. QC processed β†’ Chain state updated (latest_qc), commit rule checked (Phase 2; shard crate)

6. Determinism: Why It Matters

What is Determinism?

Deterministic means: given the same initial state and same sequence of events, you always get the same final state and same sequence of actions.

Why Determinism Matters

What Breaks Determinism?

How Hyperscale-rs Maintains Determinism

7. Composing State Machines

NodeStateMachine

The NodeStateMachine composes multiple sub-state machines:

NodeStateMachine
β”œβ”€β”€ BeaconCoordinator        (beacon chain: epochs, topology)
β”œβ”€β”€ ShardCoordinator         (per-shard HotStuff-2)
β”œβ”€β”€ ExecutionCoordinator     (execution + cross-shard waves; ConflictDetector inside)
β”œβ”€β”€ MempoolCoordinator       (transaction pool)
β”œβ”€β”€ ProvisionCoordinator     (cross-shard provisions)
β”œβ”€β”€ OutboundProvisionTracker   (ACK until execution certs)
└── RemoteHeaderCoordinator  (remote headers)

How They Compose

  1. NodeStateMachine receives an event
  2. Routes event to appropriate sub-state machines
  3. Each sub-state machine processes the event
  4. Actions from all sub-machines are collected
  5. Sub-machines can generate internal events for each other

Exploring Composition

Open crates/node/src/state/mod.rs and find the handle / dispatch helpers. Notice how it:

8. Knowledge Check Quiz

9. Practical Assignment

Assignment: Trace an Event Through the System

Tasks:

  1. Choose an Event:
    • Pick ProtocolEvent::BlockHeaderReceived or ProtocolEvent::ViewChangeTimer
    • Read its definition in crates/core/src/protocol_event.rs
  2. Trace Through NodeStateMachine:
    • Open crates/node/src/state/mod.rs
    • Find where your chosen event is handled
    • Trace which sub-state machines it goes to
    • Note what actions are generated
  3. Trace Through Sub-State Machine:
    • Open the relevant subsystem file (e.g., crates/shard/src/coordinator.rs or crates/node/src/state/participation/shard.rs)
    • Find the handler for your event
    • Trace the logic step-by-step
    • Note what state changes occur
    • Note what actions are returned
  4. Create a Diagram:
    • Draw a flow diagram showing: Event β†’ State Machine β†’ Actions
    • Include state changes
    • Include any internal events generated
  5. Write a Summary:
    • Document the flow in your learning journal
    • Explain what happens at each step
    • List any questions you have

Success Criteria:

  • βœ… You can trace an event from entry to actions
  • βœ… You understand which state machines are involved
  • βœ… You can explain the flow to someone else
  • βœ… You have a diagram showing the flow

Threading & concurrency ladder (Hyperscale-rs context)

Your StateMachine::handle is synchronous and pure; a real node still runs in an OS full of threads and async runtimes. Below: one sentence each, from foundations toward details that show up in hyperscale-rs (especially production). For a code-level walkthrough of the production shard-loop thread and run_shard_loop, see E2E harnesses β€” production Part II.

  1. Process vs thread β€” A process is an isolated program; threads share memoryβ€”hyperscale uses a dedicated thread for the hot ShardLoop path and separate Tokio/async work for network and timers.
  2. Multitasking / time-sharing β€” The OS preempts threads; long synchronous work inside ShardLoop::step still blocks every consensus event on that thread until step returns.
  3. Concurrency vs parallelism β€” Concurrency interleaves many tasks (events + channels); parallelism is simultaneous CPUsβ€”hyperscale runs the state machine sequentially on one thread while pools (PooledDispatch) parallelize crypto, validation, and execution where offloaded.
  4. Cooperative vs preemptive β€” The OS preempts threads preemptively; between iterations the pinned loop yields to the scheduler, but a single heavy step is still one uninterrupted chunk of work on that thread unless work is split or offloaded.
  5. Async (Tokio) vs blocking β€” Tokio runs async tasks on a pool and awaits I/O; hyperscale’s ShardLoop uses synchronous step on the pinned thread while libp2p/RPC/timer code runs async and sends NodeInput in via channels.
  6. Message passing (channels) β€” Threads communicate by sending NodeInput instead of ad-hoc shared mutable state; the pinned loop is the single writer for the hot state machine progression path.
  7. Locking / data races β€” Locks serialize shared data; hyperscale reduces races by isolating hot state on one thread and using Arc + channels for handoff (storage bridges like SharedStorage still require careful design).
  8. CPU affinity (β€œpinning”) β€” Affinity tells the OS which cores may run a thread; production tries to pin the shard-loop thread for steadier scheduling, not because the loop is test-only code.
  9. Thread pools / work distribution β€” Pools run many units of work across workers; hyperscale’s dispatch pools handle heavy work so it need not all run inside step, with results coming back as callback channel events.
  10. Deterministic simulation vs production β€” Simulation drives the same ShardLoop::step from one driver thread with fake time and fake network; production drives it from a pinned thread with wall-clock time and real libp2p gossipsub over QUIC between validators (wallet submit stays HTTP, separate from that mesh)β€”same logic, different harness.
  11. Backpressure β€” When producers outpace consumers, queues grow or you drop/block; production uses batch deadlines and flush_expired_batches so validation and I/O do not grow without bound.
  12. NUMA / cache effects (niche) β€” On large servers, which core and which memory node matter for cache and NUMA; pinning the hot thread is a small step toward predictable behavior, not a full NUMA story by itself.