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.
The stack
Section titled “The stack”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. |
The actors
Section titled “The actors”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.
How a command flows
Section titled “How a command flows” your app │ ask(CreateInvoiceCommand) ▲ the reply (InvoiceCreatedEvent), ▼ │ also on coordinatorEvents┌──────────────────────────────────────────────────┴───┐│ WalletCoordinatorActor │└──┬──────────────┬───────────────┬──────────────┬─────┘ │ │ │ │ ▼ ▼ ▼ ▼WalletManager InvoiceCoord. PaymentCoord. SPVActor ◀── HeaderSyncActor ◀── peers │ │ │ │ (spiffynode) ▼ ▼ ▼ ▼BitcoinWallet Invoice (asks WalletManager, ARCActor ──▶ ARC / ArcadeAggregate Aggregate ARCActor) │ │ └──────┬───────┘ (PaymentChannelManagerActor → PaymentChannelAggregate) ▼ Event store (Eventador: Isar or PostgreSQL) │ ▼ ProjectionActors → WalletProjection / InvoiceProjection / ChannelProjection │ ▼ Read models (ReadModelStorage) ◀── queries from the coordinator and actorsTake CreateInvoiceCommand:
- The coordinator sends
InvoiceCoordinatorActoraCreateInvoiceMessage. - The invoice coordinator asks
WalletManagerActorfor fresh addresses, which the wallet’sBitcoinWalletAggregatederives and journals. - It spawns an
InvoiceAggregate, which validates the command and journals anInvoiceCreatedEvent(invoice.created) to the event store. - The invoice projection applies the event to the invoice read model.
- The invoice coordinator replies once the read model holds the invoice. The coordinator emits
InvoiceCreatedEventwith the command’srequestId:ask()completes with it, and it is published oncoordinatorEventstoo.
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.
Use the coordinator
Section titled “Use the coordinator”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.