diff --git a/doc/member_merkle_audit_findings.txt b/doc/member_merkle_audit_findings.txt index 9b86e28b..cd299d60 100644 --- a/doc/member_merkle_audit_findings.txt +++ b/doc/member_merkle_audit_findings.txt @@ -1,6 +1,9 @@ member_sync / merkle_sync — аудит контрактов, 26.10.2026 -Текущий статус: дефекты удаления корректной истории и доставки db_sync исправлены +Текущий статус: согласованный перенос истории на MESSAGES Merkle реализован (раздел13). +ASAN/UBSan/LSan10/10 PASS; окончательный общий прогон после уточнения stream-фикстур идёт. +Результаты прежних этапов ниже не являются проверкой нового переноса. +Дефекты удаления корректной истории и доставки 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. @@ -819,3 +822,85 @@ round7,stateSERVE,READY в очереди. Применение старых PAG логическое обоснование требует п.0 и согласованного индекса, а не опирается на неизменённый старый движок. новая модель и её тесты ещё не реализованы. Перенос нельзя объявлять проверенным текущими203/203 и8/8: эти результаты относятся к уже выполненному исправлению раздела11. + +13. Реализация MESSAGES, свежий handshake и ограниченные транзакции + +Пользователь одобрил вариантБ раздела12, уточнив два обязательных условия: +- Не ограничивать исторические сообщения по deleted автора: ограничение входа в группу отдельно. +- Не фиксировать всю новую историю одной транзакцией: короткая локальная вставка/страница, + сеть вне транзакции, первичная проверка и построение индекса ограниченными порциями. + +Реализовано: +- db_sync оставляет общую SQLite, подписанные записи, публичное API и PUSH/ACK(message_id). + Собственный bulk-автомат, chain_hash, sparse-поиск и сетевой OFFSET удалены. + MT_DATA_MESSAGES=2 использует общий merkle_sync; namespace=group_id, ключ=message_id[32]. + Канонический лист включает ключ/timestamp/author/длину/точное тело/signature и исключает local id, + attrs/flags/доставку. Одинаковая подписанная история не зависит от локального порядка получения. +- Wire v4:55-байтовый заголовок с двумя случайными128-битными round_id/accept_id. + Старые PAGE/DONE/ERROR после cancel/start не проходят новый challenge; дубликаты BEGIN/READY + не сбрасывают применение. Ошибка RNG обеих сторон даёт MT_ERR_IO до установки нового раунда. + Полные корни и revision подтверждаются общими CHECK/DONE; равный count не означает успех. +- Страница MESSAGES проверяет полный32-байтовый курсор, лист, строгий порядок и подписи до BEGIN. + До16 записей и ограничение размера wire; данные/индекс/next_id фиксируются атомарно. + Обычная вставка меняет один путь дерева в своей короткой транзакции. + Preflight проверяет до16 строк вне write-lock, затем сверяет собственные байты в коротком commit. + Второй этап строит до16 листьев/путей за commit; timer1 tick возвращает управление I/O между шагами. + Число строк ограничено, но жёсткий временной предел commit не обещается: диск/наполнение листа влияют. +- VALID/BAD_FORMAT/BAD_SIGNATURE/DEFERRED/ERROR сохранены. Удаляются только доказанно неверные + сохранённые байты; SQL/OpenSSL/NOMEM/неизвестный ключ не удаляют историю и не публикуют пустой корень. + Ошибка оставляет pending; поздний общий ключ, включая событие другого канала, возобновляет preflight. + Ключ автора читается независимо от deleted; допуск пересылающего READY-пира проверяется отдельно. +- Миграция схемы сохраняет подписанные строки и local_attrs/flags/счётчики доставки. + Производные message_id/bucket вычисляются заново; ошибка DDL/copy/COMMIT откатывает перестройку. + Подпись неправильной длины — доказанный неверный формат, корректной длины проверяется preflight. +- Новые data_ops apply_page/can_run/on_result; узкая отмена dataset/ns и dataset/peer. + MESSAGES сохраняет sound_on-разрешение спящего пира; остальные модели используют default sleep policy. + Отзыв/возврат допуска и sleep/sound переоценивают сессии на прежнем UP, не затрагивая чужие datasets. + Android retry сохраняет отменяемое ожидание активной фазы, inline callback отказа памяти не рекурсивен. +- on_insert bulk отложен за пределы data_ops и видит committed страницу; rollback отменяет уведомления. + Done получает только MT_OK, включая автоматические раунды; самоотписка, удаление следующей подписки + и канала безопасны. Remove сохраняет таблицу, отменяя только её MESSAGES/таймеры/уведомления. +- TTL исправлен на миллисекунды Unix и удаляет до16 собственных неподтверждённых строк за commit. + Удаление и очистка MESSAGES-индекса атомарны; далее корень gated/pending до восстановления. + Это уточнение реализации к плану: массовая очистка сбрасывает производный индекс вместо обновления + каждого удалённого пути. Не создаёт tombstone; другая сохранённая копия может вернуться при обмене. + +Проверки новой реализации: +- test_db_sync:реальный ETCP/READY,2/3 узла, PUSH/ACK и случайные слияния;200 одинаковых записей, + затем отличие первой при равном count и сдвиг local id на900. Итог201 на обоих, полный корень одинаков. +- test_db_sync_integrity:11 введённых crypto/SQL/COMMIT/BLOB/timer отказов, реальные подписи/триггеры; + сохранность хорошей копии, доказанная порча/формат0/1/63/65/255/4001, два канала, индекс/next_id rollback. + Плотный лист40 записей, несколько страниц, вставка до курсора требует следующей сверки, replay без дублей. + Отказ второй записи и COMMIT страницы откатывает все строки/корень/уведомления; on_insert только после commit. + Подписи preflight проверяются при sqlite3_get_autocommit=1. Разные id/attrs/flags/доставка не меняют корень. + История deleted автора выдаётся; поздний ключ через событие другого канала сохраняет строку и запускает индекс. + TTL дренирует19 expired в двух коротких commit и оставляет fresh/ACKed. Tracked allocations восстановлены. +- test_merkle_protocol:старые токены/фазы/курсоры, reattach, дубликаты BEGIN/READY, RNG обеих сторон, + backpressure, timeout, apply/SQLite faults, модельный sleep-допуск и восстановление на том же соединении. +- test_db_sync_schema:сохранность полей, повторный restart, rollback DDL после copy, очистка только доказанной порчи. +- test_dm_invite:настоящий JOIN и UTUN_TEST_HISTORY_TIMEOUT=1; принудительно потерянный обмен30с + завершается timeout, повтор восстанавливает историю, тест exit0. +- Общий первый прогон199/203:две изолированные db_sync-фикстуры не создавали общий движок, + purge-фикстура использовала недопущенного peer2; исправлены setup/teardown и реальный допущенный peer. + Повтор202/203, STUN PASS:stream-сценарий зависел от standby предыдущих сценариев. + DEBUG и SQL-диагностика показали новые версии только у автора и sleep=1 на центральном пиру; + wake-only PASS. Тест явно восстанавливает activity центрального узла и пробуждает lifecycle-получателя + после сохранённой проверки перехода forced-active → standby. Production stream не менялся. +- ASAN/UBSan/LSan10/10 PASS,detect_leaks=1:db_sync,integrity,schema,merkle_protocol,merkle_sync, + member_adapter,chat_feedback,chat_poll,chat_poll_network,dm_invite. Core/libuasync/Opus инструментированы + согласованно в /tmp/utun-member-purge-sanitizers. Нативные/ASAN-объекты не смешивались. + Отдельной Makefile-цели test_member_sync нет: исключена ошибочная попытка implicit build; + соответствующие проверки входят в member_adapter. Новая sanitizer-сборка/запуск10 целей exit0. +- Временная цель через tests/Makefile: /tmp/history-standby-check.c/.mk, макрос UTUN_HAVE_STANDBY, + настоящий history-код/SQLite/uasync и управляемый standby callback. Inline OOM даёт один retry без + рекурсии, wake освобождает ожидание, allocations восстановлены;exit0. Это не запуск Android-приложения. + +Документы db_sync.h/db_sync_arch.md и merkle_sync.h/merkle_sync_doc.md приведены к реализации. +Ограничения:индекс согласован при записи через модель; произвольная внешняя порча SQLite +выявляется при соответствующем чтении/перепроверке, непрерывный scrub всех байтов не заявляется. +Тесты на несколько тысяч записей, отдельные изменения строго между CHECK/DONE и платформенные +GUI/Android/Windows/FreeBSD прогоны в этом этапе не выполнялись. Общий аудит чата частичный. +Логи: /tmp/history-merkle-asan-results.log, /tmp/history-merkle-asan-test*.log, +/tmp/history-merkle-final-test_db_sync_integrity.log, /tmp/history-merkle-final-test_merkle_protocol.log, +/tmp/history-merkle-dm-timeout.log, /tmp/history-standby-check.log. +Окончательный общий прогон /tmp/history-merkle-verified-check.log пока выполняется. diff --git a/tests/test_chat_sync_stress.c b/tests/test_chat_sync_stress.c index 24932748..c8f759fc 100644 --- a/tests/test_chat_sync_stress.c +++ b/tests/test_chat_sync_stress.c @@ -209,10 +209,11 @@ int main(void) { uint32_t records_a = 0, records_b = 0; uint64_t t_insert = get_time_tb(); + int large = getenv("UTUN_TEST_HISTORY_LARGE") != NULL; for (int b = 0; b < TOTAL_BATCHES && test_phase == 0; b++) { - int n = rand() % (MAX_PER_BATCH + 1); + int n = large ? 250 : rand() % (MAX_PER_BATCH + 1); if (n == 0) continue; - int side = rand() & 1; + int side = large ? b & 1 : rand() & 1; struct DB_SYNC_INSTANCE* si = side ? si_b : si_a; uint64_t node = side ? NODE_ID_B : NODE_ID_A; diff --git a/tests/test_merkle_protocol.c b/tests/test_merkle_protocol.c index 87949eed..96fed132 100644 --- a/tests/test_merkle_protocol.c +++ b/tests/test_merkle_protocol.c @@ -285,6 +285,21 @@ static void test_send_rejection(void) { t_destroy(&f); puts("PASS: transport rejection retains ownership; packet is freed exactly once"); } +static void test_checkpoint_revision(void) { + for (int changed = 0; changed < 2; changed++) { + struct fixture f; t_init(&f); + struct ms_session* s = t_session(&f); s->state = MS_SERVE; s->waiting = 0; + uint8_t root[32] = {0}; + t_receive(&f, MS_CHECK, 7, 0, root, sizeof(root)); + assert(s->tx && s->tx->dgram[9] == MS_DONE && !f.callbacks); + if (changed) merkle_sync_changed(&f.inst, MT_DATA_MEMBERS, "1001"); + uasync_poll(f.inst.ua, 0); + if (changed) assert(!f.callbacks && s->requests && s->revision != s->verified_revision); + else assert(f.callbacks == 1 && f.result == MT_OK && !s->requests); + t_destroy(&f); + } + puts("PASS: change between CHECK and DONE handoff retains the request for a new checkpoint"); +} static void t_cancel_done(uint64_t peer, const char* ns, int result, void* arg) { struct fixture* f = arg; @@ -478,7 +493,7 @@ int main(void) { size_t baseline = u_get_allocated_count(); test_malformed_and_old_round(); test_rejected_data(); test_sibling_walk(); test_cancel_waiters(); test_page_size_and_cursor(); test_readonly_tree(); test_backpressure_and_disconnect(); test_peer_restart(); - test_send_rejection(); + test_send_rejection(); test_checkpoint_revision(); test_error_handoff(); test_blocked_error_timeout(); test_pull_lifecycle(); test_dataset_isolation(); test_full_cursor(); test_permission_rounds(); test_model_policy(); test_reattach_tokens(); test_handshake_failures(); assert(u_get_allocated_count() == baseline);