Learning Objectives
- Understand the state machine pattern
- Learn event-driven design principles
- Explore Event and Action types in hyperscale-rs
- Understand determinism and why it matters
- See how state machines compose in hyperscale-rs
1. What is a State Machine?
Definition
A state machine is a computational model that consists of:
- States - The possible configurations of the system
- Transitions - Rules for moving from one state to another
- Inputs - Events that trigger transitions
- Outputs - Actions produced by transitions
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?
- Deterministic - Same state + same event = same result
- Testable - Easy to test all state transitions
- Predictable - Behavior is well-defined
- Composable - Can combine multiple 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
- Synchronous - No async, no .await
- Pure - No I/O, no locks, no side effects
- Deterministic - Same inputs always produce same outputs
- Simple - Just state + event → actions
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:
- It's very simple - just three methods
- The
handle method takes an event and returns actions
- Time is managed explicitly (for determinism)
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/event.rs to see all event types. Examples:
enum Event {
// Timers
ProposalTimer,
CleanupTimer,
// Network Messages
BlockHeaderReceived { header: BlockHeader, .. },
BlockVoteReceived { vote: BlockVote },
// Internal Events
QuorumCertificateFormed { block_hash: Hash, qc: QuorumCertificate },
BlockCommitted { block_hash: Hash, .. },
// ... many more
}
Event Sources
- Network - Messages from other validators
- Timers - Scheduled events (e.g., proposal timer)
- Internal - Generated by other state machines
- Client - User-submitted transactions
Event Priority
Events at the same timestamp are processed by priority:
- Internal (0) - Consequences of prior processing
- Timer (1) - Scheduled timers
- Network (2) - External messages
- 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: ShardGroupId, message: OutboundMessage },
BroadcastStateVote { shard: ShardGroupId, 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:
- SimulationRunner - Deterministic simulation
- ProductionRunner - Real networking, storage, async
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
- Timer fires →
Event::ProposalTimer
- State machine handles → Checks if this node is proposer
- If proposer → Builds block, returns
Action::BroadcastToShard
- Runner executes → Sends block header to network
- Other nodes receive → Create
Event::BlockHeaderReceived
- They vote → Return
Action::BroadcastToShard (vote)
- Votes collected →
Event::QuorumCertificateFormed
- QC processed → Chain state updated (
latest_qc), commit rule checked (Module 1.2; bft 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
- Testing - Can reproduce bugs exactly
- Debugging - Can replay execution step-by-step
- Simulation - Can test in controlled environment
- Consistency - All nodes produce same results
What Breaks Determinism?
- ❌ Random number generation (without seed)
- ❌ System time (use explicit time parameter)
- ❌ Network timing (simulate with delays)
- ❌ Thread scheduling (single-threaded in state machine)
How Hyperscale-rs Maintains Determinism
- ✅ Explicit time parameter (
set_time)
- ✅ No random number generation in state machine
- ✅ Synchronous execution (no async in state machine)
- ✅ Deterministic event ordering (priority system)
7. Composing State Machines
NodeStateMachine
The NodeStateMachine composes multiple sub-state machines:
NodeStateMachine
├── BftState (consensus)
├── ExecutionState (transaction execution)
├── MempoolState (transaction pool)
├── ProvisionCoordinator (cross-shard coordination)
└── LivelockState (deadlock prevention)
How They Compose
NodeStateMachine receives an event
- Routes event to appropriate sub-state machines
- Each sub-state machine processes the event
- Actions from all sub-machines are collected
- Sub-machines can generate internal events for each other
Exploring Composition
Open crates/node/src/state.rs and find the handle method. Notice how it:
- Routes events to different sub-machines
- Collects actions from all sub-machines
- Coordinates between sub-machines
9. Practical Assignment
Assignment: Trace an Event Through the System
Tasks:
- Choose an Event:
- Pick
Event::BlockHeaderReceived or Event::ProposalTimer
- Read its definition in
crates/core/src/event.rs
- Trace Through NodeStateMachine:
- Open
crates/node/src/state.rs
- Find where your chosen event is handled
- Trace which sub-state machines it goes to
- Note what actions are generated
- Trace Through Sub-State Machine:
- Open the relevant sub-state machine file (e.g.,
crates/bft/src/state.rs)
- Find the handler for your event
- Trace the logic step-by-step
- Note what state changes occur
- Note what actions are returned
- Create a Diagram:
- Draw a flow diagram showing: Event → State Machine → Actions
- Include state changes
- Include any internal events generated
- 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