Обратная связь с мемпулом
Коллация не видит внутреннего устройства DAG-мемпула напрямую. Якоря с внешними сообщениями приходят снаружи, само внешнее сообщение тоже сначала проходит через мемпул, а мастерчейн время от времени меняет то, что мемпулу нужно знать: набор валидаторов, конфигурацию консенсуса, границу уже обработанных якорей. Всю эту границу закрывает один компонент, адаптер мемпула — общая точка для разных потоков: через него коллатор читает якоря, сеть кладёт внешние сообщения на обработку, а менеджер коллации сообщает мемпулу, что изменилось на стороне блокчейна.
Адаптер мемпула как единственная точка соприкосновения
Домен коллации общается с DAG-мемпулом через единственный интерфейс из семи методов.
Два метода несут обратную связь от коллации к мемпулу: сообщить об обновлении состояния мастерчейна (handle_mc_state_update) и сообщить, что мастер-блок с таким seqno подписан (handle_signed_mc_block). Второй ещё и применяет к мемпулу отложенные изменения наборов валидаторов и конфигурации, а при первом вызове запускает мемпул.
Два метода отдают якоря коллатору: получить якорь по идентификатору и получить якорь, следующий за указанным. Оба ждут, если якоря ещё нет. Такое чтение делает только коллатор, не менеджер коллации.
Оставшиеся три метода служебные: выбросить из кэша якоря до заданной границы (синхронный вызов), положить в мемпул внешнее сообщение (тоже синхронный) и задать конфигурацию консенсуса и genesis из глобальной конфигурации узла ещё до старта мемпула.
Рабочая реализация держит сессию настоящего движка консенсуса, собственную базу мемпула и буфер входящих внешних сообщений. Однонодовая — для сети из одного узла: без движка консенсуса и без базы, якоря она печатает сама по таймеру. Заглушка нужна для тестов и отладки и генерирует якоря синтетически. В сборке узла она не участвует. Узел выбирает между рабочей и однонодовой при старте, по флагу режима одного узла.
Внешние сообщения входят в узел не через коллацию, а прямо в мемпул. Со стороны сети, а не со стороны коллации, вызывается только один метод адаптера — тот самый, что кладёт сообщение на обработку. И рабочая, и однонодовая реализации в нём делают одно и то же: кладут сырые байты во входной буфер движка консенсуса, без разбора и без очереди коллатора. Отсюда асимметрия границы. Внешнее сообщение попадает в узел через мемпул и возвращается в коллацию уже упорядоченным, внутри якоря, а коллатор напрямую внешние сообщения никогда не принимает.
Что коллатор получает от мемпула
Якорь, который адаптер отдаёт коллатору, состоит из пяти полей: идентификатора (того же номера, что и раунд мемпула, в котором якорь произведён), ссылки на предыдущий якорь (пусто у якоря сразу после genesis или после невосстановимого разрыва), автора якоря, времени цепочки в миллисекундах и списка внешних сообщений. У якоря есть три метода чтения сообщений от смещения: посчитать сообщения для шарда, проверить, есть ли хоть одно, получить итератор с индекса. Это то же самое смещение, с которым коллатор возвращается к частично обработанному якорю.
Готовые якоря лежат в общем кэше адаптера — упорядоченной по идентификатору карте под замком чтения-записи (RwLock), рядом с одним уведомлением для ожидающих. Оба метода чтения устроены одинаково: читатель сначала подписывается на уведомление и только потом заглядывает в карту. Такой порядок закрывает гонку, в которой якорь добавляется между проверкой и началом ожидания.
Запрос якоря по идентификатору либо возвращает якорь, либо честно отвечает, что его не будет, либо ждёт: промежуточного «пока нет» тут не предусмотрено. Запрос следующего якоря устроен так же, но дополнительно сверяет ссылку найденного якоря на предыдущий. Ошибка возвращается, только если эта ссылка есть, но противоречит содержимому кэша. В одном случае ссылка найденного якоря указывает мимо запрошенного, хотя сам найденный якорь стоит в кэше сразу следом за ним. В другом ссылка указывает ровно на запрошенный якорь, хотя самого запрошенного в кэше уже нет. Так адаптер обнаруживает нарушенную связность собственного кэша. Отсутствие ссылки на предыдущий якорь ошибкой не считается: так помечен якорь сразу после genesis или после разрыва истории.
Повторная вставка якоря с уже занятым идентификатором допустима, только если содержимое совпадает целиком. Иначе это форк якоря: расхождение по ссылке на предыдущий, времени цепочки, числу сообщений или их составу либо по автору. На такой вставке кэш возвращает ошибку, а обработчик якорей останавливает свою задачу — продолжать коллацию на несогласованной истории мемпула нельзя.
Дедупликация и сборка якоря
Мемпул отдаёт адаптеру не готовые якоря, а поток из трёх видов сообщений: смена паузы, разрыв истории или закоммиченный якорь. Первое отражается в кэше флагом. Второе пересоздаёт дедупликацию заново, потому что после разрыва окно уникальности заведомо неполно и доверять ему нельзя. Только третье превращается в якорь и попадает в кэш.
Сборка якоря — тяжёлая работа, и она вынесена в отдельный поток. История якорей разворачивается в полезные нагрузки, а те проходят две ступени дедупликации. Сначала, ещё до разбора, по хешу сырых байтов: так дёшево отсеиваются побайтово одинаковые повторы. Затем уцелевшие байты разбираются в сообщения, и уже по хешу разобранной ячейки отсеиваются копии, отличавшиеся только представлением байт. За каждую ступень отвечает свой дедупликатор, и оба помнят сообщение ограниченное число раундов: если сообщение приходит снова, этот срок продлевается заново. Поэтому сообщение, которое клиент шлёт непрерывно, никогда не станет уникальным повторно, а сообщение, замолчавшее дольше окна, станет.
В сети из одного узла мемпула в привычном виде нет вовсе: адаптер сам печатает якорь по таймеру из накопленных во входном буфере сообщений, пропуская их через ту же дедупликацию. Время цепочки в таком якоре — это часы самого узла, а не согласованное время консенсуса, поэтому все детерминированные по времени решения коллации в этом режиме опираются на локальные часы. Обратная связь тоже упрощена: обновление состояния сразу применяется без очереди обновлений состояния, потому что в сети из одного узла этот же узел сам подписывает каждый мастер-блок.
Контекст обновления состояния мастерчейна
Когда менеджер коллации сообщает мемпулу, что изменилось на стороне блокчейна, он собирает один снимок — контекст обновления состояния из девяти полей: идентификатор мастер-блока и его время цепочки, верхний обработанный якорь, сведения о консенсусе и конфигурация консенсуса, флаг перемешивания валидаторов мастерчейна и три набора валидаторов (прежний, текущий и следующий), каждый вместе со своим хешем.
Контекст собирается в одном месте, из уже готовых данных мастерчейна и кэша наборов валидаторов, без обращений к диску. Разбор набора валидаторов из конфигурации — операция недешёвая, а сами наборы меняются редко, раз в сессию, поэтому кэш сравнивает лишь хеш и переиспользует уже разобранный набор, если тот не изменился.
Из контекста адаптер выводит для мемпула две вещи: полные наборы валидаторов в порядке конфигурации блокчейна и подмножества валидаторов мастерчейна, но только для прежнего и текущего наборов. Подмножество следующего набора посчитать нельзя: его раунд переключения набора валидаторов ещё неизвестен, а без него порядок после перемешивания не определён.
Адаптер избегает пересылать этот пересчитанный набор мемпулу заново без нужды: рабочая реализация хранит последний переданный контекст и сравнивает его с новым не по содержимому, а по идентификатору из семи значений, из которых все наборы и подмножества выводятся детерминированно. Если наборы совпали, пиры мемпулу заново не передаются. Отличаются — передаются. Такое сравнение работает только пока genesis не менялся: смена genesis обрабатывается не обновлением пиров, а перезапуском сессии мемпула.
Два сигнала мемпулу
Об одном и том же мастер-блоке менеджер коллации сообщает мемпулу двумя сигналами, и порядок между ними жёсткий.
Первый — уведомление об обновлении состояния мастерчейна, тот самый контекст, переданный мемпулу. Он отправляется по мастер-блоку, о котором менеджер уже знает, но который ещё может быть не подписан: сразу после успешной коллации мастер-блока, если конфигурация консенсуса не менялась, при обработке отложенного обновления в момент коммита или после синхронизации на применённый из сети мастер-блок.
Второй сигнал — сообщение о подписи мастер-блока, за которым сразу следует чистка кэша якорей адаптера по границе верхнего обработанного якоря. Второй сигнал отправляется, только когда блок уже подписан и сохранён, и всегда позже первого сигнала по тому же блоку. Три точки его отправки различаются источником верхнего обработанного якоря:
- по событию применения собственного блока — минимум из якоря самого мастер-состояния и якорей обновившихся топ-блоков шардов;
- при коммите уже применённого из сети мастер-блока — запись кэша блоков;
- после синхронизации — данные мастерчейна.
Смысл разделения — в необратимости: изменения наборов валидаторов и конфигурации консенсуса, а тем более сдвиг границы известных якорей, нельзя применять по блоку, который ещё может не набрать подписей. Первый сигнал только доставляет снимок состояния, разрешение применить его даёт второй.
Неподписанные обновления не пропадают, а ждут в очереди обновлений состояния. Она хранит неподписанные версии по seqno мастер-блока и различает две границы: последнее уже отданное мемпулу обновление и последний подписанный блок. Обновление, пришедшее позже уже отданного, встаёт в очередь, а версия по тому же seqno, что уже была в очереди, заменяется новой. Сообщение о подписи блока с номером N сразу выпускает из очереди все накопленные обновления вплоть до N включительно, строго по возрастанию seqno: так одна подпись может протолкнуть мемпулу сразу несколько ранее накопленных версий. Из этого правила есть только два исключения, и сама очередь обслуживает оба: обновление по нулевому seqno (инициализация состояния) и обновление, пришедшее уже по подписанному блоку. Оба отдаются мемпулу немедленно, минуя ожидание в очереди.
Отложенное обновление при смене конфигурации консенсуса
Отдельный случай — когда собственный собранный менеджером кандидат мастер-блока сам меняет конфигурацию консенсуса. Уведомлять мемпул сразу тогда нельзя: смена конфигурации консенсуса заводит новый genesis, а применить его по ещё не подписанному блоку значило бы перезапустить мемпул на блоке, который может не набрать подписей. Поэтому менеджер откладывает не только уведомление, но и всю работу с сессиями коллации: данные мастерчейна ложатся в отдельный слот на одно значение, а сама работа не начинается вовсе, пока блок не подтверждён.
Отложенное обновление состояния разбирается при коммите подтверждённого мастер-блока, и разбирается избирательно. Если в слоте лежат данные для блока не старше коммитимого, слот очищается в любом случае, но к обработке данные берутся только при точном совпадении блоков. Если совпадение есть, менеджер уведомляет мемпул тем же вызовом, что и в обычном случае, а затем, если это не запрещено отдельным флагом на случай уже запущенной отмены коллации, продолжает работу с сессиями.
Эта работа с сессиями коллации и коллацией — отдельная процедура менеджера с тремя режимами: обновить сессии и запустить или возобновить коллацию, обновить только сессии без запуска коллации (например, если синхронизация ещё не дошла до последнего мастер-блока пачки) или не трогать сессии вовсе. Именно первый режим срабатывает и в обычном случае, и после разбора отложенного обновления.
В этом режиме менеджер по каждому шарду решает, сохранить его сессию коллации или заменить: сессия сохраняется, только если совпали короткий хеш подмножества валидаторов и идентификатор сессии верификации — пара «catchain_seqno плюс раунд переключения набора валидаторов». При любом расхождении старая сессия закрывается, а на её место запускается новая, даже если состав шарда не менялся. Отсюда практический эффект отсрочки: пока мастер-блок с новой конфигурацией консенсуса не подтверждён, сессии коллации на местах продолжают работать по старому набору и переключаются только вместе с мемпулом, одним и тем же вызовом.
Смена genesis на стороне адаптера
Пока менеджер коллации откладывает уведомление на своей стороне, адаптер на своей стороне готовится к смене заранее. Обработчик обновления состояния проверяет ожидаемую смену genesis ещё до постановки контекста в очередь, то есть по блоку, который может быть ещё не подписан. Если сессия мемпула уже запущена и новый genesis перекрывает genesis текущей сессии, адаптер запоминает seqno этого блока и сразу закрывает кэш якорей на чтение по границе нового genesis. С этого момента и до подписи блока коллатор не получает от мемпула ни одного якоря выше этой границы. Закрытие ничего не удаляет, оно только прячет от читателей всё, что лежит или появится выше неё, — судьбу этого хвоста решит переоткрытие.
Кэш переоткрывается при обработке контекста уже подписанного блока, когда обновление наконец доходит: из очереди, которую выпускает сообщение о подписи, либо, в двух исключениях, о которых шла речь выше, напрямую от обновления состояния мастерчейна, минуя очередь. Хвост, ещё не прочитанный к этому моменту, сбрасывается, если новый genesis действительно перекрывает genesis сессии, и остаётся нетронутым иначе. Если сессии мемпула ещё не было вовсе, адаптер собирает конфигурацию первого старта. Развилка одна: есть ли в глобальной конфигурации узла genesis, перекрывающий genesis состояния. Если да — он и побеждает genesis из состояния, но только если его стартовый раунд не отстаёт от верхнего обработанного якоря, а время не отстаёт от времени цепочки блока. Если хотя бы одно из условий нарушено, старт прерывается ошибкой. Если перекрывающего genesis в конфигурации узла нет, genesis и конфигурация консенсуса берутся из состояния мастерчейна как есть.
Если сессия уже работает, genesis нового контекста сверяется с genesis сессии:
- совпал — пиры сессии обновляются, но только если изменился набор данных, из которого они выводятся, а сама сессия продолжает работать;
- отличается, но не перекрывает текущий — сессия тоже остаётся, а в лог уходит предупреждение о редком случае узла, стартовавшего с пустой базой;
- перекрывает — сессия останавливается, в конфигурацию будущей сессии записываются новый genesis и новая конфигурация консенсуса из контекста, и адаптер тут же запускает новую сессию заново.
Новая сессия стартует от верхнего обработанного якоря подписанного блока и отказывается стартовать, если оценка нужной глубины истории не дотягивается до раунда переключения набора валидаторов, действовавшего прежде: тогда узел не смог бы восстановить историю, в которой этот набор ещё участвовал. Проверяется и размер текущего набора валидаторов: для одного узла есть отдельная реализация адаптера, а при наборе валидаторов ровно из двух узлов адаптер, в обеих реализациях, отказывается стартовать.
Коллация
Карта раздела Collator — место коллатора в узле, вход и выход коллации, четыре меры времени и как части раздела опираются друг на друга
Верификация мастер-блока
Как верификатор узла собирает подписи набора валидаторов на локально построенный мастер-блок: сессии на приватных оверлеях, порог по суммарному весу, кэш подписей на будущие блоки и путь подписей в доказательство блока