TIP-1017: ValidatorConfig V2
Protocol Version: T2
Status: In Review
Authors: Janis, Howy
Status: In Review
Authors: Janis, Howy
Abstract
TIP-1017 introduces ValidatorConfig V2, a new precompile for managing consensus validators with append-only semantics. Unlike the original ValidatorConfig, V2 replaces the mutableactive boolean with addedAtHeight (set when adding an entry) and deactivatedAtHeight fields (set when deactivating), enabling nodes to reconstruct the validator set for any historical epoch using only current state. The new design also requires Ed25519 signature verification when adding validators to prove key ownership.
Motivation
The original ValidatorConfig precompile allows validators to be updated arbitrarily, which creates challenges for node recovery:- Historical state dependency: To determine the validator set at a past epoch, nodes must access historical account state, which requires retaining and indexing all historical data.
- Key ownership verification: V1 does not verify that the caller controls the private key corresponding to the public key being registered, allowing potential key squatting attacks.
- Validator re-registration: V1 allows deleted validators to be re-added with different parameters, complicating historical queries.
- Validators are immutable after creation (no
updateValidator) addedAtHeightanddeactivatedAtHeightfields enable historical reconstruction from current state- Ed25519 signature verification proves key ownership at registration time
- Public keys remain reserved forever (even after deactivation); addresses are unique among current validators but can be reassigned via
transferValidatorOwnership
Key Design Principle
By recordingaddedAtHeight and deactivatedAtHeight, nodes can determine DKG players for any epoch using only current state. When preparing for a DKG ceremony in epoch E+1, a node reads the contract at boundary(E) and filters:
Specification
Precompile Address
Interface
Behavior
Validator Lifecycle
Unlike V1, validators in V2 follow a strict lifecycle:- Addition:
addValidatorcreates an immutable validator entry withaddedAtHeightset to the current block height anddeactivatedAtHeight = 0 - Active period: Validator participates in consensus while
deactivatedAtHeight == 0 - Deactivation:
deactivateValidatorsetsdeactivatedAtHeightto the current block height - Preserved: The validator entry remains in storage forever for historical queries
Signature Verification
When adding a validator, the caller must provide an Ed25519 signature proving ownership of the public key. Namespace:b"TEMPO_VALIDATOR_CONFIG_V2_ADD_VALIDATOR"
Message:
b"TEMPO_VALIDATOR_CONFIG_V2_ROTATE_VALIDATOR" is used instead.
Determining Active Validators
Reading this contract alone is not sufficient to determine who the active validators (signers) are during a given epoch. The contract only records which validators are eligible to participate in DKG ceremonies—it does not record DKG outcomes. To determine the actual validators for epochE+1:
- Read the DKG outcome from block
boundary(E) - The DKG outcome contains the Ed25519 public keys of successful DKG players
- Match these public keys against the contract via
validatorByPublicKey()to obtain validator addresses and IP addresses
Address Validation
- ingress: Must be in
<ip>:<port>format - egress: Must be in
<ip>format
[2001:db8::1]:8080.
IP Address Uniqueness
Only the ingress IP address must be unique among active validators:- No two active validators can have the same ingress IP
- Egress addresses have no uniqueness constraint
- Deactivated validators excluded from checks, IP reuse is allowed after deactivation
Consensus Layer Behavior
IP Address Changes: When a validator’s IP address changes viasetIpAddresses, the consensus layer is expected to update its peer list on the next finalized block.
Validator Addition and Deactivation: When validators are added or deleted (this also applies to rotation), there is no warmup period: deactivated validators are immediately removed from the set of players on the next epoch, while activated validators are immediately added on the next epoch.
DKG Player Selection: The consensus layer determines DKG players for epoch E+1 by reading state at boundary(E) and filtering:
Uniqueness Constraints
Both the validator address and public key must be globally unique across all validators (including deleted ones):AddressAlreadyHasValidator: Reverts if the address has ever been registeredPublicKeyAlreadyExists: Reverts if the public key has ever been registered
Invariants
-
Append-only array: The
validatorsArraylength only increases; it never decreases. -
Immutable identity: Once a validator is added, its
publicKey,index, andaddedAtHeightfields never change. Theingressandegressfields can be updated viasetIpAddresses.validatorAddresscan only be changed by the contract owner. -
Delete-once: A validator’s
deactivatedAtHeightcan only transition from 0 to a non-zero value, never back to 0 or to a different non-zero value. -
Unique addresses: No two validators (including deleted ones) can have the same
validatorAddress. -
Unique public keys: No two validators (including deleted ones) can have the same
publicKey. -
Historical consistency: For any height H, the active validator set consists of validators where
addedAtHeight <= H && (deactivatedAtHeight == 0 || deactivatedAtHeight > H). -
Signature binding: The signature message includes
chainId,contractAddress,validatorAddress,ingress, andegress, preventing replay across chains, contracts, or parameter changes.
Migration from V1
Overview
The migration uses a two-pronged approach:- New hardfork: Timestamp-based activation
- Manual migration: Admin migrates validators one at a time, then calls
initializeIfMigrated()to flip the flag
Hardfork-Based Switching
The CL determines which contract to read based on:- Whether hardfork is active (timestamp-based)
- Whether V2 is initialized (reads
isInitialized()from V2)
Manual Migration
V2 uses manual migration where the admin explicitly migrates validators one at a time and then callsinitializeIfMigrated() to flip the initialized flag.
Migration Functions (Owner Only)
migrateValidator(idx):
- Reverts if
isInitialized() == true - Reverts if
idx != validatorsArray.length(ensures sequential migration) - On first call, copies
ownerfrom V1 if V2 owner isaddress(0) - Creates a V2 validator entry with validator address copied from V1
- Active V1 validators get
addedAtHeight > 0anddeactivatedAtHeight == 0 - Inactive V1 validators get
addedAtHeight == deactivatedAtHeight > 0
initializeIfMigrated():
- Reverts if
validatorsArray.length < V1.getAllValidators().length - Copies
nextDkgCeremonyfrom V1 to V2 - Sets
initialized = true - After this call, CL reads from V2 instead of V1
transferValidatorOwnership(validatorAddress, newAddress):
- Reverts if caller is not owner and not the validator
- Updates the validator with the new address
- Updates lookup maps
Timeline
Migration Steps
For Existing Networks
- Release new node software with hardfork support
- Schedule the fork by updating chainspec with target
hardforkTime - At fork activation: CL reads from V1 (since
isInitialized() == false) - Admin migrates validators by calling
migrateValidator(idx)for each validator - Admin calls
initializeIfMigrated(): Setsinitialized = true, CL now reads from V2
For New Networks
- Call
initializeIfMigrated()to setinitialized = trueandinitializeAtHeight = 0 - Call
addValidator()for each initial validator - Set
<hardforkTime> = 0to activate V2 immediately - V1 Validator Config contract/precompile is not necessary in this flow