ZYCORD docs
Русский
ZycordДокументацияСетевой протокол

Сетевой протокол

Что узлы Zycord говорят друг другу и какие правила не дают протоколу пиров превратиться в усилитель. Это путеводитель по нормативной спецификации, а не её замена.

Эта страница — путеводитель, а не спецификация

Спецификация — это spec/: файлы параметров и корпус векторов. Там, где эта страница расходится с деревом, право дерево — реализация соответствует, когда она воспроизводит genesis id и проходит векторы, и ничто на этой странице этого не меняет. Дальше — карта того, где лежат требования.

Транспорт#

TCP, затем TLS 1.3. Идентичность пира — это ключ Ed25519, несомый в самоподписанном сертификате X.509, который предъявляют обе стороны. Цепочки сертификатов не проверяются — нет ни удостоверяющего центра, ни того, что он мог бы удостоверить — поэтому проверка требует ровно одного сертификата, требует, чтобы его открытый ключ был Ed25519, и извлекает этот ключ как идентичность пира.

Получаемое свойство — это привязка канала к ключу, а не полномочия: TLS гарантирует, что завершивший рукопожатие владеет закрытым ключом для предъявленной идентичности и что поток нельзя прочесть или изменить при передаче. Он ничего не говорит о том, честен ли этот пир. Ничто в протоколе не даёт полномочий на основании идентичности.

Даты действительности сертификата — фиксированные константы, а не окно вокруг текущего времени, и это намеренно: относительное окно ставит приём сертификата в зависимость от согласия часов, а сеть, распадающаяся по расхождению часов, — это сеть, распадающаяся по причине, не имеющей отношения к консенсусу.

Реализации не должны переиспользовать ключ какого-либо другого слоя в качестве идентичности пира. Узел генерирует новый при каждом запуске и никогда не записывает его на диск.

Кадрирование#

offset  size  field
0       4     length   uint32, little-endian - payload length, excluding this header
4       1     kind     uint8  - see below
5       n     payload
  • length должна сверяться с MaxMessageBytes = 8 МиБ и с потолком своего вида сообщения до любого выделения памяти. Получатель, выделяющий память по заявленной длине, имеет удалённо эксплуатируемую ошибку исчерпания памяти независимо от того, что идёт дальше.
  • kind 0 отвергается. Вид выше наибольшего, известного сборке, — это вид, добавленный более новой сборкой: его полезная нагрузка читается и отбрасывается в пределах MaxUnknownPayloadBytes = 64 КиБ, с ограниченной частотой на пира, а не приводит к закрытию соединения. Сверх этой частоты он засчитывается как избыточный запрос.

Виды сообщений#

ЗначениеИмяНаправлениеПолезная нагрузка
1helloобе стороны, первымРукопожатие
2certificateрассылкаssz(Certificate)
3block-announceрассылкаЗаголовок плюс упорядоченные хеши экземпляров сертификатов
4get-blockзапрос32-байтовый идентификатор блока ‖ индекс фрагмента u32
5blockответОдин фрагмент ssz(Block)
6get-headersзапросЛокатор
7headersответСерия заголовков
8get-peersзапроспусто
9peersответСписок адресов
10service-cardрассылка, по возможностиПодписанное объявление сервиса
11block-unavailableответИдентификатор блока ‖ причина: ниже нижней границы тел у отправителя или никогда не хранился
12open-intentрассылка, по возможностиОткрытый интент для принудительного пути

Специфичной для сети кодировки консенсусного объекта не существует, и именно это делает «идентификатор того, что я получил» проверяемым утверждением.

Версия протокола — 3, и рукопожатие требует точного совпадения. hello несёт её вместе с идентификатором сети — идентификатором генезис-блока, который обязывается к каждому параметру консенсуса — высотой и работой отправителя, его адресом прослушивания, словом возможностей и его нижней границей тел — наименьшей высотой, тело блока которой он отдаёт. Вид сообщения, закрытый возможностью, никогда не отправляется пиру, который её не объявил, а бит возможности, не определённый сборкой, игнорируется, но никогда не приводит к отказу. На get-block, который получатель не станет обслуживать, отвечают block-unavailable, а не оставляют запрашивающего ждать до истечения его срока.

open-intent — единственный трафик принудительного пути: интент пользователя без указанного секвенсора, который любой производитель блоков может исполнить в конце своего блока. Получившийся принудительный сертификат никогда не рассылается как сертификат; он рождается в блоке, и каждый узел проверяет его там.

Каждое входящее сообщение стоит отправителю#

Это та часть спецификации, которую полезнее всего прочесть целиком, потому что именно здесь протокол пиров обычно даёт течь. Ею управляют два правила.

Правило упорядочивания по стоимости#

Работа выполняется в порядке возрастания стоимости, и сообщение тарифицируется до дорогого шага, который за ним следует. Получатель, который сначала проверяет, а тарифицирует потом, построил усилитель: то, что дёшево отправить, оказывается дорого проверять.

У каждого исхода есть цена#

Никакого безымянного Free нет. Каждый вид сообщения, пересечённый с каждым исходом, который он может дать, попадает в таблицу с приписанной оценкой. Исход, проваливающийся мимо таблицы без тарификации, — это тот самый дефект, ради предотвращения которого правило существует — и на практике к нему приходили путями более узкими, чем нетарифицированная строка: реализация, уводившая некорректный кадр в сторону от единственного, кто его оценивал, делала этот кадр бесплатным на всё время, пока запрос оставался неотвеченным, не добавляя ни строки ни в одну таблицу.

Обслуживание тоже тарифицируется. Байты блоков — единственный ответ, на порядки больший, чем запрос, который его просит, поэтому у них есть собственный бюджет байтов поверх счёта запросов.

Оценки привязаны к идентичности пира: недействительное сообщение стоит 20, нарушение протокола — 50, необслуженное тело — 10, а избыточный запрос — 5; полезное сообщение приносит 1, а пир банится при −100. Дорогие проверки — эпохи ключа и вычисления доказательства работы — бюджетируются на каждого пира и возмещаются, когда блок оказывается полезным, так что объявление, которое ни к чему не ведёт, не может заставить узел перемалывать работу.

Ретрансляция и синхронизация#

Ретрансляция начинается с хешей. Сертификаты рассылаются сами по себе и проверяются на действительность при поступлении, без состояния и параллельно; ретранслятору не нужно состояние, чтобы фильтровать спам. Блоки передаются как заголовок плюс упорядоченные хеши экземпляров, сопоставляемые с мемпулом получателя, и получатель проверяет работу заголовка, прежде чем запрашивать какое-либо тело. Тело передаётся фрагментами по 4 МиБ, каждый — отдельным запросом и ответом, так что предел кадра ограничивает сообщение и никогда не ограничивает блок. Тела — это данные цепочки: блок действителен, только если его тела можно получить, а узел, выполняющий обрезку, сообщает при рукопожатии, где проходит его нижняя граница.

Синхронизация выбирает по проверенной работе. Заголовки проверяются как цепочка до того, как загружаются тела, и узел предпочитает пира, чьи заголовки доказали наибольшую работу, а не того, кто заявляет наибольшую высоту. Конкурирующая ветвь принимается, только если она подключается в пределах горизонта отката в 1 024 блока и несёт больше работы. Закреплённые в релизе контрольные точки, принимаемые только для блоков возрастом не менее одной эпохи, отвергают противоречащую им цепочку, какой бы тяжёлой она ни была; оператор может добавить точку флагом. Начиная с перехода на доказательство стейка в Era 2, выбор делается по длине: побеждает цепочка, которая занимает больше всего слотов (техническое описание, §11), а горизонт отката и закреплённые точки остаются защитой от переписывания ключами, залог которых с тех пор выведен.

Управление соединениями и защита от затмения#

В этом разделе больше измеренных отказов, чем в любом другом, и каждое требование ниже существует потому, что реализация без него была затмена в измерении.

  • Цели исходящих соединений должны выбираться с учётом разнообразия адресов, чтобы один хостинговый диапазон не мог заполнить исходящие слоты узла.
  • Цели исходящих соединений должны быть также ограничены на каждый источник рассылки. Одного разнообразия адресов недостаточно по причине арифметической, а не оценочной: группа адресов — это свойство адреса, которым атакующий владеет, а адрес, который пир заявляет, — это выдуманные им байты, и любые четыре байта являются действительным адресом узла IPv4. Атакующий, не имеющий адресов вовсе, чеканит новую группу разнообразия на каждую строку по цене одного кадра. Чего он выдумать не может, так это соединения, по которому пришло заявление.
  • Сокет, по которому пришло входящее соединение, не должен становиться целью исходящего. Это исходный адрес пира — эфемерный порт, выбранный его ОС, а не то, на чём он слушает — так что слот, потраченный на дозвон туда, потрачен на адрес, который не может ответить.
  • Оба ограничения должны считаться против соединений, которые узел удерживает, а не против одного вызова выбора. Иначе цикл дозвона, исключающий уже подключённых пиров, каждый раунд восстанавливает оба бюджета полными, и ограничение задерживает атакующего на один раунд на каждую квоту вместо того, чтобы его ограничивать. Измерено: один рассказчик занял 2, затем 4, затем 6, затем 8 из 8 исходящих слотов за четыре раунда.
  • Хранилище пиров должно сохраняться на диск и должно быть ограниченным. Узел, начинающий с чистого листа после каждого перезапуска, даёт атакующему свежую возможность при каждом перезапуске; узел, начинающий с наследием атакующего, даёт ему ту же возможность навсегда.
  • Ограниченное хранилище не должно отвергать корректный адрес из-за того, что оно заполнено; оно вытесняет ради него. Честный адрес, с которым ни разу не связывались, ничем не лучше выдуманного, с которым ни разу не связывались, поэтому атакующий, заполнивший хранилище первым, запирает его для всего, что предложат потом — включая собственный список начальной загрузки оператора.
  • Что именно выбирает вытеснение, важнее самого факта вытеснения. «Берите жертву из самой многочисленной группы» читается как наводнение вытесняет само себя и является таковым лишь пока наводнение — самое многочисленное в хранилище. На узле, загрузившемся от одного отзывчивого пира, самая многочисленная группа — это адресная книга этого пира. Измерено на реализации с таким порядком: 200 выдуманных адресов из одного источника вытеснили 200 честных, ничего не стоив атакующему.
  • Среди неразличимых записей окончательное разрешение ничьей не должно опираться ни на что, выбираемое рассылающим пиром — включая адрес — и это относится к выбору ровно так же, как к вытеснению. Измерено на реализации, чей селектор проваливался до строки адреса: 8 честных адресов против 8 выдуманных дали 8 выдуманных из 8 и ноль честных исходящих соединений за десять раундов.

Что намеренно отсутствует#

ОтсутствуетПочему
Обход NATЦена названа, а условие пересмотра измеряется, а не предполагается: доля узлов, доступных для входящих подключений, в публичном testnet — первое условие пересмотра решения.
СжатиеКомпрессор на потоке, управляемом атакующим, — это поверхность атаки ради экономии полосы, потребность в которой никто не измерял.
Подписи на уровне сообщенийТранспорт аутентифицирует канал; консенсусные объекты несут собственные подписи. Третий слой аутентифицировал бы ретранслятора, а от этого не зависит ни одно решение.
Идентификаторы запросовЗапрос и ответ сопоставляются по соединению и порядку, поэтому синхронизация идёт по собственному соединению — что убирает конечный автомат. Там, где пир недоступен для дозвона, синхронизация может идти по существующему соединению рассылки и тогда обязана сопоставлять ответы по содержимому.
Согласование версийРукопожатие требует точной версии протокола и разрывает соединение при несовпадении. То, что развивается без повышения версии, узко и ограничено: новый вид сообщения за битом возможности, который более старая сборка читает и отбрасывает в пределах 64 КиБ с ограниченной частотой.

Чего не покрывает идентификатор сертификата#

Здесь это стоит повторить, потому что от этого зависит правило ретрансляции. Идентификатор сертификата обязывается к тому, что сертификат разрешает, и никогда к тому, что он лишь демонстрирует, поэтому подписи лежат вне прообраза идентификатора. Это делает свидетельство тем, что любой в пути может подменить: возьмите сертификат, замените его подпись мусором и распространяйте — тот же идентификатор, теперь уже недействительный экземпляр.

Это закрывают два правила. Блок обязывается к свидетельству, которое несёт, через корень над хешами экземпляров, а не над идентификаторами. И ретрансляция работает с экземплярами, а не с идентификаторами: узел, получивший экземпляр, чьё свидетельство не проходит проверку, отбрасывает этот экземпляр без ущерба для идентификатора — идентификатор не помечается, не кешируется как недействительный, и более поздний экземпляр, который проверку проходит, ретранслируется как обычно. Изувеченная копия стоит своему изувечителю полосы на её отправку и не стоит сертификату ничего.

Реализация этого с нуля#

Независимая реализация, проходящая эталонные векторы, — равноправный участник, а не форк — в этом и весь смысл такой спецификации. Начните с spec/README.md для консенсусных объектов, затем проверьте свою работу командой zcd vectors.