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:
38
README.md
38
README.md
@@ -1,6 +1,6 @@
|
|||||||
# DChain
|
# DChain
|
||||||
|
|
||||||
Блокчейн-стек для децентрализованного мессенджера:
|
Блокчейн-стек для децентрализованного мессенджера + социальной ленты:
|
||||||
|
|
||||||
- **PBFT** консенсус с multi-sig validator governance и equivocation slashing
|
- **PBFT** консенсус с multi-sig validator governance и equivocation slashing
|
||||||
- **Native Go контракты** рядом с WASM (wazero) — нулевая задержка для
|
- **Native Go контракты** рядом с WASM (wazero) — нулевая задержка для
|
||||||
@@ -8,6 +8,12 @@
|
|||||||
- **WebSocket push API** — клиент не опрашивает, все события прилетают
|
- **WebSocket push API** — клиент не опрашивает, все события прилетают
|
||||||
на соединение
|
на соединение
|
||||||
- **E2E-шифрованный relay mailbox** на libp2p gossipsub с TTL live-detection
|
- **E2E-шифрованный relay mailbox** на libp2p gossipsub с TTL live-detection
|
||||||
|
(1:1 чаты через NaCl box; посты в ленте — plaintext-публичные)
|
||||||
|
- **Социальная лента v2.0.0** (заменила каналы): публичные посты
|
||||||
|
с оплатой за размер (автор платит, хостящая релей-нода получает);
|
||||||
|
on-chain граф подписок + лайки; off-chain просмотры + хэштеги;
|
||||||
|
мандаторный server-side scrubber метаданных (EXIF/GPS-стрип + FFmpeg
|
||||||
|
sidecar для видео); share-to-chat c embedded post-карточкой
|
||||||
- **Система обновлений:** build-time версия → `/api/well-known-version`,
|
- **Система обновлений:** build-time версия → `/api/well-known-version`,
|
||||||
peer-version gossip, `/api/update-check` против Gitea releases,
|
peer-version gossip, `/api/update-check` против Gitea releases,
|
||||||
`update.sh` с semver guard
|
`update.sh` с semver guard
|
||||||
@@ -142,7 +148,8 @@ sudo systemctl enable --now dchain-update.timer
|
|||||||
| `node/` | HTTP + WS API, SSE, metrics, access control |
|
| `node/` | HTTP + WS API, SSE, metrics, access control |
|
||||||
| `node/version/` | Build-time version metadata (ldflags-инжектимый) |
|
| `node/version/` | Build-time version metadata (ldflags-инжектимый) |
|
||||||
| `vm/` | wazero runtime для WASM-контрактов + gas model |
|
| `vm/` | wazero runtime для WASM-контрактов + gas model |
|
||||||
| `relay/` | E2E mailbox с NaCl-envelopes |
|
| `relay/` | E2E mailbox (1:1 envelopes) + public feed-mailbox (post bodies, view counter, hashtag index) |
|
||||||
|
| `media/` | Server-side metadata scrubber (EXIF strip + FFmpeg sidecar client) |
|
||||||
| `identity/` | Ed25519 + X25519 keypair, tx signing |
|
| `identity/` | Ed25519 + X25519 keypair, tx signing |
|
||||||
| `economy/` | Fee model, rewards |
|
| `economy/` | Fee model, rewards |
|
||||||
| `wallet/` | Optional payout wallet (отдельный ключ) |
|
| `wallet/` | Optional payout wallet (отдельный ключ) |
|
||||||
@@ -179,6 +186,33 @@ sudo systemctl enable --now dchain-update.timer
|
|||||||
Scoped WS-топики (`addr:`, `inbox:`, `typing:`) требуют auth через
|
Scoped WS-топики (`addr:`, `inbox:`, `typing:`) требуют auth через
|
||||||
Ed25519-nonce; публичные (`blocks`, `tx`, `contract_log`) — без.
|
Ed25519-nonce; публичные (`blocks`, `tx`, `contract_log`) — без.
|
||||||
|
|
||||||
|
### Relay (E2E messaging)
|
||||||
|
| Endpoint | Описание |
|
||||||
|
|----------|----------|
|
||||||
|
| `POST /relay/broadcast` | Опубликовать pre-sealed envelope (E2E-путь, рекомендован) |
|
||||||
|
| `GET /relay/inbox?pub=<x25519>` | Прочитать входящие конверты |
|
||||||
|
| `DELETE /relay/inbox/{id}` | Удалить envelope (требует Ed25519-подписи владельца) |
|
||||||
|
|
||||||
|
Детали — [`docs/api/relay.md`](docs/api/relay.md). `/relay/send` оставлен
|
||||||
|
для backward-compat, но ломает E2E (nod-релей запечатывает своим ключом)
|
||||||
|
и помечен как non-recommended.
|
||||||
|
|
||||||
|
### Social feed (v2.0.0)
|
||||||
|
| Endpoint | Описание |
|
||||||
|
|----------|----------|
|
||||||
|
| `POST /feed/publish` | Загрузить тело поста + EXIF-скраб + вернуть fee |
|
||||||
|
| `GET /feed/post/{id}` | Тело поста |
|
||||||
|
| `GET /feed/post/{id}/attachment` | Сырые байты картинки/видео (cache'able) |
|
||||||
|
| `GET /feed/post/{id}/stats?me=<pub>` | `{views, likes, liked_by_me?}` |
|
||||||
|
| `POST /feed/post/{id}/view` | Бамп off-chain счётчика просмотров |
|
||||||
|
| `GET /feed/author/{pub}?before=<ts>&limit=N` | Посты автора (пагинация `before`) |
|
||||||
|
| `GET /feed/timeline?follower=<pub>&before=<ts>&limit=N` | Merged лента подписок |
|
||||||
|
| `GET /feed/trending?window=24&limit=N` | Топ по `likes × 3 + views` за окно |
|
||||||
|
| `GET /feed/foryou?pub=<pub>&limit=N` | Рекомендации (неподписанные авторы) |
|
||||||
|
| `GET /feed/hashtag/{tag}?limit=N` | Посты по хэштегу |
|
||||||
|
|
||||||
|
Детали + спецификация — [`docs/api/feed.md`](docs/api/feed.md).
|
||||||
|
|
||||||
### Docs / UI
|
### Docs / UI
|
||||||
- `GET /swagger` — **Swagger UI** (рендерится через swagger-ui-dist).
|
- `GET /swagger` — **Swagger UI** (рендерится через swagger-ui-dist).
|
||||||
- `GET /swagger/openapi.json` — сырая OpenAPI 3.0 спека.
|
- `GET /swagger/openapi.json` — сырая OpenAPI 3.0 спека.
|
||||||
|
|||||||
@@ -480,8 +480,11 @@ export const CLIENT_PROTOCOL_VERSION = 1;
|
|||||||
*/
|
*/
|
||||||
export const CLIENT_REQUIRED_FEATURES = [
|
export const CLIENT_REQUIRED_FEATURES = [
|
||||||
'chain_id',
|
'chain_id',
|
||||||
|
'feed_v2', // social feed (v2.0.0) — PostCard, timeline, forYou
|
||||||
'identity_registry',
|
'identity_registry',
|
||||||
|
'media_scrub', // server-side EXIF strip — we rely on this for privacy
|
||||||
'onboarding_api',
|
'onboarding_api',
|
||||||
|
'relay_broadcast', // /relay/broadcast for E2E envelopes (not /relay/send)
|
||||||
'relay_mailbox',
|
'relay_mailbox',
|
||||||
'ws_submit_tx',
|
'ws_submit_tx',
|
||||||
];
|
];
|
||||||
|
|||||||
@@ -14,7 +14,8 @@ Swagger UI на `/swagger`. Эти два эндпоинта можно выкл
|
|||||||
|---------|----------|
|
|---------|----------|
|
||||||
| [chain.md](chain.md) | Блоки, транзакции, балансы, адреса, netstats, validators |
|
| [chain.md](chain.md) | Блоки, транзакции, балансы, адреса, netstats, validators |
|
||||||
| [contracts.md](contracts.md) | Деплой, вызов, state, логи контрактов |
|
| [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)
|
## Discovery / metadata (always on)
|
||||||
|
|
||||||
|
|||||||
279
docs/api/feed.md
Normal file
279
docs/api/feed.md
Normal 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)
|
||||||
@@ -1,28 +1,83 @@
|
|||||||
# Relay API
|
# 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:**
|
**Request body:**
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"recipient_pub": "<x25519_hex>",
|
"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
|
```bash
|
||||||
MSG=$(echo -n "Hello, Bob!" | base64)
|
MSG=$(echo -n "Hello, Bob!" | base64)
|
||||||
curl -X POST http://localhost:8081/relay/send \
|
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\"}"
|
-d "{\"recipient_pub\":\"$BOB_X25519\",\"msg_b64\":\"$MSG\"}"
|
||||||
```
|
```
|
||||||
|
|
||||||
**Response:**
|
**Response:** `{"id": "env-abc123", "recipient_pub": "...", "status": "sent"}`
|
||||||
```json
|
|
||||||
{
|
|
||||||
"id": "env-abc123",
|
|
||||||
"recipient_pub": "...",
|
|
||||||
"status": "sent"
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Broadcast конверта
|
## Broadcast конверта (legacy alias)
|
||||||
|
|
||||||
### `POST /relay/broadcast`
|
### `POST /relay/broadcast`
|
||||||
|
|
||||||
Опубликовать pre-sealed конверт (для light-клиентов, которые шифруют на своей стороне).
|
См. выше. Это основной E2E-путь.
|
||||||
|
|
||||||
**Request body:**
|
**Request body:**
|
||||||
```json
|
```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
|
```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:**
|
**Response:** `{"id": "env-abc123", "status": "deleted"}`
|
||||||
```json
|
|
||||||
{"id": "env-abc123", "status": "deleted"}
|
**Errors:** `403` если подпись/идентичность не совпадает; `503` если
|
||||||
```
|
на ноде не настроен identity-resolver (DCHAIN_DISABLE_INBOX_DELETE).
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
@@ -2,16 +2,18 @@
|
|||||||
|
|
||||||
## Обзор
|
## Обзор
|
||||||
|
|
||||||
DChain — это L1-блокчейн для децентрализованного мессенджера. Архитектура разделена на четыре слоя:
|
DChain — это L1-блокчейн для децентрализованного мессенджера + социальной ленты. Архитектура разделена на четыре слоя:
|
||||||
|
|
||||||
```
|
```
|
||||||
┌─────────────────────────────────────────────────────────────┐
|
┌─────────────────────────────────────────────────────────────┐
|
||||||
│ L3 Application — messaging, usernames, auctions, escrow │
|
│ L3 Application — messaging, social feed, usernames, │
|
||||||
|
│ auctions, escrow │
|
||||||
│ (Smart contracts: username_registry, governance, auction, │
|
│ (Smart contracts: username_registry, governance, auction, │
|
||||||
│ escrow; deployed on-chain via DEPLOY_CONTRACT) │
|
│ escrow; native feed events; deployed on-chain) │
|
||||||
├─────────────────────────────────────────────────────────────┤
|
├─────────────────────────────────────────────────────────────┤
|
||||||
│ L2 Transport — relay mailbox, E2E NaCl encryption │
|
│ L2 Transport — relay mailbox (1:1 E2E) + │
|
||||||
│ (relay/, mailbox, GossipSub envelopes, RELAY_PROOF tx) │
|
│ feed mailbox (public posts + attachments)│
|
||||||
|
│ (relay/, media scrubber, GossipSub envelopes, RELAY_PROOF) │
|
||||||
├─────────────────────────────────────────────────────────────┤
|
├─────────────────────────────────────────────────────────────┤
|
||||||
│ L1 Chain — PBFT consensus, WASM VM, BadgerDB │
|
│ L1 Chain — PBFT consensus, WASM VM, BadgerDB │
|
||||||
│ (blockchain/, consensus/, vm/, identity/) │
|
│ (blockchain/, consensus/, vm/, identity/) │
|
||||||
@@ -189,6 +191,50 @@ Alice node1 (relay) Bob
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
## Социальная лента (v2.0.0)
|
||||||
|
|
||||||
|
Публичные посты в Twitter/VK-стиле. Заменила channel-модель (каналы
|
||||||
|
удалены полностью). Живёт гибридно — метаданные on-chain, тела в relay
|
||||||
|
feed-mailbox:
|
||||||
|
|
||||||
|
```
|
||||||
|
Alice node1 (hosting relay) Bob
|
||||||
|
│ │ │
|
||||||
|
│─ POST /feed/publish ──────▶│ │
|
||||||
|
│ (body + EXIF-scrub) │ │
|
||||||
|
│ │ store in feed-mailbox │
|
||||||
|
│ │ (TTL 30 days) │
|
||||||
|
│ │ │
|
||||||
|
│◀── response: content_hash, │ │
|
||||||
|
│ estimated_fee_ut, id │ │
|
||||||
|
│ │ │
|
||||||
|
│─ CREATE_POST tx ──────────▶│── PBFT commit ──────────▶ │
|
||||||
|
│ (fee = 1000 + size × 1) │ │
|
||||||
|
│ │ on-chain: post:<id> │
|
||||||
|
│ │ │
|
||||||
|
│ │ │─ GET /feed/timeline
|
||||||
|
│ │◀── merge followed authors │ (from chain)
|
||||||
|
│ │ │
|
||||||
|
│ │── GET /feed/post/{id} ───▶│
|
||||||
|
│ │ (body from mailbox) │
|
||||||
|
```
|
||||||
|
|
||||||
|
**Экономика:**
|
||||||
|
- Автор платит `BasePostFee(1000) + size × PostByteFee(1)` µT
|
||||||
|
- Вся плата уходит `hosting_relay` пубкею — компенсация за хранение
|
||||||
|
- `MaxPostSize = 256 KiB` — hard cap, защищает мейлбокс от abuse
|
||||||
|
|
||||||
|
**Метаданные:**
|
||||||
|
- EXIF/GPS/camera-info удаляется обязательно на сервере (in-proc для
|
||||||
|
изображений, через FFmpeg-сайдкар для видео)
|
||||||
|
- Лайки / follows — on-chain транзакции (provable + anti-Sybil fee)
|
||||||
|
- Просмотры — off-chain counter в relay (on-chain было бы абсурдно)
|
||||||
|
- Хэштеги — авто-индекс при publish, inverted-index в BadgerDB
|
||||||
|
|
||||||
|
Детали — [api/feed.md](api/feed.md).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## Динамические валидаторы
|
## Динамические валидаторы
|
||||||
|
|
||||||
Сет валидаторов хранится on-chain в `validator:<pubkey>`. Любой текущий валидатор может добавить или убрать другого (ADD_VALIDATOR / REMOVE_VALIDATOR tx). После commit такого блока PBFT-движок перезагружает сет без рестарта ноды.
|
Сет валидаторов хранится on-chain в `validator:<pubkey>`. Любой текущий валидатор может добавить или убрать другого (ADD_VALIDATOR / REMOVE_VALIDATOR tx). После commit такого блока PBFT-движок перезагружает сет без рестарта ноды.
|
||||||
|
|||||||
@@ -147,8 +147,12 @@ txchron:<block20d>:<seq04d> → tx_id (recent-tx index)
|
|||||||
balance:<pubkey> → uint64 (µT)
|
balance:<pubkey> → uint64 (µT)
|
||||||
stake:<pubkey> → uint64 (µT)
|
stake:<pubkey> → uint64 (µT)
|
||||||
id:<pubkey> → JSON RegisterKeyPayload
|
id:<pubkey> → JSON RegisterKeyPayload
|
||||||
chan:<channelID> → JSON CreateChannelPayload
|
post:<postID> → JSON PostRecord (v2.0.0 social feed)
|
||||||
chan-member:<ch>:<pub> → ""
|
postbyauthor:<pub>:<ts>:<id> → "" (chrono index, newest-first scan)
|
||||||
|
follow:<A>:<B> → "" (A follows B)
|
||||||
|
followin:<B>:<A> → "" (reverse index for Followers())
|
||||||
|
like:<postID>:<liker> → "" (presence)
|
||||||
|
likecount:<postID> → uint64 (cached counter, O(1) reads)
|
||||||
contract:<contractID> → JSON ContractRecord
|
contract:<contractID> → JSON ContractRecord
|
||||||
cstate:<contractID>:<key> → bytes
|
cstate:<contractID>:<key> → bytes
|
||||||
clog:<ct>:<block>:<seq> → JSON ContractLogEntry
|
clog:<ct>:<block>:<seq> → JSON ContractLogEntry
|
||||||
|
|||||||
@@ -44,10 +44,10 @@ curl -s http://localhost:8080/api/well-known-version | jq .
|
|||||||
},
|
},
|
||||||
"protocol_version": 1,
|
"protocol_version": 1,
|
||||||
"features": [
|
"features": [
|
||||||
"access_token", "chain_id", "channels_v1", "contract_logs",
|
"access_token", "chain_id", "contract_logs",
|
||||||
"fan_out", "identity_registry", "native_username_registry",
|
"fan_out", "feed_v2", "identity_registry", "media_scrub",
|
||||||
"onboarding_api", "payment_channels", "relay_mailbox",
|
"native_username_registry", "onboarding_api", "payment_channels",
|
||||||
"ws_submit_tx"
|
"relay_broadcast", "relay_mailbox", "ws_submit_tx"
|
||||||
],
|
],
|
||||||
"chain_id": "dchain-ddb9a7e37fc8"
|
"chain_id": "dchain-ddb9a7e37fc8"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,7 +12,7 @@
|
|||||||
// returned forever (even if the implementation moves around internally). The
|
// returned forever (even if the implementation moves around internally). The
|
||||||
// client uses them as "is this feature here or not?", not "what version is
|
// client uses them as "is this feature here or not?", not "what version is
|
||||||
// this feature at?". Versioning a feature is done by shipping a new tag
|
// this feature at?". Versioning a feature is done by shipping a new tag
|
||||||
// (e.g. "channels_v2" alongside "channels_v1" for a deprecation window).
|
// (e.g. "feed_v3" alongside "feed_v2" for a deprecation window).
|
||||||
//
|
//
|
||||||
// Response shape:
|
// Response shape:
|
||||||
//
|
//
|
||||||
@@ -55,15 +55,17 @@ const ProtocolVersion = 1
|
|||||||
// a breaking successor (e.g. `channels_v1`, not `channels`).
|
// a breaking successor (e.g. `channels_v1`, not `channels`).
|
||||||
var nodeFeatures = []string{
|
var nodeFeatures = []string{
|
||||||
"access_token", // DCHAIN_API_TOKEN gating on writes (+ optional reads)
|
"access_token", // DCHAIN_API_TOKEN gating on writes (+ optional reads)
|
||||||
"channels_v1", // /api/channels/:id + /members with X25519 enrichment
|
|
||||||
"chain_id", // /api/network-info returns chain_id
|
"chain_id", // /api/network-info returns chain_id
|
||||||
"contract_logs", // /api/contract/:id/logs endpoint
|
"contract_logs", // /api/contract/:id/logs endpoint
|
||||||
"fan_out", // client-side per-recipient envelope sealing
|
"fan_out", // client-side per-recipient envelope sealing
|
||||||
|
"feed_v2", // social feed: posts, likes, follows, timeline, trending, for-you
|
||||||
"identity_registry", // /api/identity/:pub returns X25519 pub + relay hints
|
"identity_registry", // /api/identity/:pub returns X25519 pub + relay hints
|
||||||
|
"media_scrub", // mandatory EXIF/GPS strip on /feed/publish
|
||||||
"native_username_registry", // native:username_registry contract
|
"native_username_registry", // native:username_registry contract
|
||||||
"onboarding_api", // /api/network-info for joiner bootstrap
|
"onboarding_api", // /api/network-info for joiner bootstrap
|
||||||
"payment_channels", // off-chain payment channel open/close
|
"payment_channels", // off-chain payment channel open/close
|
||||||
"relay_mailbox", // /relay/send + /relay/inbox
|
"relay_broadcast", // /relay/broadcast (E2E envelope publish)
|
||||||
|
"relay_mailbox", // /relay/inbox (read), /relay/send (legacy non-E2E)
|
||||||
"ws_submit_tx", // WebSocket submit_tx op
|
"ws_submit_tx", // WebSocket submit_tx op
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user