Rand Protocol
Документация
Руководства

Стейкинг

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

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

Реестр

Одна строка на каждого валидатора, когда-либо вносившего залог, ключ — его адрес в base58:

ValidatorEntry {
    public_key,                  // Dilithium2: signs blocks, and this validator's staking actions
    stake:    u64,               // bonded: the weight an epoch's set gives it
    pending:  Vec<(u64, u64)>,   // unbonding, as (release_epoch, amount), oldest first
    rewards:  u64,               // fees earned as a block proposer, unpaid
    payout:   ShieldedAddress,   // rand1… — where a withdraw pays
    nonce:    u64,               // replay protection for its signed actions
    activation_epoch: u64,       // under a `staking` section: the first epoch this row may be in a set
}

Все поля публичны, а реестр хешируется в корень состояния каждого блока. Строка никогда не удаляется: валидатор, вывевший из залога всё, сохраняет строку со stake = 0.

константазначениесмысл
MIN_STAKE1,000 RANDстейк, необходимый строке, чтобы вообще попасть в набор эпохи
MAX_VALIDATORS100наибольший размер набора эпохи
UNBONDING_EPOCHS2число эпох, которое выведенная из залога сумма ждёт, прежде чем её можно будет забрать
epoch_blocks1,000 в цепочке 20число блоков в эпохе; параметр генезиса

Эпохи

epoch(h) = h / epoch_blocks. Набор для эпохи e ≥ 1 выводится из реестра по состоянию на последний блок эпохи e − 1: берутся все строки со stake ≥ MIN_STAKE, из них первые MAX_VALIDATORS по (stake по убыванию, address по возрастанию), затем набор сортируется по адресу. Лидер вида, голосующие за блок и проверка сертификата используют набор той эпохи, которой принадлежит блок. Отсюда два следствия:

  • Залог не становится весом до одной из следующих границ. В реестре он появляется сразу, но набор меняется только на границе эпохи: на ближайшей — в цепочке без секции staking, позже — в цепочке 20 (ниже).
  • Вывод из залога лишает веса на ближайшей границе, а сумма ждёт две эпохи, чтобы стать доступной для вывода. Валидатор перестаёт предлагать блоки задолго до того, как сможет забрать средства.

Если вывод набора даёт пустой результат, переносится предыдущий набор. Узел, запущенный с --validator, ключа которого нет ни в одном текущем наборе, остаётся наблюдателем, пока эпоха не примет его, поэтому валидатору, вошедшему в залог после генезиса, перезапуск не нужен; rand_status сообщает is_validator (ключ здесь) и active_validator (он в текущем наборе) по отдельности.

Секция staking

Генезис может содержать секцию staking (fullnode v0.5.4, audit v4 STAKE-2). Без неё приведённые выше правила — это всё, как в цепочке 14. Каждая цепочка, начиная с цепочки 15, её содержит. В цепочке 20 включено следующее:

"staking": {
  "faucet_budget_per_epoch": "10000000000000", "bond_activation_epochs": 2,
  "max_weight_bps": 3333, "max_stake_entry_bps_per_epoch": 2500,
  "admission_by_vote": true, "registration_v2": true,
  "faucet_recipients": ["rand1…", "…16 wallets…"],
  "faucet_minters": ["<validator address>", "…18 validators…"]
}
полецепочка 20правило
bond_activation_epochs2залог, применённый в эпоху e, ставится в очередь на эпоху e + 1 + 2; это относится и к регистрации, и к пополнению. Его сумма сразу попадает в stake, но весом становится только тогда, а поставленный в очередь стейк нельзя вывести из залога
max_stake_entry_bps_per_epoch2,500 (25 %)наибольший стейк, регистрации и пополнения вместе, который становится весом на одной границе, как доля уже активного там веса: ⌊active · bps / 10,000⌋. Строки, срок которых наступил, но не уместившиеся в лимит, ждут следующей границы в порядке внесения залога. При 26,000 RAND активного стейка в цепочке 20 это 6,500 RAND за границу. Доля вошедшего после k границ не превышает 1 − 1.25^−k при любом масштабе, после двух — около трети
admission_by_votetrueзалог, регистрирующий новый ключ, отклоняется (NotAdmitted), пока валидаторы, обладающие более чем двумя третями веса голосования, не проголосуют за этот ключ (ниже); пополнение голосования не требует
max_weight_bps3,333вес ни одного валидатора не превышает 33.33 % от суммарного веса его набора; излишек обрезается в наборе, а stake в реестре не меняется
registration_v2trueрегистрация подписывает rand-register-2 поверх хеша генезиса, идентификатора цепочки, адреса валидатора и адреса выплат; регистрация v1 отклоняется
faucet_budget_per_epoch10,000 RANDfaucet чеканит за эпоху не больше этой суммы (сверх неё — FaucetBudgetExhausted; счётчик обнуляется со следующей эпохой)
faucet_recipients16 кошельковMint faucet платит только этим кошелькам (FaucetRecipientNotAllowed); именно этот список позволяет faucet работать рядом с мостом
faucet_minters18 валидаторовMint faucet может быть подписан только этими ключами валидаторов (MinterNotAllowed); проверка выполняется при допуске и при применении блока, как бы давно ни был внесён в залог другой ключ. В цепочке 20 это 18 из 26 валидаторов генезиса. Список присутствует в каждом генезисе, начиная с цепочки 16
slashingотсутствуетсекции slashing нет, поэтому ни один валидатор не штрафуется и не блокируется (Что публично)

При наличии секции счётчик faucet и очередь залогов добавляются к корню состояния под доменом rand-state-5, а лист валидатора — rand-validator-leaf-4. В цепочке 20 также есть динамические цены газа и состояние программ RPL-2, добавленные после них, что переводит её домен в rand-state-8, а допуск по голосованию оборачивает этот корень ещё раз как H("rand-state-admitted-1", root ‖ admitted_root). В цепочке 15 списка faucet_minters не было: там каждый узел помещал в пул и пересылал Mint faucet, только если его подписал валидатор генезиса (MinterNotAllowed, как политика узла), чтобы валидатор, внёсший залог после генезиса, не мог исчерпать бюджет через пул. Со списком тот же отказ становится правилом валидности.

История: в цепочках с 15 по 19 admission_by_vote не было, поэтому зарегистрироваться мог любой ключ с 1,000 RAND, а бюджет входа задавался фиксированным max_stake_entry_per_epoch в 10,000 RAND за границу. Фиксированная величина подбирается под один генезис; доля же удерживает долю вошедшего одинаковой при любом стейке. Оба поля одновременно задать нельзя.

Допуск по голосованию

Новое в fullnode v0.6.8 (audit v6, STAKE-2), включено для цепочки 20. При 26 ключах по 1,000 RAND блокирующая треть набора — это около 13,000 RAND купленного стейка, а штрафов нет. По этому правилу набор сам решает, каким новым ключам вообще можно регистрироваться.

  • Голос — это действие AdmitValidator { candidate, signatures }, без пакета и без комиссии. Каждая подпись — это подпись Dilithium2 валидатора над blake3("rand-admit-validator-1" ‖ genesis hash ‖ candidate address), в порядке возрастания адресов голосующих. Голосующие должны обладать строго более чем двумя третями веса набора голосования — это арифметика кворум-сертификата: в цепочке 20 это 18 из 26 валидаторов. Голос привязан к одному генезису и не требует nonce: допуск расходуется регистрацией, которую он разрешает.
  • Набор голосования — это собственный вывод реестра для текущей эпохи. Залог в очереди в нём весом не является, а вывод из залога исключает из него сразу. Кандидат не может проголосовать за себя.
  • Допущенный набор — это состояние консенсуса, не более 256 ключей, ожидающих регистрации (сверх этого — AdmissionSetFull). Его отдаёт rand_getAdmitted; rand_getLimits.admission_by_vote сообщает, требуется ли в цепочке голосование.
# each voting validator, offline
rand-node admit sign --candidate <registration hex or public key> --key node.key.json --genesis-hash <hash>
# whoever collects the lines
rand-node admit submit --candidate <…> --signature <line> --signature <line> … --rpc http://127.0.0.1:8545

Затем залог проходит, как описано ниже. rand_getGenesisHash возвращает хеш, нужный для admit sign.

Четыре команды

Какой бинарный файл отвечает за какое действие, определяется тем, какой ключ его подписывает: залог тратит ноты, поэтому это команда кошелька; вывод из залога и снятие подписываются ключом Dilithium2 валидатора, который есть только у узла.

действиекомандана чём передаётсячто оплачивает
регистрацияrand-node registerни на чём — печатает блобничего
Bondrand bondпакет, у которого burn_r — это стейк0.001 RAND из нот кошелька
Unbondrand-node unbondни на чём (bundle: null)ничего
Withdrawrand-node withdrawни на чём (bundle: null)0.001 RAND из снимаемой суммы

Регистрация

rand-node register --key node.key.json --payout "$(rand --key payout.key.json address)" --v2 [--rpc http://127.0.0.1:8545]

Печатает адрес валидатора и шестнадцатеричную Registration: его открытый ключ, адрес выплат и подпись. С --v2, который требуется в цепочке 20, подпись делается под rand-register-2 поверх хеша генезиса цепочки, идентификатора цепочки, адреса валидатора и адреса выплат; без него — форма v1, blake3("rand-register" ‖ bincode(chain_id, payout)), которую цепочка 20 отклоняет (в цепочке с binding_domain: 1, такой как цепочка 20, сообщение регистрации в любом случае соответствует v2). Идентификатор цепочки и, с --v2, хеш генезиса читаются через RPC, поэтому регистрация, подписанная для одной цепочки, отклоняется в другой. Адрес выплат — это адрес кошелька, а не ключ узла, и это единственное поле, которое последующее пополнение изменить не может. Передайте шестнадцатеричную строку тому, у кого находится стейк.

Залог

rand bond <validator base58> <amount in RAND> [--registration <hex>] [--fee <RAND>] [--no-wait] [--cuda]

Обычная экранированная (shielded) транзакция: кошелёк доказывает пакет 4-in-4-out со скрытым активом, который несёт каждая транзакция начиная с chain 14; сумма стейка находится в сжигании RAND, burn_r, и реестр принимает залог, только если burn_r == amount. В chain 20 пакет несёт два доказательства, как и любой пакет начиная с chain 17: доказательство пакета уровня 14 и доказательство авторизации уровня 10 над ключом траты. По умолчанию оба строятся локально, примерно за две минуты на ноутбуке с Apple Silicon (Память для доказательства). С опцией кошелька --prover доказательство пакета строит парный prover, а доказательство авторизации остаётся на машине кошелька. --registration нужна ровно тогда, когда валидатора ещё нет в реестре, и отклоняется, когда он там есть; кошелёк сначала запрашивает реестр. В chain 20 регистрирующий залог отклоняется с NotAdmitted, если набор валидаторов предварительно не проголосовал за допуск ключа (Допуск голосованием); для пополнения голосование не нужно. Регистрирующий залог составляет не менее 1,000 RAND; пополнение может быть любым ненулевым. Цепочка узнаёт лишь то, что стейк этого валидатора вырос на такую сумму; какие ноты платили и кому они принадлежали, скрыто так же, как при переводе: поставить стейк на валидатора может любой, не раскрыв ничего, кроме суммы.

submitted bond 8c3f…e1
  0 RAND out, 1000 RAND burned, 0.999 RAND change, fee 0.001 RAND, anchored at height 412
2nRdFC…: stake 1000 RAND, counting as consensus weight from epoch 69

Эпоха, которую печатает кошелёк, — это ближайшая граница. Она не учитывает задержку активации: в chain 20 залог, зафиксированный в эпоху 68, получает вес не раньше эпохи 71, а позже — если исчерпан бюджет входа, четверть активного веса на одну границу.

Вывод из залога

rand-node unbond <amount in RAND> --key node.key.json [--rpc …] [--no-wait]

Переносит amount из stake в pending с release_epoch = epoch + 2 и увеличивает nonce. Бесплатна, без пакета, подписывается над blake3("rand-unbond" ‖ bincode(chain_id, validator, amount, nonce)) в chain 14–19 и над blake3("rand-unbond-2" ‖ bincode(genesis_hash, chain_id, validator, amount, nonce)) в chain 20 (genesis binding_domain: 1); фиксируется за время одного блока. rand-node v0.6.8 выбирает форму в соответствии с цепочкой, с которой работает. Вывод суммы больше стейка или нулевой отклоняется; несколько выводов в одну эпоху сливаются в одну строку pending.

Вывод средств

rand-node withdraw <amount in RAND> --key node.key.json [--rpc …] [--no-wait]

Выплачивает освобождённую стоимость — rewards плюс строки pending, чей release_epoch наступил, начиная с самых старых — в ноту-депозит на адрес payout из реестра. Подписывается валидатором (в chain 20 под rand-withdraw-2, сначала над хешем genesis, как и вывод из залога) и не содержит пакета, но не бесплатна: базовая комиссия 0.001 RAND уходит пропозеру блока, который её применяет, и вычитается из суммы, так что вывод 1,000 RAND создаёт ноту стоимостью 999.999, а сумма, не покрывающая базовую комиссию, отклоняется. Саму ноту вычисляет цепочка:

cm = H_CM(payout.pk, from = 0, amount − 0.001 RAND, asset 0, time, r)

time — высота головы цепочки на момент выполнения команды (печатается и связана подписью), принимается в любой точке окна пакета (1,024 блока в chain 20, чей genesis задаёт proof_window_blocks; 256 в более ранних цепочках); r публикуется, чтобы цепочка никогда не принимала обязательство из сети. После этого кошелёк получателя находит ноту сканированием, без каких-либо сообщений ему:

rand --key payout.key.json balance      # balance: 999.999 RAND

Вход и выход

# on the joining validator's machine
rand-node register --key node.key.json --payout "$(rand --key payout.key.json address)" --v2
rand-node run --datadir ./data --key node.key.json --validator --bootstrap /ip4/…/p2p/…
#   it syncs and observes; rand_status says is_validator true, active_validator false

# on chain 20: validators holding more than two thirds of the weight vote the key in
rand-node admit sign --candidate <hex from above> --key <their node.key.json> --genesis-hash <hash>
rand-node admit submit --candidate <hex from above> --signature <line> … --rpc http://127.0.0.1:8545

# on the machine holding the stake
rand bond <validator base58> 1000 --registration <hex>
rand validators                       # the new row, active false until it activates

# from its activation epoch on (on chain 20, three boundaries after the bond at the earliest)
rand-node status                      # active_validator: true
rand-node unbond 1000 --key node.key.json     # out of the set at the next boundary
#   wait two epochs
rand-node withdraw 1000 --key node.key.json    # a note worth 999.999 RAND at the payout address

Проверьте арифметику кворума до вывода из залога: набору из n валидаторов нужно, чтобы онлайн было больше 2/3 стейка. В chain 20 все 26 валидаторов находятся ровно на MIN_STAKE, поэтому любой вывод из залога выбрасывает этот узел из набора следующей эпохи, и из 26 должны оставаться в работе 18. Валидатор, который работает, но не голосует, тоже засчитывается против кворума: начиная с chain 14 валидатор не голосует за блок, опережающий его собственные часы более чем на 15 с, поэтому валидатор с отстающими часами перестаёт голосовать без всякой ошибки. Запустите на нём NTP (Запуск валидатора).

Genesis заполняет реестр напрямую — по одной записи на --validator <key or hex pubkey>,<stake>,<payout> — и отклоняет стейк ниже минимума. Genesis chain 20 заполняет 26 строк по 1,000 RAND каждая, с теми же ключами, что и в chain 19.

Начиная с v0.5.11 сборка также несёт genesis vesting: публичный реестр заблокированных по времени выделений, из которых безотзывная запись может внести заблокированный RAND как стейк валидатора (rand-node vesting bond) и забрать его обратно (rand-node vesting unbond). Для этого в genesis нужна секция vesting. Ни в одной цепочке её нет, включая chain 20, и без неё эти действия отклоняются при допуске. (v0.6.8 также позволяет отозвать отзывную запись vesting только порогу подписантов в пользу фиксированной казны, и тоже лишь при наличии секции vesting.)

Что публично

публичноскрыто
реестркаждая строка: адрес, стейк, очередь вывода, невыплаченные награды, адрес выплаты, nonceничего
Bondвалидатор, сумма, приложена ли регистрация, burn_r пакетакакие ноты платили, кто ими владеет, сдача
AdmitValidator (chain 20)кандидат и каждый голосовавший с его подписьюничего другого не существует
Unbondвалидатор, сумма, nonceничего другого не существует
Withdrawвалидатор, сумма, nonce, time и r нотыкто может открыть ноту, и каждая её последующая трата

Награды — это комиссии блоков, которые предложил валидатор. Блочных наград и инфляции нет, а в chain 20 нет ни слэшинга, ни помещения валидаторов в jail: валидатор, который ведёт себя недобросовестно, ничего не теряет из своего стейка, и средство здесь — на стороне операторов. Fullnode v0.6.8 действительно несёт правило слэшинга за эквивокацию лидера (аудит v6, STAKE-1: два подписанных заголовка блока на один view одним ключом уничтожают equivocation_bps стейка и помещают ключ в jail), но лишь при наличии в genesis секции staking.slashing, а в chain 20 её нет (rand_getLimits.slashing равно null); fullnode рекомендует его только тогда, когда стейком владеет более одного оператора. (Код агрегации блоков также несёт действие SlashAggregator для агрегаторов, подписавших два разных агрегата на одном nonce; в chain 20 оно не активно, так как в ней нет секции агрегации.)

Чтение состояния

rand validators        # the whole register
rand status            # is_validator / active_validator for the node asked
→ {"method":"rand_getValidators","params":[]}
← [{ "address": "2nRdFC…", "stake": "1000000000000",
     "pending": [{ "release_epoch": 71, "amount": "1000000000000" }],
     "rewards": "4000000", "payout": "rand1…", "nonce": 3, "active": true, "jailed_until": null }]
→ {"method":"rand_getAdmitted","params":[]}
← { "admission_by_vote": true, "admitted": [], "max": 256 }
→ {"method":"rand_getEpoch","params":[]}
← { "epoch": 69, "epoch_blocks": 1000, "next_set": ["2nRdFC…", "ByDkxs…"] }

Суммы — десятичные строки единиц. active показывает, входит ли строка в набор, работающий в текущую эпоху; jailed_until равно null, кроме случаев наличия секции слэшинга; admitted перечисляет ключи, за которые проголосовали, но которые ещё не зарегистрировались; next_set — проекция, которую сдвигает каждый залог и каждый вывод из залога до границы. GET /validators в эксплорере добавляет share_percent, blocks_proposed и последнюю предложенную высоту.

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