Document verified history deletion and message-specific delivery

This commit is contained in:
evgeny committed 2026-10-26 18:26:58 +01:00
1 parent aa97e053cd
commit 143788cdcd
2 files changed
+108 -25

No files matched your search

+32 -23
View File
@@ -1,10 +1,9 @@
# db_sync — децентрализованная реплицируемая таблица с цепным хешированием
Аудит 26.10.2026 частичный. Подтверждён дефект: отказ инициализации OpenSSL при
повторной вставке удаляет корректную историю. Испорченный входящий дубликат также
может удалить корректную сохранённую копию. Runtime-код не исправлялся;
продолжение остановлено по AGENTS.md. Условия удаления и воспроизведение —
[отчёт, раздел10](../../doc/member_merkle_audit_findings.txt).
Аудит 26.10.2026 частичный. Исправлены удаление корректной истории при отказе
проверки/неверном входящем повторе и SQL доставки. ACK_PUSH теперь содержит
message_id; старый формат не поддерживается. Условия удаления, проверки и
оставшиеся границы аудита — [отчёт, разделы10–11](../../doc/member_merkle_audit_findings.txt).
## 1. Архитектура
@@ -47,7 +46,7 @@
- `next_id` — автоинкрементный ID записей
- `last_timestamp_ms` — последний использованный timestamp (монотонный)
- `peers[]` — динамический массив SI_PEER (состояние синхронизации с каждым пиром)
- `recalc` — отложенный пересчёт цепных хешей (асинхронно, батчами по 50)
- `recalc` — позиция, pending и таймер пересчёта; ошибка сохраняет pending, батчи по50
- `ttl_timer` — периодическая TTL-очистка (каждый час)
- `on_insert` / `on_insert_arg` — коллбэк при вставке записи
@@ -73,6 +72,7 @@ CREATE TABLE "msg_GENERAL" (
flags INTEGER DEFAULT 0, -- DB_REC_FLAG_WAS_SENT (0x01)
data BLOB, -- JSON-данные записи
author_signature BLOB NOT NULL, -- Ed25519(ts || data), 64 байта
message_id BLOB NOT NULL UNIQUE CHECK(length(message_id)=32),
local_attrs TEXT DEFAULT '', -- локальные атрибуты (не реплицируются)
delivered_peers INTEGER DEFAULT 0, -- счётчик пиров, принявших PUSH
delivery_chain TEXT DEFAULT '', -- цепочка peer_id hex через запятую
@@ -95,7 +95,7 @@ chain_hash[0] = SHA256(0x00..00 [32] || id_0 || ts_0 || author_0 || sig_0)
|----------|----------|
| **Цепной хеш** | Каждая запись ссылается на хеш предыдущей — защита от вставки/удаления в середине цепочки |
| **Ed25519-подписи** | Каждая запись подписана автором. Проверяется при вставке (и локальной, и от пира) |
| **Async recalc** | Пересчёт `chain_hash` батчами по 50 записей (через `uasync_set_timeout`) — не блокирует event loop |
| **Async recalc** | Батчи по50 с атомарным продвижением после COMMIT; перед обменом flush завершается синхронно, ошибка прерывает раунд |
| **Master/Slave** | Узел с бОльшим `node_id` — мастер (шлёт `INIT_SYNC`). Узел с меньшим — слейв (шлёт `REQUEST_SYNC`) |
| **Периодическая проверка** | Каждые 5 секунд — поиск пиров с sync_state=0 и запуск синхронизации, детекция таймаутов (15с) |
| **TTL-очистка** | Каждый час — удаление неподтверждённых (`flags&1==0`) записей локального автора старше `db_sync_ttl` |
@@ -371,20 +371,22 @@ tp=10 → entry[0]: pos=9 (tp - 1)
### 3.7 DB_MSG_ACK_PUSH (0x06)
**Отправитель:** получатель PUSH → автору
**Отправитель:** получатель PUSH → переславшему пиру
**Назначение:** подтверждение получения PUSH-записи
```
┌──────┬──────────┬──────────────┐
│ 0x06 │ ts (8) │ author (8) │
└──────┴──────────┴──────────────┘
total: 17 bytes
┌──────┬──────────────────┐
│ 0x06 │ message_id (32) │
└──────┴──────────────────┘
total: 33 bytes
```
- `ts` — timestamp записи (uint64, host byte order)
- `author` — node_id автора (uint64, host byte order)
- `message_id` — SHA256 идентичности записи (group_id, timestamp, подпись; см. chat_feedback.h).
При получении ACK_PUSH автор помечает запись флагом `DB_REC_FLAG_WAS_SENT` (0x01) и обновляет `delivery_chain`/`delivered_peers`.
Получатель ACK одним UPDATE находит ровно эту запись, добавляет peer_id в delivery_chain
и увеличивает delivered_peers только при первом ACK от этого пира. Для локального автора
также устанавливается DB_REC_FLAG_WAS_SENT. Постановка PUSH в очередь доставкой не считается.
ACK отправляется и при корректном дубликате; записи с одинаковыми автором/временем независимы.
### 3.8 DB_MSG_SYNC_DONE (0x07)
@@ -488,7 +490,8 @@ tp=10 → entry[0]: pos=9 (tp - 1)
1. `db_sync_flush_recalc()` — синхронно завершает все отложенные пересчёты
2. Парсит `from`, `count`, `vp`, `want_from`
3. Для каждой записи (макс. 32): `si_parse_record()` → `db_record_insert()`
- Проверка Ed25519-подписи (если неверна — удаление записи)
- Проверка длины тела и подписи до чтения64 байт; проверка подписи конкретных входящих байтов
- Если строка уже есть, независимо проверяются сохранённые байты: только доказанная порча допускает DELETE
- Проверка дубликата по `(timestamp, author_signature)`
- Вычисление chain_hash
- Вставка в SQLite, запуск async recalc
@@ -497,6 +500,11 @@ tp=10 → entry[0]: pos=9 (tp - 1)
5. Если `want_from != DB_WANT_FROM_NONE`: отправляет встречный SEND_DATA
6. Если `want_from == DB_WANT_FROM_NONE` и `sync_state == 1`: отправляет `SYNC_DONE`
Ошибка формата, подписи, ключа, SQL или пересчёта прерывает раунд без подтверждения его позиций.
Уже успешно вставленные записи остаются в БД; повторный раунд принимает их как дубликаты.
При выдаче пачки сохранённая порча удаляется после завершения SELECT в своей транзакции;
пачка старого раунда не отправляется. После COMMIT сбрасываются позиции всех пиров и пересчёт начинается с0.
### 4.5 Обработка SYNC_DONE
1. `db_sync_flush_recalc()`
@@ -511,14 +519,13 @@ tp=10 → entry[0]: pos=9 (tp - 1)
1. При локальной вставке (`db_sync_insert_signed`):
- Формирует PUSH-сообщение
- Рассылает всем пирам с `sync_state >= 1`
- Обновляет `delivery_chain` для каждого пира
2. При получении PUSH:
- Вставляет запись через `db_record_insert()`
- Шлёт `ACK_PUSH` обратно автору
- После успешной вставки/корректного повтора шлёт `ACK_PUSH(message_id)` переславшему пиру
- Если запись вставлена в середину (не в конец) — сбрасывает `synced_pos` всех пиров до позиции вставки-1
3. При получении ACK_PUSH:
- Помечает запись флагом `WAS_SENT`
- Обновляет `delivery_chain`
- Для локального автора помечает запись флагом `WAS_SENT`
- Идемпотентно обновляет `delivery_chain`/`delivered_peers` по message_id
---
@@ -528,14 +535,16 @@ tp=10 → entry[0]: pos=9 (tp - 1)
Требуемый контракт удаления: завершённая проверка установила неверность подписи самих сохранённых байтов
либо доказан повреждённый формат этой записи. Отсутствие ключа, отказ OpenSSL/памяти/SQL не доказывают порчу.
Неверная входящая копия не доказывает неверность корректной строки с тем же ключом.
Текущий db_verify_author_sig объединяет эти причины, а db_record_insert и si_send_data_batch используют отказ для DELETE;
это подтверждённый неисправленный дефект, а не реализованный требуемый контракт.
db_verify_author_sig различает VALID/BAD_SIGNATURE/BAD_FORMAT/DEFERRED/ERROR.
Операционные ошибки, включая SQLITE_NOMEM при чтении BLOB, сохраняют строки.
DELETE и замена повреждённой строки атомарны; сброс позиций и планирование цепочки выполняются после COMMIT.
2. **Цепной хеш:** SHA256-цепочка защищает от вставки/удаления записей в середине истории. При проверке (`db_sync_chain_verify`) или при обнаружении расхождения во время отправки — запускается пересчёт с позиции ошибки.
3. **Публичные ключи:** Ed25519 публичный ключ пира получается из трёх источников (по приоритету):
- Локальный `inst->my_ed25519_pubkey` (для своих записей)
- `topo_node_sqlite_get_ed25519_pubkey()` (из БД узлов)
- `conn->peer_ed25519_pubkey` (из ETCP handshake)
4. **Отсутствие удаления/редактирования:** записи не редактируются и не удаляются явно — только TTL-очистка неподтверждённых.
4. **Удаление:** кроме TTL неподтверждённых, удаляются доказанно повреждённые сохранённые строки.
Полный аудит TTL, финального сравнения хешей и callbacks ещё не завершён.
---