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

9.2 KiB
Raw Blame History

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 (GetHandlers), not the full 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 (0x6d300x6d32)
  • 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 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.