Перевод оригинальной статьи на английском.
Разработчикам
Делаем GenLayer безопасным на 100%. Часть 1: архитектура
Как консенсусные контракты, узлы-валидаторы и GenVM складываются в архитектуру эшелонированной защиты для безопасных Intelligent Contracts.
Edgars Nemše8 мин чтения
Мы начинаем серию статей о том, как приближаем безопасность GenLayer к 100% настолько, насколько это вообще возможно. Причина проста. GenLayer относится к критически важным системам: он хранит средства и выносит суждения, так что ошибка здесь обернётся не только простоем. Из-за неё очень многие люди могут потерять очень много денег. Мы относимся к этому предельно серьёзно.
Мы ориентируемся на системы с похожим профилем отказов, где критична каждая ошибка: системы управления полётом, ПО для промышленных систем управления. Наш подход называется эшелонированной защитой: много независимых уровней тестирования и проверки. Тесты на примерах и сквозные прогоны всего стека перед каждым слиянием. Генеративное тестирование на соответствие эталонной модели. Конвейер CI, который намеренно ломает код, чтобы доказать, что тесты это заметят. Формальная модель протокола консенсуса на TLA+ с машинно проверенными доказательствами его ключевых свойств. И многое другое.
Но первым уровнем защиты служит сама архитектура: хорошо спроектированная система устраняет целые классы ошибок ещё до того, как написан первый тест. Поэтому и тесты невозможно понять без неё: все они напрямую следуют из того, как устроена система. Так что в этой первой статье мы разберём ключевые компоненты и интерфейсы между ними. Я исхожу из того, что вы уже знаете, что такое GenLayer и какую задачу он решает. Если нет, начните со статьи Optimistic Democracy: ИИ-нативный консенсус, который меняет коммерцию и нашей документации. Здесь же мы сосредоточимся на инженерной работе за кулисами.
GenLayer состоит из трёх основных компонентов, которые взаимодействуют друг с другом: консенсусных контрактов в GenLayer Chain, узла-валидатора и GenVM. Схема ниже показывает, как они связаны, а дальше мы разберём каждый из них по очереди.
Консенсусные контракты
Каждая транзакция GenLayer проходит через машину состояний, реализованную в виде контрактов на Solidity в GenLayer Chain, EVM-роллапе. Контракты отслеживают весь жизненный цикл: транзакция ставится в очередь, лидер предлагает её результат, валидаторы голосуют, транзакция принимается, а после окна апелляции становится окончательной. Апелляции передают ту же транзакцию всё более крупным группам валидаторов, вплоть до всего набора. Здесь же хранится и сам набор валидаторов: валидаторы вносят стейк, чтобы в него войти, а для каждой транзакции контракты выбирают из набора лидера и группу валидаторов.
Чейн служит единственным источником истины. Ни один другой компонент не хранит авторитетного состояния: если вы хотите знать, на каком этапе жизненного цикла находится транзакция, ответ дают контракты. Каждое изменение состояния порождает событие, и именно по этим событиям остальная система понимает, что делать дальше.
Чейн выполняет и роль коммуникационного слоя. Узлы никогда не общаются друг с другом напрямую: нет ни одноранговой (peer-to-peer) сети, ни gossip. Предложение лидера оформляется транзакцией, голос тоже транзакцией, и все их упорядочивает чейн. Так целый класс проблем консенсуса исчезает ещё до того, как успевает возникнуть. Нет форков, потому что все читают одну и ту же каноническую последовательность событий. Нет двойных коммитов и двойного голосования (эквивокации), потому что контракт структурно отвергает второй голос, а не обнаруживает его постфактум. В нашем протоколе нет логики обработки сетевого разделения и смены представления (view change), потому что доставка и упорядочение сообщений лежат на базовом чейне. Даже тайм-ауты объективны: «лидер не ответил вовремя» становится фактом, который контракт может проверить по времени блока, а не локальным мнением отдельного узла. Чейн фиксирует, что произошло; Optimistic Democracy остаётся решить только, что это значит.
Цена в том, что каждый шаг протокола становится ончейн-транзакцией, поэтому время блока задаёт нижнюю границу задержки консенсуса. Это сознательный компромисс: мы платим задержкой за то, что целые категории отказов распределённых систем просто исчезают. А ещё это значит, что полная история каждого решения консенсуса хранится как упорядоченный журнал, который можно воспроизвести заново, и это пригодится нам дальше в этой серии.
Узел-валидатор
Узел-валидатор запускают операторы. Сначала нужно чётко понять, чем он не является: он не узел GenLayer Chain. GenLayer Chain устроен как обычный EVM-чейн со своими полными узлами и своим RPC, и именно там происходит упорядочение. Узел-валидатор выступает клиентом этого чейна: он подключается к RPC узла чейна, подписывается на события и отправляет транзакции. Да, получается два уровня узлов и два уровня RPC. Зато разделение труда чёткое: узлы чейна согласуют порядок событий, а узлы-валидаторы действуют в ответ на них.
Внутри одного бинарного файла объединены три компонента:
- Воркер консенсуса. Основная работа: узел следит за событиями чейна и, когда его выбирают лидером или валидатором для транзакции, исполняет эту транзакцию через GenVM (на тех моделях, которые настроил оператор), а затем отправляет голоса и их раскрытия. Для чейна это просто ещё один аккаунт, который отправляет подписанные транзакции.
- Полный узел. Следит за чейном и поддерживает состояние GenLayer. Чейн хранит упорядоченный журнал и состояние консенсуса, а состояние самих Intelligent Contracts производное: узел воспроизводит принятые транзакции через GenVM и хранит полученное состояние в собственной локальной базе данных.
- RPC. Интерфейс со стороны GenLayer, к которому обращаются пользователи, SDK и инструменты. Специфичные для GenLayer запросы на чтение обслуживаются из производного состояния; стандартные методы
eth_передаются напрямую узлу чейна. Транзакции всегда уходят в чейн: сквозная передача лишь избавляет вас от необходимости держать больше одного подключения.
Получается, что снизу узел потребляет один RPC, а сверху предоставляет другой. Тот же бинарный файл можно запускать и без валидации: он всё равно синхронизируется и обслуживает RPC. Но главная его работа, о которой и пойдёт речь в этой серии, состоит в валидации. Именно за неё вы вносите стейк.
GenVM
Именно в GenVM контракты на самом деле исполняются, и её устройство позволяет работать Optimistic Democracy: она сочетает детерминизм с контролируемой недетерминированностью. Вызовы LLM и доступ к вебу появляются ровно там, где их запрашивает разработчик, и больше нигде.
Верхний уровень каждого контракта работает в детерминированном режиме. GenVM устроена как WASM-песочница: детерминированный набор инструкций, изолированная память и никакой возможности обратиться к внешнему миру, кроме хост-функций, которые предоставляет VM (хранилище, calldata, сообщения между контрактами). Поэтому каждый валидатор, исполняющий одну и ту же транзакцию, получает в точности одинаковый результат, вплоть до хеша.
Внутри этого исполнения контракт может открывать недетерминированные блоки. Каждый блок работает в собственном изолированном экземпляре GenVM, и этот экземпляр получает дополнительные хост-функции для чтения веба и вызова LLM. Больше нигде этих возможностей нет: детерминированный код до них не дотянется, нужных импортов у него просто нет.
Консенсус напрямую повторяет это разделение. Детерминированное исполнение должно в точности совпадать на каждом узле, как в любом обычном блокчейне. А в недетерминированных блоках результаты разных валидаторов вполне законно могут различаться, и именно здесь в дело вступают голосование Optimistic Democracy и принцип эквивалентности.
Благодаря этому же мы можем разместить в блокчейне полноценный интерпретатор CPython и использовать обычный Python как язык контрактов. Мы не изобретали детерминированный диалект Python: GenVM компилирует настоящий интерпретатор в WASM и запускает его в песочнице. Код контрактов работает без изменений, а любая недетерминированность, которая есть внутри самого Python, оказывается заперта в детерминированной среде.
Раз в блокчейне работает настоящий интерпретатор, контрактам доступна вся мощь языка, включая такие вещи, как eval. Контракт может выполнить код, которого не существовало на момент развёртывания: LLM может написать код прямо во время исполнения, а контракт выполнит его в том же вызове. И это по-прежнему безопасно и детерминированно, потому что всё, что выполняется, живёт в той же песочнице, с теми же хост-функциями и ничем больше.
Интерпретатор поставляется как раннер, то есть адресуемая по содержимому среда исполнения, зафиксированная хешем в заголовке контракта. Поэтому каждый валидатор, исполняющий ваш контракт, запускает побайтово идентичную среду, вплоть до сборки интерпретатора.
Впрочем, Python задаёт вариант по умолчанию, а не границу возможностей. Интерфейс контрактов GenVM построен на WASM: контрактом может быть всё, что компилируется в WASM. Если хотите, пишите контракт прямо на Rust или C: раннер Python служит лишь стандартной средой, которая упрощает самый частый случай.
Одна транзакция от начала до конца
- Пользователь отправляет транзакцию в чейн: напрямую узлу чейна или через сквозную передачу узла-валидатора. Она попадает в консенсусные контракты и ставится в очередь.
- Контракты выбирают лидера и группу валидаторов и объявляют об этом в событии.
- Узел лидера получает событие и исполняет транзакцию в своей GenVM: детерминированный код в песочнице, недетерминированные блоки через контролируемые хост-функции. Результат отправляется обратно в чейн как предложение лидера.
- Узел каждого валидатора заново исполняет транзакцию в собственной GenVM и сравнивает свой результат с предложением. При сравнении проверяется, приемлем ли результат лидера, а не идентичен ли он: это и есть принцип эквивалентности, а критерии для каждого вызова задаёт разработчик контракта. Голоса сначала фиксируются, затем раскрываются.
- Если большинство согласно, транзакция принимается, и её решение вступает в силу. Если же большинство отвергает предложение, сеть переходит к новому лидеру и пробует снова. А если консенсус так и не достигнут, транзакция завершается как неопределённая (undetermined), и состояние контракта остаётся нетронутым. Принятый результат тоже ещё не окончателен: в течение окна апелляции любой может подать апелляцию и передать транзакцию более крупной группе валидаторов.
- Окно закрывается, и транзакция становится окончательной. Теперь это навсегда.
Каждая стрелка на этом пути пересекает один из двух стыков: между контрактами и узлом (события вниз, транзакции вверх) и между узлом и GenVM (исполнение и результат).
Первый уровень защиты
Через всё, что описано в этой статье, проходит одна общая мысль: при эшелонированной защите первым уровнем служит сама архитектура. Она сразу устраняет целые классы ошибок: раз вся коммуникация идёт через чейн, форки, двойные голоса и обработка сетевого разделения перестают быть нашими задачами. Она даёт свойства, которые выполняются по построению, а не подтверждаются тестами: детерминизм обеспечивает WASM-песочница, воспроизводимость обеспечивают раннеры, зафиксированные хешем. И она проводит границы, благодаря которым систему вообще можно тестировать: каждый стык на схеме становится местом, где один слой можно проверить по чётко определённому интерфейсу.
Следующие уровни касаются всего, что архитектура не может гарантировать по построению. У каждого компонента свои режимы отказа, поэтому и подход к тестированию у каждого свой: консенсусные контракты мы тестируем как машину состояний, узел-валидатор проверяем на реальном поведении контрактов, а в GenVM убеждаемся, что унаследованный нами детерминизм действительно соблюдается. И раз компоненты должны работать вместе, тестирование не останавливается на их границах: оно охватывает стыки и сочетания этих уровней, вплоть до сквозных прогонов всего стека. Об этом и пойдёт речь в остальных частях серии.



