Rand Protocol
Документация
Основные понятия

Консенсус и финальность

Chained HotStuff в том виде, в каком его исполняет узел: кворумы, лидеры, правило фиксации по трём последовательным раундам, таймауты, эпохи — и что означает «финальность» для клиента в testnet.

Эта страница нужна, чтобы понять, когда транзакция финальна, сколько валидаторов должно быть в сети и что означают поля блока, которые вы видите в RPC и в обозревателе. Консенсус — это цепочечный HotStuff BFT с кворумами, взвешенными по стейку; в chain 21, как и в chain 20, это 18 из 26 валидаторов.

Правила, которые выполняет узел

правилокак реализовано
Кворумсертификат кворума (QC) — набор голосов Dilithium2 от валидаторов, держащих строго более 2/3 совокупного стейка
Расписание лидеровчистый round robin: лидер представления v — validators[v mod n], множество отсортировано по адресу. Стейк взвешивает голоса, а не выбор лидера
Предложение блокалидер представления v предлагает блок, как только у него есть QC для v − 1 или кворум сообщений NewView для v. Предложение, которое применило бы недопустимую транзакцию, отбрасывает эту транзакцию, а не блок
Голосование и безопасностьвалидатор голосует один раз за представление и только за блок, безопасный относительно его блокировки (его justify QC новее заблокированного QC, либо блок продолжает заблокированный блок). Состояние безопасности сбрасывается на диск через fsync в RocksDB до того, как голос покинет узел
Домен подписив genesis с consensus_domain: 1 (chain 21 и каждая цепочка начиная с 15) каждая подпись голоса, new-view и предложения покрывает хеш genesis (rand-vote-2, rand-newview-2, rand-block-2), поэтому подпись для одной цепочки не проходит проверку ни в одной другой. Chain 14 и более ранние подписывали без него. Начиная с chain 20 binding_domain: 1 распространяет то же правило на доказательства транзакций и подписанные действия (Транзакции)
Соседние предложенияреплика хранит один блок на пару (представление, лидер); второй, иной блок от того же лидера для того же представления отклоняется как эквивокация. Предложение, опережающее собственное представление реплики более чем на 8 представлений (PROPOSAL_VIEW_WINDOW), отклоняется до проверки подписи; заполненное спекулятивное дерево вытесняет блоки с побочных ветвей, прежде чем что-либо отклонить
Снятие блокировкиблокировка никогда не понижается автоматически (CON-4, v0.6.8). Она уступает только предложению, чей justify QC старше её, либо поступлению заблокированного блока. Кворум подписанных ответов NotHeld записывается в журнал для оператора и ничего не снимает, а валидатор фиксирует свои голоса, чтобы никогда не подписывать NotHeld для блока, за который он голосовал. Каждый сертифицированный блок выше головы сохраняется на диск и восстанавливается при перезапуске, поэтому перезапуск всего флота не оставляет QC на блоке, которого ни у кого нет. Валидатор, чей заблокированный блок утрачен безвозвратно, освобождается вручную, офлайн: rand-node safety release-lock
Сборка QCкаждый валидатор собирает QC локально из голосов, распространяемых по сети, поэтому цепочка продолжает фиксировать блоки, даже когда следующий лидер не в сети
Фиксацияцепочечное правило трёх QC: блок фиксируется, когда три сертификата выстраиваются поверх него в идущих подряд представлениях v, v+1, v+2, и эта цепочка прослеживается обратно до зафиксированной головы
Тайм-ауты и смена представлениябазовый --view-timeout-ms 3 000 ms, удваивается при каждом следующем подряд тайм-ауте, сбрасывается при любом прогрессе. Пейсмейкер на сертификатах тайм-аута (CH-1, v0.6.8): реплика, у которой сработал таймер, остаётся в своём представлении и подписывает NewView с запросом следующего. Она входит в представление по QC для предыдущего представления или когда валидаторы, держащие более 2/3 стейка, запросили это представление или более позднее; она присоединяется (подписывает собственный запрос), когда более 1/3 запрашивают представление впереди её собственного. NewView одного валидатора никого не сдвигает. Представления ограничены (MAX_VIEW_AHEAD = 10⁶)
Темплидер предлагает пустой блок, когда ничего не ожидает, не раньше чем через --block-interval-ms (по умолчанию 1 000 ms) после последнего блока, увиденного от любого предлагающего
Границы голосов и рассылкиголоса, опережающие более чем на 8 представлений, игнорируются, и учитывается один голос на пару (представление, голосующий); рассылка консенсуса ограничивается по каждому пересылающему пиру (всплеск 256, 64/s); предложение с неизвестным родителем сохраняется как осиротевшее, только если его автор лидирует в своём представлении в наборе текущей эпохи или следующей, оно находится в пределах границы дерева от зафиксированной головы, а его размер и корень транзакций корректны (пул осиротевших блоков ограничен 64 MiB, а начиная с v0.6.1 — 16 осиротевшими блоками на предлагающего). Начиная с v0.6.8 предложение ретранслируется, а осиротевший блок сохраняется, только если список его транзакций хешируется в корень, на который указывает заголовок, и ни одна транзакция не повторяется (GOSSIP-2)
Лимиты блокаmax_block_bytes цепочки в байтах транзакций (по умолчанию 4 MiB; 19 MiB, 19 922 944 байт, в genesis chain 21; 20 MiB в chain 20) и 2 000 транзакций; лимиты обеспечиваются предлагающим и повторно каждой репликой в Ledger::apply_block
Компактные блоки (v0.8.0, только узел)лидер рассылает CompactProposal (тег 4 в теме консенсуса): заголовок, его подпись и хеши транзакций — 166 416 байт при 26 валидаторах и 2 000 хешей — вместо полного блока. Реплика предварительно проверяет заголовок, восстанавливает блок из своего мемпула и кеша недавно виденных транзакций и запрашивает по хешу только недостающие тела (SyncRequest::Transactions, по 512 за запрос; их считает rand_status.compact_fetched). Старый узел не может декодировать этот кадр, поэтому изменение вводилось вместе с выпуском chain 21; смешанный набор валидаторов перестал бы фиксировать блоки
Применение вне цикла (v0.8.0, только узел)последователь применяет входящее предложение в блокирующем потоке (не более 4 одновременно, до 8 в ожидании, не более 2 на предлагающего), тогда как предварительные проверки и вставка остаются в цикле консенсуса; второй блок лидера для того же представления ждёт за первым и отклоняется как эквивокация, когда первый уже вставлен. rand-node run --apply-proposals-inline возвращает применение в цикле
Время блокав цепочке с разделом bridge: timestamp_ms блока не может предшествовать родительскому (TimestampRewind) и превышать его более чем на 60 s (TimestampLeap), а валидатор не голосует за блок, опережающий его собственные часы более чем на 15 s; см. ниже

Правило фиксации по трём идущим подряд представлениям несёт нагрузку: его ослабление вновь открывает ошибку конфликтующей финальности, на которую у fullnode есть регрессионный тест. Всё, что клиент видит через RPC, зафиксировано: отдаются только зафиксированные блоки, а поток WebSocket newHeads отправляет одно уведомление на каждый зафиксированный блок, по порядку.

Таблица соответствует v0.8.0 (c0878c3f) — выпуску, на котором chain 21 работает с 2026-10-12; две строки v0.8.0 — изменения только узла, не меняющие ни голосов, ни правил фиксации, а остальное — то, что v0.6.8 принесла в chain 20. Три правила появились в v0.6.8 (аудит v6) и действуют с запуска chain 20 2026-10-01: пейсмейкер на сертификатах тайм-аута (CH-1), отсутствие автоматического снятия блокировки (CON-4) и проверка списка транзакций в предложении (GOSSIP-2). В chain 18 и 19 (v0.6.7) подписанный NewView одного валидатора переводил каждую реплику в его представление, а блокировка понижалась по подписанным ответам NotHeld от кворума стейка. Пейсмейкер и правило блокировки меняют то, что валидаторы подписывают и когда, поэтому v0.6.8 вводилась остановкой и запуском всех узлов разом вместе с выпуском chain 20, а не узел за узлом.

Числа testnet

В chain 21 26 валидаторов, у каждого стейк ровно 1 000 RAND, и все 26 — в её genesis на ключах и peer id, на которых работали chain 17–20. В chain 15 в genesis было восемнадцать, на ключах chain 14, а ещё восемь были связаны 2026-09-27 по регистрациям v2 и стали активны с эпохи 43. Стейк одинаков, поэтому кворум — это подсчёт голов: чтобы зафиксировать хоть что-то, в сети должны быть 18 из 26. При 17 в сети представления сменяются, но ничего не финализируется. Набору из n равных валидаторов нужно более 2/3 стейка: 2 из 2, 3 из 4, 5 из 6, 13 из 18, 18 из 26. Предел веса в genesis (3 333 bps на валидатора) не ограничивает набор равных стейков.

Блоки идут с темпом по умолчанию 1 000 ms. В chain 15 на практике они появлялись примерно каждые 1,2 s (в среднем 1,18 s за первые 29 часов); в chain 20 обозреватель показывал среднее 1,34 s на высоте 3 270 (2026-10-01, avg_block_time_ms), а в chain 21 — 1,17 s на высоте 2 283 (2026-10-12). Обозреватель сообщает среднее время блока, которое наблюдает (avg_block_time_ms в GET /stats). Представление валидатора опережает высоту всякий раз, когда представления истекают по тайм-ауту, поэтому rand_getHead возвращает оба значения.

Эпохи

Набор валидаторов заново выводится из реестра каждые epoch_blocks = 1 000 блоков (параметр генезиса). epoch(h) = h / 1000; набор для эпохи e ≥ 1 выводится из реестра в состоянии на последний блок эпохи e − 1: берётся каждая строка со stake ≥ 1 000 RAND, первые 100 по стейку, отсортированные по адресу. В цепочке без секции staking бонд получает вес на ближайшей границе. В chain 20 он ждёт ещё две эпохи, а на каждую эпоху действует бюджет входа в 2 500 bps от активного связанного веса на границе (6 500 RAND, пока набор держит 26 000), и бонд, регистрирующий нового валидатора, отклоняется, если набор предварительно не проголосовал за этот ключ через AdmitValidator (staking.admission_by_vote). Бюджет chain 19 был фиксированным: 10 000 RAND за эпоху, и голосования за допуск там не было. Вывод бонда снижает вес на ближайшей границе, а сама сумма становится доступной для вывода только через две эпохи. rand_getEpoch возвращает эпоху, длину эпохи и прогноз следующего набора. См. Стейкинг.

Что несёт блок

{
  "hash": "647b…", "height": 50, "view": 92, "parent": "2d41…",
  "proposer": "3v3VBJ…", "timestamp_ms": 1788000123456,
  "tx_root": "0000…", "state_root": "a1b2…", "justify_view": 91,
  "tx_count": 0, "transactions": []
}

view — это view, в котором блок был предложен, а justify_view — view QC, который блок несёт для своего родителя. timestamp_ms устанавливает предлагающий валидатор в границах, описанных в следующем разделе. state_root фиксирует реестр после блока. В chain 21 (поля chain 20, в которые, как показано ниже, добавлено состояние perps и multisig):

inner      = blake3("rand-state-8" || tree_root || nullifier_root || validators_root || programs_root
                                   || bridge_root || tokens_root
                                   || faucet_epoch || faucet_minted_in_epoch || bond_queue_root
                                   || gas_price || byte_price
                                   || program_state_root)
state_root = blake3("rand-state-admitted-1" || inner || admitted_root)

Chain 19 фиксировала те же поля без program_state_root, под rand-state-7, без обёртки.

Состав компонентов зависит от генезиса. Цепочка без необязательных секций фиксирует первые четыре под rand-state-2. Секция bridge добавляет bridge_root. Секция tokens добавляет после него tokens_root и переопределяет домен всего хеша как rand-state-4; генезис с секцией bridge обязан иметь секцию tokens, поэтому у chain 21, как и у chain 20, есть обе. Секция aggregation следом добавляет корень агрегаторов (под rand-state-3 в цепочке без токенов); у chain 21 её нет. Секция staking последней добавляет два эпохальных счётчика крана и корень очереди бондов (rand-bond-queue-1) и переопределяет домен всего хеша как rand-state-5, что бы ещё ни было включено; у chain 14 такой секции не было, и она хешировала под rand-state-4, а цепочки с 15 по 17 хешировали под rand-state-5. Секция vesting добавляет после этого корень реестра вестинга и переопределяет домен хеша как rand-state-6; у chain 21 её нет. Секция gas с контроллером dynamic добавляет после всего остального текущие gas_price и byte_price, каждое как big-endian u64, и переопределяет домен хеша как rand-state-7. Это был домен chain 19, а chain 20 и chain 21 сохраняют цены в своём корне: они меняются один раз за блок, после транзакций блока и до вычисления корня (Комиссии). Секция gas с фиксированными ценами в корень не входит. Секция program_state (RPL-2) добавляет после цен корень ячеек и хранилищ каждой программы и переопределяет домен хеша как rand-state-8: это домен chain 20. Секция perps (RPL-3, v0.8.0) добавляет после этого состояние биржи и переопределяет домен хеша как rand-state-9: это домен chain 21; её секция multisig оборачивает реестр multisig-счетов ещё раз под rand-state-multisig-1 (а секция fees.usd_subsidy, которой в chain 21 нет, добавила бы проголосованную цену под rand-state-price-1). Состояние стейкинга из аудита v6 вплетено вокруг этого, а не внутрь: при staking.admission_by_vote (chain 20 и chain 21) корень равен blake3("rand-state-admitted-1" || inner || admitted_root), а при staking.slashing (пока ни в одной цепочке) вторая обёртка добавляет набор заблокированных (jailed) валидаторов. tokens_root — корень Меркла листьев blake3("rand-token-leaf-1" || bincode(TokenInfo)) в порядке индекса, поэтому метаданные каждого токена, его полномочия, mint_nonce, предложение и сумма locked каждого обеспечения являются консенсусным состоянием: два узла, расходящихся хотя бы в одной строке токена, расходятся относительно цепочки. В chain 20 корень токенов также включает расширение реестра (лимит max_tokens, сожжённую плату за регистрацию, границу стоимости ноты и скользящие окна лимитов выпуска моста) под rand-token-registry-3. Корень моста охватывает эмитентов, наборы хранителей (guardian), потраченные дайджесты аттестаций, порядковый номер сжигания, PQ-ключи хранителей, ключ паузы, флаг паузы и два governance-nonce; реестр активов вынесен из него в tokens_root. Мост chain 20 использует rules_v2, поэтому добавляются nonce ротации и глобальные параметры лимита, а домен — rand-bridge-state-5 (у chain 14 был rand-bridge-state-4); затем этот корень оборачивается нижней границей входящего воспроизведения (rand-bridge-replay-floor-1), а в chain 20 ещё и правилами ротации с ожидающими ротациями (rand-bridge-rotation-1) и группой комиссий (rand-bridge-fees-1). Реестр валидаторов хешируется в validators_root (домен листа rand-validator-leaf-4 в chain 20, с activation_epoch каждой строки; rand-validator-leaf-2 в цепочке без секции staking), поэтому узел, расходящийся хотя бы в одной строке реестра, расходится относительно цепочки.

Время блока ограничено (B2)

В цепочке с мостом временная метка блока определяет истечение набора хранителей и сутки, к которым относится суточный лимит выпуска обеспечения, поэтому лидер, способный перевести часы вперёд, мог бы досрочно закончить льготное окно ротированного набора или перескочить через суточный лимит. У chain 21, как и у chain 20, есть секция bridge, поэтому действуют три правила, все с условием на неё (и секция perps ограничивает время блока так же):

правилограницавид
без отката назадtimestamp_ms ≥ parent.timestamp_ms (равенство допускается), иначе BlockError::TimestampRewind; предлагающий валидатор выставляет max(now, parent.timestamp_ms)валидность, проверяется и при воспроизведении, и при синхронизации
без скачка вперёдtimestamp_ms ≤ parent.timestamp_ms + MAX_TIMESTAMP_STEP_MS (60 000 ms), иначе BlockError::TimestampLeapвалидность, проверяется и при воспроизведении, и при синхронизации
дрейф часоввалидатор отказывается голосовать за блок, опережающий его собственные часы более чем на MAX_CLOCK_DRIFT_MS (15 000 ms)только правило голосования: блок остаётся в спекулятивном дереве, кворум с согласованными часами всё равно может его заверить, а воспроизведение зафиксированной истории это правило никогда не применяет

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

timestamp_ms генезиса должен быть близок к реальному времени запуска. Если он слишком далеко в прошлом, время блока отстаёт от настенных часов, пока ограничение на шаг не позволит его догнать, не более чем на 60 секунд за блок. Если он более чем на 15 секунд в будущем, блок 1 не фиксируется вовсе: каждый голос, включая голос самого лидера, удерживается, пока настенное время не достигнет этой метки. После долгой остановки такое же наверстывание может переключить сутки лимита выпуска чаще одного раза за реальные сутки, но не более чем на лимит одних дополнительных суток за каждые полные сутки накопленного отставания.

Финальность для клиента

  • Транзакция финальна, когда rand_getTransaction(hash) возвращает блок. Принятие через rand_sendTransaction означает лишь допуск в мемпул.
  • Блок, выданный RPC или проиндексированный обозревателем, зафиксирован. Окна финальности, которое нужно пережидать, нет; руководство обозревателя говорит то же самое.
  • Заголовок из rand_subscribe ["newHeads"] — это заголовок, который узел не потеряет: ничего не отправляется, пока блок не зафиксирован в хранилище.
  • Узел, отставший от сети, заново проверяет всё: каждый сертификат, каждый пакет и доказательство вызова, каждую квитанцию, прежде чем принять синхронизированный блок. Подписи финальности никогда не заменяют доказательства.

Шпаргалка по восстановлению, из собственной документации полного узла: если высота не растёт при низком peer_count, это сетевая проблема; если высота не растёт при нормальном числе пиров, онлайн менее 2/3 стейка; CORRUPT CHAIN при запуске означает, что узел обрезал повреждённый хвост и заново синхронизируется.

Последняя проверка по коммиту fullnode c0878c3f (chain 21) от 2026-10-12. Если код и эта страница расходятся, прав код; сообщите нам.