Skip to content

Architecture overview

This page names the actors LibSpiffyActorSystem.initialize() starts and shows how a command travels through them, so you know what you are talking to when you ask() the coordinator.

libspiffy is built on five packages:

Package Role in libspiffy
dactor The actor runtime. Every libspiffy component is an actor with its own mailbox, spawned in a LocalActorSystem.
eventador Event sourcing: the aggregates (AggregateRoot), the event store (Isar or PostgreSQL), the EventRegistry, projections and ProjectionActor.
duraq with duraq_isar A durable queue with retry and backoff. ARCActor keeps broadcasts that failed in it so they are retried, including after a restart.
spiffynode The BSV P2P client: peer connections and header download.
dartsv Keys, addresses, scripts, transaction building and signing.

initialize() spawns these actors, in this order. The names in brackets are the actor names in the actor system.

Actor What it does
ProjectionActor × 3 (projection-wallet-projection, projection-invoice-projection, projection-channel-projection) Eventador actors that run WalletProjection, InvoiceProjection and ChannelProjection: they follow the event store and update the read models.
WalletManagerActor (wallet-manager) Routes wallet commands to one BitcoinWalletAggregate per wallet, spawning each on demand, and preloads existing wallets at startup.
InvoiceCoordinatorActor (invoice-coordinator) Creates invoices (asking the wallet for addresses), spawns one InvoiceAggregate per invoice, answers invoice queries from the read model.
SPVActor (spv-actor) Validates incoming BEEF: merkle proofs against the header chain, outputs against the invoice, the fee. Credits the wallet through its aggregate.
HeaderSyncActor (header-sync) Keeps the BlockHeaderChain in step with peers, stores header batches, reports reorganisations to SPVActor.
ARCActor (arc-actor) Broadcasts to ARC or Arcade, fetches the policy fee rate, follows transaction status, and retries failed broadcasts from a DuraQ queue. It spawns a SubmissionWatcherActor child that re-queries submissions ARC answered as still in flight.
PaymentCoordinatorActor (payment-coordinator) Builds payments: selects and reserves UTXOs, builds and signs the transaction (or hands it to a plugin), collects its ancestry into BEEF.
BenfordCoordinatorActor (benford-coordinator) Splits UTXOs into amounts drawn from a Benford distribution, for privacy.
PaymentChannelManagerActor (payment-channel-manager) Runs payment channels: one PaymentChannelAggregate per channel, funding, payments, settlement and refunds.
ImportActor (import-actor) Imports a wallet’s history from a BlockchainDataSource. Spawned only when you pass blockchainDataSource to initialize().
WalletCoordinatorActor (wallet-coordinator) The coordinator: the public entry point. It turns each command into messages for the actors above, tracks the multi-step flows, waits for read models where a reply promises them, answers each request with one reply carrying its requestId, and publishes every CoordinatorEvent on coordinatorEvents.

Your app does not hold WalletCoordinatorActor itself. libspiffy.coordinator is a WalletCoordinator that wraps it: ask() sends a request and completes with the reply that carries the request’s requestId, tell() sends without waiting, and on<E>() filters the event stream.

When P2P is on, libspiffy also creates a spiffynode PeerManager and a SpiffyNodeBridge that feeds headers from peers to HeaderSyncActor. These are objects, not actors.

WalletCoordinatorActor uses two helpers that are not actors: ChannelP2PAdapter, which turns channel protocol messages into ChannelP2PMessageToSendEvents for your transport and back, and ProofP2PAdapter, which asks the counterparty who paid you for a fresh merkle proof when a reorganisation leaves a received payment’s ancestry unproven (outbound messages are P2PMessageToSendEvents). libspiffy owns no transport: your app delivers these messages.

your app
│ ask(CreateInvoiceCommand) ▲ the reply (InvoiceCreatedEvent),
▼ │ also on coordinatorEvents
┌──────────────────────────────────────────────────┴───┐
│ WalletCoordinatorActor │
└──┬──────────────┬───────────────┬──────────────┬─────┘
│ │ │ │
▼ ▼ ▼ ▼
WalletManager InvoiceCoord. PaymentCoord. SPVActor ◀── HeaderSyncActor ◀── peers
│ │ │ │ (spiffynode)
▼ ▼ ▼ ▼
BitcoinWallet Invoice (asks WalletManager, ARCActor ──▶ ARC / Arcade
Aggregate Aggregate ARCActor)
│ │
└──────┬───────┘ (PaymentChannelManagerActor → PaymentChannelAggregate)
▼
Event store (Eventador: Isar or PostgreSQL)
│
▼
ProjectionActors → WalletProjection / InvoiceProjection / ChannelProjection
│
▼
Read models (ReadModelStorage) ◀── queries from the coordinator and actors

Take CreateInvoiceCommand:

  1. The coordinator sends InvoiceCoordinatorActor a CreateInvoiceMessage.
  2. The invoice coordinator asks WalletManagerActor for fresh addresses, which the wallet’s BitcoinWalletAggregate derives and journals.
  3. It spawns an InvoiceAggregate, which validates the command and journals an InvoiceCreatedEvent (invoice.created) to the event store.
  4. The invoice projection applies the event to the invoice read model.
  5. The invoice coordinator replies once the read model holds the invoice. The coordinator emits InvoiceCreatedEvent with the command’s requestId: ask() completes with it, and it is published on coordinatorEvents too.

Reads never touch the event store. GetBalanceQuery is answered by the coordinator straight from the read model. CQRS & event sourcing covers the write and read sides in detail.

The getters walletManager, invoiceCoordinator, paymentCoordinator, spvActor, arcActor, headerSyncActor and channelManager on LibSpiffyActorSystem return the internal actors’ ActorRefs. Prefer the coordinator. It keeps the correlation state for multi-step flows (BEEF validation, then SPV validation, then broadcast) and matches each reply to its request; a message sent straight to an internal actor bypasses it, and the coordinator publishes no reply for that work.