Address: 0x286b9783b3245a253c3350c60d91cd50cc5e4c5cSource: contracts/src/ValidatorRegistry.sol · node-side reader: crates/yc-execution/src/validator_registry.rs
The node does not read this registry unless you tell it to. yc-node calls activeSet() only when the VALIDATOR_REGISTRY environment variable is set on that node, and only at epoch boundaries. In the deployed testnet compose file that variable is set on the indexer service, not on any yc-node-* service — so today the consensus validator set comes from genesis and nothing rotates from this contract. See Consensus integration for the full rule.

What is stored per validator

The registry holds two keys per validator, not one: a 48-byte BLS public key (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.
It reverts with 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 no deregister(). 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 single onlySlasher 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.
The indexer’s slasher daemon (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: ...).
A queued proposal is consumed at every boundary even when it is subsequently rejected. A set that fails the minimum-size or quorum-overlap guard is discarded, not retried; it must be re-proposed at the next boundary.

What BLS keys are used for

  1. Verifying each peer’s commit signature on /internal/bft/proposal responses.
  2. Verifying the aggregated commit signature carried by an incoming block.
If the active set lists this node at some index but with a BLS public key that differs from the one the node holds, 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.