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

+76 -2
View File
@@ -1,7 +1,9 @@
member_sync / merkle_sync — аудит контрактов, 26.10.2026
Текущий статус: продолжение аудита db_sync остановлено на подтверждённом удалении
корректной истории при ошибке проверки и испорченном входящем дубликате (раздел10).
Текущий статус: дефекты удаления корректной истории и доставки db_sync исправлены
после rebase на03b05094 (раздел11). Общий аудит db_sync остаётся частичным.
Новый check.sh:203/203, proxy9/9,burst1/1,load8/8,exit0,195с; STUN PASS отдельно и в общем наборе.
ASAN/UBSan/LSan текущего исправления:8/8 PASS с detect_leaks=1.
Дефекты очистки MEMBERS и запуска feedback исправлены и проверены.
Исключение скрывает результаты, сохраняя историю; отложенные ключи и их межканальное
разрешение поддерживаются. Архитектурные пробелы раздела8 устранены реализацией раздела9.
@@ -550,3 +552,75 @@ BAD_FORMAT, DEFERRED, ERROR), сохранение корректной суще
Предупреждение и уточнение контракта добавлены в db_sync.h/arch.md, задачи — в doc/tasks.md.
Новые полный check/sanitizers/GUI/Android/Windows/FreeBSD не запускались; STUN пропущен.
Результаты прошлых исправлений member/feedback сохраняются и не объявляются покрытием новых случаев.
11. Исправление db_sync после rebase — 26.10.2026
По запросу пользователя выполнен fetch/rebase на origin/master=03b05094.
Три локальных коммита аудита перенесены без конфликтов; range-diff:3/3 идентичны.
Резервная ветка backup/db-sync-pre-rebase-61f5ab4f сохраняет исходный HEAD.
Прежний запрет запуска STUN отменён новым запросом пользователя проверить его после rebase.
Реализация:
- Проверка конкретных байтов возвращает VALID, BAD_SIGNATURE, BAD_FORMAT, DEFERRED или ERROR.
Только SC_ERR_AUTH_FAILED завершённой операции означает BAD_SIGNATURE; CRYPTO и SQL lookup error — ERROR.
Неизвестный ключ — DEFERRED. SQLITE_NOMEM при чтении BLOB не превращается в BAD_FORMAT.
Формат здесь означает непустое тело до4000 байт и подпись ровно64 байта, не проверку JSON-схемы приложения.
- Перед удалением дубликата внутри BEGIN IMMEDIATE независимо проверяется сохранённая строка.
Корректная строка переживает неверную входящую копию. Доказанная сохранённая порча удаляется;
при корректном входе вместо неё атомарно сохраняется исправная копия.
Любой отказ чтения, DELETE, INSERT или COMMIT приводит к ROLLBACK без продвижения next_id/позиций.
- Выдача пачки проверяет длины и подпись самой сохранённой строки. SELECT завершается до DELETE по rowid.
Одна доказанно битая строка удаляется в собственной транзакции, затем пачка старого раунда не отправляется.
После COMMIT запускается пересчёт с0 и сбрасываются позиции/состояния пиров; другой канал не затрагивается.
- Recalc имеет отдельный pending, не зависящий от наличия таймера. Батч50 атомарен;
только успешный COMMIT продвигает позицию. Отказ SQL/хеша/таймера сохраняет работу.
Flush возвращает ошибку; обработчики не публикуют заведомо неготовые хеши как успешный результат.
Битая длина сохранённой подписи, обнаруженная recalc, удаляется с проверкой SQL и COMMIT.
- Парсер не передаёт короткую подпись дальше к чтению64 байт. Ошибка вставки/проверки/формата
прерывает SEND_DATA без подтверждения позиций и SYNC_DONE; успешно вставленная часть остаётся для повтора.
Старый peer-empty цикл теперь продвигается на фактически отправленное число записей при ограничении буфера.
- ACK_PUSH изменён с(timestamp,author) на message_id[32], общий размер33 с типом; совместимость не поддерживается.
SQL больше не использует отсутствующую author. Один UPDATE обновляет только нужную запись,
повтор ACK от того же пира не увеличивает счётчик; WAS_SENT выставляется только для локального автора.
Постановка PUSH в очередь доставкой не считается. Корректный дубликат PUSH тоже получает ACK.
Подготовка/bind/step/finalize SQL и неизвестный message_id диагностируются.
Проверки и доказательства:
- Прежний /tmp/db-sync-audit-verify-failure.c теперь exit0, сохранение2/2 PASS;
положительные контроли удаления неверной подписи и тела4001 PASS, другой канал и allocations сохранены.
Лог /tmp/db-sync-preservation-fixed.log. Исторический EXPLAIN старого неверного SQL
в этом стенде ожидаемо остаётся SQLITE_ERROR; новую доставку проверяют штатные тесты ниже.
- Новый tests/test_db_sync_integrity.c использует собственную SQLite, настоящие Ed25519 подписи,
uasync и точечные --wrap отказы.10 введённых отказов: VerifyInit входа/сохранённой копии/выдачи,
BLOB NOMEM во вставке/выдаче/recalc, COMMIT удаления/recalc/вставки, создание recalc-таймера.
Дополнительно реальные SQL-триггеры запрещают DELETE, обновление хеша и ACK; проверены откат и повтор.
Неизвестный ключ и SQL lookup error сохраняют историю. Проверены две таблицы, reset позиций,
замена порчи корректной копией, длины подписи0/1/63/65/255, тело4001 и55 записей (несколько батчей).
ACK для двух подписей одного автора при одном timestamp обновляет только конкретную запись и идемпотентен.
После teardown tracked allocations восстановлены. Лог /tmp/db-sync-integrity-final.log,exit0.
- tests/test_db_sync.c дополнен двумя сообщениями одного автора с одинаковым timestamp:
до poll доставки нет, затем реальные PUSH/ACK подтверждают оба сообщения независимо.
Все прежние сценарии2/3 узлов, merge, mid/tail PUSH, reply и randomized reconnect также PASS.
Лог /tmp/db-sync-network-final.log; схема — /tmp/db-sync-schema-final.log, оба exit0.
- Полный ./check.sh в отдельном root network/mount namespace:203/203 PASS,0 failed/0 skipped;
proxy9/9,burst1/1 (8388608 байт),load8/8 (134217728 байт),exit0,195с.
Лог /tmp/db-sync-full-final-check.log. Финальная целевая сборка без предупреждений.
- STUN: отдельный запуск и общий набор PASS. Прежнее падение assert(bind) после rebase не воспроизведено;
причина прежнего отказа и первый падающий коммит не установлены, исправление STUN не заявляется.
В общем STUN-логе есть штатно пропущенный подслучай bind-to-device (нет разрешения);
основные RFC5780/fallback/cache/lifecycle/isolation проверки выполнены.
Логи /tmp/stun-after-rebase.log и tests/logs/test_stun_nat.log.
- Отдельная копия HEAD9131789c: ASAN+UBSan и detect_leaks=1,halt_on_error=1;
core/libuasync/Opus согласованно инструментированы.8/8 PASS: db_sync_integrity,db_sync,
db_sync_schema,member_adapter,chat_feedback,chat_poll,chat_poll_network,merkle_protocol.
/tmp/db-sync-asan-results.log и /tmp/db-sync-asan-test_*.log; признаков sanitizer errors нет.
Границы:
Полный аудит db_sync не закрыт. В существующем db_handle_sync_done ветка одинакового count
с разными chain_hash всё ещё явно принимает состояние как converged; это отдельный дефект
финального критерия, не проверявшийся новым runtime-стендом. Wire раундов не имеет generation ID;
поведение поздних пакетов прежнего раунда требует отдельного анализа. Также остаются TTL
(единицы времени/пересчёт после удаления) и жизненный цикл callback-списка.
Эти места не включены в доказанное покрытие текущего исправления. GUI/Android/Windows/FreeBSD не запускались.
Следующий этап аудита требует отдельного исправления финального критерия; по AGENTS.md
дальнейшее расширение текущей реализации остановлено на этом замечании.
+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 ещё не завершён.
---