14 KiB
member_sync — адаптер участников каналов
Поверх merkle_sync синхронизирует source=0 записи таблиц peers_<channel_id>. Транспортный движок не занимается подписями и версиями участников.
Сверка контрактов member_sync.h и этого документа с реализацией завершена 26.10.2026. Найденные дефекты очистки, зависимых состояний FEEDBACK и интеграции MESSAGES исправлены. Остальные модули чата проверяются отдельно. Объём и результаты: отчёт, разделы 6, 9 и 13.
Использование
- member_sync_init вызывается при инициализации chat_sync и перестраивает производный индекс.
- member_sync_start подключает канал к пиру и запрашивает подтверждение совпадения данных.
- member_sync_put изменяет локальную запись, атомарно обновляет индекс и после commit уведомляет движок.
- Прямые broadcast/send_to не нужны: локальная версия возвращается второму узлу при обратном проходе.
- cancel/destroy снимают ожидания; отдельного публичного API рассылки нет.
Записи и применение
У записи два независимо версионируемых блока: участник (join_ts, update_ts) и владелец (adm_tags.ver). Непустой owner-блок — целый JSON-object с уникальными ключами и строковыми значениями. ver — каноническая десятичная строка1..INT_MAX,без знака/ведущих нулей/суффикса; пустой блок имеет версию0. Проверка member_sync_owner_version не заменяет проверку подписи. Неверный формат или SQL-отказ не превращаются в нулевую версию для замены записи. Удаление используется для повреждённых записей. Конфликт одинаковых версий с разным содержимым не разрешается порядком доставки: отсутствие прогресса завершает Merkle-раунд ошибкой конфликта.
Входящая страница сначала целиком проверяется на корректность раскладки, длин и строк. Затем записи применяются под savepoint member_page. Ошибка откатывает всю страницу вместе с индексом. Уведомления GUI/топологии и changed выполняются после commit. Каждый member_sync_apply_record также имеет savepoint, поэтому ошибка пересчёта не оставляет запись без индекса. Неудачный ROLLBACK TO не сопровождается RELEASE частичных данных:собственная операция пробует полный ROLLBACK,при чужой транзакции полный откат обязан выполнить caller. При внешней транзакции вызывающая сторона после commit вызывает member_sync_record_committed с полной маской результата для каждой изменённой записи: одного changed недостаточно для уведомления свойств и планировщика группы. Локальный put доверяет блоку участника и подписи приглашения; подпись непустого блока владельца проверяется также локально.
Редактор chat_member_tags загружает проверенный owner-снимок и хранит его до commit. Собственная BEGIN IMMEDIATE сравнивает исходное содержимое/подпись с текущими; устаревший editor возвращает-2 без перезаписи. Версия/история ролей и подготовленный editor обновляются только после commit;SQL/подпись/память сохраняют возможность повтора. No-op сохраняет версию/подпись без commit hook/уведомлений. admin/moder дописывают e/d,не удаляют историю. GUI trampoline применяет частичный объект к загруженному снимку,сохраняя неизвестные поля и роли,не указанные формой. Контракты загрузки/замены/commit и срок жизни editor описаны в chat_core.h.
MS_APPLY_FEEDBACK отмечает изменение удостоверения, trusted key или deleted, влияющее на feedback. Такое изменение и пересчёт зависимых feedback-листьев во всех каналах входят в транзакцию MEMBERS. Ошибка пересчёта откатывает запись, общий ключ и оба индекса; сохранённый feedback не удаляется. Обычное обновление имени/профиля/stream options не выставляет этот бит и не запускает feedback-раунды. При обработке страницы маска сохраняется до внешнего commit; JOIN также передаёт полный результат. После commit разрешённые этим битом уведомления будят feedback и обновляют адресные результаты. member_sync_record_committed также переоценивает допуск пересылающих пиров MESSAGES и ускоряет проверку истории, ожидающей общего ключа автора, включая другие каналы. Это не фильтр исторических сообщений по deleted автора; допуск истории описан в db_sync_arch.md.
verify_and_purge владеет отдельной транзакцией канала: требует autocommit на входе, открывает BEGIN IMMEDIATE до чтения, проверяет записи и размеры BLOB/строк, сохраняет собственный список повреждённых node_id и закрывает SELECT. Удаление строк, пересчёт MEMBERS и зависимых feedback-листьев выполняются до общего COMMIT. Если FEEDBACK ещё не зарегистрирован при startup purge, его индекс позднее строит chat_feedback_init. При отказе чтения/проверки/памяти/SQL/индекса/commit выполняется rollback; ошибка возвращается как -1 без публикации удаления. Чужая транзакция отклоняется до изменений. Если SQLite не может выполнить rollback и транзакция остаётся открытой, ошибка явно логируется и сбрасывается inst.running: требуется перезапуск приложения.
После commit очистка уведомляет оба набора Merkle и затронутые результаты, снимает mute, удаляет соединение из конкретной CHAT-группы и уведомляет props-потребителей; общий транспорт других владельцев сохраняется. Результат >=0 — число подтверждённых удалений. Ошибка публикации после commit не означает откат уже сохранённых данных. Props-подписчик может снять подписку и повторно вызвать очистку; stop/destroy выполняются вне callbacks.
Локальный верификатор возвращает 1 (сохранить), 0 (доказанная порча), -1 (операционная ошибка). Отсутствие подписи/ожидаемой зависимости сохраняет запись; отказ SQL/OpenSSL не считается доказательством неверной подписи. verify_signature отдельно различает MS_VERIFY_DEFERRED и MS_VERIFY_ERROR. Проверка source=1 placeholders не выполняется. verify_and_purge_all останавливается на первом отказе; атомарность — по каналу, результаты ранее очищенных каналов сохраняются. init освобождает движок при отказе очистки, start отклоняет запрос до создания Merkle-сессии, без done callback.
Проверяются привязка node_id к X25519, доверенный Ed25519, подписи блока участника и владельца. Для дерева приглашений родитель может прийти позже ребёнка; finish повторно проверяет зависимости перед подтверждением корня. Это не отдельная карантинная таблица: промежуточные записи существуют в БД до finish, поэтому MT_OK нельзя заменять фактом их присутствия. finish проверяет имеющиеся ненулевые подписи непосредственных пригласивших; рекурсивного обхода цепочки предков нет, отсутствие сертификата само по себе пропускается. validate_pubkey и validate_peer проверяют наличие строки без фильтра source/deleted; допуск CHAT-группы отдельно проверяет topo_node_sqlite_member_allowed. author_key требует source=0/deleted=0, доверенный ключ, join и сертификат непосредственного пригласившего. Возвращает ACTIVE, INACTIVE, PENDING, INVALID или ERROR; выходной ключ меняется только при ACTIVE. PENDING означает недостающий ключ/сертификат, а не отзыв членства или ошибку SQLite/OpenSSL. source=0 само по себе не доказывает полный допуск: dependency может разрешиться позднее. Ключи nodes общие; поступление ключа через другой канал пересматривает авторов с таким node_id/signed_by. verified_ed25519 сохраняет строгий контракт 0/-1 для radio/attachments и разрешает только ACTIVE.
Канонический хеш
SHA256 по полям: node_id:8, x25519:32, ed25519:32, join_sig:64, join_ts:8, update_sig:64, update_ts:8, userinfo_len:2, userinfo, adm_tags_len:2, adm_tags, adm_tags_sig:64, signed_by:8, signature:64.
Целые big-endian; отсутствующие подписи представлены нулями. userinfo ограничен 1024 байтами, adm_tags — 8192 байтами UTF-8 без NUL. Обе длины — uint16 big-endian. Хеш и подпись владельца покрывают весь adm_tags без обрезания. Хеш включает userinfo и дерево приглашений. Формат сообщений, подписываемых моделью, определяется build_join_msg/build_update_msg/build_sign_msg отдельно.
Страница на проводе
Начинается с count:2 big-endian. Далее count записей: node_id:8, x25519:32, ed25519:32, flags:1, при HAS_JOIN — join_sig:64 и join_ts:8, update_sig:64, update_ts:8, userinfo_len:2, userinfo, adm_tags_len:2, adm_tags, adm_tags_sig:64, signed_by:8, signature:64.
Целые big-endian. Встроенные NUL и хвостовые байты запрещены. get_page перечисляет только один лист, в возрастающем порядке node_id, строго после курсора. SQLite использует signed INTEGER; внутри одного листа знак ключей одинаков, поэтому SQL-порядок совпадает с unsigned. Следующая запись, не помещающаяся в буфер, остаётся для следующей страницы. Ошибки SQL и повреждённые размеры BLOB возвращаются вызывающему коду.
Диагностика
Категория member_sync: ошибки содержат канал, node_id и причину отказа. DEBUG показывает применение версий и размеры страниц, TRACE — входы и расчёт листа. Хеши родителей вычисляет merkle_tree; адаптер никогда не перечисляет все записи для внутреннего узла.