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

Mempool

Что узел делает с транзакцией между её получением и включением в блок: порядок допуска, отслеживание конфликтов вместо nonce, четыре рабочих потока верификации, счётчик нагрузки пиров и кэш вердиктов, как proposer выбирает блок и почему проигравшая двойная трата исчезает сама.

Используйте эту страницу, чтобы понять, что происходит с транзакцией после того, как rand_sendTransaction её принял, и до того, как её включит блок: почему её отклонили, если отклонили; почему она может исчезнуть из пула, так и не получив отказа; в каком порядке заполняется блок; и что поток некорректных доказательств может и чего не может сделать с валидатором. Мемпул находится в crates/randprotocol-node/src/mempool.rs; защиты перед ним — в admission.rs.

Нет отправителей, а значит, нет нонсов

В цепочке на основе счетов мемпул упорядочивает транзакции каждого отправителя по нонсу. В экранированной цепочке отправителей нет: пакет не несёт ни адреса, ни подписи, и два пакета связаны лишь тогда, когда затрагивают одну и ту же ноту. Поэтому упорядочивать по отправителям нечего. Учёт нонсов заменяет отслеживание конфликтов: пул индексирует нуллификаторы и коммитменты каждой ожидающей транзакции и отклоняет новую, которая претендует на любой из них. То же относится к немногим счётчикам, для каждого значения которых реестр принимает ровно одну транзакцию: регистрационный нонс валидатора (Unbond или Withdraw), mint_nonce токена (общий для TokenMint и SetAuthority), индекс реестра, которому соответствует RegisterToken, а также pause_nonce и list_nonce моста. Каждый из них — слот, который может занимать не более одной транзакции пула. Из двух таких транзакций могла бы попасть в блок самое большее одна, а удержание второй стоило бы proposer’у места в блоке, а каждому узлу — проверки доказательства в каждом раунде gossip.

Пул вмещает не более 10 000 транзакций (Mempool::new(10_000) в node.rs) и, начиная с v0.6.8, не более байтов на восемь блоков: MAX_POOL_BLOCKS = 8 умножить на max_block_bytes цепочки, то есть 152 MiB в цепочке 21 (восемь блоков по 19 MiB; 160 MiB в цепочке 20) (аудит v6, CH-7). До этого ограничивалось только количество, тогда как одна транзакция с доказательством занимает от 1,3 MB до более чем 12 MB. Каждая транзакция проверяется по реестру на вершине дерева консенсуса, а не по последнему зафиксированному блоку: по состоянию, на котором proposer действительно строил бы блок.

Допуск: сначала дешёвое, затем дорогое

Транзакция, пришедшая по RPC или gossip, проходит эти шаги по порядку, и первый отказ и есть ошибка, которую вы видите:

  1. Дубликат. Хеш уже есть в пуле: already in mempool.
  2. Конфликт. Нуллификатор, коммитмент или один из слотов нонсов и индексов, описанных выше, уже занят ожидающей транзакцией: conflicts with a pending transaction over <hex>. Для токенового слота hex — это отображаемый ключ: слово-метка (tokn для mint_nonce, tidx для индекса регистрации), затем индекс и нонс. Аттестация моста, за которую состязались два релея, различается по своему дайджесту: conflicts with a pending transaction over bridge attestation <hash>.
  3. Переполнение. mempool full. Проверка ёмкости идёт последней из трёх. При достижении лимита по байтам новая транзакция может занять место записей пула, которые стоят строго ниже неё, — по комиссии сверх текущего минимума каждой на KiB, затем по комиссии, — вытесняя самые дешёвые первыми, пока она не поместится; если их недостаточно или она лишь равна им, отклоняется именно она. Governance-действия находятся вне обоих лимитов и никогда не вытесняются: четыре governance-действия моста PauseMints, UnpauseMints, RegisterBridgedToken, ListBacking, а с v0.6.8 также RotatePqGuardians, RotatePauseKey, их формы …V2, CancelRotation, AdmitValidator и SlashEquivocation. Их байты и записи по-прежнему учитываются в лимитах. PauseMints — тормоз на случай компрометации ключа guardian’а: без пакета, без комиссии, и это та единственная транзакция, которая должна попасть в блок, пока атакующий выпускает токены, поэтому полный пул не должен её не пускать. Исключение ограничено шагом конфликтов: pause_nonce и list_nonce допускают по одной транзакции в пуле за раз, так что четыре действия моста могут добавить сверх лимита не более двух записей; остальные тратят каждое свой нонс или свой кворум.
  4. Дешёвая половина Ledger::validate. Всё, что может устареть между допуском и включением и не требует доказательства: якорь — один из последних 1 024 корней в цепочках 20 и 21 (proof_window_blocks из genesis; 256 без него), time лежит внутри окна, нуллификаторы не потрачены, коммитментов ещё нет, дайджест аттестации не занят, регистрационный нонс стейкинг-действия актуален, mint_nonce или индекс регистрации токенового действия совпадает с тем, на котором находится реестр (wrong mint nonce, wrong token index, если цепочка ушла дальше), нонс паузы или листинга совпадает с текущим нонсом моста. Коммитменты, которые транзакция создаёт, вычисляются тоже здесь, включая ноту, которую сам реестр выводит для Withdraw, BridgeAttest или выплаты агрегатору: её нет в передаваемых данных, но она тоже не должна ни с чем совпадать. Лимиты размера проверяются до какого-либо декодирования: доказательство больше max_proof_bytes цепочки (по умолчанию 2 MiB, 4 MiB в цепочке 20), аттестация больше 16 KiB или транзакция больше блока (max_block_bytes: по умолчанию 4 MiB, 20 MiB в цепочке 20) отклоняется сразу, так что раздутый блоб не оплачивает никакой работы. Доказательство пакета в запечатанной (pruned) маркерной форме отклоняется на этом же шаге вне синхронизации запечатанной формы, как PrunedFormOutsideSync. Затем, по-прежнему ничего не проверяя, пул читает заголовки доказательств. Первыми идут три правила: вызов, у которого таблица входов, keccak или sha256 содержит менее 2^7 строк, в пул не попадает (CallRevealsPrivateInputs), как и деплой, который не мог бы удержать ни один вызов (ProgramUncallable), и транзакция, у пакета или доказательства вызова которой есть поле заголовка, какое честный prover не записал бы (NonCanonicalProof). Последние два правила также являются правилами реестра в цепочке 20 под hardening_v6. С v0.6.8 заголовок, чья заявленная степень превышает потолок самой высокой таблицы, отклоняется как NonCanonicalProof до того, как проверка начнёт его обходить (аудит v6, ZKV-4), а паника в любом месте этой предварительной проверки означает отказ этой транзакции (VerifierPanicked), а не падение узла. Затем минимум комиссии: в цепочке 20 пул декодирует лимит газа, который заявляет доказательство вызова или invoke, и отклоняет комиссию ниже BUNDLE_BASE + gas_price × limit + byte_price × ⌈bytes/1024⌉ по ценам вершины, плюс плату за ячейки у invoke, как FeeTooLow (Комиссии). Это собственное правило блока, так что пул требует ровно то же, что и блок.
  5. Дорогая половина, вне цикла консенсуса. Транзакция ставится в очередь глубиной не более 64 к одному из четырёх воркеров проверки. Воркер выполняет остаток Ledger::validate в блокирующей задаче: дайджест пакета, лимит газа, который заявляет доказательство пакета (с цепочки 18 ровно 20 479, иначе BundleGasLimit), доказательство пакета и, начиная с цепочки 17, его доказательство авторизации относительно привязки транзакции (в цепочке 21 — относительно hc_auth или, для доказательства мультиподписной формы, hc_auth_multisig; с v0.8.0 каждый вердикт Ok хранится в кеше проверок на 32 768 записей, так что при применении блока доказательство заново не проверяется), а для вызова — его собственное доказательство и минимум комиссии. Проверка идёт по снимку вершины, который клонируется лениво, не чаще одного раза за смену вершины и только пока что-то ожидает. Транзакция из gossip, заставшая очередь полной, отбрасывается — не проверяется и не пересылается, — вместо того чтобы блокировать цикл, который голосует; со следующим раундом gossip пир принесёт её снова.
  6. Повторная проверка и помещение в пул. insert_verified выполняет шаг 4 заново на вершине, на которой транзакция действительно попадает в пул, потому что вершина могла сдвинуться, пока проверялось доказательство, затем принимает её и, для gossip-трафика, пересылает.

Шаги 1–4 — это Mempool::precheck; шаг 5 — единственный, где проверяется STARK. Поскольку эта цена оплачивается на воркере, поток некорректных доказательств занимает четыре потока и никогда — цикл консенсуса. Транзакции и действия перечисляют собственный порядок допуска в реестре, который шаги 4 и 5 вместе и выполняют.

Две защиты и кеш для gossip

Перед допуском две вещи применяются только к транзакциям, приходящим от пиров; отправки по RPC их пропускают, потому что этот порт принадлежит самому оператору и уже ограничен лимитом тела запроса. Единственный путь RPC, который измеряется, — faucet, ниже.

  • Счётчик байтов для каждого пира. Начиная с v0.6.8 каждый gossip-кадр списывается с байтового бюджета переславшего его пира ещё до декодирования: всплеск в восемь кадров максимального передаваемого размера и пополнение по два кадра в секунду; сверх этого кадр отбрасывается недекодированным (аудит v6, GOSSIP-1). Идентификатор транзакции, который хеширует всё доказательство, вычисляется только в пределах квоты пересылающего пира.
  • Счётчик транзакций для каждого пира. Token bucket на каждого пересылающего пира: всплеск 16, пополнение 4 в секунду (PEER_TX_BURST, PEER_TX_PER_SEC). Он учитывает пира, переславшего транзакцию, а не её автора, которого защищённый блокчейн назвать не может.
  • Кэш вердиктов. Транзакция, которую этот узел уже отклонил по причине, зависящей только от её байтов, например из-за неверного доказательства, неверной подписи или части недопустимого размера, отклоняется повторно из FIFO-кэша на 8 192 записи (REFUSED_CACHE_ENTRIES), без повторной проверки. Вердикт, который говорит о состоянии этого узла, такой как неизвестный якорь или потраченный нуллификатор, никогда не кэшируется: иначе узел, отставший на один блок, настроил бы себя против транзакций, которые вот-вот станут допустимыми. Список работает как белый список, поэтому новая ошибка считается состоянием, пока кто-либо не решит иначе. Эти записи показывают, где проходит граница:
    • BadPauseSignature (the pause signature does not verify) с v0.6.8 не является постоянной (аудит v6, BRG-18). Она проверяет подпись по текущему ключу паузы цепочки, который могут сменить правила моста v2, а значит, это состояние: узел, отставший на один блок от ротации, иначе отклонял бы PauseMints нового держателя до перезапуска. Индекс PQ-сосигнатуры вне допустимого диапазона вышел из кэша по той же причине. Вплоть до v0.6.7, в цепочках 18 и 19, обе кэшировались как постоянные. Теперь повторная неверная пауза стоит одной проверки Dilithium2 на каждую доставку, ограниченной счётчиком пира.
    • PrunedFormOutsideSync намеренно не является постоянной. Маркерная форма запечатанного пакета хеширует в идентификатор самой исходной транзакции, а кэш ключуется по этому идентификатору, поэтому кэширование отказа позволило бы любому gossip-пиру цензурировать честную транзакцию во всей сети.
    • Заявленный в доказательстве лимит газа есть его собственные байты, сравниваемые с константой genesis, поэтому BundleGasLimit и AuthGasLimit являются постоянными. FeeTooLow не является: в цепочке с учётом газа нижняя граница движется вместе с ценами. Три политики заголовка из шага 4 тоже никогда не кэшируются: каждая выражает знание данной сборки, которое более поздняя сборка может изменить.
    • В цепочке со списком допущенных получателей фаусета (цепочки 20 и 21) FaucetRecipientNotAllowed является постоянной: она сравнивает получателя самого минта с константой genesis. FaucetBudgetExhausted не является: это состояние, и тот же минт будет принят снова в следующую эпоху.
    • BadSignature токен-действия тоже не является постоянной, в отличие от BadSignature действия стейкинга: мint-авторитет токена есть состояние, SetAuthority может передать его другому ключу, и одни и те же байты могут быть отклонены до фиксации этой ротации и приняты после неё. Побайтовые отказы токенов (неверное имя или символ, слишком много десятичных знаков, нулевая сумма, индекс токена 0) кэшируются; так же кэшируются поле сжигания, которое действие не вправе задавать, минт, чей коммитмент не раскрывается в его сумму, и депозит через мост, чьё ослепление не является выведенным.

Лимит фаусета

rand_mint не требует комиссии и стоит одной транзакции в пуле на вызов, а в цепочке 21, как и в цепочке 20, фаусет включён, и за мостом стоит реальная стоимость. Genesis ограничивает, что он может делать: минт платит только 16 кошелькам из списка допущенных и не более 10 000 RAND за эпоху в 1 000 блоков (Staking), а подписать его может только один из 18 ключей из списка genesis staking.faucet_minters (MinterNotAllowed, никогда не кэшируется: правило реестра с цепочки 16, политика узла в цепочке 15, где этим списком были валидаторы genesis). Сверх этого каждый узел ограничивает свой фаусет сам: восемь минтов подряд, пополнение по одному в секунду (FAUCET_MINT_BURST, FAUCET_MINT_PER_SEC). Сверх лимита вызов отвечает faucet is rate limited on this node (8 mints back to back, refilling at 1/s); try again shortly. Лимит действует на процесс узла, а не на вызывающего, потому что у порта RPC нет идентичности пира, которую можно было бы учитывать, а защищаемый ресурс — пул этого узла. Вместе с изъятием из ограничения ёмкости, описанным выше, наплыв запросов к фаусету не может заполнить пул раньше PauseMints.

Gossipsub работает с валидацией на уровне приложения, поэтому транзакция пересылается другим пирам только после того, как она проверена здесь. Пир, заливающий сеть недопустимыми транзакциями, ограничивается счётчиком, получает ответы из кэша и ничего не отнимает у цикла консенсуса.

Сборка блока

Когда валидатор ведёт view, он запрашивает у пула кандидатов. candidates_within оставляет только те транзакции, которые всё ещё применимы к реестру, на котором строится блок, повторно выполняя дешёвую половину допуска: якорь всё ещё в окне, time всё ещё в нём, нуллификаторы ещё не потрачены, коммитменты ещё не присутствуют. Также отбрасывается токен-действие, мимо mint_nonce или индекса регистрации которого цепочка уже прошла, и пауза или листинг с устаревшим nonce. С v0.6.8 она заново оценивает каждый вызов и invoke по ценам, действующим на вершине, на которой строится блок, исходя из лимита газа, запомненного при допуске, и пропускает транзакцию, комиссия которой теперь ниже нижней границы; та остаётся в пуле и предлагается снова, когда цены упадут (аудит v6, CH-9). С цепочки 20 остальное упорядочивается сначала по управлению, затем по превышению комиссии над текущей нижней границей на КиБ транзакции, затем по комиссии, от большей к меньшей, при равенстве по хешу, так что каждый честный предлагающий, строящий на одном и том же пуле, выбирает один и тот же блок, и заполняется до бюджета в 2 000 транзакций и лимита блока цепочки (MAX_BLOCK_TXS; max_block_bytes, по умолчанию 4 МиБ, 20 МиБ в цепочке 20 и 19 МиБ в цепочке 21). Блоку не хватает именно байтов, поэтому превышение берётся на КиБ. Цепочка без секции gas и узел без политики газа пропускают этот ключ и упорядочивают по управлению, комиссии и хешу, как в цепочке 15. Управление первым — вторая половина гарантии паузы: PauseMints не платит комиссии по замыслу, так как должна работать из кошелька без RAND, поэтому простой порядок по комиссии поставил бы аварийный тормоз позади каждой платящей транзакции.

Ещё одно правило действует, пока готов вызов или invoke (аудит v6, POOL-2; политика узла в каждой цепочке, а не правило допустимости). Только они платят цену за байт, а перевод, платящий чуть больше своей плоской базы, обгоняет их по КиБ, так что поток переводов мог бы не пускать готовый вызов ни в один блок. Поэтому транзакции с плоской комиссией занимают в первом проходе не более трёх четвертей байтов блока; после размещения вызовов оставшееся место заполняется отложенными переводами в их порядке. Место в блоке не теряется, а при отсутствии готовых вызовов ничего не меняется (Fees).

Транзакция, слишком большая для оставшегося места, пропускается, но блок на этом не заканчивается: меньшая за ней всё равно проходит. Перевод в цепочке 18 занимает около 2,85 МБ: доказательство пакета 1,49 МБ и доказательство авторизации 1,36 МБ, поэтому в блок цепочки 20 размером 20 МиБ помещалось около семи, в блок цепочки 21 размером 19 МиБ помещается шесть такого размера (размеры доказательств для набора ограничений 9 здесь ещё не измерены), а в блок по умолчанию 4 МиБ — один. В цепочке 15, при около 1,43 МБ на перевод, в 20 МиБ помещалось четырнадцать. Под нагрузкой решает, какие именно, комиссия (Fees). С v0.8.0 пул также служит источником, из которого реплика восстанавливает блок: лидер рассылает заголовок и хеши транзакций (компактный блок, Consensus), а реплика по хешу запрашивает только те тела, которых нет в её пуле и кэше недавних транзакций; разосланная транзакция, отклонённая счётчиком пересылающего пира, хранится непроверенной в кэше переполнения на один блок для этого восстановления и в пул не попадает никогда.

Очистка

После каждой фиксации prune удаляет из пула ровно то, что пропустила бы candidates_within: транзакции, чья нота потрачена кем-то другим, чей коммитмент уже существует, мимо чьего nonce или индекса регистрации цепочка уже прошла, или чей якорь либо time вышли за пределы окна (1 024 блока в цепочках 20 и 21, 256 вплоть до цепочки 19). Пакет, проигравший гонку двойной траты, поэтому исчезает сам, не породив никому ошибки. Принятие rand_sendTransaction не есть фиксация: опрашивайте rand_getTransaction, пока он не вернёт блок, а если окно якоря истекло без него, перестройте пакет на свежем якоре и отправьте снова.

Наблюдение

rand_status сообщает mempool_size, verify_queue (транзакции, ожидающие воркера) и refused_cache (записи в кэше вердиктов). verify_queue, стоящий на 64, означает, что доказательства поступают быстрее, чем их проверяют четыре воркера, около 16 мс на каждое при прогретом ключе; растущий mempool_size при полных блоках означает, что включение определяет комиссия, а не порядок поступления.

Чего здесь ещё нет

  • Без замены. Начиная с chain 18 цены газа и байтов сдвигают нижнюю границу комиссии вызова в каждом блоке, вплоть до их потолков; нижняя граница комиссии перевода фиксирована. Замены пулованной транзакции другой, с более высокой комиссией, над теми же нотами не существует: правило конфликтов её отклоняет. Вызов, вытесненный ценой, ждёт в пуле непредложенным, пока цены не упадут или пока не истечёт окно его якоря. В v0.6.7 (chain 18 и 19) пулованный вызов сохранял нижнюю границу, при которой был принят, предлагался и после роста цен и терпел неудачу при пробном применении у предложителя блока с ошибкой FeeTooLow. В документе fullnode о блочном пространстве упорядоченный по комиссии мемпул назван шагом перед увеличением блока, а агрегация на уровне блока — тем, что снимает потолок на блок.
  • Без истечения по возрасту. Пулованная транзакция покидает пул только при включении в блок, при вытеснении по лимиту в байтах или по окнам якоря и времени: 1 024 блока в chain 20 и 21, около 23 минут при 1,34 с на блок, которые explorer показал в chain 20 (1,17 с на первых 2 300 блоках chain 21). До chain 19 окна составляли 256 блоков, около пяти минут.
  • Без RPC мемпула. Метода, перечисляющего ожидающие транзакции, нет; единственные представления — mempool_size в rand_status и количество, объём в байтах и возраст старейшей в rand_getMempoolInfo, а explorer индексирует только зафиксированные блоки.

Последняя проверка по коммиту fullnode c0878c3f (chain 21) от 2026-10-12, а также fullnode v0.6.8 `c9c9bd3c` (chain 20): `crates/randprotocol-node/src/mempool.rs` (`MAX_POOL_BLOCKS`, `make_room`, `is_governance`, the call reservation in `candidates_within`), the commits for CH-7, CH-9, POOL-2, BRG-18, GOSSIP-1 and ZKV-4, and `rand_getLimits` on chain 20, fullnode v0.6.7 `86941a1`: `crates/randprotocol-node/src/mempool.rs`, `admission.rs`, `crates/randprotocol-core/src/ledger/mod.rs` (the gas floor), fullnode `90e7716`: `docs/architecture.md` §8, `docs/rpc.md` (changelog, the 2026-09-28 gas entries), fullnode `ed88241`: `docs/confidential.md` (transaction binding). Если код и эта страница расходятся, прав код; сообщите нам.