Работа в сети

Mikhail Savenkov
7 min readFeb 26, 2022

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

При построении любой децентрализованной одноранговой (P2P) системы необходимо решить проблему трансляции сетевых адресов (NAT). Большинство наших машин, ноутбуков, планшетов и телефонов находятся за маршрутизаторами и брандмауэрами, что затрудняет установление прямых соединений между одноранговыми системами. Именно здесь на помощь приходит сетевой уровень.

В то время как некоторые другие сетевые уровни требуют от пользователя настройки перенаправления портов на маршрутизаторе для решения проблемы NAT, мы разработали нашу реализацию с акцентом на доступность, используя комбинацию WebRTC и WebSockets для нашего транспортного уровня. Они используют множество методов, чтобы помочь узлам установить прямую связь. Другими словами, реализация узлов Iron Fish работает прямо из коробки, либо в среде CLI, либо даже непосредственно в браузере. Это упрощает использование Iron Fish для любого пользователя, независимо от его технических возможностей.

Когда узел запускается впервые, он должен знать по крайней мере об одном другом узле, к которому он может подключиться (так называемый загрузочный узел), который познакомит его с другими узлами в сети. Это первоначальное подключение к узлу начальной загрузки происходит через WebSocket, а все последующие подключения к узлу используют WebRTC. В этом разделе будет описано, как именно узлы соединяются друг с другом, образуя сеть для поддержки протокола Iron Fish, начиная с того, как запускается новый узел.

Последовательность запуска

При запуске нового узла происходит следующее: новый узел случайным образом выбирает один загрузочный узел из предоставленного списка и открывает с ним WebSocket-соединение. Если пользователь хочет подключиться к определенному узлу на этом этапе, он может использовать конфигурационный файл или командную строку для указания предпочтительного узла (узлов) начальной загрузки. Загрузочный узел посылает свою идентификацию новому узлу.
Загрузочный узел передает новому узлу список аналогов.
Используя этот список, новый узел решает, к каким узлам подключиться.
Третий шаг повторяется с каждым новым пиром, с которым соединяется узел, до тех пор, пока не будет выполнено максимальное количество соединений (до 50).
Чтобы максимизировать прочность нашей сети и предотвратить ее фрагментацию, узел будет отдавать предпочтение подключению к тем сверстникам, к которым в настоящее время подключено относительно небольшое количество известных сверстников.
Как конечный пользователь, все это происходит без вашего участия или даже без вашего ведома. Когда вы запускаете узел, с вашей стороны вы увидите, что ваш узел подключается к загрузочному узлу, а затем быстро подключается ко многим другим.

Жизненный цикл одноранговых соединений
Пока узел находится в сети, он пытается поддерживать полное и исчерпывающее знание о состоянии своих сверстников и всех сверстников, подключенных к этим сверстникам (на два уровня глубже), периодически проверяя любые изменения. Узлы не имеют никаких знаний о более удаленных сверстниках. Учитывая это, давайте рассмотрим, как узлы общаются друг с другом, передавая сообщения.

Передача сообщений
Сообщение — это согласованный формат части информации для обмена между узлами. Существует несколько типов сообщений и способов их взаимодействия с узлами в зависимости от ситуации.

Сообщение

Сообщение — это согласованный формат части информации для обмена между узлами. Существует несколько типов сообщений и способов их взаимодействия с узлами в зависимости от ситуации.

Типы сообщений
Сетевой уровень Iron Fish в настоящее время имеет четыре различных внутренних типа сообщений:

Идентификация: сообщение, с помощью которого одноранговый компьютер может идентифицировать себя для другого.
Сигнал: сообщение, используемое для сигнализации о сеансе связи в реальном времени (RTC) между двумя равными узлами.
Список пиров: сообщение, содержащее список пиров, к которым данный узел подключен в данный момент.
Cannot Satisfy Request: сообщение об ошибке при возникновении проблемы.
Все сообщения в сети Iron Fish направляются по одному из следующих четырех стилей, в зависимости от различных факторов.

Сплетни: Эти сообщения достигают каждого узла в сети. Когда узел получает сплетенное сообщение, он проверяет его и пересылает другим подключенным узлам. Это используется для распространения изменений в блокчейне среди всех узлов сети. Подробнее об этом мы поговорим в следующем разделе.
Fire and Forget: Эти сообщения направляются конкретному подключенному узлу (невозможно отправить сообщение узлу, к которому вы не подключены). От сверстника не ожидается никакого ответа или подтверждения получения. Это полезно, когда вам не нужно убеждаться в том, что данные были получены правильно.
Прямой RPC: здесь запрос сообщения отправляется определенному подключенному аналогу, и система ожидает ответа. Поток удаленного вызова процедур (RPC) состоит из двух потоков: один для запроса и один для ответа. Он используется в качестве подложки для глобального RPC.
Глобальный RPC: Эти сообщения не адресованы конкретному аналогу. Сетевая библиотека выбирает равного для отправки сообщения и повторяет попытки (до определенного ограниченного количества) с другим равным, если не получает ответа, или если ответ недействителен. Алгоритм выбора рандомизирован и взвешен, чтобы отдать предпочтение сверстникам, которые, как известно, с большей вероятностью ответят на зарегистрированный тип сообщения.
Давайте немного углубимся в то, как работает протокол сплетен.

Протокол сплетен

Протокол Gossip Protocol в основном используется для трансляции новых блоков и транзакций всем узлам сети Iron Fish. Для наглядности: узлы, соединенные вместе, образуют сеть, а блокчейн — это структура данных, о которой они договариваются.

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

В следующем разделе подробно описано, как в настоящее время реализована трансляция на основе сплетен в Iron Fish.

Базовая реализация
Шаг 1
Когда новый узел выходит в сеть и соединяется с равным узлом, равный узел передает список своих прямых равных узлов.

Представим, что наша текущая сеть состоит из нового узла A, соединенного с узлами B и C. Сам C соединен с узлами D и E. Сам D соединен с F и G. Визуально это выглядит так, как показано на рисунке ниже.

Визуализация того, как связаны узлы нашего примера.

Узлы знают только о своих непосредственных соседях и о соседях своих соседей. Узел A не будет знать об узлах за пределами D и E.

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

Шаг 2
Когда узел A решит передать новое сообщение, он разошлет сообщение типа Gossip всем своим подключенным сверстникам (в данном примере C и B).

Затем каждый последующий узел будет передавать сообщение другим узлам, пока сообщение не получит вся сеть. В этом примере C передает сообщение D и E. Затем D передает сообщение F и G.

Оптимизация
Чтобы уменьшить перегрузку сети, мы внедрили следующие усовершенствования протокола Gossip.

Локальная история
Чтобы избежать бесконечной передачи одного и того же сообщения, каждый узел хранит набор всех сплетенных сообщений, которые он видел. Когда узел получает сообщение типа сплетни, которое уже есть в этом наборе (это означает, что оно уже встречалось ранее), он игнорирует это сообщение. Набор, в котором хранятся эти сообщения типа сплетен, имеет определенный размер, а старые сообщения удаляются в порядке очереди.

Рокировка соседей
Чтобы избежать спама дублирующимися сообщениями, мы применили еще два решения:

Когда узел A передает сообщение узлу B, узел B не отправляет его обратно узлу A.
Когда узел A отправляет сообщение узлу B, узел B (зная, что A уже позаботился об этом) будет избегать отправки сообщений любому узлу, к которому подключены узлы A и B.
В приведенном ниже примере узел A подключен к B, C, D, E и хранит список соединений с пирами на два уровня вглубь.

Когда узел A передает сообщение, распространение происходит в два этапа:

Узел A передает сообщение узлам B, C, D и E.
Узел B пересылает сообщение в G. Он не пересылает его в C и E, поскольку знает, что узел A подключен к ним и уже отправил его. Узел C пересылает сообщение узлу H. Узел D пересылает сообщение узлу I, а узел E — узлу F.

Когда узел F передает сообщение, в данном примере распространение происходит в четыре этапа:

Узел F передает сообщение узлу E.
Узел E пересылает сообщение узлам A и B.
Узел B пересылает сообщение узлу G. Он не пересылает сообщение узлу A, поскольку знает, что E подключен к A. Он не пересылает сообщение узлу C, поскольку знает, что тот подключен к A.
Узел C и узел D пересылают сообщение узлам H и I.

Перспективы
Мы изучаем, как улучшить распространение блока на уровне приложения до запуска основной сети. Вместо того чтобы отправлять весь блок, узел будет сначала отправлять заголовок блока. Затем узел-аналог может проверить, что он еще не получил его, прежде чем запрашивать данные полного блока. Мы также рассматриваем другие реализации, такие как IBLTs или Minisketch, чтобы понять плюсы и минусы каждого решения.

В следующей части мы рассмотрим Хранилища. Следите за обновлениями.

--

--