Are you an LLM? Read llms.txt for a summary of the docs, or llms-full.txt for the full context.
Skip to content

Concurrency Model

Evolve uses a hybrid sync/async model optimized for deterministic execution while maximizing performance where possible.

Overview

The STF is single-threaded and synchronous for determinism. Parallelism is used only where it doesn't affect execution order:

┌──────────────────────────────────────────────────────────────┐
│                    PARALLEL (Pre-consensus)                   │
│  Signature verification, cache warming, network I/O          │
└──────────────────────────────────────────────────────────────┘


┌──────────────────────────────────────────────────────────────┐
│                   SEQUENTIAL (Block execution)                │
│  Transaction processing, state transitions, event emission    │
└──────────────────────────────────────────────────────────────┘


┌──────────────────────────────────────────────────────────────┐
│                    PARALLEL (Post-commit)                     │
│  Indexing, notifications, cache updates                       │
└──────────────────────────────────────────────────────────────┘

Why Single-Threaded Execution?

RequirementWhy Sequential
DeterminismSame order = same result on all nodes
AtomicityCheckpoint/restore assumes single writer
SimplicityNo complex concurrency bugs
State consistencyNo race conditions on storage

Where Parallelism is Used

1. Signature Verification

Before block execution, signatures can be verified in parallel:

// Done before STF receives the block
let valid_sigs: Vec<bool> = transactions
    .par_iter()
    .map(|tx| verify_signature(tx))
    .collect();

2. Storage Cache

The storage layer uses sharded locking for concurrent reads:

// 256 shards for cache access
struct ShardedCache {
    shards: [RwLock<HashMap<Key, Value>>; 256],
}
 
impl ShardedCache {
    fn shard_for_key(&self, key: &Key) -> usize {
        hash(key) % 256
    }
}

3. Database I/O

Async I/O for disk operations that don't affect execution order:

// Background commit doesn't block next block
tokio::spawn(async move {
    storage.commit_to_disk(changes).await;
});

Send/Sync Analysis

TypeSendSyncReason
AccountCodeYesYesStored in shared registry
ExecutionStateNoNoContains &mut references
InvokerNoNoContains &mut references
CommonwareStorageYesYesNeeds cross-thread access

Best Practices

Do

  • Use Evolve collections (deterministic by design)
  • Pass data through the Environment trait
  • Use block time instead of system time
  • Derive randomness from chain state

Don't

  • Spawn threads in module code
  • Use async in module code
  • Access external state (files, network, env vars)
  • Use HashMap/HashSet for iteration

Performance Characteristics

OperationTime ComplexityNotes
Storage read (cached)O(1)Sharded lock, no contention
Storage read (miss)O(log n)RocksDB lookup
Storage writeO(1)Overlay only during execution
CheckpointO(1)Just captures indices
RestoreO(k)k = changes since checkpoint
CommitO(n)n = total overlay size