mirror of
https://github.com/Permissionless-Software-Foundation/psf-memo.git
synced 2026-09-21 16:52:01 -07:00
192 lines
9.2 KiB
Markdown
192 lines
9.2 KiB
Markdown
# 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 SLP’s 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 Go’s 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 other’s 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 integrator’s 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 Go’s satoshi aggregation exactly (output script types beyond P2PKH)?
|
|||
|
|
|
|||
|
|
Document updates belong in this `dev-docs/` folder when decisions change.
|