Files
psf-memo/psf-memo-indexer/dev-docs/design-decisions-and-tradeoffs.md
T

192 lines
9.2 KiB
Markdown
Raw Normal View History

2026-08-26 09:30:44 -07:00
# Design Decisions and Tradeoffs
This document records **why** the Memo indexer stack looks the way it does, including deliberate limitations and comparisons to adjacent systems.
## 1. Mirror the SLP indexer architecture
**Decision:** Two processes (block + TX), separate `psf-memo-db`, Clean Architecture folders, axios DB adapters, ZMQ + RPC, epoch zip backups, Express `/tx-start` gate.
**Reason:** PSF already operates `psf-slp-indexer-g2` + `psf-slp-db` in production. Reusing the pattern reduces training cost, Docker layout, and incident playbooks.
**Tradeoff:** Memo does not need SLPs DAG ordering or UTXO graph, so some SLP machinery is absent—but the **process and deployment** model stays parallel.
| Aspect | SLP stack | Memo stack |
|--------|-----------|------------|
| DB port | 5020 | 5021 |
| TX REST port | 5454 | 5455 |
| Parser | `slp-parser` + Lokad ID | `memo-parser` + `0x6d` prefix |
| DB entities | tokens, utxos, addrs | posts, likes, follows, … |
## 2. Separate database service (not embedded LevelDB)
**Decision:** Indexer never opens LevelDB files directly.
**Pros:**
- Aligns with psf-slp-db backup/restore workflow
- Avoids multi-process file lock issues
- Clear boundary for future read replicas or query services
**Cons:**
- HTTP latency on every write
- More moving parts in development (three processes)
**Alternative rejected:** Single Node process with embedded LevelDB—simpler locally but diverges from PSF production norms.
## 3. Scope limited to Go indexer “core social” handlers
**Decision:** v1 implements the handlers in [memo/index/node/obj/op_return/main.go](https://github.com/memocash/index/blob/master/node/obj/op_return/main.go) (`GetHandlers`), not the full [memo-protocol.md](../../memo-protocol.md) table.
**Included:** name, post, reply, like, profile, profile pic, follow/unfollow, topic message, topic follow/unfollow.
**Excluded (v1):**
- Polls, mute, send money
- MIP-0009 token sale actions (`0x6d30``0x6d32`)
- SLP handler present in Gos registry (`slp_tokenHandler`)
- Planned actions (`0x6d08`, `0x6d0b`, …)
**Reason:** Ship a useful social subgraph first; token and poll logic add validation and cross-tx dependencies.
**Risk:** Indexer state diverges from memo.sv for excluded actions until handlers are added.
## 4. Parallel processing within blocks (no DAG sort)
**Decision:** Filter and process Memo txs concurrently within a block using `p-queue`. Default concurrency is 20 for both phases (`FILTER_CONCURRENCY`, `MEMO_TX_CONCURRENCY`). Filter results are reordered to match block tx order before processing starts.
**SLP context:** SEND transactions may spend token outputs created earlier in the same block; DAG sort orders them correctly and writes must stay serial when UTXO state overlaps.
**Memo context:** Social actions do not consume each others UTXOs in a token graph. Most handler writes are keyed by `txid` and are independent across transactions. Parallel `processMemoTx` is safe for typical blocks.
**Soft ordering:** Likes that reference a post in the same block may compute `tip = 0` if the post handler has not finished yet. Replies still persist parent/child links and post bodies regardless of completion order. Profile/follow handlers keyed by address can race if the same signer emits multiple updates in one block (rare).
**Within a single tx:** Multiple Memo `OP_RETURN` outputs in one transaction are still dispatched sequentially inside `processMemoTx`.
**Tradeoff:** Higher throughput vs. HTTP contention on `psf-memo-db`. Lower `MEMO_TX_CONCURRENCY` if the DB service becomes the bottleneck.
**Alternative rejected:** Full serial processing—simple but leaves RPC/DB latency on the table during IBD when blocks contain many unrelated Memo txs.
## 5. Scan all transaction outputs for OP_RETURN
**Decision:** `findMemoOutputs` checks every `vout`, not only `vout[0]`.
**Reason:** Go `node/obj/saver/op_return.go` iterates all outputs; Memo has no protocol rule fixing OP_RETURN to the first output.
**Cost:** Slightly more script parsing per tx during filter and process.
## 6. Custom script pushdata parser (with selective bch-js)
**Decision:** `parseScriptPushDatas` implements Bitcoin push opcode walking in JavaScript; `@psf/bch-js` is used for signer address extraction from inputs.
**Reason:** Unit tests must run without a full node; decompile behavior must be predictable for standard `OP_RETURN` Memo txs.
**Tradeoff:** Non-standard scripts may parse differently than `btcd`—edge cases go to `processErrors`.
## 7. Signer = first decodable input address
**Decision:** Walk `vin` until `Script.getAddressFromScriptSig` succeeds.
**Matches:** Go indexer behavior when building `parse.OpReturn.Addr`.
**Limitation:** Multi-input txs with multiple signers attribute the action to the first resolved address only—consistent with reference indexer, not necessarily with user intent for complex txs.
## 8. Idempotency at transaction level only
**Decision:** `ptxs` store marks completed txs; handlers may write multiple LevelDB keys per tx without a cross-store transaction.
**Go indexer:** Uses richer `db.Save([]db.Object{...})` batches.
**Failure mode:** Crash mid-handler could leave partial writes; re-run skips entire tx if `ptx` was written— or duplicates sub-objects if ptx write failed last.
**Mitigation path:** Write ptx last (current code writes ptx after handlers—good) or add DB batch API.
## 9. Process errors are non-fatal
**Decision:** Invalid pushdata counts or oversize fields log to `processErrors` and return; they do not stop the block.
**Reason:** Chain history contains malformed or experimental Memo txs; one bad tx must not halt IBD.
**Ops note:** Monitor `processErrors` growth for parser bugs vs true invalid chain data.
## 10. Start block 525000
**Decision:** Default `START_BLOCK_HEIGHT=525000` from Go `BeginningOfMemoHeight`.
**Tradeoff:** Earlier Memo experiments exist on-chain but are excluded unless the operator lowers the height and reindexes from genesis of Memo data.
## 11. psf-memo-db: thin controller, no GraphQL
**Decision:** Level routes call LevelDB directly; no Mongoose, no GraphQL.
**Go memo/index** exposes GraphQL with attach resolvers loading from sharded DB—the read path is a separate product layer.
**PSF stack** stops at indexed storage + REST CRUD for writers. Read APIs are the integrators responsibility.
**Future:** A `memo-query.js` adapter (like `slp-query.js`) could encapsulate common reads without pulling in full GraphQL.
## 12. Mempool vs confirmed indexing
**Decision:** TX indexer uses `blockHeight = tip + 1` for unconfirmed txs; block indexer re-processes confirmed blocks.
**Nuance:** A mempool tx may be indexed twice (mempool + block) but `ptx` idempotency prevents duplicate handler effects.
**Edge case:** Tx never confirms—remains indexed with provisional height; may be acceptable for social UX or require GC policy later.
## 13. Docker layout under `production/docker`
**Decision:** Dockerfiles live in `psf-memo-indexer/production/docker/{memo-db,block-indexer,tx-indexer}/`, not repo roots—matching psf-slp-indexer-g2.
**Reason:** Single compose file orchestrates three services; build contexts point at `psf-memo-db` sibling repo and indexer root.
## 14. JavaScript vs extending the Go indexer
**Decision:** New JS implementation rather than operating memocash/index directly.
**Pros for PSF:**
- Same language and patterns as SLP indexer maintenance
- No CGO / Go toolchain requirement for PSF deployers
- Independent release cycle from memocash
**Cons:**
- Two implementations to keep in sync with protocol changes
- Go indexer has sharding, GraphQL, and mature handlers PSF omitted
**When to prefer Go:** Full memo.sv parity, production-scale sharded cluster, SLP-in-Memo token indexing.
**When to prefer PSF JS:** Unified ops with psf-slp-indexer-g2, custom downstream consumers of LevelDB REST.
## 15. Testing strategy
**Decision:** Unit tests with mocks for RPC, DB, and parser fixtures (Go `post_test.go` tx hex)—no mandatory e2e full node in CI.
**Tradeoff:** Integration bugs (RPC format changes, ZMQ topic names) need manual or staged e2e tests.
**Recommended e2e smoke:** Index block ≥ 525000 containing a known Memo post from [memo-protocol.md](../../memo-protocol.md) explorer links; verify `GET /level/post/:txid`.
## Decision log (quick reference)
| Topic | Choice |
|-------|--------|
| Architecture pattern | Clean Architecture |
| Chain interface | RPC + ZMQ |
| Storage | psf-memo-db REST + LevelDB |
| Processes | 2 indexers + 1 DB |
| Protocol breadth | Go `GetHandlers` subset |
| Ordering | Block order, no DAG |
| OP_RETURN location | Any output |
| Auth on DB API | None (v1) |
| Read API | Out of scope (v1) |
## Open questions for future contributors
1. **Handler parity:** Which excluded memo-protocol actions are required next (polls vs token sale)?
2. **Query layer:** Should PSF add `memo-query` REST on psf-memo-db or a separate service?
3. **Reorg handling:** v1 does not unwind reorgs; is depth-0 BCH acceptable for social indexing?
4. **Address indexing:** Should posts be secondary-indexed by `addr` for feed APIs?
5. **Alignment with Go:** Should reply/like tip logic match Gos satoshi aggregation exactly (output script types beyond P2PKH)?
Document updates belong in this `dev-docs/` folder when decisions change.