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>
This commit is contained in:
vsecoder
2026-04-18 22:06:06 +03:00
parent 6425b5cffb
commit 1a8731f479
9 changed files with 479 additions and 45 deletions

View File

@@ -14,7 +14,8 @@ Swagger UI на `/swagger`. Эти два эндпоинта можно выкл
|---------|----------|
| [chain.md](chain.md) | Блоки, транзакции, балансы, адреса, netstats, validators |
| [contracts.md](contracts.md) | Деплой, вызов, state, логи контрактов |
| [relay.md](relay.md) | Relay-mailbox: отправка/приём encrypted envelopes |
| [relay.md](relay.md) | Relay-mailbox: 1:1 E2E-шифрованные envelopes |
| [feed.md](feed.md) | Социальная лента: публикация постов, лайки, подписки, рекомендации, хэштеги (v2.0.0) |
## Discovery / metadata (always on)

279
docs/api/feed.md Normal file
View File

@@ -0,0 +1,279 @@
# 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:**
```json
{
"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:**
```json
{
"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:**
```json
{
"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'ом клиента:
```html
<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:**
```json
{
"post_id": "...",
"views": 127,
"likes": 42,
"liked_by_me": true
}
```
### `GET /feed/author/{pub}?limit=N&before=<ts>`
Посты конкретного автора, newest-first.
Пагинация: передавайте `before=<created_at>` самого старого уже
загруженного поста, чтобы получить следующую страницу.
**Response:**
```json
{"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`
```json
{
"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`
```json
{"type": "DELETE_POST", "fee": 1000, "payload": {"post_id": "..."}}
```
Только автор может удалить. Soft-delete (post остаётся в chain, но
помечен `deleted=true`; читатели получают 410).
### `FOLLOW` / `UNFOLLOW`
```json
{"type": "FOLLOW", "to": "<target_ed25519>", "fee": 1000, "payload": {}}
```
Двусторонний on-chain индекс: `follow:<A>:<B>` + `followin:<B>:<A>`.
`UNFOLLOW` зеркально удаляет оба ключа.
### `LIKE_POST` / `UNLIKE_POST`
```json
{"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)

View File

@@ -1,28 +1,83 @@
# Relay API
REST API для работы с шифрованными сообщениями через relay-сеть.
REST API для работы с зашифрованными 1:1-сообщениями через relay-сеть.
Сообщения шифруются E2E с использованием NaCl (X25519 + XSalsa20-Poly1305). Relay хранит зашифрованные конверты и доставляет их получателям.
Сообщения шифруются E2E с использованием NaCl (X25519 + XSalsa20-Poly1305).
Relay хранит зашифрованные конверты до 7 дней (настраиваемо через
`DCHAIN_MAILBOX_TTL_DAYS`) и доставляет их получателям по запросу или
через WebSocket push.
## Отправить сообщение
> **Это API для 1:1 чатов.** Для публичных постов и ленты — см.
> [`feed.md`](feed.md).
### `POST /relay/send`
Server-side guarantees (v1.0.x hardening):
- `envelope.ID` пересчитывается канонически (`sha256(nonce||ct)[:16]`)
на `mailbox.Store` — защита от content-replay.
- `envelope.SentAt` переписывается серверным `time.Now().Unix()` — клиент
не может back-date/future-date сообщения.
- `/relay/broadcast` и `/relay/inbox/*` обёрнуты в rate-limiter
(`withSubmitTxGuards` / `withReadLimit`); одиночный атакующий не может
через FIFO-eviction выбросить реальные сообщения получателя.
- `DELETE /relay/inbox/{id}` требует Ed25519-подпись владельца, связанную
с x25519-ключом через identity-реестр.
Зашифровать и отправить сообщение получателю.
## Опубликовать envelope
### `POST /relay/broadcast` — **рекомендованный E2E-путь**
Клиент запечатывает сообщение через NaCl `box` своим X25519-privkey на
публичный ключ получателя → отправляет готовый envelope. Сервер
проверяет размер, канонизирует id/timestamp, сохраняет в mailbox и
гошепит пирам.
**Request body:**
```json
{
"envelope": {
"id": "<hex>",
"sender_pub": "<x25519_hex>",
"recipient_pub": "<x25519_hex>",
"sender_ed25519_pub": "<ed25519_hex>",
"fee_ut": 0,
"fee_sig": null,
"nonce": "<base64 24B>",
"ciphertext": "<base64 NaCl box>",
"sent_at": 1710000000
}
}
```
**Response:** `{"id": "<canonical_id>", "status": "broadcast"}`
> Поле `envelope.id` сервер **перезапишет** на
> `hex(sha256(nonce||ciphertext)[:16])`; клиент может прислать любое
> непустое значение.
```bash
curl -X POST http://localhost:8081/relay/broadcast \
-H "Content-Type: application/json" \
-d @envelope.json
```
---
## Отправить plaintext через релей (legacy / non-E2E)
### `POST /relay/send` — **⚠ НЕ E2E**
Нода запечатывает сообщение **своим** X25519-ключом. Получатель не
сможет расшифровать без знания privkey релея, и отправитель не
аутентифицируется. Оставлено для backward-compat; в продакшн-чатах
используйте `/relay/broadcast`.
**Request body:**
```json
{
"recipient_pub": "<x25519_hex>",
"msg_b64": "<base64_encoded_message>"
"msg_b64": "<base64_encoded_plaintext>"
}
```
| Поле | Тип | Описание |
|------|-----|---------|
| `recipient_pub` | string | X25519 public key получателя (hex) |
| `msg_b64` | string | Сообщение в base64 |
```bash
MSG=$(echo -n "Hello, Bob!" | base64)
curl -X POST http://localhost:8081/relay/send \
@@ -30,22 +85,15 @@ curl -X POST http://localhost:8081/relay/send \
-d "{\"recipient_pub\":\"$BOB_X25519\",\"msg_b64\":\"$MSG\"}"
```
**Response:**
```json
{
"id": "env-abc123",
"recipient_pub": "...",
"status": "sent"
}
```
**Response:** `{"id": "env-abc123", "recipient_pub": "...", "status": "sent"}`
---
## Broadcast конверта
## Broadcast конверта (legacy alias)
### `POST /relay/broadcast`
Опубликовать pre-sealed конверт (для light-клиентов, которые шифруют на своей стороне).
См. выше. Это основной E2E-путь.
**Request body:**
```json
@@ -132,18 +180,35 @@ curl "http://localhost:8081/relay/inbox/count?pub=$MY_X25519"
---
### `DELETE /relay/inbox/{envID}?pub=<hex>`
### `DELETE /relay/inbox/{envID}?pub=<x25519hex>`
Удалить сообщение из inbox.
Удалить сообщение из inbox. **Требует подписи** Ed25519-ключа, чья
identity связана с `?pub=<x25519>` через on-chain identity-реестр —
защита от grief-DELETE любым знающим ваш pubkey.
**Request body:**
```json
{
"ed25519_pub": "<hex>",
"sig": "<base64 Ed25519 signature>",
"ts": 1710000000
}
```
Где `sig = Ed25519.Sign(priv, "inbox-delete:<envID>:<x25519pub>:<ts>")`.
`ts` должен быть в пределах ±5 минут от серверного времени (anti-replay).
```bash
curl -X DELETE "http://localhost:8081/relay/inbox/env-abc123?pub=$MY_X25519"
# подпись: printf 'inbox-delete:env-abc123:%s:%d' "$MY_X25519" "$TS" | sign
curl -X DELETE "http://localhost:8081/relay/inbox/env-abc123?pub=$MY_X25519" \
-H "Content-Type: application/json" \
-d "{\"ed25519_pub\":\"$MY_ED25519\",\"sig\":\"$SIG\",\"ts\":$TS}"
```
**Response:**
```json
{"id": "env-abc123", "status": "deleted"}
```
**Response:** `{"id": "env-abc123", "status": "deleted"}`
**Errors:** `403` если подпись/идентичность не совпадает; `503` если
на ноде не настроен identity-resolver (DCHAIN_DISABLE_INBOX_DELETE).
---