This document describes the structured transaction state machine implemented for issue #171. The state machine enforces strict, deterministic state transitions to prevent inconsistent transfer statuses.
The remittance lifecycle follows a structured state machine with five states:
- Description: Initial state when remittance is created
- Entry Point: When
create_remittance()is called - Characteristics: Funds are locked in the contract
- Next Valid States: SUBMITTED, FAILED
- Description: Remittance submitted for processing by agent
- Entry Point: When agent accepts the remittance
- Characteristics: Agent has acknowledged and is processing
- Next Valid States: PENDING_ANCHOR, FAILED
- Description: Awaiting anchor/external confirmation
- Entry Point: When external confirmation is required
- Characteristics: Waiting for external system confirmation
- Next Valid States: COMPLETED, FAILED
- Description: Successfully completed, agent received payout
- Entry Point: When
confirm_payout()succeeds - Characteristics: Terminal state, no further transitions allowed
- Next Valid States: None (terminal)
- Description: Failed at any stage, funds refunded to sender
- Entry Point: When
cancel_remittance()is called or processing fails - Characteristics: Terminal state, no further transitions allowed
- Next Valid States: None (terminal)
┌───────────┐
│ INITIATED │
└─────┬─────┘
│
├──────────────┐
│ │
▼ ▼
┌───────────┐ ┌────────┐
│ SUBMITTED │ │ FAILED │ (Terminal)
└─────┬─────┘ └────────┘
│ ▲
├──────────────┤
│ │
▼ │
┌────────────────┐ │
│ PENDING_ANCHOR │──┤
└────────┬───────┘ │
│ │
├──────────┘
│
▼
┌───────────┐
│ COMPLETED │ (Terminal)
└───────────┘
| From State | To State | Condition |
|---|---|---|
| INITIATED | SUBMITTED | Agent accepts remittance |
| INITIATED | FAILED | Creation fails or cancelled early |
| SUBMITTED | PENDING_ANCHOR | External confirmation required |
| SUBMITTED | FAILED | Processing fails |
| PENDING_ANCHOR | COMPLETED | Confirmation received, payout successful |
| PENDING_ANCHOR | FAILED | Confirmation fails or timeout |
All transitions not listed above are invalid and will be rejected with ContractError::InvalidStateTransition.
Examples of invalid transitions:
- INITIATED → PENDING_ANCHOR (must go through SUBMITTED)
- INITIATED → COMPLETED (must go through SUBMITTED and PENDING_ANCHOR)
- SUBMITTED → COMPLETED (must go through PENDING_ANCHOR)
- COMPLETED → any state (terminal state)
- FAILED → any state (terminal state)
Transitioning from a state to itself is allowed (idempotent):
- INITIATED → INITIATED ✓
- SUBMITTED → SUBMITTED ✓
- PENDING_ANCHOR → PENDING_ANCHOR ✓
- COMPLETED → COMPLETED ✓
- FAILED → FAILED ✓
This enables safe retries without errors.
pub enum RemittanceStatus {
Initiated,
Submitted,
PendingAnchor,
Completed,
Failed,
}Methods:
is_terminal()- Checks if status is terminal (COMPLETED or FAILED)can_transition_to(&self, to: &RemittanceStatus)- Validates if transition is allowednext_valid_states(&self)- Returns list of valid next states
validate_transition(from, to)
- Centralized validation function
- Returns
Ok(())for valid transitions - Returns
Err(ContractError::InvalidStateTransition)for invalid transitions - Allows idempotent transitions (same → same)
transition_status(env, remittance, new_status)
- Atomically updates remittance status with validation
- Ensures all-or-nothing updates
- Logs transitions in debug builds
is_terminal_status(status)
- Checks if status is terminal
get_valid_next_states(status)
- Returns vector of valid next states
- Same input always produces same result
- No randomness or external dependencies
- Transition rules are fixed and explicit
- Status updates are all-or-nothing
- No partial writes possible
- Storage integrity maintained
- COMPLETED and FAILED states cannot transition
- Prevents data corruption
- Ensures finality
- No panics or unwraps
- All invalid transitions return explicit errors
- Clear error messages for debugging
- Repeated submissions with same status are safe
- Enables retry logic without errors
- Prevents duplicate processing
// Remittance starts in INITIATED state
let remittance_id = contract.create_remittance(
&sender,
&agent,
&100,
&None
)?;
// Status: INITIATED// 1. Agent accepts remittance
contract.submit_remittance(&remittance_id)?;
// Status: INITIATED → SUBMITTED
// 2. External confirmation required
contract.request_anchor_confirmation(&remittance_id)?;
// Status: SUBMITTED → PENDING_ANCHOR
// 3. Confirmation received, complete payout
contract.confirm_payout(&remittance_id)?;
// Status: PENDING_ANCHOR → COMPLETED (Terminal)// From any non-terminal state, can transition to FAILED
contract.cancel_remittance(&remittance_id)?;
// Status: * → FAILED (Terminal)let remittance = contract.get_remittance(&remittance_id)?;
// Check if terminal
if remittance.status.is_terminal() {
// Cannot transition further
}
// Check if specific transition is valid
if remittance.status.can_transition_to(&RemittanceStatus::Completed) {
// Transition is allowed
}
// Get all valid next states
let next_states = remittance.status.next_valid_states();| Old State | New State | Notes |
|---|---|---|
| Pending | Initiated | Initial state |
| Processing | Submitted | Being processed |
| Settled | Completed | Successfully completed |
| Cancelled | Failed | Cancelled/failed |
| Failed | Failed | Already failed |
- Add new states to RemittanceStatus enum
- Update create_remittance() to use INITIATED
- Add submit_remittance() function for INITIATED → SUBMITTED
- Add request_anchor_confirmation() for SUBMITTED → PENDING_ANCHOR
- Update confirm_payout() to transition PENDING_ANCHOR → COMPLETED
- Update cancel_remittance() to transition to FAILED
- Update all status checks to use new states
- Update tests to use new state machine
Comprehensive test coverage includes:
-
Valid Transitions (6 tests)
- All valid state transitions
-
Idempotent Transitions (5 tests)
- Same state → same state for all states
-
Invalid Transitions (10 tests)
- Invalid transitions from each state
-
Terminal State Protection (8 tests)
- COMPLETED cannot transition to any state
- FAILED cannot transition to any state
-
Terminal Status Checks (5 tests)
- Verify terminal status detection
-
Valid Next States (5 tests)
- Verify correct next states for each state
-
Atomic Transitions (3 tests)
- Valid atomic update
- Invalid atomic update (status unchanged)
- Idempotent atomic update
Total: 42 unit tests
Should cover:
- Complete success flow (INITIATED → SUBMITTED → PENDING_ANCHOR → COMPLETED)
- Early failure (INITIATED → FAILED)
- Mid-process failure (SUBMITTED → FAILED)
- Late failure (PENDING_ANCHOR → FAILED)
- Concurrent update attempts
- Retry scenarios (idempotency)
Returned when:
- Attempting invalid transition (e.g., INITIATED → COMPLETED)
- Attempting to transition from terminal state
- Attempting out-of-order transition
Error Code: 8
Message: "Invalid state transition attempted"
-
Always validate before transition
validate_transition(¤t_status, &new_status)?;
-
Use atomic transition function
transition_status(&env, &mut remittance, new_status)?;
-
Check terminal status before operations
if remittance.status.is_terminal() { return Err(ContractError::InvalidStatus); }
-
Handle idempotent retries
// This is safe - won't error if already in target state transition_status(&env, &mut remittance, RemittanceStatus::Submitted)?;
- No additional storage overhead
- Status stored as enum variant (minimal space)
- No additional lookups required
- Validation is O(1) - simple match statement
- No iteration or complex logic
- Negligible gas cost increase
| Aspect | Before | After |
|---|---|---|
| States | 3 (Pending, Completed, Cancelled) | 5 (Initiated, Submitted, PendingAnchor, Completed, Failed) |
| Validation | Implicit | Explicit with validation |
| Terminal Protection | Partial | Complete |
| Idempotency | No | Yes |
| Error Handling | Generic | Specific (InvalidStateTransition) |
- COMPLETED and FAILED states cannot be modified
- Prevents unauthorized status changes
- Ensures finality of transactions
- All transitions validated before execution
- No implicit state changes
- Clear audit trail
- All-or-nothing status updates
- No partial state corruption
- Storage integrity maintained
- Same input always produces same result
- No race conditions
- Predictable outcomes
- All errors returned explicitly
- No unexpected contract halts
- Graceful error handling
- ✅ Structured state machine with 5 defined states
- ✅ Strict, deterministic state transitions
- ✅ Explicit validation before all transitions
- ✅ Terminal state protection (COMPLETED, FAILED)
- ✅ Atomic status updates
- ✅ Idempotent transitions
- ✅ No panics or unwraps
- ✅ Comprehensive unit tests (42 tests)
- ✅ Explicit error codes (InvalidStateTransition)
- ✅ Storage integrity maintained
- ✅ No breaking changes to storage structure
- ✅ Passes CID integrity checks
- ✅ No race conditions
Potential improvements:
- State History - Track all state transitions with timestamps
- Transition Events - Emit events on state changes
- Conditional Transitions - Add conditions for transitions (e.g., time-based)
- State Metadata - Attach metadata to each state (reason, timestamp, actor)
- Rollback Support - Allow admin to rollback non-terminal states in emergencies
- Error Handling - Error codes and handling
- API Reference - Complete API documentation
- Types - Type definitions
- Transitions - Transition logic