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

Ноты и дерево обязательств

Модель нот, на которой строится каждый перевод: обязательства, нуллификаторы, конверты, дерево глубиной 32, анкоры, а также окна анкоров и времени — и то, как кошелёк сканирует цепочку в поисках собственных нот.

Используйте эту страницу, чтобы создавать или проверять всё, что читает пул: кошелёк, который сканирует свои ноты, индексатор, который отслеживает обязательства и нуллификаторы, или инструмент, который проверяет, что публикует транзакция. Стоимость в этой сети представлена множеством нот; сеть хранит их обязательства, но не содержимое.

Нота

Нота — это { pk, from, amount, asset, time, r }: 28 слов по 32 бита, 112 байт, в раскладке pk(8) from(8) amount_lo amount_hi asset time r(8). pk — публичное значение ноты владельца (выводится из ключа просмотра), from — отправителя, amount — u64 в единицах (1 RAND = 10⁹ единиц), asset — u32 (0 для RAND, иначе плотный индекс токена в реестре токенов для токена RPL, мостового или нативного; см. токены RPL), time — высота блока, на которую нацелился отправитель, а r — восемь случайных слов маскирования.

Открытый текст никогда не появляется в сети. Появляются:

  • её обязательство cm = H(CM, words), хеш Poseidon2 от 28 слов с доменным тегом 4, добавляемый как лист дерева;
  • её конверт: открытый текст, запечатанный на адрес владельца так, что прочитать его может только владелец или отправитель через свой исходящий ключ просмотра. Начиная с цепочки 18 (включая цепочку 21) конверт также содержит зашифрованную памятку. Несколько нот, которые сеть вычисляет из публичных полей, вообще не имеют конверта (ниже).

Трата ноты публикует её нуллификатор H(NF, nk, cm), хеш Poseidon2 с доменным тегом 3 от ключа просмотра владельца и обязательства. Без nk нуллификатор невозможно связать с его обязательством. Множество нуллификаторов предотвращает двойную трату.

Конверт

Envelope { kem_ct, to_receiver, to_sender, body }
частьбайтычто это
kem_ct1 088инкапсуляция ML-KEM-768 на адрес получателя
to_receiver60ключ транзакции TxKey, обёрнутый под инкапсулированным секретом
to_sender60тот же TxKey, обёрнутый под исходящим ключом просмотра отправителя ovk
body652нота в 112 байт и поле памятки в 512 байт под TxKey (140 байт, одна нота, до цепочки 17 включительно)

Каждая часть — это ChaCha20-Poly1305 со случайным 12-байтовым нонсом в начале и 16-байтовым тегом, а ассоциированные данные — это тег (rand-envelope-receiver, rand-envelope-sender, rand-envelope-body), за которым следует обязательство ноты; поэтому ключ не может открыть конверт, который он не запечатывал, а открытая нота привязана к листу, под которым она находится. Это 1 860 байт шифртекста; префиксы длины bincode доводят его до 1 892 в передаче. Генезис цепочки 20 задаёт envelope_bytes: 1860, и её реестр отклоняет конверт ноты любой другой длины (envelope is 1348 bytes, this chain requires exactly 1860); больше ничего в нём он не проверяет. До цепочки 17 у конверта не было поля памятки, и он занимал 1 348 байт (1 380 в передаче), а реестр лишь ограничивал его 2 048 байтами, что по-прежнему действует для цепочки, в генезисе которой нет envelope_bytes.

Поле памятки имеет вид len (u16 LE) ‖ UTF-8 text ‖ zero padding, 512 байт, то есть не более 510 байт текста. Каждый конверт ноты начиная с цепочки 18 содержит его, есть памятка или нет: выход сдачи, фиктивный выход и платёж без памятки дополняются одинаково, и ничто в сети не отличает памятку, её длину или её отсутствие. Памятки нет ни в ноте, ни в обязательстве, ни в каком-либо доказательстве. Её может прочитать ровно тот, кто и так может прочитать ноту: владелец, отправитель через ovk или любой, кому передали TxKey этого выхода. Некорректное поле памятки (неверная длина, недопустимый UTF-8, ненулевое дополнение) открывается как отсутствие памятки и никогда не лишает владельца ноты. Памятка — это чужой текст, поскольку любой может отправить пылевую ноту на публичный адрес: показывайте её как недоверенную, заменив управляющие символы и символы формата Unicode, как это делает кошелёк rand. rand send --memo запечатывает памятку в выход платежа; rand notes --memo и rand history --memo показывают её.

Дерево, якоря и окна

Обязательства добавляются в одно дерево Poseidon2 глубиной 32, от старых к новым, и никогда не удаляются. Лист i — это i-я нота, когда-либо созданная сетью; генезисные депозитные ноты — первые листья. Пакет добавляет четыре листа и четыре нуллификатора независимо от того, что он перемещает, включая фиктивные. Пустой лист состоит из одних нулей, а узел — это H(NODE, left, right) (доменный тег 7). Узел хранит только границу дерева, поэтому состояние консенсуса никогда не содержит листьев; rand_getWitness по требованию восстанавливает полное дерево из сохранённых листьев, и поэтому это самое дорогое чтение, которое обслуживает узел.

Пакет доказывает принадлежность своих входов относительно якоря, корня дерева. Два правила сохраняют честность якоря:

константазначениеправило
ANCHOR_WINDOW256 блоков; 1 024 в цепочках 20 и 21якорь должен быть одним из последних window корней на конец блока; корень, через который дерево проходит в середине блока, никогда не бывает якорем
TIME_WINDOW256 блоков; 1 024 в цепочках 20 и 21time пакета должно лежать в пределах [height − window, height]

Оба окна равны 256 блокам, если только генезис не задаёт proof_window_blocks (v0.6.8, от 256 до 4 096), которое заменяет оба сразу. Цепочки 20 и 21 задают 1 024; в цепочках с 14 по 19 этого поля не было.

Окна существуют потому, что построение доказательства требует реального времени: доказательство пакета уровня 14 занимает около 100 секунд на ноутбуке (а начиная с цепочки 17 кошелёк строит ещё и второе доказательство авторизации уровня 10, около 7 секунд). 256 блоков составляли около пяти минут при блоках по 1,17 с в цепочке 18, и делегированное доказательство на медленном доказывающем узле было отклонено на 294 с; 1 024 блока цепочки 20 — это около 23 минут при 1,34 с, о которых сообщал обозреватель. Кошелёк, проигравший эту гонку, получает anchor is not one of the last 1024 roots и строит доказательство заново. rand_getAnchor без параметра возвращает корень головного блока на его конец — единственный якорь, относительно которого следует строить доказательство.

Пакет

Каждая транзакция, перемещающая стоимость, несёт ровно один фиксированный пакет 4-в-4 со скрытым активом (начиная с цепочки 14; до цепочки 13 включительно он был 2-в-2), доказанный гостевой программой zkVM скрытого актива, дайджест которой hc_bundle зафиксирован в файле генезиса (60af094acfe65d85fdb18fb3d06cf9085dcf28c96e59e87f1ee527226e6e3fce, гостевая программа пакета v3, в цепочках с 17 по 20; в цепочках 14 и 15 это был 83d3a3704a1fcdb9bae7136c0a947ffa34bd53f055388395ed705fe8cacd0ef8, гостевая программа v1):

Bundle { anchor, nullifiers[4], commitments[4], fee, burn_a, burn_r, burn_asset, time, envelopes[4], proof,
         auth_commit, auth_proof }     // the last two since chain 17

Слоты 0–1, входные и выходные, несут приватный актив A; слоты 2–3 всегда несут RAND. При платеже только в RAND слоты 0–1 фиктивны, а стоимость и комиссия проходят через слоты 2–3. При переводе токена токен проходит через слоты 0–1, а комиссия в RAND оплачивается из слотов 2–3 того же доказательства. A — это свидетельское слово доказательства и не входит ни в одно публичное поле, поэтому перевод любого токена RPL — это Action::None с burn_a = burn_r = burn_asset = 0, поле в поле той же формы, что и платёж в RAND.

Доказательство утверждает, что каждый настоящий вход — это лист под anchor и несёт актив своего слота, что четыре нуллификатора — именно те, что опубликованы, что каждая группа сходится по балансу (in0 + in1 = out0 + out1 + burn_a в приватном активе, in2 + in3 = out2 + out3 + fee + burn_r в RAND) и что тратящий владеет ключами — не раскрывая, какие листья, какие суммы, какой актив и кто. Три поля сжигания — единственное публичное заявление пакета о стоимости, покидающей пул: burn_a и burn_asset ненулевые только при TokenBurn или BridgeBurn, где они равны сумме и активу действия; burn_r — сожжённый RAND, ненулевой только при Bond (ставка) и RegisterAggregator (залог). Комиссия всегда в RAND.

Начиная с цепочки 17 право траты разделено между двумя доказательствами (разделённая авторизация). Гостевая программа пакета v3 принимает ключ просмотра nk и соль там, где прежние гостевые программы принимали ключ траты: она выводит pk_self = H(PK, nk), требует, чтобы каждый вход принадлежал ему, и включает c = H(AUTH, nk ‖ salt) в публикуемый дайджест. Вторая гостевая программа, гостевая программа авторизации (hc_auth в генезисе, 1e4e347f44cf86750b30a9a4bdf9ec9256efe353d4ff8017451eca7d195639c1), принимает ключ траты sk и ту же соль, выводит nk = H(NK, sk) и публикует то же c. Пакет несёт c как auth_commit, а доказательство уровня 10 гостевой программы авторизации — как auth_proof, и реестр требует, чтобы оба доказательства проверялись относительно одной и той же привязки транзакции и чтобы два значения c совпадали. Поэтому доказывающая сторона, владеющая nk и солью, может построить доказательство пакета, но не доказательство авторизации, и именно на этом держится делегированное доказательство. Соль — это 256 свежих случайных бит на пакет, поэтому auth_commit публичен и ничего не связывает. Кошелёк, который доказывает сам для себя, строит и то и другое доказательство, поэтому самостоятельно доказанная и делегированная транзакции выглядят в сети одинаково.

Фиксированная форма означает, что слоты часто фиктивные: фиктивный вход — это нота с нулевой стоимостью, принадлежащая тратящему (её нуллификатор всё равно публикуется), фиктивный выход — нота с нулевой суммой, а выход сдачи присутствует, даже когда сдача равна нулю. Транзакция, выглядевшая иначе при отсутствии сдачи, раскрыла бы, что сдачи не было. Следствие для кошельков: каждая из двух групп пакета тратит не более двух нот. Платёж в RAND, залог, развёртывание или вызов тратят не более двух нот RAND; перевод или сжигание токена тратят не более двух нот токена и не более двух нот RAND на комиссию. Стоимость, распределённую по трём и более нотам одной группы, нужно сначала консолидировать отправкой на собственный адрес.

Начиная с цепочки 14 доказательство также привязано к своей транзакции: оно строится над хешем всей транзакции с обнулёнными доказательствами и проверяется относительно него, поэтому его нельзя перенести на другое действие, другие конверты или другую цепочку; начиная с цепочки 20 хеш охватывает и хеш генезиса, так что его нельзя перенести и на цепочку с тем же идентификатором (Транзакции).

Что публично, а что скрыто

публично в цепочкескрыто
перевод RAND или любого токена RPLякорь, все четыре нуллификатора, все четыре обязательства, комиссия, burn_a = burn_r = burn_asset = 0, time, четыре шифртекста конвертов, доказательство, auth_commit и доказательство авторизациикто отправил, кому заплачено, сумма, мемо, какой актив перемещён, сдача, какие листья потрачены, какие слоты были фиктивными
сжигание токена или сжигание через моствсё, что публикует перевод, а также burn_asset и burn_a, равные активу и сумме действия (и, для сжигания через мост, комиссия релеера, обеспечивающий токен и адрес назначения)какие ноты были сожжены и кто их сжёг
выпуск из кранаобязательство, конверт, сумма, валидатор, выполнивший выпуск, и, начиная с цепочки 14, раскрытие ноты (pk, time, r), по которому допуск пересчитывает обязательствокаждая последующая трата ноты
генезисный депозит{ cm, envelope, amount, opening: { pk, time, r } } в файле генезиса, с asset в раскрытии для ноты токена из списка (генезис цепочки 21 содержит три ноты zUSD, генезис цепочки 20 содержал две): pk владельца, сумма и актив публичны по замыслукогда и во что нота потрачена позже
вывод, депозит через мост, выпуск токена или начальный выпуск при регистрациисумма, получатель, time ноты и ослепление r (обязательство цепочка вычисляет сама)каждая последующая трата ноты
выплата invoke или выпуск программы (начиная с цепочки 20), платёж мультиподписного счёта или выплата perp (цепочка 21)сумма, актив, получатель и r; time ноты совпадает со временем пакетакаждая последующая трата ноты
нота комиссии моста (начиная с цепочки 20)сумма, актив и time, производное ослепление, казначейство генезиса в роли владельца и отсутствие конвертакаждая последующая трата ноты

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

Таблица описывает то, что скрывают доказательства цепочки 21, как скрывали и доказательства цепочки 20, но так было не всегда. В цепочках 14 и 15 само доказательство пакета раскрывало больше (обзор zkVM от 2026-09-27, INT-2 / GV-1): бегущая сумма LogUp каждой таблицы публиковалась без ослепления, а гостевая программа v1 пропускала проверки Меркла и актива для фиктивного входа и ветвилась по каждому биту пути Меркла, поэтому по доказательству пакета любой мог определить, какие из четырёх входных слотов настоящие, а значит, перемещал ли пакет токен или только RAND, и число единичных битов в индексе каждого потраченного листа. Закрыли это два изменения. Цепочка 16 перешла на набор ограничений 7, который ослепляет каждый конечный член LogUp и устанавливает для каждой объявленной таблицы нижнюю границу в 2^7 строк, и на гостевую программу пакета v2 без ветвлений, в которой каждый слот, фиктивный или настоящий, выполняет полную проверку принадлежности и актива одними и теми же инструкциями. Гостевая программа v3, на которой работают цепочки с 17 по 20, — это отношение v2, где вместо ключа траты используются nk и соль, и она тоже без ветвлений. Пакеты, зафиксированные в цепочках 14 и 15, сохраняют то, что раскрыли. Доказательство в цепочке 20 по-прежнему показывает свой заголовок: для пакета это одна фиксированная форма (уровень 14, объявленный лимит газа 20,479 для каждого пакета), а для Call или Invoke — уровень, объявленный лимит газа и то, использовал ли вызов прекомпилят keccak или sha256 и примерно как часто (Доказательства).

Ноты без конверта

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

  • Ноты комиссии моста (начиная с цепочки 20). При включённом bridge.fees цепочка добавляет ноту zUSD на казначейство генезиса при каждом депозите и каждом сжигании (Токены RPL). У неё нет конверта: хранилище индексирует её с пустым конвертом, запечатанным ни для кого, а rand_getCompactBlocks и rand_getCommitments показывают её именно так, для депозита — после ноты депозита, для сжигания — после четырёх нот пакета. Каждое слово ноты публично: сумма из транзакции и генезисного курса, актив и time из транзакции, from = 0, ослепление blake3("rand-bridge-fee-r-1" ‖ mu) для депозита или blake3("rand-bridge-burn-fee-r-1" ‖ txid) для сжигания, а владелец — recipient из генезиса.
  • Ноты, чей конверт ничего не связывает. Депозит через мост, выпуск токена, начальный выпуск при регистрации и, начиная с цепочки 20, выплаты invoke и выпуски программ (в цепочке 21 также платежи мультиподписного счёта и выплата perp) несут конверт, запечатанный тем, кто отправил транзакцию, но цепочка вычисляет ноту из публичных полей действия и никогда не сверяет с ней конверт. Небрежный или враждебный отправитель может запечатать мусор, поэтому получатель не должен на него полагаться.

Поэтому кошелёк rand восстанавливает такие ноты сам (wallet::rebuilt_notes_with) отдельным публичным проходом рядом с пробной расшифровкой: для каждой такой транзакции, называющей его pk, и для нот комиссии, если его адрес — казначейство, он вычисляет ноту и записывает её в тот лист, чьё обязательство она хеширует. Восстановленная нота — лишь кандидат: узел, лгущий о транзакции, ставке комиссии или хеше, добивается лишь того, что нота остаётся ненайденной, но никогда не засчитывает несуществующую. Начиная с v0.6.8 заголовки, которые возвращает rand_getBlocks, несут транзакции депозитов, выпусков и регистраций в public_notes, а в цепочке с комиссиями моста — и сжигания, с удалёнными доказательствами и под настоящим идентификатором, поэтому проход читает только страницы заголовков. В v0.6.8 invoke в public_notes нет, так что при работе с узлом v0.6.8 кошелёк находит выплату invoke только через её конверт, который запечатывает вызывающий и который никто не проверяет. В main fullnode invoke там тоже есть, с удалёнными доказательствами, начиная с d0bb73fb; это изменение только для узла, пока не внедрённое в парк цепочки 20, и кошельку для него изменений не требуется.

Сканирование: как кошелёк находит свои ноты

Узел отвечает «вот всё дерево», но никогда «вот ваш баланс». Кошелёк:

  1. постранично читает листья через rand_getCommitments(from_index, limit) (не более 1,000 за вызов) — или, начиная с fullnode v0.8.0, их заголовки сканирования через rand_getScanLeaves(from_index, limit): те же строки с конвертом, усечённым до первых 1,208 байт (kem_ct ‖ to_receiver ‖ to_sender), чего достаточно для пробной расшифровки, так что кошелёк читает полный конверт только для тех листьев, которые открывают его ключи, — это в 2.26 раза меньше байт на лист для листьев цепочки 20, — и возвращается к полным страницам на узле, отвечающем -32601, — либо читает поблочный поток rand_getCompactBlocks(from_height, to_height) (не более 128 блоков за вызов); каждая транзакция с пакетом перечисляет там четыре ноты и четыре нуллификатора, и ни один читатель не отличит фиктивный слот от настоящего;
  2. пробно расшифровывает каждый конверт: как получатель — через свой ключ декапсуляции ML-KEM, как отправитель — через ovk; оставляет ноты, чей pk совпадает с его собственным; и восстанавливает ноты с публичной суммой по их транзакциям (выше);
  3. вычисляет нуллификатор каждой оставленной ноты и отмечает те, что присутствуют в rand_getNullifiers(from_height, limit);
  4. суммирует остальные.

rand_getTreeInfo сообщает, как далеко нужно сканировать: { "next_index", "root", "nullifiers" }. Для траты кошельку нужен путь Меркла глубиной 32 уровня для каждого реального входа. Кошелёк rand хранит собственную копию дерева обязательств, строит её во время сканирования из тех же страниц rand_getCommitments и сам вычисляет каждый путь, поэтому единственный вопрос о дереве, который отправка задаёт узлу, — это rand_getAnchor, не называющий ни одного листа. rand_getWitness(index) остаётся на узле для других клиентов. Он сообщает узлу, какой лист вызывающая сторона собирается потратить, и это была крупнейшая утечка пула до появления локального дерева: клиенту, которому приходится им пользоваться, следует запустить собственный узел или запрашивать свидетельства, которые ему не нужны, наряду с теми, которые нужны.

rand sync        # scanned 41 leaves and 37 blocks; 1 notes, 1 unspent
rand notes       # index, asset, amount, height, spent, pending, memo
rand history     # notes this wallet created for others, opened through ovk

Заметка находится в состоянии pending между rand send --no-wait и ближайшим sync, который увидит либо её нуллификатор, либо блоки позже time + window (1 024 в цепочках 20 и 21, читается из rand_getLimits.proof_window_blocks; 256 без него), после чего пакет уже никогда не будет принят и заметка снова доступна для траты.

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