Files
dchain/docs/api/feed.md
vsecoder 1a8731f479 docs: update README + api docs + architecture for v2.0.0 feed
README
  - Mention social feed in the one-line description and feature bullets
  - Add relay + feed endpoint tables to the API overview (was
    previously empty on messaging)
  - List media/ package in the repo structure

docs/api/
  - New docs/api/feed.md: full reference for /feed/publish, fetch,
    stats, view, author, timeline, trending, foryou, hashtag; all
    on-chain CREATE_POST / DELETE_POST / FOLLOW / LIKE payloads;
    fee economics; server-side scrubbing contract.
  - docs/api/relay.md rewritten: /relay/broadcast is now the primary
    E2E path with a complete envelope schema; /relay/send kept but
    flagged ⚠ NOT E2E; DELETE /relay/inbox/{id} documented with the
    new Ed25519 signed-auth body.
  - docs/api/README.md index: added feed.md row.

docs/architecture.md
  - L2 Transport layer description updated to include the feed
    mailbox alongside the 1:1 relay mailbox.
  - New "Социальная лента (v2.0.0)" section right after the 1:1
    message flow: ASCII diagram of publish + on-chain commit +
    timeline fetch, economic summary, metadata-scrub summary.

docs/node/README.md
  - Removed stale chan:/chan-member: keys from the BadgerDB schema
    reference; replaced with the v2.0.0 feed keys (post:,
    postbyauthor:, follow:, followin:, like:, likecount:).

docs/update-system.md
  - Example features[] array updated to match the actual node output
    (channels_v1 removed, feed_v2 / media_scrub / relay_broadcast added).

Node feature flags
  - api_well_known_version.go: dropped channels_v1 tag (the
    /api/channels/:id endpoint was removed in the feed refactor);
    added feed_v2, media_scrub, relay_broadcast so clients can
    feature-detect the v2.0.0 surface.
  - Comment example updated channels_v2/v1 → feed_v3/v2.

Client
  - CLIENT_REQUIRED_FEATURES expanded to include the v2.0.0 feature
    flags the client now depends on (feed_v2, media_scrub,
    relay_broadcast); checkNodeVersion() will flag older nodes as
    unsupported and surface an upgrade prompt.

All 7 Go test packages green; tsc --noEmit clean.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-18 22:06:06 +03:00

11 KiB
Raw Blame History

Feed API (v2.0.0)

REST API для социальной ленты: публикация постов, лента подписок, рекомендации, трендинг, хэштеги. Введено в v2.0.0, заменило channel-модель.

Архитектура

Лента устроена гибридно — метаданные on-chain, тела постов off-chain в relay feed-mailbox:

┌─────────────────────┐                    ┌──────────────────────────┐
│   on-chain state    │                    │   feed mailbox (BadgerDB)│
│                     │                    │                          │
│  post:<id> →        │                    │  feedpost:<id> →         │
│    {author, size,   │    POST            │    {content, attachment, │
│     content_hash,   │    /feed/publish   │     hashtags, type, ts}  │
│     hosting_relay,  │───────────────────►│                          │
│     reply_to?,      │                    │  feedview:<id> →         │
│     deleted?}       │                    │    uint64 (counter)      │
│                     │                    │                          │
│  follow:<A>:<B>     │                    │  feedtag:<tag>:<ts>:<id> │
│  like:<post>:<liker>│                    │    (hashtag index)       │
│  likecount:<post>   │                    │                          │
└─────────────────────┘                    └──────────────────────────┘

Разделение решает два противоречия: on-chain-состояние обеспечивает provable авторство + экономические стимулы (оплата за байт); off-chain тела позволяют хранить мегабайты картинок без раздувания истории блоков. Hosting relay получает плату автора (через CREATE_POST tx); другие ноды могут реплицировать через gossipsub (роадмап).

Оплата постов

Автор поста платит BasePostFee + Size × PostByteFee µT за публикацию. Полный fee кредитуется на баланс hosting-релея.

Константа Значение Примечание
BasePostFee 1 000 µT (0.001 T) Совпадает с MinFee (block-validation floor)
PostByteFee 1 µT/byte Размер = контент + attachment + ~128 B метаданных
MaxPostSize 256 KiB Hard cap, больше — 413 на /feed/publish

Пример:

  • 200-байтовый текстовый пост → ~1 328 µT (~0.0013 T)
  • Текст + 30 KiB WebP-картинка → ~31 848 µT (~0.032 T)
  • Видео 200 KiB → ~205 128 µT (~0.21 T)

Server-side metadata scrubbing

Обязательная для всех аттачментов на /feed/publish:

  • Изображения (JPEG/PNG/GIF/WebP): decode → downscale до 1080 px → re-encode как JPEG Q=75 через stdlib. EXIF/GPS/ICC/XMP/MakerNote стираются by construction (stdlib JPEG encoder не пишет их).
  • Видео/аудио: переадресуется во внешний FFmpeg-сайдкар (контейнер docker/media-sidecar/). Если сайдкар не настроен через DCHAIN_MEDIA_SIDECAR_URL и оператор не выставил DCHAIN_ALLOW_UNSCRUBBED_VIDEO, видео-аплоад отклоняется с 503.
  • MIME mismatch: если magic-байты не соответствуют заявленному Content-Type — 400. Защита от PDF'а, замаскированного под картинку.

Endpoints

POST /feed/publish

Загружает тело поста, запускает скраббинг, сохраняет в feed-mailbox, возвращает метаданные для последующей on-chain CREATE_POST tx.

Request:

{
  "post_id":         "<hex 16 B>",
  "author":          "<ed25519_hex>",
  "content":         "Text of the post",
  "content_type":    "text/plain",
  "attachment_b64":  "<base64 image/video/audio>",
  "attachment_mime": "image/jpeg",
  "reply_to":        "<optional parent post_id>",
  "quote_of":        "<optional quoted post_id>",
  "sig":             "<base64 Ed25519 sig>",
  "ts":              1710000000
}

sig = Ed25519.Sign(author_priv, "publish:<post_id>:<raw_content_hash_hex>:<ts>")

где raw_content_hash = sha256(content + raw_attachment_bytes) — клиентский хэш до серверного скраба.

ts в пределах ±5 минут от серверного времени (anti-replay).

Response:

{
  "post_id":          "<echo>",
  "hosting_relay":    "<ed25519_hex of this node>",
  "content_hash":     "<hex sha256 of post-scrub bytes>",
  "size":             1234,
  "hashtags":         ["golang", "dchain"],
  "estimated_fee_ut": 2234
}

Клиент затем собирает CREATE_POST tx с этими полями и сабмитит на /api/tx. См. buildCreatePostTx в клиентской библиотеке.

GET /feed/post/{id}

Тело поста со статами.

Response:

{
  "post_id":        "...",
  "author":         "...",
  "content":        "...",
  "content_type":   "text/plain",
  "hashtags":       ["golang"],
  "created_at":     1710000000,
  "reply_to":       "",
  "quote_of":       "",
  "attachment":     "<base64 bytes>",
  "attachment_mime": "image/jpeg"
}

Errors: 404 если не найден; 410 если on-chain soft-deleted.

GET /feed/post/{id}/attachment

Сырые байты аттачмента с корректным Content-Type. Используется нативным image-loader'ом клиента:

<img src="http://node/feed/post/abc/attachment" />

Cache-Control: public, max-age=3600, immutable — content-addressed ресурс, кэш безопасен.

POST /feed/post/{id}/view

Инкремент off-chain счётчика просмотров. Fire-and-forget, не требует подписи (per-view tx был бы нереалистичен).

Response: {"post_id": "...", "views": 42}

GET /feed/post/{id}/stats?me=<pub>

Агрегат для отображения. me опционален — если передан, в ответе будет liked_by_me.

Response:

{
  "post_id":      "...",
  "views":        127,
  "likes":        42,
  "liked_by_me":  true
}

GET /feed/author/{pub}?limit=N&before=<ts>

Посты конкретного автора, newest-first.

Пагинация: передавайте before=<created_at> самого старого уже загруженного поста, чтобы получить следующую страницу.

Response:

{"author": "<pub>", "count": 20, "posts": [FeedPostItem]}

GET /feed/timeline?follower=<pub>&limit=N&before=<ts>

Лента подписок: merged посты всех, на кого подписан follower, newest- first. Требует, чтобы on-chain FOLLOW-транзакции уже скоммитились.

Пагинация через before идентично /feed/author.

GET /feed/trending?window=24&limit=N

Топ постов за последние N часов, ранжированы по likes × 3 + views.

window — часы (1..168), по умолчанию 24.

Пагинация не поддерживается — это топ-срез, не список.

GET /feed/foryou?pub=<pub>&limit=N

Рекомендации. Простая эвристика v2.0.0:

  1. Кандидаты: посты последних 48 часов
  2. Исключить: авторы, на которых уже подписан; уже лайкнутые; свои
  3. Ранжировать: likes × 3 + views + 1 (seed чтобы свежее вылезало)

Никаких ML — база для v2.2.0 Feed algorithm (half-life decay, mutual-follow boost, hashtag-affinity).

GET /feed/hashtag/{tag}?limit=N

Посты, помеченные хэштегом #tag. Tag case-insensitive. Хэштеги авто-индексируются на /feed/publish из текста (regex #[A-Za-z0-9_\p{L}]{1,40}, dedup, cap 8 per post).

On-chain события

Клиент сабмитит эти транзакции через обычный /api/tx endpoint.

CREATE_POST

{
  "type": "CREATE_POST",
  "fee":  <estimated_fee_ut>,
  "payload": {
    "post_id":        "...",
    "content_hash":   "<base64 32-byte sha256>",
    "size":           1234,
    "hosting_relay":  "<ed25519_hex>",
    "reply_to":       "",
    "quote_of":       ""
  }
}

Валидации on-chain: size > 0 && size <= MaxPostSize, fee >= BasePostFee + size × PostByteFee, reply_to и quote_of взаимно исключают друг друга. Дубликат post_id отклоняется.

DELETE_POST

{"type": "DELETE_POST", "fee": 1000, "payload": {"post_id": "..."}}

Только автор может удалить. Soft-delete (post остаётся в chain, но помечен deleted=true; читатели получают 410).

FOLLOW / UNFOLLOW

{"type": "FOLLOW", "to": "<target_ed25519>", "fee": 1000, "payload": {}}

Двусторонний on-chain индекс: follow:<A>:<B> + followin:<B>:<A>. UNFOLLOW зеркально удаляет оба ключа.

LIKE_POST / UNLIKE_POST

{"type": "LIKE_POST", "fee": 1000, "payload": {"post_id": "..."}}

Дубль-лайк отклоняется. Кэшированный counter likecount:<post> хранит O(1) счётчик.

Pricing / economics (резюме)

Операция Cost Куда уходит
CREATE_POST 1000 + size×1 µT Hosting relay
DELETE_POST 1000 µT Block validator
FOLLOW / UNFOLLOW 1000 µT Block validator
LIKE_POST / UNLIKE_POST 1000 µT Block validator
/feed/post/{id}/view 0 Off-chain counter

Причина такого разделения: основные бизнес-действия (публикация) едут на «хостера контента», а мелкие социальные tx (лайк/подписка) — на валидатора сети, как обычные tx.

Клиентские вспомогательные функции

В client-app/lib/feed.ts:

  • publishPost(params) — подпись + POST /feed/publish
  • publishAndCommit(params) — publish + CREATE_POST tx в одну операцию
  • fetchTimeline, fetchForYou, fetchTrending, fetchAuthorPosts, fetchHashtag, fetchPost, fetchStats, bumpView
  • buildCreatePostTx, buildDeletePostTx, buildFollowTx, buildUnfollowTx, buildLikePostTx, buildUnlikePostTx
  • likePost, unlikePost, followUser, unfollowUser, deletePost (build + submitTx bundled)