Как узел синхронизируется
Вы устанавливаете новый клиент, а сеть работает уже годы. То, как узел наверстывает упущенное — и пять способов, которыми он мог бы это делать — это аккуратная маленькая деривация: начинаем с самого медленного и параноидального метода, а затем по делу зарабатываем каждое сокращение.
- как Ethereum хранит состояние
- финальность
Вы устанавливаете новый узел и нажимаете «Запустить». Сеть работает уже годы; там сотни миллионов аккаунтов и цепочка блоков, уходящая назад до genesis. Ваш узел не знает ничего из этого. Прежде чем он сможет валидировать хотя бы один новый блок или ответить на вопрос «какой баланс у Алисы?», он должен наверстать упущенное — достичь точно того же текущего состояния, что и все остальные, и убедиться самостоятельно, что это состояние верно.
Это можно сделать не одним способом. Их пять, и они не произвольны: каждый — исправление конкретного недостатка предыдущего. Начнём с самого медленного и параноидального метода из возможных, а затем по делу заработаем каждое сокращение. (Это напрямую опирается на trie, которое хранит состояние, и финальность — если они туманны, сначала пробегитесь по ним.)
Две вещи и противоречие между ними
«Быть синхронизированным» означает, что ваш узел хранит две вещи: текущее состояние — каждый аккаунт и контракт, то самое, что зафиксировано в stateRoot 32-байтовый корень дерева Меркла-Патриции, который является отпечатком всего мирового состояния. Он есть в каждом заголовке блока; два узла с одинаковым stateRoot хранят идентичное состояние. последнего блока — и достаточно цепочки, чтобы продолжать валидировать новые блоки по мере их поступления.
Вся тема — это одно противоречие. Можно пересчитать всё самостоятельно с самого начала и никому не доверять — медленно, но пуленепробиваемо. Или можно скачать готовое состояние от пира и доверять его правильности — быстро, но легковерно. Каждый режим синхронизации — это разная точка на шкале между доверием и скоростью (и третья ось: сколько диска вы готовы сжечь). Посмотрите, где оказывается каждый.
→ Шаг 2: никому не доверяем. Повторяем всю историю.
Повторяем каждый блок начиная с genesis
Дизайн с нулевым доверием: скачиваем каждый блок от genesis до головы и повторно выполняем каждую транзакцию по порядку, применяя каждую к нашей локальной копии состояния — точно так же, как это делали исходные валидаторы. После последнего блока вычисленный нами stateRoot совпадёт с stateRoot блока на голове, и мы будем знать это, потому что прошли каждый шаг сами. Это полная синхронизация (full sync) — точка отсчёта, с которой сравнивают все остальные методы.
Ничто не заслуживает большего доверия: вы лично верифицировали каждый переход состояния в истории Ethereum. (Если сохранить все промежуточные состояния, получим архивный узел — то, что запускают блок-эксплореры: несколько терабайт на современной path-based раскладке, до ~15–20 ТБ на старой hash-based.)
→ Шаг 3: прекращаем повторную обработку. Просто скачиваем состояние.
Скачиваем trie вместо того, чтобы его восстанавливать
Вот прыжок. Состояние — это дерево Меркла (Merkle trie), а дерево Меркла самоверифицируется: каждый узел ссылается на дочерние по их хешу, поэтому если скачанный узел хешируется в значение, которое ожидал родитель, — он не может быть поддельным. Выбираем недавний блок pivot, временно принимаем его stateRoot на веру и скачиваем лежащее под ним дерево напрямую от пиров — сначала корень, потом каждый дочерний по его хешу, проверяя каждый узел по мере поступления. Никакого повторного выполнения. Это была быстрая синхронизация (fast sync).
Вы верифицируете только голову, следя за заголовками блоков и консенсусом, а математика Меркла подтверждает скачанное состояние. Дни сжимаются до часов.
→ Шаг 4: забываем о форме дерева. Скачиваем сырые данные оптом.
Скачиваем плоские диапазоны, затем лечим
trie — это просто индекс над простыми парами key → value. Так что прекращаем запрашивать его узел за узлом. Вместо этого запрашиваем у пиров непрерывные диапазоны плоских данных аккаунтов — «все аккаунты от 0x00… до 0x20…» — отдаваемые прямо из плоского снимка (snapshot) большими последовательными кусками, каждый с доказательством диапазона (небольшое доказательство Меркла, подтверждающее, что кусок корректен и полон относительно stateRoot). Скачиваем всё состояние несколькими толстыми запросами диапазонов, локально восстанавливаем trie из плоских данных, а затем проходим один финальный heal-проход, чтобы залатать немногие узлы, сместившиеся во время скачивания. Это snap sync — сегодняшний стандарт в geth и reth.
Та же модель доверия, что у быстрой синхронизации — верифицируем голову, доверяем доказательствам для состояния — но полоса пропускания используется эффективно. Полный узел mainnet синхронизируется за несколько часов.
→ Шаг 5: вообще не храним состояние.
Храним только заголовки; запрашиваем остальное с доказательством
Отбрасываем состояние полностью. Заголовок блока крошечный — несколько сотен байт — и он несёт stateRoot. Храним только цепочку заголовков. Когда нам что-то нужно — баланс Алисы, слот хранилища — запрашиваем у полного узла вместе с доказательством Меркла: короткую цепочку хешей соседей от листа до stateRoot, который у нас уже есть. Проверяем доказательство сами по доверенному корню. Если сходится — ответ подлинный; полный узел не может солгать, не сломав хеш. Это лёгкий клиент (light client).
Вы храните килобайты вместо сотен гигабайт, и каждый ответ по-прежнему криптографически верифицирован — вы обменяли хранение состояния на доказывание каждого фрагмента по требованию.
→ Шаг 6: начинаем с недавней точки, которой можно доверять.
Доверяем одному недавнему финализированному блоку и пропускаем остальное
Все методы выше тихо идут вперёд от genesis, доверяя самой тяжёлой цепочке. Но у Proof of Stake есть тонкость: узел, синхронизирующийся от genesis, может быть обманут давно мёртвыми валидаторами, подписывающими альтернативную древнюю историю (атака дальнего диапазона). Исправление одновременно является самым большим ускорением во всей теме. Не начинайте с genesis — начните с недавней финализированной контрольной точки (checkpoint): корня блока возрастом в несколько недель, взятого из доверенного источника (ваш клиент поставляется со списком; вы можете перепроверить по блок-эксплореру или у друга). Поскольку она финализирована, её откат сжёг бы треть всего застейканного ETH — так что это надёжный якорь. Отсюда выполняем snap sync состояния и идём вперёд. Это checkpoint sync (он же weak-subjectivity sync) — именно так реальные узлы и запускаются.
Это единственный режим, меняющий точку начала, а не метод загрузки, и он сочетается с остальными: современные клиенты объединяют слой консенсуса, синхронизированный через checkpoint, со слоем выполнения, синхронизированным через snap, — и узел, бывший офлайн месяцами, может снова выйти в сеть за минуты.
Что вы обменяли — явно
| Метод | Модель доверия | Время синхронизации | Диск |
|---|---|---|---|
| Полная / архивная | повторяет всю историю — не доверяет ничему | дни–недели | сотни ГБ – ТБ |
| Быстрая (устаревшая) | верифицирует голову, доверяет Меркл для состояния | часы+ | ~размер состояния |
| Snap (стандарт) | верифицирует голову, доверяет доказательствам диапазонов | несколько часов | ~размер состояния |
| Лёгкий клиент | цепочка заголовков + доказательство на каждый запрос | секунды | КБ–МБ |
| Checkpoint | доверяет одному недавнему финализированному корню | минуты | в паре со snap |
Столбец, который важен — модель доверия: всё справа — это лишь цена, которую вы платите, а всё слева — то, что вы готовы принять на веру. Snap-от-checkpoint победил, потому что он жертвует почти ничем реальным — финализированный корень настолько же надёжен, насколько вообще что-то может быть надёжным — ради почти всей скорости.