🔄

Module M005

Output Validation & State Machines

DirectEd x CATS Hackathon
Aiken Development Workshop Series

Duration: 2 hours

Format: 1 hour lecture + 1 hour exercises

SLIDE 2

Module Overview

In M004, you mastered input validation. Now we focus on OUTPUTS and learn the most powerful pattern: STATE MACHINES!

What You'll Learn

  • Validate transaction outputs
  • Extract output datums
  • Build state machines
  • Implement state transitions
  • Compare input/output states
  • Create stateful DApps

Why State Machines?

  • 🔢 Counters & accumulators
  • 🗳️ Voting systems
  • 💰 Vesting schedules
  • 🏷️ Auction systems
  • 📊 Workflow management
Key Concept: State machines let your validators manage changing state over time using the input-output pattern!
SLIDE 3

Understanding Transaction Outputs

Outputs are the NEW UTxOs created by a transaction.

Output Structure

pub type Output { address: Address, // Where it goes value: Value, // ADA + tokens datum: Datum, // Inline, hash, or none reference_script: Option<ScriptHash>, }

Why Outputs Matter

  • Define where value goes
  • Carry new state in datums
  • Enable state continuity
  • Create state machines

Output Types

  • Continuing: Back to script
  • Terminating: To wallet
  • Multiple: Split value
SLIDE 4

Filtering Outputs by Address

Just like inputs, filter outputs to find those going to your validator.

Manual Filtering

use aiken/collection/list fn outputs_at( outputs: List<Output>, addr: Address, ) -> List<Output> { list.filter( outputs, fn(output) { output.address == addr } ) } // Usage let continuing = outputs_at( self.outputs, script_addr )

Single Output Check

// Ensure exactly one output when continuing is { [single] -> { // Validate this output True } _ -> False } // Or using Vodka use vodka/single.{ single_script_output } expect Some(output) = single_script_output( self.inputs, self.outputs, input_ref )
Pattern: For state machines, ensure exactly ONE continuing output!
SLIDE 5

Extracting Output Datums

For state machines, we use inline datums in outputs.

Extraction Pattern

pub type CounterDatum { count: Int, owner: ByteArray, } validator counter { spend(datum, redeemer, input_ref, self) { // 1. Get current state from input expect Some(current_state) = datum // 2. Get continuing output expect Some(output) = single_script_output( self.inputs, self.outputs, input_ref ) // 3. Extract output datum (NEW state) expect InlineDatum(new_state_data) = output.datum expect new_state: CounterDatum = new_state_data // 4. Validate state transition new_state.count == current_state.count + 1 } }
Pattern: Input datum = current state, Output datum = new state
SLIDE 6

What is a State Machine?

A state machine is a system that transitions between defined states based on actions.

State Machine Concept

📊

State 1

count = 5

Increment

📈

State 2

count = 6

In eUTxO Model

  • State = UTxO datum
  • Transitions = spend & create
  • Rules = validator logic

Real Examples

  • Counter: count values
  • Voting: vote tallies
  • Auction: bid amounts
  • Vesting: claimed amounts
SLIDE 7

State Machines in eUTxO

In Cardano's eUTxO model, state transitions happen through the input-output pattern.

The State Transition Pattern

📥

INPUT UTxO

datum: State1

value: 10 ADA

Current State

🔐

VALIDATOR

Checks:

✓ Valid transition?

✓ Rules followed?

📤

OUTPUT UTxO

datum: State2

value: 10 ADA

New State

Key Insight: Validator compares input state (datum) with output state (datum) to validate the transition!
SLIDE 8

Defining State in Datums

State is stored in the datum of a UTxO.

Counter State

pub type CounterState { count: Int, owner: ByteArray, } // States: // count = 0, 1, 2, 3...

Voting State

pub type VotingState { yes_votes: Int, no_votes: Int, deadline: Int, is_closed: Bool, } // States change as // votes are cast

Auction State

pub type AuctionState { seller: ByteArray, current_bidder: ByteArray, current_bid: Int, deadline: Int, } // State changes with // each new bid

Vesting State

pub type VestingState { beneficiary: ByteArray, total_amount: Int, claimed_amount: Int, vesting_start: Int, } // claimed_amount // increases over time
Design Tip: Include fields that track state AND fields that stay constant (like owner)
SLIDE 9

Defining State Transitions (Actions)

Redeemers define the possible actions that trigger state transitions.

Counter Actions

pub type CounterAction { Increment Decrement Reset } // Each action causes // a state change

Voting Actions

pub type VotingAction { VoteYes VoteNo CloseVoting } // VoteYes → yes_votes++ // VoteNo → no_votes++ // Close → is_closed=True

Auction Actions

pub type AuctionAction { PlaceBid CloseAuction CancelAuction } // PlaceBid → update bid // Close → finalize // Cancel → abort

Vesting Actions

pub type VestingAction { Claim { amount: Int } CancelVesting } // Claim → increase // claimed_amount // Cancel → terminate
SLIDE 10

Implementing State Transitions

Validate that state changes correctly based on the action.

Counter Example

validator counter { spend(datum, redeemer, input_ref, self) { // Get current state expect Some(current_state) = datum // Get continuing output expect Some(output) = single_script_output(self.inputs, self.outputs, input_ref) // Get new state expect InlineDatum(new_state_data) = output.datum expect new_state: CounterState = new_state_data // Validate transition based on action when redeemer is { Increment -> and { new_state.count == current_state.count + 1, new_state.owner == current_state.owner, // Owner unchanged } Decrement -> and { new_state.count == current_state.count - 1, new_state.owner == current_state.owner, } Reset -> and { new_state.count == 0, new_state.owner == current_state.owner, } } } }
SLIDE 11

Initialize-Update-Close Pattern

Most state machines follow a three-phase lifecycle.

🌱

1. INITIALIZE

Create first state

Lock funds, set initial values

🔄

2. UPDATE

Transition states

Multiple times, changing state

🏁

3. CLOSE

Finalize & distribute

Unlock funds, end lifecycle

Initialize

  • Create first UTxO
  • Set initial state values
  • Lock funds at script

Update

  • Consume current UTxO
  • Create new UTxO
  • Validate state change

Close

  • Consume final UTxO
  • Distribute value to wallets
  • No continuing output
SLIDE 12

Real Example: Voting System

Voting State Machine

pub type VotingState { yes_votes: Int, no_votes: Int, deadline: Int, is_closed: Bool, } pub type VotingAction { VoteYes VoteNo CloseVoting } validator voting { spend(datum, redeemer, input_ref, self) { expect Some(current_state) = datum when redeemer is { VoteYes -> { expect Some(output) = single_script_output(self.inputs, self.outputs, input_ref) expect InlineDatum(new_state_data) = output.datum expect new_state: VotingState = new_state_data and { current_state.is_closed == False, // Must be open is_before_deadline(self.validity_range, current_state.deadline), new_state.yes_votes == current_state.yes_votes + 1, // Increment yes new_state.no_votes == current_state.no_votes, // No change new_state.deadline == current_state.deadline, // Unchanged new_state.is_closed == False, // Still open } } VoteNo -> { // Similar to VoteYes, but increment no_votes True } CloseVoting -> { and { is_after_deadline(self.validity_range, current_state.deadline), current_state.is_closed == False, // No continuing output needed (terminates) } } } } }
SLIDE 13

State Comparison Pattern

The core pattern for state machines:

4-Step Validation Process

validator state_machine { spend(datum, redeemer, input_ref, self) { // STEP 1: Get current state from input datum expect Some(current_state) = datum // STEP 2: Get continuing output expect Some(output) = single_script_output( self.inputs, self.outputs, input_ref ) // STEP 3: Get new state from output datum expect InlineDatum(new_state_data) = output.datum expect new_state: MyState = new_state_data // STEP 4: Validate the state transition when redeemer is { MyAction -> { and { // Check fields changed correctly new_state.field1 == expected_value, // Check fields stayed the same new_state.owner == current_state.owner, // Additional checks... } } } } }
Always: Compare current_state (input) with new_state (output)!
SLIDE 14

State Invariants: Field Preservation

Some fields should NEVER change during state transitions.

❌ Bad: No Preservation

// Only checking count changed new_state.count == current_state.count + 1 // PROBLEM: Owner could change! // Attacker could set themselves // as owner in the output

✅ Good: Preserve Invariants

and { // Check what SHOULD change new_state.count == current_state.count + 1, // Check what MUST NOT change new_state.owner == current_state.owner, new_state.created_at == current_state.created_at, }

Helper Function Pattern

// Reusable field preservation check fn fields_preserved(current: State, new: State) -> Bool { and { new.owner == current.owner, new.created_at == current.created_at, new.identifier == current.identifier, } } // Use in validator and { new_state.count == current_state.count + 1, fields_preserved(current_state, new_state), }
SLIDE 15

Value Conservation in State Machines

Track value changes alongside state changes.

Vesting Example

pub type VestingState { beneficiary: ByteArray, total_amount: Int, claimed_amount: Int, } pub type VestingAction { Claim { amount: Int } } validator vesting { spend(datum, redeemer, input_ref, self) { expect Some(current_state) = datum when redeemer is { Claim { amount } -> { expect Some(output) = single_script_output(self.inputs, self.outputs, input_ref) expect InlineDatum(new_state_data) = output.datum expect new_state: VestingState = new_state_data // Get input and output values expect Some(input) = find_input(self.inputs, input_ref) let input_value = lovelace_of(input.output.value) let output_value = lovelace_of(output.value) and { // State update new_state.claimed_amount == current_state.claimed_amount + amount, // Value conservation output_value == input_value - amount, // Minimum balance remains output_value >= 2_000_000, // Fields preserved new_state.beneficiary == current_state.beneficiary, new_state.total_amount == current_state.total_amount, } } } } }
SLIDE 16

Testing State Machines

Test all state transitions and edge cases.

✅ Test Valid Transitions

Each action with correct state changes

test counter_increment_succeeds() { let current = CounterState { count: 5, owner: #"abc" } let new = CounterState { count: 6, owner: #"abc" } let tx = build_transition_tx(current, new) counter.spend(Some(current), Increment, ref, tx) }

❌ Test Invalid Transitions

Wrong state changes should fail

test counter_wrong_increment_fails() fail { let current = CounterState { count: 5, owner: #"abc" } let new = CounterState { count: 10, owner: #"abc" } // Wrong! let tx = build_transition_tx(current, new) counter.spend(Some(current), Increment, ref, tx) }

❌ Test Field Changes

Unchanging fields modified should fail

test counter_owner_change_fails() fail { let current = CounterState { count: 5, owner: #"abc" } let new = CounterState { count: 6, owner: #"xyz" } // Changed owner! let tx = build_transition_tx(current, new) counter.spend(Some(current), Increment, ref, tx) }
SLIDE 17

Hands-On Exercises

Build your own state machines! 🔄

Exercise 1: Simple Counter (15 min)

Extract input/output datums, validate count increments by 1

Exercise 2: Voting System (25 min)

Implement VoteYes/VoteNo with time constraints

Exercise 3: Token Accumulator (25 min)

Track accumulated amount, validate value changes match state

Exercise 4: Multi-Stage State Machine (30 min)

Task manager: Created → Assigned → Completed states

SLIDE 18

Assignment M005

Build a Complete State Machine

Choose One Scenario

  1. Option A: Vesting Contract
  2. Option B: Auction System
  3. Option C: Voting/Poll System

Must Include

  • Datum with 4+ state fields
  • Redeemer with 3+ actions
  • State transition logic
  • Input/output comparison
  • Field preservation
  • Value tracking

Testing Requirements

  • 8+ comprehensive tests
  • All transition paths
  • Valid state changes ✅
  • Invalid changes ❌
  • Field preservation tests
  • Complete lifecycle

Submission

  • GitHub repository URL
  • State machine diagram
  • Test results
  • README with explanation
Deadline: Before Module M006
SLIDE 19

Common Issues & Solutions

❌ Cannot extract output datum

Check: expect InlineDatum(data) = output.datum then cast to type

❌ State transition always fails

Use trace to debug: trace @"Current", trace current_state

❌ Cannot find continuing output

Use single_script_output(self.inputs, self.outputs, input)

❌ Fields change when they shouldn't

Always validate: new_state.owner == current_state.owner

✓ Build mock state transitions carefully

let current = State { count: 5 } let new = State { count: 6 } // Explicit change
SLIDE 20

Key Takeaways

You can now:

✅ Validate transaction outputs

✅ Extract and validate output datums

✅ Understand state machine patterns

✅ Implement state transitions

✅ Compare input and output states

✅ Build Initialize-Update-Close patterns

✅ Create stateful DApps with validators

Next: Module M006 - Minting Policies & NFT Development 🎨
SLIDE 21

The Power of State Machines

🔄
State machines unlock sophisticated DApp development!

What You Can Build

💰 Vesting contracts with progressive unlocks
🗳️ Voting systems with tallied results
🏷️ Auction platforms with bid tracking
📊 Multi-stage workflows and processes
🎮 Game state management on-chain
The input-output pattern is the key to Cardano's power! 🚀
🔄

Fantastic Work!

Module M005 Complete

You can now build stateful validators with state machines!

Master the input-output pattern 🔄

See you in M006! 🚀

/ 22