Address:
0x286b9783b3245a253c3350c60d91cd50cc5e4c5cSource: contracts/src/ValidatorRegistry.sol · node-side reader: crates/yc-execution/src/validator_registry.rsWhat is stored per validator
BLS_PUBKEY_LENGTH) used for block proposals and the aggregated commit, and a 32-byte
ed25519 public key (ED_PUBKEY_LENGTH) which is the node’s transaction-signing identity.
p2pEndpoint is what the node uses to refresh its validator peer list.
Constants that gate registration and slashing:
Registering
register is not payable. The stake is pulled with yce.transferFrom(msg.sender, ...),
so the caller must approve the registry for at least stakeAmount first.StakeTooLow below MIN_STAKE, AlreadyRegistered if the caller is already
active, and InvalidBlsKeyLength / InvalidEdKeyLength if either key is the wrong size.
On success engagementScore is seeded to 100, active becomes true, and
ValidatorRegistered(who, stake, p2pEndpoint) is emitted. Re-registering after exit()
reuses the existing slot in validatorList.
Leaving
There is noderegister(). Exit is two-phase:
1
requestUnbond()
Marks the validator and records
unbondRequestedAt. Emits ValidatorUnbondRequested(who, unlocksAt).2
Wait UNBOND_PERIOD
7 days.
3
exit()
Returns the remaining stake and emits
ValidatorExited(who, returned). Reverts with
UnbondNotReady before the period elapses.updateEndpoint(string p2pEndpoint) changes the advertised endpoint without unbonding.
Reads
activeSet() returns five parallel arrays in registration order and filters on active
only — a jailed-but-active validator is still returned. Canonical ordering is applied
node-side, not by the contract.
Slashing and inactivity leak
All punitive entry points are gated by a singleonlySlasher address (set in the
constructor, changeable via setSlasher). There is no DAO and no owner, and there is no
slash(address, uint256) taking an absolute amount — every penalty is a basis-point
fraction of the validator’s stake, burned via IYCEBurnable.burn.
yscan/indexer/src/slasher.ts) is the process that calls these.
Events
Consensus integration
This is the part most often misread, so it is stated precisely.When the registry is read
apply_epoch_boundary (crates/yc-node/src/dag.rs) returns immediately unless
block_number != 0 && block_number % EPOCH_LENGTH == 0. EPOCH_LENGTH is a compile-time
constant of 100 blocks (crates/yc-types/src/constants.rs), not an environment variable;
changing it is a hard fork. It is not read every slot.
onchain_validator_set then reads VALIDATOR_REGISTRY from the environment. If the variable
is unset — or holds something that is not a 20-byte hex address — it returns an empty vector
silently, no activeSet() call is made, and no set is proposed.
The boundary hook is reached only from the FireDAG commit path (dag.rs) and the sync path
(crates/yc-node/src/sync.rs). A node running the legacy single-leader path calls
maybe_rotate_at_epoch directly and never consults the registry at all.
How a set is admitted
1
Decode
read_active_validator_set performs a read-only EVM call to activeSet() from the zero
address and decodes the five arrays into OnChainValidator { address, bls_public_key, ed_public_key, p2p_endpoint, stake_wei }.2
Canonical order
The set is sorted ascending by address — in
decode_active_set, then again in
maybe_rotate_at_epoch. Registration order in the contract is irrelevant.3
Minimum size
Fewer than 4 entries and no proposal is queued at all.
maybe_rotate_at_epoch independently
rejects any proposed set below 4 with epoch rotation REJECTED ... BFT minimum is 4.4
Identical-set check
If the proposal is structurally identical to the live set — same length, and pairwise the
same address and the same BLS public key — then stakes are copied into the live entries in
place and the function returns without rotating. Ed25519 keys, endpoints, stakes and
engagement scores are not part of that comparison, so a pure stake change is a refresh,
never a rotation.
5
Quorum-overlap guard
Let
retained be the number of proposed addresses that were already in the live set. The
rotation is rejected unless retained >= 2 * old_count / 3 + 1 and
retained >= 2 * new_count / 3 + 1 (integer division). For a 4-validator set both quorums
are 3. On failure the node logs epoch rotation REJECTED ... unsafe quorum overlap and
keeps the current set.6
Apply
Attestation buffers are remapped old index to new index by address, the new BLS set becomes
authoritative from
block_number + 1, and the DAG frontier is reset. The node logs
epoch rotation APPLIED at block N: X → Y validators (retained R, my_index now: ...).What BLS keys are used for
- Verifying each peer’s commit signature on
/internal/bft/proposalresponses. - Verifying the aggregated commit signature carried by an incoming block.
attach_own_keys refuses to attach and the node stays mute — it follows
the chain without signing, and logs STAYING MUTE. That is a local decision by that node; it is
not a block-rejection rule.
Running a candidate node
A candidate is a node that holds its own keys, follows the chain, and starts signing only once an epoch rotation admits it.
Both of the candidate’s keys derive from that one seed: the ed25519 keypair is the seed itself,
and the BLS keypair is derived from
hash("YC_BLS_v1" || seed). Register that BLS public key
on-chain — registering any other one means the node will refuse to sign after admission.
Note that this differs from the genesis validators, whose BLS seeds are derived from their
index rather than from an ed25519 seed.
Checking a node’s own view: yc_nodeIdentity
This is the fastest way to confirm whether a rotation actually admitted your node:
role flips
from candidate to validator and signing becomes true at the boundary block.