Клиент оверлея
Публичный оверлей даёт узлу список известных участников и правила членства (см. статью «Оверлеи»), но сам не решает, кому из них адресовать конкретный запрос и как понять, что участник отвечает исправно. Эту прикладную надстройку даёт клиент публичного оверлея: он строится из сетевого слоя, самого оверлея и своей конфигурации и держит два независимых пула пиров: соседей, через которых приложение отправляет запросы и скачивает данные, и валидаторов, которых отслеживает отдельно как цели широковещания. Клиент дёшево клонируется и разделяет одно состояние между держателями. Он живёт в крейте tycho-core, а не в tycho-network: это потребитель сетевого слоя, а не его часть.
Набор соседей
При создании клиента соседи набираются случайным выбором из текущих записей публичного оверлея — участник оборачивается в сущность «сосед» с его идентификатором узла, сроком годности, унаследованным от срока действия записи оверлея, и стартовым временем ответа из конфигурации. Выбор на этом шаге чисто случайный: у только что заведённого соседа ещё нет очков надёжности, поэтому все стартуют с одинаковым баллом, а предпочтение между соседями появляется позже, уже из оценки надёжности. Размер набора ограничен параметром keep. Если в оверлее записей меньше, соседей окажется меньше.
Дальше пять фоновых задач клиента поддерживают набор, у каждой — независимый цикл выполнения и своя ответственность: обновление подбирает и вытесняет соседей, пинг проверяет их живость, перенос очков актуализирует индекс выбора, очистка пересчитывает индекс не по таймеру, а по уведомлению оверлея об удалении записи, а метрики экспортируют статистику соседей (единственная из пяти, которую можно выключить отдельным флагом). Все задачи привязаны к жизни клиента и снимаются при его уничтожении. Сами по себе они касаются только соседей: отслеживание валидаторов ведут отдельные фоновые задачи внутри резолвера валидаторов.
Обновление набора
Периодическая задача обновления подбирает из записей оверлея случайных кандидатов с запасом, компенсирующим уже выбывших, и передаёт их в обновление набора соседей. Обновление в свою очередь сначала удаляет из текущего набора ненадёжных (очки ниже порога) и просроченных по сроку годности соседей, затем — если набор всё ещё заполнен до предела — дополнительно вытесняет соседа с наихудшими очками, и только после этого добавляет новых кандидатов, пока размер снова не достигнет keep. Уже присутствующий в наборе кандидат из списка новых исключается, чтобы не задвоиться. Если в оверлее нет ни одной записи, задача не крутит таймер вхолостую, а ждёт уведомления оверлея о появлении записей и только тогда продолжает.
Пинг
Отдельная задача периодически берёт список активных соседей, выбирает из него одного случайного и посылает ему оверлейный запрос проверки живости. Корректный ответ засчитывается соседу как успех, любой сбой — неверный ответ, сетевая ошибка или таймаут — отражается на его статистике. Пинг бьёт по всем известным соседям, а не только по уже надёжным: так свежий или временно просевший сосед получает шанс восстановить очки в паузах между прикладными запросами.
Оценка надёжности и наказания
У каждого соседа есть очки надёжности — счётчик в диапазоне от 0 до 128, стартующий с 64. Каждый учтённый запрос двигает этот счётчик: успех прибавляет 8 очков, неуспех вычитает 8, причём арифметика насыщающая и за пределы диапазона не выходит. Учёт запроса заодно сдвигает битовую историю последних неудач и обновляет скользящее среднее время ответа соседа.
Транспортные исходы (успех, сетевая ошибка, таймаут) клиент распознаёт и учитывает сам, но корректность ответа по существу знает только прикладной код: ответ на запрос отдаётся ему с методами подтверждения и отклонения, а получение ответа и решение по нему разделены — это решение можно вынести и позже. Разделение намеренное: клиент оверлея не понимает прикладную семантику, поэтому доставленный, но негодный по смыслу ответ должен пометить как отклонённый именно вызывающий код, иначе плохой сосед не потеряет очки.
Сосед считается надёжным, пока его очки не ниже порога 16. Ненадёжные соседи выбывают и при обновлении набора, и при пересчёте индекса выбора. Стартовые 64 очка дают новичку заметный запас над этим порогом: чтобы выпасть, сосед должен накопить неудачи или разом получить наказание за злонамеренность.
Поверх обычного штрафа за неуспешный запрос существует отдельное наказание пира: разовое вычитание фиксированной величины за распознанную провинность. Мелкая провинность вроде бессмысленного ответа вычитает 4 очка, медлительность (таймаут или медленное соединение) вычитает 8, а злонамеренность вычитает 128, то есть весь диапазон, обнуляя очки соседа сразу. Вид наказания зависит от типа ошибки: таймаут отправки или запроса и сетевая ошибка таймаута дают наказание за медлительность, а сетевые ошибки битого адреса или сертификата, как и обращение к пиру, неизвестному транспорту, дают наказание за злонамеренность. Прочие сетевые ошибки отдельно не наказываются: они лишь учитываются как обычный неуспешный запрос с обычным штрафом. Так «медленный, но живой» сосед теряет немного и может восстановиться, а сосед с битым сертификатом, адресом или вовсе неизвестный транспорту обнуляется сразу.
Выбор адресата запроса
Когда клиенту нужен адресат и явный сосед не задан, он выбирает одного из соседей вероятностно, а не всегда лучшего: чем выше расчётный балл соседа, тем выше вероятность, что выберут именно его, но и слабые соседи не исключены из розыгрыша полностью — если их балл вообще участвует в выборе.
Для этого сырые очки надёжности соседа перед попаданием в индекс выбора корректируются. Если последние четыре запроса к соседу подряд неуспешны, из балла вычитается штраф в 16 очков. Иначе, если очки уже высоки (не ниже 120), начисляется бонус за скорость: при среднем времени ответа меньше 160 мс бонус растёт по мере уменьшения этого времени, минимум на 1 и максимум на 16 очков. Штраф за серию отказов и бонус за скорость взаимоисключающи в одном пересчёте. Итоговый балл участвует в выборе, только если он не ниже порога надёжности 16, иначе сосед из индекса исключается вовсе. Лишь уже хорошо зарекомендовавшие себя соседи получают бонус за скорость: быстрый, но ненадёжный сосед приоритета не получит.
Сам выбор устроен как взвешенная случайная выборка: соседи выстроены по накопленным весам, равным их расчётному баллу, и в диапазоне суммарного веса выбирается случайное число, которое и указывает на выбранного соседа. Так нагрузка распределяется по нескольким хорошим соседям, а не концентрируется на одном.
Когда адресатов нужно сразу несколько, чтобы разослать запрос параллельно, используется отдельный алгоритм: взвешенная выборка без повторов по тем же расчётным баллам, с необязательным фильтром, оставляющим только надёжных соседей среди выбранных. Фактическое число возвращённых адресатов может быть меньше запрошенного: и потому что соседей всего меньше, и потому что фильтр надёжности отсеивает часть уже выбранных.
Очки соседей меняются на каждом запросе, но в индекс выбора переносятся не сразу, а пакетно: периодическая фоновая задача переносит накопленные очки в индекс на своём интервале, а дополнительно это же происходит каждый раз, когда оверлей уведомляет об удалении своей записи — так соседа, которого оверлей уже счёл выбывшим, убирают из выбора быстро, не дожидаясь таймера. Оба пересчёта заодно отбрасывают из набора ненадёжных и просроченных соседей и защищены сравнением с обменом: если набор соседей успел параллельно измениться, устаревший пересчёт отменяется, чтобы не затереть более свежий набор.
Валидаторы и широковещание
Клиент отслеживает валидаторов отдельным механизмом: резолвером валидаторов, который не пересекается с пулом соседей и не участвует в их оценке надёжности. Резолверу подаётся набор идентификаторов текущих валидаторов. Одна фоновая задача разрешает их контактные данные через резолвер узлов самого оверлея, а другая параллельно поддерживает небольшое проверенное пингом подмножество заведомо живых валидаторов. Это подмножество и отдаётся наружу как цели широковещания. Разделение на «все разрешённые» и «проверенно живые цели» сознательное: широковещанию не нужно слать всем валидаторам подряд, ему нужен короткий список тех, кто с высокой вероятностью ответит.
Набор валидаторов обновляется автоматически на каждом ключевом блоке: резолвер подписан на поток блоков, реагирует только на ключевые, берёт из конфигурации блокчейна в мастер-блоке текущий валидаторский набор и передаёт его дальше. Неключевые блоки игнорируются. Если набор валидаторов не удаётся получить из уже загруженной конфигурации, ошибка логируется, а прежний набор остаётся в силе. Тот же способ обновления набора доступен и вручную, например чтобы задать валидаторов сразу при старте, до первого ключевого блока.
Резолвер периодическим пингом пересобирает цели широковещания: берёт снимок разрешённых валидаторов, отбрасывает заведомо негодных, перемешивает остаток случайным образом и параллельно пингует кандидатов, пока не наберёт нужное число живых или не исчерпает список. Ответившие идут в цели, на месте неответивших подставляются следующие по очереди. Случайное перемешивание перед пингом распределяет широковещательную нагрузку по разным валидаторам между циклами. Публикация нового списка целей защищена от гонки со сменой набора: если за время пингов пришёл более новый набор валидаторов, устаревший результат целей не публикуется.
Валидатор считается негодным для целей, если его контактная запись уже просрочена либо создана достаточно давно — больше 30 минут назад: резолвер предпочитает валидаторов со свежо подтверждённой контактной информацией, считая слишком старую запись поводом перепроверить пира пингом заново, а не полагаться на неё.
Прикладной код получает текущие цели широковещания одним вызовом и рассылает им сообщения. Клиент отправляет валидатору одностороннее сообщение напрямую через оверлей, по идентификатору узла. В отличие от сообщений соседям, отправка валидатору не оборачивается собственным таймаутом и не влияет ни на какие очки — у валидаторов нет оценки надёжности, только периодическая проверка живости пингом. Прикладной код сам решает, скольким и каким целям из списка разослать сообщение.
Конфигурация
Работа с соседями и с валидаторами настраивается двумя отдельными разделами общей конфигурации клиента оверлея: neighbors и validators. Оба целиком опциональны, при отсутствии применяются значения по умолчанию.
Раздел neighbors:
| Параметр | По умолчанию | Что задаёт |
|---|---|---|
update_interval | "2m" | Период обновления списка соседей |
ping_interval | "30s" | Период пинга соседей |
apply_score_interval | "10s" | Период переноса очков соседей в индекс выбора |
update_metrics_interval | "5s" | Период экспорта метрик соседей |
keep | 5 | Максимальное число соседей в пуле |
max_ping_tasks | 5 | Максимум одновременных задач пинга (клиент этот параметр не использует) |
default_roundtrip | "300ms" | Стартовое время ответа для только что добавленного соседа |
send_timeout | "500ms" | Таймаут одностороннего сообщения соседу |
query_timeout | "1s" | Таймаут двунаправленного запроса к соседу |
enable_metrics | true | Включать ли фоновую задачу метрик соседей |
Раздел validators:
| Параметр | По умолчанию | Что задаёт |
|---|---|---|
ping_interval | "60s" | Период пинга разрешённых валидаторов при пересборке целей широковещания |
ping_timeout | "1s" | Таймаут одного пинга валидатора |
keep | 5 | Максимум валидаторов-целей широковещания |
max_ping_tasks | 5 | Максимум одновременных задач пинга (не используется: параллельность пинга валидаторов на деле ограничена значением keep) |
send_timeout | "500ms" | Таймаут односторонней отправки валидатору, также задаёт верхнюю границу адаптивного таймаута широковещания внешних сообщений |
Транспорт
QUIC-транспорт сетевого слоя Tycho — как узлы устанавливают соединение, идентифицируют друг друга по ключу и обмениваются запросами и сообщениями.
Blockchain RPC
Клиент и сервис обмена блоками, ключевыми блоками, архивами и устойчивыми состояниями поверх публичного оверлея, широковещание внешних сообщений и лимиты трафика.