Сетевой протокол
Что узлы 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должен лежать в[1, 9]. Ноль и всё, что выше наибольшего известного вида, — это нарушение протокола, а не неизвестное расширение: в протоколе 1 нет лазейки для прямой совместимости, потому что поле версии, проверяемое при рукопожатии, делает её ненужной.
Виды сообщений#
| Значение | Имя | Направление | Полезная нагрузка |
|---|---|---|---|
| 1 | hello | обе стороны, первым | Рукопожатие |
| 2 | certificate | рассылка | ssz(Certificate) |
| 3 | block-announce | рассылка | Заголовок плюс идентификаторы сертификатов |
| 4 | get-block | запрос | 32-байтовый идентификатор блока ‖ индекс фрагмента u32 |
| 5 | block | ответ | Один фрагмент ssz(Block) |
| 6 | get-headers | запрос | Локатор |
| 7 | headers | ответ | Серия заголовков |
| 8 | get-peers | запрос | пусто |
| 9 | peers | ответ | Список адресов |
Специфичной для сети кодировки консенсусного объекта не существует, и именно это делает «идентификатор того, что я получил» проверяемым утверждением.
Каждое входящее сообщение стоит отправителю#
Это та часть спецификации, которую полезнее всего прочесть целиком, потому что именно здесь протокол пиров обычно даёт течь. Ею управляют два правила.
Правило упорядочивания по стоимости#
Работа выполняется в порядке возрастания стоимости, и сообщение тарифицируется до дорогого шага, который за ним следует. Получатель, который сначала проверяет, а тарифицирует потом, построил усилитель: то, что дёшево отправить, оказывается дорого проверять.
У каждого исхода есть цена#
Никакого безымянного Free нет. Каждый вид сообщения, пересечённый с каждым
исходом, который он может дать, попадает в таблицу с приписанной оценкой. Исход, проваливающийся
мимо таблицы без тарификации, — это тот самый дефект, ради предотвращения которого правило существует — и на практике
к нему приходили путями более узкими, чем нетарифицированная строка: реализация, уводившая некорректный кадр
в сторону от единственного, кто его оценивал, делала этот кадр бесплатным на всё время, пока запрос оставался
неотвеченным, не добавляя ни строки ни в одну таблицу.
Обслуживание тоже тарифицируется. Байты блоков — единственный ответ, на порядки больший, чем запрос, который его просит, поэтому у них есть собственный бюджет байтов поверх счёта запросов.
Управление соединениями и защита от затмения#
В этом разделе больше измеренных отказов, чем в любом другом, и каждое требование ниже существует потому, что реализация без него была затмена в измерении.
- Цели исходящих соединений должны выбираться с учётом разнообразия адресов, чтобы один хостинговый диапазон не мог заполнить исходящие слоты узла.
- Цели исходящих соединений должны быть также ограничены на каждый источник рассылки. Одного разнообразия адресов недостаточно по причине арифметической, а не оценочной: группа адресов — это свойство адреса, которым атакующий владеет, а адрес, который пир заявляет, — это выдуманные им байты, и любые четыре байта являются действительным адресом узла IPv4. Атакующий, не имеющий адресов вовсе, чеканит новую группу разнообразия на каждую строку по цене одного кадра. Чего он выдумать не может, так это соединения, по которому пришло заявление.
- Сокет, по которому пришло входящее соединение, не должен становиться целью исходящего. Это исходный адрес пира — эфемерный порт, выбранный его ОС, а не то, на чём он слушает — так что слот, потраченный на дозвон туда, потрачен на адрес, который не может ответить.
- Оба ограничения должны считаться против соединений, которые узел удерживает, а не против одного вызова выбора. Иначе цикл дозвона, исключающий уже подключённых пиров, каждый раунд восстанавливает оба бюджета полными, и ограничение задерживает атакующего на один раунд на каждую квоту вместо того, чтобы его ограничивать. Измерено: один рассказчик занял 2, затем 4, затем 6, затем 8 из 8 исходящих слотов за четыре раунда.
- Хранилище пиров должно сохраняться на диск и должно быть ограниченным. Узел, начинающий с чистого листа после каждого перезапуска, даёт атакующему свежую возможность при каждом перезапуске; узел, начинающий с наследием атакующего, даёт ему ту же возможность навсегда.
- Ограниченное хранилище не должно отвергать корректный адрес из-за того, что оно заполнено; оно вытесняет ради него. Честный адрес, с которым ни разу не связывались, ничем не лучше выдуманного, с которым ни разу не связывались, поэтому атакующий, заполнивший хранилище первым, запирает его для всего, что предложат потом — включая собственный список начальной загрузки оператора.
- Что именно выбирает вытеснение, важнее самого факта вытеснения. «Берите жертву из самой многочисленной группы» читается как наводнение вытесняет само себя и является таковым лишь пока наводнение — самое многочисленное в хранилище. На узле, загрузившемся от одного отзывчивого пира, самая многочисленная группа — это адресная книга этого пира. Измерено на реализации с таким порядком: 200 выдуманных адресов из одного источника вытеснили 200 честных, ничего не стоив атакующему.
- Среди неразличимых записей окончательное разрешение ничьей не должно опираться ни на что, выбираемое рассылающим пиром — включая адрес — и это относится к выбору ровно так же, как к вытеснению. Измерено на реализации, чей селектор проваливался до строки адреса: 8 честных адресов против 8 выдуманных дали 8 выдуманных из 8 и ноль честных исходящих соединений за десять раундов.
Что намеренно отсутствует#
| Отсутствует | Почему |
|---|---|
| Обход NAT | Цена названа, а условие пересмотра измеряется, а не предполагается: доля узлов, доступных для входящих подключений, в публичном testnet — первое условие пересмотра решения. |
| Сжатие | Компрессор на потоке, управляемом атакующим, — это поверхность атаки ради экономии полосы, потребность в которой никто не измерял. |
| Подписи на уровне сообщений | Транспорт аутентифицирует канал; консенсусные объекты несут собственные подписи. Третий слой аутентифицировал бы ретранслятора, а от этого не зависит ни одно решение. |
| Идентификаторы запросов | Запрос и ответ сопоставляются по соединению и порядку, поэтому синхронизация идёт по собственному соединению — что убирает конечный автомат. Там, где пир недоступен для дозвона, синхронизация может идти по существующему соединению рассылки и тогда обязана сопоставлять ответы по содержимому. |
| Механизм расширения с прямой совместимостью | Протокол 1 проверяет свою версию при рукопожатии и разрывает соединение при несовпадении. Расширения приходят как протокол 2. |
Чего не покрывает идентификатор сертификата#
Здесь это стоит повторить, потому что от этого зависит правило ретрансляции. Идентификатор сертификата обязывается к тому, что сертификат разрешает, и никогда к тому, что он лишь демонстрирует, поэтому подписи лежат вне прообраза идентификатора. Это делает свидетельство тем, что любой в пути может подменить: возьмите сертификат, замените его подпись мусором и распространяйте — тот же идентификатор, теперь уже недействительный экземпляр.
Это закрывают два правила. Блок обязывается к свидетельству, которое несёт, через корень над хешами экземпляров, а не над идентификаторами. И ретрансляция работает с экземплярами, а не с идентификаторами: узел, получивший экземпляр, чьё свидетельство не проходит проверку, отбрасывает этот экземпляр без ущерба для идентификатора — идентификатор не помечается, не кешируется как недействительный, и более поздний экземпляр, который проверку проходит, ретранслируется как обычно. Изувеченная копия стоит своему изувечителю полосы на её отправку и не стоит сертификату ничего.
Реализация этого с нуля#
Независимая реализация, проходящая
эталонные векторы, — равноправный участник, а не форк — в этом
и весь смысл такой спецификации. Начните с
spec/README.md для консенсусных объектов, затем проверьте
свою работу командой zcd vectors.