Files
psf-memo-indexer/dev-docs/architecture.md
T

178 lines
6.3 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Architecture
## System context
```mermaid
flowchart TB
subgraph node [BCH_full_node]
RPC[JSON-RPC]
ZMQ[ZMQ_rawtx_rawblock]
end
subgraph indexer [psf_memo_indexer]
BI[Block_indexer_process]
TI[TX_indexer_process]
BI -->|GET_tx-start| TI
end
subgraph db [psf_memo_db]
Koa[Koa_REST]
LDB[(LevelDB_instances)]
Koa --> LDB
end
RPC --> BI
RPC --> TI
ZMQ --> BI
ZMQ --> TI
BI -->|axios_level_CRUD| Koa
TI -->|axios_level_CRUD| Koa
```
## Two-process indexer design
The indexer splits work the same way as [psf-slp-indexer-g2](https://github.com/Permissionless-Software-Foundation/psf-slp-indexer-g2):
| Process | Responsibility | Why separate |
|---------|----------------|--------------|
| **Block indexer** | Historical catch-up (IBD) and confirmed blocks via ZMQ | Heavy, sequential block work; must finish before mempool indexing is trustworthy for “at tip” |
| **TX indexer** | Unconfirmed transactions via ZMQ | High volume, duplicate ZMQ events; isolated so IBD memory/CPU does not contend with mempool flood |
Coordination is deliberately minimal: after IBD, the block indexer issues `GET http://{TX_REST_API_IP}:{TX_REST_API_PORT}/tx-start`. The TX indexer polls that flag every two seconds until set, then subscribes to ZMQ.
**Tradeoff:** No shared memory or message queue between processes—only HTTP and the database. Simplicity and operational parity with SLP outweigh lower latency startup.
## Clean Architecture (indexer)
Both repos follow [Clean Architecture](https://christroutner.github.io/trouts-blog/blog/clean-architecture): dependencies point inward; framework and I/O live at the edges.
### psf-memo-indexer layers
```text
Entry points (framework)
psf-memo-block-indexer.js
psf-memo-tx-indexer.js
Controllers
keyboard.js — graceful stop (q key)
tx-rest-api.js — Express /tx-start
Use cases
index-blocks.js — processBlock, processMemoTx (parallel per block)
filter-block.js — filterMemoTxs (parallel pre-check, block order preserved)
state.js — synced block height
action-types/*.js — per Memo action handlers
utils.js
Adapters
rpc.js — full node JSON-RPC
zmq.js — @psf/bitcoincash-zmq-decoder
transaction.js — fetch tx + memo detection cache
status-db.js, *-db — axios → psf-memo-db
tx-indexer.js — start signal to TX process
backup-db.js — POST /level/backup
Libraries
memo-parser.js — OP_RETURN pushdata, signer address
memo-codes.js — action prefixes and limits
```
**Rule:** Use cases contain business rules (valid pushdata counts, reply parent links, like tips). Adapters only move bytes on the network or disk API.
### psf-memo-db layers
```text
index.js → bin/server.js (Koa)
Controllers
rest-api/index.js
rest-api/level/* — CRUD handlers
rest-api/health/
Use cases
index.js — minimal stub (parity with psf-slp-db)
Adapters
level-db.js — open/close LevelDB instances
db-backup.js — zip / restore
```
Level CRUD bypasses use cases intentionally—same as `psf-slp-db`s `/level` controller calling `adapters.level.*Db.put` directly. The DB service is a thin persistence plane, not a domain model server.
## Repository layout (indexer)
| Path | Purpose |
|------|---------|
| `config/index.js` | Environment-driven settings |
| `src/lib/` | Pure protocol parsing (no I/O) |
| `src/adapters/` | External systems |
| `src/use-cases/` | Indexing orchestration and handlers |
| `src/controllers/` | HTTP and keyboard |
| `production/docker/` | Docker Compose, per-service Dockerfile and `.env` |
| `test/unit/` | Mocha + c8 |
## Repository layout (database)
| Path | Purpose |
|------|---------|
| `bin/server.js` | Koa bootstrap (mirrors psf-slp-db) |
| `config/env/` | `SVC_ENV` profiles |
| `src/adapters/level-db.js` | Twelve LevelDB stores under `leveldb/current/` |
| `src/controllers/rest-api/level/` | Generic CRUD + status + backup routes |
| `leveldb/current/{name}/` | Runtime data (gitignored) |
| `leveldb/zips/` | Epoch backups |
## LevelDB schema (summary)
Each store is a separate LevelDB with JSON values. Keys are txids, addresses, or composite strings depending on entity. Full detail: [psf-memo-db.md](./psf-memo-db.md).
| Store | Typical key | Value role |
|-------|-------------|------------|
| `status` | `status` | `syncedBlockHeight`, `chainBlockHeight`, `startBlockHeight` |
| `posts` | txid | Author address, text, timestamp |
| `postParents` | child txid | Parent txid (reply) |
| `postChildren` | parent txid | Reverse index for replies |
| `likes` | like txid | Post txid, liker, optional tip |
| `names` | address | Display name + provenance txid |
| `profiles` | address | Profile text |
| `profilePics` | address | Avatar URL |
| `follows` | `follower:followeePkHash` | Follow/unfollow event |
| `rooms` | composite | Topic posts and topic follows |
| `processErrors` | txid | Validation / parse failures |
| `ptxs` | txid | Idempotency marker (already processed) |
## External dependencies
| Package | Used by | Role |
|---------|---------|------|
| `@psf/bch-js` | Indexer | Script decompile for signer address; optional REST |
| `@psf/bitcoincash-zmq-decoder` | Indexer | Decode `rawtx` / `rawblock` ZMQ messages |
| `zeromq` | Indexer | Subscribe to full node |
| `@chris.troutner/retry-queue` | Indexer | Retry RPC on transient failure |
| `axios` | Indexer | psf-memo-db REST client |
| `p-queue` / `p-retry` | Indexer | Parallel block filter and parallel per-tx processing within a block |
| `express` | Indexer | TX indexer control API |
| `level` | psf-memo-db | Embedded JSON LevelDB |
| `koa` + `koa-router` | psf-memo-db | REST server |
**Not used:** `slp-parser`, Lokad ID checks, DAG sorting, token UTXO graphs.
## Deployment topology
```text
production/docker/
├── docker-compose.yml
├── memo-db/ → build context: ../../psf-memo-db
├── block-indexer/ → build context: indexer repo root
└── tx-indexer/
```
Volumes mount `.env` and start scripts per service, matching the SLP production pattern. LevelDB data can persist under `production/data/leveldb`.