Skip to content

Latest commit

Β 

History

History
671 lines (495 loc) Β· 26.9 KB

File metadata and controls

671 lines (495 loc) Β· 26.9 KB

Taliro Architecture πŸ—οΈ

This document provides an in-depth overview of Taliro's architecture, design patterns and operational flows.


Table of Contents


Overview πŸ—ΊοΈ

Taliro is a UTXO-based blockchain implementation demonstrating clean architecture principles in a distributed systems context.
The system is composed of multiple cooperating subsystems communicating through a centralized command pattern, ensuring sequential consistency and avoiding race conditions.

High-Level Architecture

flowchart TD
    HTTP["<ins>**HTTP Dev API**</ins><br />(Axum)"]
    P2P["<ins>**P2P Network**</ins><br />(libp2p)<br /><br />- Gossipsub<br />- Kademlia<br />- Taliro (Request/Response)"]
    UC[Application Use Cases]
    NODE_CMD[<ins>**Node**</ins><br />Command Orchestration<br />Event Loop]
    BC[Blockchain]
    MP[Mempool]
    UTXO[UTXO Set]
    BLOCK_VAL[Block Validator]
    TX_VAL[Transaction Validator]
    SYNC_Q[Block Sync Queue]
    PROC_Q[Block Processing Queue]
    OUTBOX[Outbox]
    SLED["<ins>**Storage**</ins><br />(Sled)<br />"]
    
    HTTP --> UC
    P2P --> NODE_CMD
    UC --> NODE_CMD
    NODE_CMD --> BC
    NODE_CMD --> MP
    NODE_CMD --> UTXO
    NODE_CMD --> P2P
    NODE_CMD --> SYNC_Q
    SYNC_Q --> PROC_Q
    BC --> BLOCK_VAL
    BC --> OUTBOX
    BLOCK_VAL --> TX_VAL
    MP --> TX_VAL
    OUTBOX --> NODE_CMD
    BC --> SLED
    UTXO --> SLED
    NODE_CMD --> SLED
    OUTBOX --> SLED
    
    %% Styling External Boundaries
    style HTTP fill:#ffcc00,stroke:#000000,stroke-width:2px,color:#000000
    style P2P fill:#ffcc00,stroke:#000000,stroke-width:2px,color:#000000
    
    %% Styling Node Orchestrator
    style NODE_CMD fill:#87ceeb,stroke:#000000,stroke-width:3px,color:#000000
    
    %% Styling Core Components
    style BC fill:#b19cd9,stroke:#000000,stroke-width:2px,color:#000000
    style MP fill:#b19cd9,stroke:#000000,stroke-width:2px,color:#000000
    style UTXO fill:#b19cd9,stroke:#000000,stroke-width:2px,color:#000000
    
    %% Styling Internal Storage
    style SLED fill:#90ee90,stroke:#000000,stroke-width:2px,color:#000000
Loading

Architectural Principles πŸ“

🫧 Clean Architecture & Domain-Driven Design

The codebase is organized into distinct layers with strict dependency rules, ensuring maintainability, testability and separation of concerns.

  • Domain Layer: Pure business logic with no external dependencies
  • Application Layer: Use cases and workflow orchestration
  • Infrastructure Layer: Concrete implementations (storage, networking)
  • Presentation Layer: External interfaces (HTTP API)
πŸ”Ž Tell me more...

🟣 Domain Layer (domain)

The core business logic layer containing:

  • Entities: Core business objects with identity
  • Value Objects: Immutable types representing domain concepts
  • Repository Traits: Abstract contracts for domain-level data persistence
  • Domain Validation: Business rule enforcement at the entity level
  • System Abstractions: Blockchain, UTXO, Network etc

πŸ”΅ Application Layer (application)

Orchestrates blockchain workflows without implementation details:

  • Use Cases: Application-specific business logic
  • Application Services: Cross-cutting concerns (authentication, authorization)
  • Queue Management: Orchestrators for async tasks
  • Outbox Relay: Reliable event publishing for atomic operations
  • Repository Traits: Abstract contracts for app-level data persistence (none yet)
  • Application DTOs: Data transfer objects for interlayer communication

🟒 Infrastructure Layer (infrastructure)

Concrete implementations of abstract contracts:

  • Repository Implementations: Sled-based blockchain data persistence
  • Network Protocol: libp2p-based P2P networking
  • Unit of Work: Atomic transactions
  • External Service Adapters: JWT handling, password hashing
  • Infrastructure DTOs: Storage-specific data models

🟑 Presentation Layer (presentation)

HTTP API and external interfaces:

  • HTTP Handlers: REST endpoints for blockchain queries
  • DTOs: API request/response models
  • Authentication Extractors: JWT token validation
  • OpenAPI Documentation: Auto-generated via utoipa

πŸ“¦ Supporting Crates

Common (common)

Shared utilities across all layers:

  • Logging: Structured logging macros
  • Error Types: Standardized blockchain error handling
  • Configuration: Configuration data types
  • Transaction Abstractions: Infrastructure-agnostic transaction management (allows for CA-compliant use cases)
  • Cross-cutting Utilities: Shared types and helper functions
Main (main)

Application entry point and dependency injection:

  • Blockchain Node Startup: P2P blockchain node bootstrapping
  • HTTP Server Startup: HTTP server initialization and middleware setup
  • Environment Setup: Configuration loading and validation
  • Dependency Wiring: Service registration and dependency injection
Macros (macros)

Custom procedural macros for code generation:

  • Logging Macros: Configurable logging macro generator

Clean Architecture Dependency Flow

The layers follow strict dependency rules to maintain clean architecture:

  • Domain depends on nothing (pure blockchain logic)
  • Application depends solely on Domain
  • Infrastructure depends on Domain and Application
  • Presentation depends on Application and Domain
  • Common is dependency-free and accessible by all layers
  • Macros is exclusively used by Common
  • Main depends on all layers to wire everything together

🧬 Dependency Inversion

All layers depend on abstraction traits for cross-layer functionality.
Concrete implementations are injected at the application entry point (main).

Internal layer dependencies are also typically abstracted to facilitate testing and modularity.
Their implementations are defined and injected at local crate level.

🍳 Single Responsibility

Each subsystem has a clearly defined purpose:

  • Node: Central command loop orchestrating most operations
  • Blockchain: Block storage and chain state management
  • Mempool: Pending transaction pool
  • UTXO Set: Unspent transaction output tracking
  • Network: P2P communication and peer management
  • Validation: Block and transaction validation logic

🎒 Sequential Consistency

All state-mutating operations flow through a single command queue, processed sequentially by the node's event loop.
This eliminates race conditions and simplifies reasoning about system state.

πŸ›‚ Type-Enforced Validation

Domain entities are represented by two type-safe variants based on their validation state:

  • Pre-Validation Types (e.g.NonValidatedBlock): Untrusted data from external sources
  • Post-Validation Types (e.g. Block): Cryptographically and structurally verified entities

Trust Boundaries:

flowchart TD
    HTTP["**HTTP Dev API**<br />(Axum)"]
    P2P["**Taliro P2P Network**<br />(libp2p)"]
    STORAGE["**Internal Storage**<br />(Sled)"]
    
    V_P2P["Validated Type<br />(from wire)"]
    NV_COMMON[NonValidated Type]
    VAL[Validator]
    V_COMMON[Validated Type]
    LOGIC[Usage in<br />Domain]

    HTTP --> NV_COMMON
    P2P --> V_P2P
    V_P2P --> NV_COMMON
    NV_COMMON --> VAL
    VAL --> V_COMMON
    STORAGE --> V_COMMON
    V_COMMON --> LOGIC
    
    %% Styling Entry Points
    style HTTP fill:#ffcc00,stroke:#000000,stroke-width:2px,color:#000000
    style P2P fill:#ffcc00,stroke:#000000,stroke-width:2px,color:#000000
    style STORAGE fill:#ffcc00,stroke:#000000,stroke-width:2px,color:#000000

    %% Styling LOGIC node (Usage in Domain)
    style LOGIC fill:#a0c8f0,stroke:#000000,stroke-width:2px,color:#000000
Loading
πŸ”Ž Tell me more...

Key Principles:

  • External Data: Always treated as non-validated, must pass validation before usage
  • Storage Data: Only persisted after validation, retrieved as validated types (trusted internal source)
  • Zero Trust: Even data from trusted peers undergoes full validation before acceptance

Design Benefits:

  • Type system enforces validation requirements at compile time
  • Impossible to accidentally use unvalidated data in critical operations
  • Clear audit trail of validation boundaries
  • Defense against malicious or buggy peers

🌱 Node Lifecycle

A Taliro node progresses through several states during its lifecycle:

stateDiagram-v2
    [*] --> Initialized
    Initialized --> Bootstrapped
    Bootstrapped --> Started
    Started --> Running
    Running --> Terminating
    Terminating --> [*]
    
    note right of Initialized
        Core subsystems created
        Repositories wired
    end note
    
    note right of Bootstrapped
        Network connected
        P2P engine online
    end note
    
    note right of Started
        Command handlers configured
        Ready to process
    end note
    
    note right of Running
        Main event loop active
        Processing commands
    end note
    
    note left of Terminating
        Graceful shutdown
        In progress
    end note
Loading

Command Pattern & Event Loop πŸ”„

πŸ—œοΈ Architecture

Taliro uses a command pattern with a centralized event loop for all node operations.
This provides:

  • Sequential consistency (no race conditions)
  • Clear audit trail (all operations logged)
  • Simplified debugging (single point of execution)
  • Backpressure handling (bounded channel)

Commands are categorized by subsystem.

🧭 Command Flow

flowchart TD
    HTTP["**HTTP API**"]
    P2P["**P2P Network**"]
    FACTORY["<code>CommandResponderFactory::build_*_cmd()</code><br /><code>(NodeCommandRequest, Future&lt;Response&gt;)</code>"]
    SENDER["<code>CommandSender::send(command)</code><br />(MPSC channel)"]
    LOOP["<code>NodeRunning</code> Event Loop<br />(Single-threaded sequential processing)"]
    DISPATCHER["<code>CommandDispatcher</code> routes to appropriate handler based on command type"]
    HANDLER["Handler Executes & Responds"]
    RESPONSE["Source awaits <code>Future</code>, processes response"]
    
    HTTP --> FACTORY
    P2P --> FACTORY
    FACTORY --> SENDER
    SENDER --> LOOP
    LOOP --> DISPATCHER
    DISPATCHER --> HANDLER
    HANDLER --> RESPONSE
    
    %% Styling Entry Points
    style HTTP fill:#ffcc00,stroke:#000000,stroke-width:2px,color:#000000
    style P2P fill:#ffcc00,stroke:#000000,stroke-width:2px,color:#000000
    
    %% Styling Exit Point
    style RESPONSE fill:#a0c8f0,stroke:#000000,stroke-width:2px,color:#000000
Loading

πŸ“Ÿ Request-Response Mechanism

Each command uses a oneshot channel for responses, providing:

  • Type-safe responses
  • Timeout capability
  • Zero-copy response delivery
  • Clear ownership semantics

Event Sources & Data Flows 🌊

Taliro processes events from two primary sources: the HTTP Dev API and the P2P Network.

Both flows converge at the Node Command Queue, ensuring:

  • Sequential Processing: No race conditions between HTTP and P2P events
  • Uniform Handling: Same validation and state update logic regardless of source
  • Decoupling: HTTP and P2P layers don't directly interact with subsystems
  • Backpressure: Bounded channel prevents overwhelming the node

πŸ‘¨πŸ»β€πŸ’» HTTP-Initiated Flow (Developer API)

flowchart TD
    REQ["**HTTP Request**"]
    HANDLER["**HTTP Handler**<br />(Presentation Layer)<br /><br />- Authenticate (optional master key)<br />- Parse HTTP payload into Presentation Request<br />- Construct Application Request DTO (Domain types)"]
    UC["**Use Case**<br />(Application Layer)<br /><br />- Perform orchestration logic<br />- Create Command(s) via Factory<br />- Dispatch Command(s) through MPSC Channel"]
    CMD["**CommandDispatcher** β†’ **Handler**<br />(Domain)<br /><br />- Validate<br />- Execute operation<br />- Perform state mutations<br />- Respond via Oneshot Channel"]
    UC2["**Use Case**<br />(Application Layer)<br /><br />- Await response future(s)<br />- Construct Application Response DTO"]
    HANDLER2["**HTTP Handler**<br />(Presentation Layer)<br /><br />- Map Application Response DTO to Presentation Response DTO<br />- Serialize to JSON (if applicable)"]
    RESP["**HTTP Response**"]
    
    REQ --> HANDLER
    HANDLER --> UC
    UC --> CMD
    CMD --> UC2
    UC2 --> HANDLER2
    HANDLER2 --> RESP
    
    %% Styling Entry Point
    style REQ fill:#ffcc00,stroke:#000000,stroke-width:2px,color:#000000
    
    %% Styling Exit Point
    style RESP fill:#a0c8f0,stroke:#000000,stroke-width:2px,color:#000000
Loading

Layer Transitions:

  • Presentation β†’ Application: HTTP handler extracts request data, maps to Application DTO (domain types), delegates to use case
  • Application β†’ Domain: Use case orchestrates operation, creates command(s) via factory, sends to node command queue
  • Domain Processing: Command dispatcher routes to appropriate handler, executes domain logic
  • Domain β†’ Application: Result returned via oneshot channel, use case awaits and processes response(s)
  • Application Processing: Use case performs additional orchestration actions
  • Application β†’ Presentation: Application layer returns Application DTO to HTTP handler
  • Presentation Processing: HTTP handler maps Application DTO to Presentation DTO, serializes to JSON

Examples:

  • User submits transaction via HTTP β†’ PlaceMempoolTransaction use case β†’ check existing UTXOs via UtxoCommand::GetUtxosByOutpoints, build new transaction, store in mempool via MempoolCommand::PlaceTransaction command β†’ return transaction
  • User requests blockchain tip β†’ GetBlockchainTipInfo use case β†’ BlockchainCommand::GetTipInfo command β†’ return tip info
  • User queries UTXO set β†’ GetUtxos use case β†’ UtxoCommand::GetUtxos command β†’ return UTXO data

🌌 P2P-Initiated Flow (Network Events)

flowchart TD
    EVENT["**P2P Network Event**"]
    LOOP["**Network Event Loop**<br />(Infrastructure)<br /><br />- libp2p swarm processes event<br />- Decode protocol message"]
    FACTORY["**Create Command via Factory**<br /><br />(e.g., HandleReceiveBlocks, ProxyNetworkEvent)"]
    QUEUE["**Send to Node Command Queue**"]
    CMD["**CommandDispatcher** β†’ **Handler**<br /><br />- Process blocks<br />- Update sync queues<br />- Trigger validation"]
    EFFECTS["**Side Effects**<br /><br />- Block processing queue updated<br />- Blockchain state updated<br />- Network broadcasts (if applicable)"]
    
    EVENT --> LOOP
    LOOP --> FACTORY
    FACTORY --> QUEUE
    QUEUE --> CMD
    CMD --> EFFECTS
    
    %% Styling Entry Point
    style EVENT fill:#ffcc00,stroke:#000000,stroke-width:2px,color:#000000
Loading

Examples:

  • Peer announces new block (Gossipsub) β†’ decode, validate, process via BlockProcessingQueue
  • Peer announces higher tip β†’ HandleReceiveBlockchainTipInfo β†’ request missing blocks
  • Peer responds to block request β†’ HandleReceiveBlocks β†’ queue for processing
  • New peer connects β†’ exchange tips, initiate sync if behind

Persistence Layer πŸ’Ύ

Taliro uses Sled, an embedded ACID-compliant key-value database.

πŸ—ƒοΈ Repository Pattern

All storage operations flow through repositories.
Domain-level entity repo traits are defined in domain.
Application-specific repo traits (if any) would be defined in application.
Repository implementations reside in infrastructure.

πŸ₯’ Unit of Work Pattern

Taliro defines an abstraction layer over Sled transactions to align with clean architecture principles.
Complex operations rely on units of work for transaction management, ensuring atomicity across repositories within a single database transaction.


Subsystem Deep Dives 🀿

πŸ€ΉπŸ»β€β™€οΈ Node (Orchestrator)

Responsibilities:

  • Central command loop coordinating all subsystem operations
  • Sequential command processing from HTTP and P2P sources
  • Command routing via dispatcher to appropriate handlers

Pattern: Single-threaded event loop with MPSC command queue

State Machine: Explicit state transitions (Initialized β†’ Bootstrapped β†’ Started β†’ Running β†’ Terminating)

⛓️ Blockchain

Responsibilities:

  • Block insertion with continuity validation
  • Tip management (active chain head)
  • Block retrieval by hash or height

State Management:

  • Persistent Storage: Blocks stored via BlockchainRepository
  • Height Index: Separate tree mapping height to block hash
  • Tip Cache: In-memory cache (protected by Mutex) for fast tip queries
  • Chain Continuity: Enforces previous hash validation before insertion

Outbox Pattern: Block insertions write an outbox entry atomically with the block, ensuring downstream effects (UTXO updates, mempool updates, tip changes) are eventually processed even after crashes.

πŸ’° UTXO Set

Responsibilities:

  • Track unspent transaction outputs
  • Validate transaction inputs against UTXO set
  • Apply block effects (consume inputs, create outputs)

CQRS Architecture:

  • UtxoSetReader: Read-only queries for transaction validation (Query side)
  • UtxoSetWriter: Write operations during block application (Command side)

Storage: Persistent via UtxoRepository, keyed by transaction outpoint (hash + output index)

Block Application: Atomic transaction deletes consumed UTXOs (inputs) and inserts new UTXOs (outputs)

Design Benefits: Separating read and write concerns allows validation to query UTXO state without blocking on write operations

🏧 Mempool

Responsibilities:

  • Transaction queuing for block inclusion
  • Conflict detection (duplicate hash prevention)
  • Mempool cleanup after block append

Implementation: In-memory hash map of pending transactions (protected by RwLock)

Operations: Add, query, and remove transactions. Cleaned up automatically when blocks are appended.

Limitations: No prioritization, size limits, or advanced conflict detection (future enhancements)

🌐 Network

Responsibilities:

  • P2P peer discovery and connection management
  • Message broadcasting via Gossipsub
  • Request/response protocol for block sync
  • Peer lifecycle tracking

libp2p Stack:

  • Transport: TCP with noise encryption and yamux multiplexing
  • Gossipsub: Pub/sub for block broadcasting
  • Request/Response: Custom Taliro protocol for block requests
  • Kademlia: DHT for peer discovery

Event Loop: Runs in separate async task, bidirectional communication with node command loop via MPSC channels

Peer Store: Tracks connected peers and their multiaddresses for reconnection

🚧 Block Processing Queue

Responsibilities:

  • Ensures in-order block processing
  • Buffers out-of-order blocks until dependencies arrive
  • Prevents concurrent processing of the same block

Pattern: Async queue consumer in separate task

Flow: Receives blocks β†’ waits for next expected height β†’ validates β†’ applies to blockchain

πŸͺ‘ Block Sync Queue

Responsibilities:

  • Manages block-related network fetch operations
  • Prevents redundant block requests from peers
  • Coordinates synchronization after tip announcements

Pattern: Tracks in-progress and completed block requests

Flow: Tip announced β†’ calculate missing heights β†’ request blocks β†’ feed to processing queue

πŸ›ƒ Validation

Responsibilities:

  • Single source of truth for validation rules
  • Split structural and contextual validation (offline checks vs stateful checks)
  • Construct validated types from non-validated inputs

Strategy: Fail-fast with detailed error reporting


Network Architecture πŸ•ΈοΈ

Taliro nodes form a mesh network with no central authority.
Peer-to-peer implementation is based on libp2p.

Nodes can specify initial peers via configuration.
New peers are discovered via network events or manual addition (dev API).

Initial Peers β†’ Dial β†’ Connection β†’ Gossipsub Subscribe β†’ Tip Exchange β†’ Sync β†’ Steady State

Reconnection: Nodes store peer addresses and can reconnect after restarts (unless using ephemeral ports).


Concurrency Model πŸ«›

Taliro runs multiple concurrent async tasks:

  • HTTP Server Task: Handles API requests (Axum runtime)
  • Node Event Loop Task: Sequential command processing
  • Network Event Loop: libp2p swarm event handling
  • Block Processing Task: BlockProcessingQueue consumer
  • Outbox Relay Task: Polls for unprocessed outbox events

Note: Database transactions are held briefly, never spanning async boundaries.


Transaction Guarantees β˜”

πŸ”‹ ACID Properties

Atomicity: State changes across multiple aggregates are committed or rolled back as a single unit, ensuring consistency.

Consistency: All validation rules are enforced before state mutations, maintaining blockchain invariants at all times.

Isolation: Concurrent operations are serialized to prevent conflicts and ensure deterministic state transitions.

Durability: Committed operations are persisted to disk and survive system crashes.

🍱 Outbox Pattern

State changes often produce side effects that must be reliably executed.
However, when these actions span multiple system boundaries, they cannot always be cleanly or reliably handled within a single atomic transaction.
We still need to ensure these side effects are applied, even in the event of crashes.

To address this, Taliro employs the outbox pattern:
An OutboxEntry is atomically written alongside the primary state mutation.
A background task continuously polls for unprocessed outbox entries, executes the associated side effects and marks them as processed.
This guarantees at-least-once execution of side effects, though not exactly-once.
Side effect handlers must be idempotent to handle potential duplicates.

State Mutation β†’ Outbox Entry Insertion β†’ [Crash?] β†’ Restart β†’ Relay Processes Entry β†’ Completion

Error Handling Strategy πŸ’€

Taliro defines a rich hierarchy of error types to represent various failure modes across the system.
You may inspect the full list of error types under common/src/error/.

Errors flow from handlers through command responders to the originating source (HTTP or P2P).
Upon reaching the source, errors are transformed into suitable responses or logged for diagnostics.

Internal logs contain full error details, while public API responses sanitize sensitive information.