Blockchain RPC
Синхронизация узла и распространение сообщений в Tycho идут не по общему транспорту напрямую, а через прикладной протокол поверх одного публичного оверлея — blockchain RPC. Клиентом узел скачивает у соседей блоки, ключевые блоки, архивы и устойчивые состояния и рассылает валидаторам внешние сообщения. Сервисом он сам отвечает на такие запросы и принимает широковещания. Обе стороны независимы, и узел может поднять только клиента для синхронизации, только сервис для раздачи данных или обе сразу. Blockchain RPC живёт в крейте tycho-core, а не в tycho-network, и строится над уже готовым клиентом оверлея (см. статью «Клиент оверлея»), которому делегирует выбор соседей, оценку надёжности и рассылку валидаторам. Сам он отвечает только за прикладную семантику данных цепочки.
Протокол
Запросы и ответы blockchain RPC — фиксированный набор TL-структур в пространстве имён схемы blockchain.*. Клиент посылает запрос, сервис отвечает одним из предусмотренных для него типов:
getNextKeyBlockIds— идентификаторы следующих ключевых блоков мастерчейна;getBlockFullиgetNextBlockFull— полный блок целиком или следующий за заданным, с доказательством и дифом очереди;getBlockDataChunk— очередной чанк тела блока;getKeyBlockProofиgetZerostateProof— доказательства ключевого блока и нулевого состояния;getPersistentShardStateInfo/getPersistentQueueStateInfoи их чанковые парыgetPersistentShardStateChunk/getPersistentQueueStateChunk— сведения об устойчивом состоянии шарда или очереди и его чанки;getArchiveInfoиgetArchiveChunk— сведения об архиве и его чанки.
Отдельно от этого набора есть overlay.ping/overlay.pong — проверка живости, общая для оверлея, а не специфичная для blockchain RPC. Тела блоков, архивов и устойчивых состояний передаются одним и тем же способом: крупный объект режется на чанки фиксированного размера, а каждый чанк адресуется своим смещением и качается независимо, без состояния сессии между чанками одного объекта.
Каждый ответ на такой запрос обёрнут в общий формат с двумя исходами: полезная нагрузка или числовой код ошибки. Код ошибки говорит о сбое обработки самого запроса, а не о содержательном отсутствии данных: «блока нет» или «состояния нет» возвращается как обычный успешный ответ с соответствующим вариантом внутри полезной нагрузки. Коды ошибок:
1— запрос некорректен, например запрошено устойчивое состояние, когда сервис их не раздаёт;2— внутренняя ошибка обработки на стороне сервиса;3— данные не найдены, например чанк по несуществующему смещению.
Клиент
BlockchainRpcClient строится поверх клиента публичного оверлея (см. статью «Клиент оверлея») и добавляет над ним прикладные операции: загрузку блоков, ключевых блоков и доказательств, архивов, устойчивых состояний и рассылку внешних сообщений. Сам клиент — тонкая, дёшево клонируемая обёртка: он не хранит скачанные данные и не проверяет их полноту на уровне хранилища, а лишь координирует, у каких соседей и в каком порядке запрашивать нужные части.
Клиент отправляет через обычный автоматический выбор адресата клиента оверлея простые запросы — те, для которых достаточно ответа от любого соседа: идентификаторы ключевых блоков, доказательства, сведения об устойчивом состоянии. Так же, автоматическим выбором одного соседа, клиент качает и блок целиком (см. «Загрузка блока» ниже). А вот загрузка архивов и устойчивых состояний устроена иначе: сначала клиент находит среди нескольких соседей того, у кого есть нужные данные, а затем качает их чанками именно у этого одного соседа. Так все чанки одного объекта приходят из одного источника.
Загрузка блока
get_block_full и get_next_block_full выбирают одного соседа автоматическим выбором клиента оверлея и запрашивают у него блок целиком. Ответ приходит вместе с этим соседом: если блока у него нет, наружу отдаётся пустой результат и тот же сосед — по нему вызывающий код может решить, что делать дальше, например повторить запрос у другого соседа.
Реакция на отсутствие блока у соседа зависит от того, чего вызывающий код ожидал. Если данные не обязаны были быть у соседа — например, при обычном опросе, — их отсутствие не наказывает соседа: запрос засчитывается ему как обычный успешный. Когда данные предполагались, но не гарантированно, отсутствие снижает оценку надёжности соседа как обычный неуспешный запрос. Если же данные обязаны были быть, их отсутствие расценивается как злонамеренность и наказывает соседа так же жёстко, как и другие формы злонамеренного поведения (см. статью «Клиент оверлея»). Загрузчик блоков не заводит собственного механизма учёта соседей, а целиком опирается на оценку надёжности и наказания клиента оверлея.
Ответ на запрос полного блока уже несёт первый чанк сжатого тела и его метаданные — полный размер и размер одного чанка. Клиент проверяет этот первый чанк на согласованность с метаданными.
Остальные чанки клиент докачивает запросами getBlockDataChunk у того же соседа, до десяти чанков параллельно, а полученные сжатые чанки распаковывает потоково, zstd-декомпрессором на отдельном потоке. Контроль ведётся по суммарному объёму принятых сжатых байт, а не по размеру уже распакованных данных.
Докачка одного чанка при ошибке повторяется до настраиваемого предела, но не дольше, чем сосед остаётся надёжным: как только сосед выпадает ниже порога надёжности, докачка у него прекращается сразу, даже если попытки ещё оставались. Между повторами — пауза в 100 мс. Этот механизм докачки общий: он используется не только для тела блока, но и для чанков архивов и устойчивых состояний.
Архивы
Клиент ищет архив по номеру мастер-блока сразу у нескольких надёжных соседей, до десяти параллельно, и останавливается на первом, кто ответил, что архив у него есть. Если вместо этого соседи отвечают, что архив ещё «слишком новый», клиент ждёт, пока так не ответит настраиваемое число соседей, прежде чем сам сообщит тот же вывод вызывающему коду: одного такого ответа недостаточно, чтобы считать архив ещё не готовым по всей сети. Ответы «слишком новый» и «не найден» для соседа не наказание — это ожидаемый, корректный ответ.
Найдя источник, клиент качает архив у него же чанками, тем же общим механизмом докачки с повторами. Полученные чанки одновременно распаковываются и на лету проходят встроенную проверку целостности архива, поэтому повреждённый архив отбрасывается сразу, не дожидаясь конца загрузки. Итоговый результат записывается в любое место, которое укажет вызывающий код.
Устойчивые состояния
Устойчивое состояние бывает двух видов: состояние шарда и состояние очереди. Поиск и загрузка для обоих идут по той же схеме, что и для архивов. Клиент опрашивает до десяти надёжных соседей на наличие нужного вида состояния для заданного блока и останавливается на первом, кто ответил, что нужное состояние у него есть. Найдя его, клиент качает состояние чанками у того же соседа, тем же механизмом докачки с повторами. В отличие от архива, у устойчивого состояния нет отдельной проверки целостности содержимого при загрузке: контролируется только объём полученных чанков.
Сервис
BlockchainRpcService — реализация сетевого сервиса: она разбирает входящие запросы blockchain RPC и отвечает на них данными из локального хранилища узла, а входящие широковещания внешних сообщений передаёт дальше в прикладной код. Сервис собирается из хранилища узла, провайдера данных RPC и, опционально, обработчика широковещаний.
Сервис отдаёт полный блок, только если у него на диске есть все компоненты этого блока, иначе отвечает, что блока нет. В ответ на запрос полного блока сервис сразу кладёт первый чанк сжатого тела, а доказательство и диф очереди грузит из хранилища параллельно. Размер чанка фиксирован — 1 МБ, а сам первый чанк может быть меньше: до этого размера, если сжатое тело блока короче одного чанка. Остальные чанки тела отдаются последующими запросами по смещению, тем же способом чтения, что и первый.
Сервис отдаёт идентификаторы следующих ключевых блоков мастерчейна списком, ограниченным меньшим из двух пределов: тем, что запросил клиент, и собственным настраиваемым потолком сервиса. Если по этому пределу насобиралось меньше блоков, чем сам предел, ответ помечается как неполный: это значит, что известная цепочка ключевых блоков на этом обрывается. А вот полная пачка ровно в предел блоков помечается как полная (incomplete = false) — так клиент понимает, что нужно запросить следующую пачку, а не что цепочка на этом обрывается.
Сервис всегда читает ключевые блоки, тела блоков и доказательства напрямую из хранилища узла. Архивы и устойчивые состояния, данные заметно большего объёма, читаются через провайдер данных RPC, подменяемую абстракцию источника: помимо чтения из того же локального хранилища, у сервиса есть реализация поверх объектного хранилища S3 и гибридная, которая сначала спрашивает один источник, а при промахе обращается к запасному. Так тяжёлые объёмы можно вынести из локального хранилища узла или совместить источники — например, свежие данные читать локально, а историю из внешнего хранилища.
Широковещание внешних сообщений
Публикация внешнего сообщения в сеть идёт через клиент: он оборачивает сообщение в широковещательный конструктор протокола и параллельно рассылает его текущим целям широковещания клиента оверлея — небольшому проверенному пингом подмножеству живых валидаторов (см. статью «Клиент оверлея»). Перед рассылкой по сети сообщение доставляется и самому узлу-отправителю через отдельный необязательный обработчик, если он задан: так отправитель тоже увидит собственное сообщение, не гоняя его по сети к себе. Метод дожидается завершения отправки всем целям, но не дольше вычисленного таймаута, и сообщает вызывающему коду, скольким целям сообщение доставлено успешно.
Таймаут ожидания не фиксирован, а вычисляется адаптивно из недавних длительностей отправок целям широковещания: чем быстрее в последнее время проходили отправки, тем короче таймаут, и наоборот. Он всегда остаётся в настраиваемых границах, но верхняя граница — не сам таймаут одиночной отправки валидатору из конфигурации клиента оверлея, а больший из двух значений: этого таймаута и нижней границы таймаута широковещания. Если нижнюю границу настроить больше таймаута отправки, итоговый таймаут превысит его. Пока истории отправок ещё нет, используется верхняя граница.
На принимающей стороне сервис обрабатывает широковещание внешнего сообщения: он проверяет, что тело начинается с ожидаемых префиксов протокола, а затем — что сообщение проходит проверку валидности внешнего сообщения. Сообщение, не прошедшее эту проверку, отбрасывается. Прошедшее проверку сообщение передаётся зарегистрированному обработчику широковещаний.
Приём собственных и пришедших по сети сообщений разведён на два разных обработчика. Обработчик сетевых широковещаний вызывается сервисом и получает вместе с сообщением метаданные отправителя. Обработчик собственных сообщений вызывается клиентом при рассылке и получает только тело, без отправителя — у собственного сообщения отправителя в этом смысле нет. Оба обработчика опциональны и задаются отдельно, при сборке сервиса и клиента соответственно, поэтому прикладной код, например мемпул, может подписаться на внешние сообщения независимо от того, пришли они по сети или созданы этим же узлом.
Лимиты трафика
Входящий трафик blockchain RPC можно ограничить ограничителем частоты сетевого слоя (см. статью «Оверлеи»), которому blockchain RPC задаёт собственную классификацию запросов и лимиты по ней. Настройка целиком опциональна: без неё входящий трафик blockchain RPC ничем не ограничен.
Каждый входящий запрос или сообщение относится к одному из классов трафика:
- лёгкие запросы — проверка живости, сведения об архиве или устойчивом состоянии, а также чанки архива и тела блока;
- тяжёлые запросы — сборка полного блока, доказательства, идентификаторы ключевых блоков и, среди прочего, чанки устойчивых состояний;
- широковещания — входящие внешние сообщения.
Лимит считается раздельно для каждого класса, адреса из белого списка проходят в обход лимитов совсем, а запрос, который не удалось классифицировать, отбрасывается.
Конфигурация
Клиент и сервис blockchain RPC конфигурируются раздельно, каждый своим набором опциональных параметров.
Раздел клиента (BlockchainRpcClientConfig):
| Параметр | По умолчанию | Что задаёт |
|---|---|---|
min_broadcast_timeout | "100ms" | Нижняя граница адаптивного таймаута широковещания внешних сообщений |
too_new_archive_threshold | 4 | Число соседей, которые должны ответить «архив слишком новый», прежде чем клиент сообщит тот же вывод вызывающему коду |
download_retries | 10 | Предел повторов при докачке чанков блоков, архивов и устойчивых состояний |
Раздел сервиса (BlockchainRpcServiceConfig):
| Параметр | По умолчанию | Что задаёт |
|---|---|---|
max_key_blocks_list_len | 8 | Верхний предел числа идентификаторов ключевых блоков в одном ответе |
serve_persistent_states | true | Раздавать ли устойчивые состояния |
rate_limits | null | Опциональные лимиты входящего трафика (таблица ниже) — null выключает ограничение |
Раздел лимитов трафика (rate_limits), если он задан:
| Параметр | По умолчанию | Что задаёт |
|---|---|---|
limiter.rejects_before_cooldown | 5 | Число отказов подряд перед охлаждением |
limiter.cooldown | "30s" | Длительность охлаждения |
limiter.prune_interval | "30s" | Период очистки состояния ограничителя |
limiter.state_ttl | "300s" | Срок жизни записи состояния источника |
whitelist | [] | IP-адреса, чьи обращения проходят в обход всех лимитов |
traffic.light_queries.rate_per_sec | 20 | Лимит лёгких запросов в секунду с одного источника |
traffic.light_queries.burst | 20 | Запас мгновенных проходов для лёгких запросов |
traffic.heavy_queries.rate_per_sec | 10 | Лимит тяжёлых запросов в секунду с одного источника |
traffic.heavy_queries.burst | 10 | Запас мгновенных проходов для тяжёлых запросов |
traffic.broadcasts.rate_per_sec | 20 | Лимит широковещаний в секунду с одного источника |
traffic.broadcasts.burst | 20 | Запас мгновенных проходов для широковещаний |
rate_limits: null означает, что вся секция лимитов не задана: раздел трафика не выключается частично, ограничение выключено целиком. whitelist: [], напротив, — это значение поля внутри уже заданной секции: пустой список, а не отсутствующее значение, поэтому обходить лимиты в этом случае некому, но сами лимиты продолжают действовать.