From 143788cdcdaa916768fa14bb3d7a9db909fee42e Mon Sep 17 00:00:00 2001 From: evgeny Date: Mon, 26 Oct 2026 16:02:21 +0100 Subject: [PATCH] Document verified history deletion and message-specific delivery --- doc/member_merkle_audit_findings.txt | 78 +++++++++++++++++++++++++++- src/chat/db_sync_arch.md | 55 ++++++++++++-------- 2 files changed, 108 insertions(+), 25 deletions(-) diff --git a/doc/member_merkle_audit_findings.txt b/doc/member_merkle_audit_findings.txt index aec57cfb..30512b0a 100644 --- a/doc/member_merkle_audit_findings.txt +++ b/doc/member_merkle_audit_findings.txt @@ -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 +дальнейшее расширение текущей реализации остановлено на этом замечании. diff --git a/src/chat/db_sync_arch.md b/src/chat/db_sync_arch.md index aedc785e..922922d6 100644 --- a/src/chat/db_sync_arch.md +++ b/src/chat/db_sync_arch.md @@ -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 ещё не завершён. ---