mirror of
https://github.com/Permissionless-Software-Foundation/psf-memo-indexer.git
synced 2026-09-21 16:52:02 -07:00
178 lines
6.2 KiB
Markdown
178 lines
6.2 KiB
Markdown
# 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
|
|||
|
|
filter-block.js — filterMemoTxs (parallel pre-check)
|
|||
|
|
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 with retries |
|
|||
|
|
| `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`.
|