Web3 и блокчейн Механизмы консенсуса: PoW, PoS, финальность, форки, атака 51%
0%

Механизмы консенсуса: PoW, PoS, финальность, форки, атака 51%

Механизмы консенсуса: PoW, PoS, финальность, форки, атака 51%

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

Подпись доказывает авторство: «эту транзакцию действительно составил владелец ключа». Хеш-цепочка доказывает целостность: «эту историю не переписали задним числом незаметно». Ни то, ни другое не отвечает на главный вопрос платёжной системы: какая из двух подписанных транзакций была первой?

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

Что именно нужно согласовать

Формально блокчейн — это реплицированный автомат состояний (replicated state machine). У каждого узла есть копия состояния (кто чем владеет) и детерминированная функция перехода apply(state, tx) -> state'. Если все узлы применят один и тот же список транзакций в одном и том же порядке, они получат идентичное состояние. Значит, задача сводится к одному: договориться о едином, растущем, неизменяемом журнале операций.

От протокола консенсуса мы хотим три свойства:

  • Согласованность (safety, agreement). Два честных узла никогда не финализируют разные значения на одной позиции журнала. Практически: если ваш узел считает блок 20 000 000 финальным, ничей другой честный узел не считает финальным другой блок на этой высоте.
  • Валидность (validity). Финализируется только то, что предложил кто-то и что проходит правила протокола. Нельзя «согласовать» транзакцию, которую никто не подписывал.
  • Живучесть (liveness). Система продолжает добавлять новые блоки. Журнал не застревает навсегда.

Плохая новость из теории: теорема FLP (Fischer, Lynch, Paterson, 1985) доказывает, что в полностью асинхронной сети детерминированного протокола, который гарантирует и safety, и liveness при хотя бы одном отказе, не существует. Все реальные системы обходят FLP одним из двух способов: добавляют случайность (Nakamoto consensus, Algorand) или принимают предположение о частичной синхронности (PBFT, Tendermint) — «сеть иногда тормозит, но не бесконечно».

Вторая сложность — модель отказов. В корпоративном кластере узлы обычно просто падают (crash faults), и хватает Raft/Paxos. В открытой сети узлы лгут целенаправленно: подписывают конфликтующие сообщения, скрывают блоки, притворяются множеством участников. Это византийские отказы (Lamport, Shostak, Pease, 1982). Классический результат: против византийских отказов нужно $n \ge 3f + 1$ узлов, чтобы пережить $f$ предателей, то есть честными должны быть более двух третей.

И тут третья, убийственная проблема: атака Сивиллы. Правило «более 2/3 узлов» работает, только если известно, сколько всего узлов. В открытой сети я запускаю десять тысяч процессов на одном ноутбуке — и у меня «большинство». Голосование по головам в интернете бессмысленно.

Прорыв Накамото в 2008 году состоял не в изобретении хеш-цепочки (она была у Haber и Stornetta в 1991) и не в цифровых подписях. Он состоял в идее: привязать право голоса к дефицитному, внешнему по отношению к системе ресурсу. Не «один узел — один голос», а «одна единица работы — один голос». Подделать себе тысячу личностей дёшево. Подделать себе тысячу мегаваттных ферм — нет.

Proof of Work изнутри

Задача, которую решает майнер

В Bitcoin блок состоит из заголовка (ровно 80 байт) и списка транзакций. Согласуется именно заголовок: он содержит хеш родителя, корень дерева Меркла (обязательство на весь набор транзакций), время, компактную запись цели и nonce.

Раскладка 80-байтового заголовка блока Bitcoin и цикл перебора nonce

Правило: блок валиден, если SHA-256(SHA-256(header)), прочитанный как 256-битное число, не превышает целевое значение target. Хеш непредсказуем, поэтому единственная стратегия — перебирать nonce и пересчитывать. Это чистая лотерея без памяти: каждая попытка независима.

Ожидаемое число хешей до успеха:

$$\mathbb{E}\lbrack H \rbrack = \frac{2^{256}}{T + 1}$$

где $T$ — текущий target. «Сложность» (difficulty) — это просто удобная нормировка: $D = T_{\max} / T$, где $T_{\max}$ — target первого блока Bitcoin. При сложности $D$ нужно примерно $D \cdot 2^{32}$ хешей.

Реализация проверки и перебора — почти дословно из спецификации:

import hashlib
import struct

MAX_TARGET = 0x00000000FFFF0000000000000000000000000000000000000000000000000000


def double_sha256(b: bytes) -> bytes:
    """Bitcoin хеширует дважды — исторически, как защита от length-extension."""
    return hashlib.sha256(hashlib.sha256(b).digest()).digest()


def bits_to_target(bits: int) -> int:
    """Компактная 4-байтовая запись nBits разворачивается в 256-битный target."""
    exponent = bits >> 24            # старший байт — показатель степени
    mantissa = bits & 0x00FFFFFF     # три младших байта — мантисса
    return mantissa * 256 ** (exponent - 3)


def mine(version, prev_hash, merkle_root, timestamp, bits, max_nonce=1 << 32):
    """Заголовок — 80 байт: 4 + 32 + 32 + 4 + 4 + 4, little-endian, хеши в обратном порядке байт."""
    target = bits_to_target(bits)
    prefix = (struct.pack("<I", version) + prev_hash[::-1] + merkle_root[::-1]
              + struct.pack("<II", timestamp, bits))
    for nonce in range(max_nonce):
        h = double_sha256(prefix + struct.pack("<I", nonce))
        # хеш интерпретируется как little-endian целое
        if int.from_bytes(h, "little") <= target:
            return nonce, h[::-1].hex()   # в UI хеш показывают big-endian
    # 2^32 вариантов nonce исчерпаны за доли секунды на современном ASIC,
    # поэтому майнер меняет extranonce в coinbase → меняется merkle_root → новое пространство
    raise RuntimeError("nonce исчерпан, нужен новый merkle_root")

Сложность. По времени — $O(2^{256}/T)$ хешей в среднем, геометрическое распределение; по памяти — $O(1)$, буквально 80 байт плюс контекст SHA-256. Именно константная память сделала возможными ASIC: чип с гигабайтами памяти не нужен, нужен только конвейер сжимающей функции. Некоторые сети (Ethereum до Merge с Ethash, Monero с RandomX) сознательно делали функцию memory-hard, чтобы уравнять GPU и ASIC — с переменным успехом.

Проверка асимметрично дешева. Найти блок — миллиарды хешей; проверить — два хеша. Это и есть суть «доказательства работы»: дорого создать, дёшево верифицировать.

Регулировка сложности

Хешрейт сети меняется на порядки. Чтобы интервал между блоками оставался стабильным, target пересчитывается. Bitcoin делает это каждые 2016 блоков (≈ 2 недели):

TWO_WEEKS = 14 * 24 * 3600


def next_target(old_target: int, actual_seconds: int) -> int:
    """Ретаргет Bitcoin: шаг коррекции ограничен четырёхкратным в обе стороны."""
    actual = max(TWO_WEEKS // 4, min(actual_seconds, TWO_WEEKS * 4))
    return min(old_target * actual // TWO_WEEKS, MAX_TARGET)

Два нюанса, которые любят на собеседованиях. Первый: в Bitcoin здесь исторический off-by-one — берётся разница времён между блоками с индексами, отстоящими на 2015, а не 2016, из-за чего средний интервал чуть меньше 10 минут. Баг не чинят, потому что починка — хардфорк ради нуля пользы. Второй: timestamp в заголовке не доверенный — майнер задаёт его сам. Протокол лишь требует, чтобы время было больше медианы одиннадцати предыдущих блоков и не более чем на 2 часа впереди сетевого времени. Отсюда прямое следствие для смарт-контрактов: время блока — это диапазон, а не точка.

Почему 10 минут, а не 10 секунд

Интервал блока — компромисс с задержкой распространения. Пока блок расходится по p2p-сети, другие майнеры продолжают работать над старым родителем. Если время распространения $t_p$ сравнимо с интервалом $t_b$, доля «осиротевших» блоков растёт как $\approx t_p / t_b$, а вместе с ней растёт и эффективное преимущество крупных пулов (они узнают о своих блоках мгновенно). Быстрые цепи вынуждены компенсировать это на уровне протокола: Ethereum до Merge платил за дядюшек (ommers) по правилу GHOST (Sompolinsky, Zohar, 2013), учитывая работу боковых ветвей в весе цепи.

Честно о цене PoW

PoW работает — Bitcoin не был успешно атакован на уровне консенсуса за пятнадцать с лишним лет. Но платить приходится честно:

  • Энергия. Безопасность буквально пропорциональна сожжённому электричеству. Это не «баг», это механизм: атака дорога ровно потому, что защита дорога.
  • Централизация железа. Производство ASIC — олигополия. Дешёвая энергия — география. В итоге хешрейт концентрируется.
  • Пулы. Индивидуальный майнер с одной картой получает блок раз в тысячелетия, поэтому все идут в пулы. Несколько крупнейших пулов регулярно суммарно контролируют больше половины хешрейта. Юридически это разные независимые майнеры, но координация технически возможна — и это реальный, а не гипотетический риск концентрации.

Форки: почему цепь — это дерево

Ключевая вещь, которую пропускают почти все популярные объяснения: блокчейн локально — это дерево, а не цепь. Узел хранит все известные валидные блоки, а «цепь» — это путь от генезиса до головы, выбранный правилом выбора форка (fork choice rule).

В Bitcoin правило — не «самая длинная цепь», а цепь с наибольшей накопленной работой. Разница существенна: цепь из 100 блоков высокой сложности тяжелее цепи из 120 блоков низкой.

Пока обе ветки существуют, узлы разделены. Как только одна становится тяжелее, все честные узлы делают реорганизацию (reorg): откатывают блоки своей ветки, возвращают их транзакции в мемпул и применяют новую ветку. Транзакция, которая была «подтверждена» три блока назад, может вернуться в статус неподтверждённой. Это нормальная работа протокола, а не сбой.

Полный цикл обработки входящего блока:

Форк консенсуса против форка правил

Слово «форк» перегружено, и путаница тут стоит денег. Различают:

Тип Что происходит Пример
Временный форк Два валидных блока на одной высоте, разрешается сам за 1–2 блока Стандартная работа сети
Soft fork Правила ужесточаются: новые блоки валидны для старых узлов, но не наоборот SegWit, Taproot в Bitcoin
Hard fork Правила ослабляются или меняются: старые узлы отвергают новые блоки The Merge, каждый апгрейд Ethereum
Chain split Хардфорк без консенсуса сообщества — две монеты навсегда Ethereum / Ethereum Classic (2016), BTC / BCH (2017)

Софтфорк совместим «сверху вниз»: старый узел видит новые блоки валидными, просто не понимает дополнительных ограничений. Хардфорк требует, чтобы обновились все — иначе сеть расколется. Именно поэтому хардфорки координируются месяцами и активируются по номеру блока или по времени.

Финальность: когда «подтверждено» значит подтверждено

Вероятностная финальность

В Nakamoto consensus строгой финальности нет. Есть экспоненциально убывающая вероятность отката. Оценка из раздела 11 white paper Bitcoin: если атакующий контролирует долю $q$ хешрейта ($p = 1 - q$ у честных), вероятность того, что он догонит цепь после $z$ подтверждений:

$$P_{\text{atk}} = 1 - \sum_{k=0}^{z} \frac{\lambda^{k} e^{-\lambda}}{k!}\left(1 - \left(\frac{q}{p}\right)^{z-k}\right), \qquad \lambda = z \frac{q}{p}$$

Числа (по расчётам Накамото и уточнению Rosenfeld, 2014):

Доля хешрейта $q$ $z=1$ $z=3$ $z=6$ $z=10$
10 % 0,2045 0,0132 0,00024 ~2·10⁻⁷
25 % 0,4557 0,1316 0,0248 0,0021
40 % 0,7043 0,4285 0,1980 0,0655
50 %+ 1,0 1,0 1,0 1,0

Отсюда фольклорное «шесть подтверждений»: при разумном предположении $q \le 0{,}1$ шесть блоков дают вероятность отката около 0,02 %. Обратите внимание на последнюю строку: при $q > 0{,}5$ никакое число подтверждений не помогает. Это не «становится очень трудно» — это математически неизбежный успех при достаточном терпении.

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

Детерминированная финальность

BFT-семейство протоколов даёт другое обещание: блок финализирован сразу, и отменить его невозможно без нарушения предположения о том, что более 2/3 участников честны. Цена — фиксированный, известный набор валидаторов и остановка сети, если больше трети выпали.

Proof of Stake

Идея и её честные проблемы

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

Красиво, но наивный PoS ломается двумя способами:

Nothing at stake. В PoW майнер физически не может работать над двумя ветками одновременно — хешрейт делится. В PoS подписать блоки на всех ветках сразу ничего не стоит. Значит, форки не разрешаются экономически. Решение — слэшинг: протокол определяет объективно детектируемые нарушения (две подписи на одной высоте, «окружающий» голос), и любой может опубликовать доказательство, после чего у нарушителя сжигается часть стейка. Наказание должно быть внутрипротокольным и объективным — вот главное отличие рабочего PoS от бумажного.

Long-range attack. Валидаторы, вышедшие из системы, забрали свой стейк и больше ничем не рискуют. Они могут задним числом собрать альтернативную историю от старого чекпоинта. Криптографически такая цепь безупречна. Решение — weak subjectivity (Buterin, 2014): новый или долго отключённый узел обязан получить недавний доверенный чекпоинт из внешнего источника. Это признанное ослабление модели доверия по сравнению с PoW, где достаточно генезис-блока и правила «самая тяжёлая цепь». Не верьте маркетингу, который это замалчивает.

Gasper: как это работает в Ethereum

Ethereum использует Gasper (Buterin et al., 2020) — гибрид: правило выбора форка LMD-GHOST плюс наложенный слой финальности Casper FFG (Buterin, Griffith, 2017).

Структура времени: слот — 12 секунд, эпоха — 32 слота (6,4 минуты). В каждом слоте детерминированно (через RANDAO) выбирается один предлагающий и набор комитетов аттестаторов; каждый валидатор аттестует ровно раз за эпоху. Порог входа — 32 ETH; после апгрейда Pectra (2025) один валидатор может консолидировать до 2048 ETH эффективного баланса, что сократило раздувание числа ключей.

Аттестация несёт три голоса разом: голову цепи (для LMD-GHOST), source-чекпоинт и target-чекпоинт (для FFG). Подписи BLS агрегируются, поэтому миллион голосов помещается в разумный объём блока.

Финальность строится на чекпоинтах — первых блоках эпох:

Слоты, эпохи, чекпоинты и финальность в Ethereum

Правило простое: ссылка (source → target), собравшая более 2/3 суммарного стейка, делает target обоснованным (justified). Когда обоснованы два чекпоинта подряд, нижний становится финализированным. От попадания транзакции в блок до финальности проходят две эпохи — около 12,8 минуты.

Почему откатить финализированное нельзя без огромных потерь: чтобы финализировать конфликтующий чекпоинт, нужно, чтобы более 1/3 стейка подписали противоречащие голоса. Такие голоса объективно детектируемы, и весь этот стейк сжигается. Это называется экономической финальностью: откат не невозможен физически, он стоит миллиарды и оставляет доказательство вины на цепи.

Отдельный механизм — inactivity leak. Если сеть не финализирует четыре эпохи подряд (например, треть валидаторов отключилась), балансы неактивных начинают квадратично утекать, пока активные снова не наберут 2/3. Система жертвует балансами отсутствующих ради восстановления живучести. Практический смысл: сеть переживает массовое отключение, но участники этого отключения теряют деньги.

Жизненный цикл валидатора со всеми штрафными путями:

Важная деталь слэшинга — корреляционный штраф. Если вас слэшнули в одиночку, потери невелики. Если одновременно слэшнули 30 % валидаторов, штраф каждого приближается ко всему стейку. Это делает экономически невыгодным запуск одного ключа на двух серверах «для надёжности» и наказывает системные ошибки провайдеров сильнее, чем частные.

Чистый LMD-GHOST уязвим к ex-ante реоргу: злонамеренный предлагающий придерживает блок и публикует его так, чтобы перебить следующего честного. Ethereum добавил proposer boost — блок, пришедший вовремя в свой слот, временно получает дополнительный вес, равный 40 % веса комитета слота. Это делает «догоняющие» атаки дороже. Обратная сторона: сама по себе эвристика, а не доказанное свойство, и исследования одиночных реоргов продолжаются.

BFT-финальность за один раунд: Tendermint

Альтернативная школа — классический BFT с известным набором валидаторов. CometBFT (бывший Tendermint), на котором работает экосистема Cosmos, даёт финальность в том же блоке.

Два раунда голосования (prevote, precommit) с порогом 2/3 дают классическую BFT-гарантию: два конфликтующих блока на одной высоте потребовали бы, чтобы более 1/3 валидаторов подписали противоречие — и это доказуемо, за это слэшат. Если предлагающий молчит, срабатывает таймаут и раунд увеличивается, предлагающий меняется.

Цена — приоритет safety над liveness: если офлайн больше трети стейка, сеть просто останавливается и не производит блоков, пока валидаторы не вернутся. Это происходило в реальности не раз в цепях Cosmos. Nakamoto consensus в такой ситуации продолжил бы работать, просто медленнее.

Сравнение честно

Свойство PoW (Bitcoin) Gasper PoS (Ethereum) BFT PoS (CometBFT)
Ресурс безопасности Энергия и железо Заблокированный стейк Заблокированный стейк
Финальность Вероятностная, ~60 мин Экономическая, ~13 мин Немедленная, ~1–6 с
Число валидаторов Не ограничено Сотни тысяч и больше Обычно 100–200
Ведёт себя при отказе 1/3 Работает медленнее Inactivity leak, восстановление Полная остановка
Модель доверия при старте Только генезис Нужен недавний чекпоинт Нужен недавний чекпоинт
Наказание за атаку Потраченная энергия Сожжённый стейк Сожжённый стейк
Стоимость участия Капитальные затраты на ASIC 32 ETH и надёжный узел Отбор в набор валидаторов

Ни один вариант не «лучше». Это разные точки на кривой между открытостью, скоростью финальности и поведением при частичном отказе.

Атака 51 %

Что атакующий может и чего не может

Самое частое заблуждение: «51 % позволяет украсть все монеты». Нет. Контроль большинства ресурса даёт власть над порядком, а не над правилами и не над подписями.

Можно:

  • Провести двойную трату: отправить монеты бирже, дождаться зачисления и вывода в другом активе, затем опубликовать более тяжёлую ветку, где отправки не было.
  • Цензурировать: не включать чьи-то транзакции в свои блоки (полностью — только при контроле, стремящемся к 100 %).
  • Осиротить чужие блоки, вытеснив остальных майнеров из вознаграждений.

Нельзя:

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

Механика двойной траты по шагам: атакующий начинает тайно майнить ветку от блока $N$, одновременно публикуя в публичной ветке платёж бирже. Биржа ждёт свои шесть подтверждений и отдаёт средства. Атакующий публикует накопленную тайную ветку, в которой платежа нет; она тяжелее, сеть реорганизуется, монеты возвращаются атакующему, а выведенное с биржи остаётся у него.

Это не теория

Реальные случаи, все проверяемые:

  • Ethereum Classic, январь 2019: реорги глубиной более 100 блоков, двойные траты примерно на 1,1 млн USD. В августе 2020 — три атаки подряд, одна с реоргом глубиной свыше 7000 блоков. Coinbase публично описала первую атаку и подняла требование до 5000+ подтверждений.
  • Bitcoin Gold, май 2018: около 18 млн USD двойных трат; повторно в январе 2020.
  • Verge, Vertcoin, Feathercoin и десятки мелких PoW-монет — атаки арендованным хешрейтом.

Общий знаменатель: малая доля от общего хешрейта своего алгоритма. Если сеть использует SHA-256, но владеет одним процентом мирового SHA-256-хешрейта, её безопасность равна цене аренды этого одного процента на несколько часов. Отсюда практическое правило инженера, а не инвестора: безопасность цепи определяется не её собственным хешрейтом, а стоимостью аренды превосходящего его хешрейта.

Selfish mining: атака дешевле 51 %

Eyal и Sirer (2013) показали, что майнер, придерживающий найденные блоки и публикующий их стратегически, получает долю вознаграждения выше своей доли хешрейта уже примерно с 25–33 %. Это не двойная трата, а искажение стимулов: рациональные майнеры начинают присоединяться к эгоистичному пулу, ускоряя концентрацию. Порог «51 %» — верхняя граница проблемы, а не нижняя.

Атака на PoS

В Gasper прямого аналога 51 % нет — есть три разных порога:

  • ~34 % стейка — можно помешать финальности (не набрать 2/3), запустив inactivity leak. Сеть выживет, атакующий потеряет часть баланса.
  • ~51 % стейка — контроль над выбором головы: цензура, реорги нефинализированных блоков.
  • ~67 % стейка — можно финализировать конфликтующие чекпоинты, но за это сжигается весь стейк атакующего.

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

Что из этого следует для вашего кода

Консенсус — не абстракция «где-то в сети». Он протекает в приложение через три конкретные вещи.

1. Реорг-безопасная индексация

Наивный индексатор, который просто читает блоки подряд и пишет события в БД, сломается на первом же реорге: в базе останутся события транзакций, которых больше нет в цепи. Правильный подход — хранить хеш блока и уметь откатываться:

from dataclasses import dataclass


@dataclass
class Head:
    number: int
    hash: str
    parent_hash: str


class ReorgAwareIndexer:
    """Индексатор, переживающий реорганизации цепи."""

    def __init__(self, rpc, db, confirmations: int = 12):
        self.rpc, self.db = rpc, db
        self.confirmations = confirmations   # глубина, ниже которой не откатываемся

    def step(self) -> None:
        next_number = self.db.last_indexed_number() + 1
        block = self.rpc.get_block(next_number)
        if block is None:
            return                                    # ждём новый блок

        expected_parent = self.db.hash_of(next_number - 1)
        if expected_parent is not None and block.parent_hash != expected_parent:
            # родитель не тот, что мы записали — произошёл реорг
            self.rollback_to_common_ancestor(next_number - 1)
            return

        self.db.apply_block(block)

    def rollback_to_common_ancestor(self, number: int) -> None:
        """Идём вниз, пока локальный хеш не совпадёт с каноническим."""
        while number > 0:
            canonical = self.rpc.get_block(number)
            if canonical is not None and canonical.hash == self.db.hash_of(number):
                break
            self.db.revert_block(number)              # откатываем все производные записи
            number -= 1

    def finalized_view(self) -> int:
        return self.rpc.get_block_by_tag("finalized").number   # то, что уже нельзя откатить

В Ethereum после Merge у RPC есть теги latest, safe и finalized. finalized — это гарантия Casper FFG, а не эвристика. Для критичных операций (зачисление средств, выдача товара) читайте finalized; для отзывчивого UI показывайте latest, но помечайте как «ожидает подтверждения».

2. Данные блока — не источник случайности

Прямое следствие того, что предлагающий блок влияет на его содержимое:

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;

contract Lottery {
    // ПЛОХО: всё внутри keccak известно или управляемо предлагающим блока
    function badRandom() external view returns (uint256) {
        return uint256(
            keccak256(abi.encodePacked(block.timestamp, block.prevrandao, msg.sender))
        ) % 100;
    }

    // ПЛОХО ПО-ДРУГОМУ: blockhash текущего блока всегда 0,
    // а история доступна лишь на 256 блоков назад
    function alsoBad() external view returns (bytes32) {
        return blockhash(block.number);      // всегда 0x00...0
    }
}

Почему это ломается. block.timestamp задаёт предлагающий с точностью до допустимого протоколом окна — в PoW это часы, в PoS предлагающий выбирает, публиковать блок или пропустить слот. block.prevrandao — это RANDAO-микс, и последний предлагающий эпохи имеет один бит влияния: опубликовать блок или промолчать, изменив итоговое значение. Одного бита достаточно, чтобы сдвинуть исход лотереи в свою пользу, если ставка окупает потерянную награду за блок. Наконец, любой контракт видит результат в той же транзакции — атакующий просто откатывает вызов через require, если исход неудачный.

Рабочие варианты: коммит-раскрытие в двух транзакциях с залогом, внешний VRF (например, Chainlink VRF), либо схемы вроде RANDAO с задержкой раскрытия. Подробнее об этом классе уязвимостей — в статье о безопасности контрактов.

3. Финальность как продуктовое решение

Сколько подтверждений ждать — это не техническая, а риск-константа. Отталкивайтесь от суммы: стоимость атаки должна превышать выигрыш. Для чашки кофе за 5 USD ноль подтверждений на L1 разумны. Для перевода на 10 млн USD — ждите финальности, а на PoW-цепях смотрите на долю сети в общем хешрейте алгоритма. Отдельная тема — финальность на L2, где к этому добавляется задержка публикации данных на L1; разберём в статье о масштабировании.

Типичные ошибки

  • «Транзакция в мемпуле — значит, прошла». Нет. Она не в цепи вообще. Её могут не включить никогда.
  • «Одно подтверждение — достаточно». Одно подтверждение при доле атакующего $q = 0{,}1$ даёт 20 % вероятность отката по формуле выше.
  • Индексация без хеша блока. Единственный способ обнаружить реорг — сравнивать parent_hash, а не номера.
  • Случайность из block.timestamp / prevrandao / blockhash. Классика потерянных денег.
  • Строгая арифметика по времени в контрактах. block.timestamp — величина с погрешностью в секунды-минуты, а не часы.
  • Вера в «новый супербыстрый консенсус» без объяснения, какое предположение он ослабил. Быстрая финальность всегда чем-то куплена: фиксированным набором валидаторов, доверием к чекпоинту, остановкой при отказах. Если в описании нет раздела о том, что ломается, — это маркетинг, и часто прикрытие для схемы отъёма денег.
  • Стейкинг «под ключ» без понимания рисков. Делегирование в кастодиальный сервис добавляет и риск слэшинга по чужой ошибке, и обычный контрагентский риск. Ключ вывода — это ваш ключ; его потеря означает потерю стейка так же окончательно, как потеря любого приватного ключа.

Мини-итог

  • Консенсус решает задачу порядка, а не подлинности: подписи и хеши её не закрывают.
  • В открытой сети голосование по головам ломается атакой Сивиллы, поэтому право голоса привязывают к дефицитному ресурсу: работе (PoW) или стейку (PoS).
  • PoW — лотерея с проверкой hash ≤ target, константной памятью и вероятностной финальностью; правило выбора форка — наибольшая накопленная работа, а не длина.
  • Цепь локально всегда дерево; реорганизации — штатное поведение, и код обязан их переживать.
  • PoS требует объективного слэшинга против nothing-at-stake и внешнего чекпоинта против long-range атак — это реальное, а не выдуманное ослабление модели доверия.
  • Gasper даёт экономическую финальность за две эпохи; BFT-протоколы вроде CometBFT — мгновенную, ценой остановки сети при отказе трети валидаторов.
  • Атака 51 % даёт власть над порядком, а не над чужими ключами; на малых цепях она реальна, дёшева и происходила многократно.
  • В коде это проявляется как реорг-безопасная индексация, чтение тега finalized и полный запрет на использование данных блока как источника случайности.

Источники

Что дальше

Мы разобрали, как сеть договаривается о порядке. Теперь посмотрим, что именно она этим порядком исполняет: аккаунты, транзакции, газ и модель состояния самой распространённой программируемой цепи.

Ethereum и EVM: аккаунты, транзакции, газ, состояние

Нашли неточность? Выделите фрагмент текста — рядом появится жучок.

Нужен разбор именно вашей ситуации?

Статья описывает общий случай. Если у вас частный — можно разобрать его отдельно, платно. А если не хватает целого материала, предложите тему: её оплачивают вскладчину, и она выходит открытой для всех.

Доска запросов