diff --git a/contracts/validator-manager/ACP99Manager.sol b/contracts/validator-manager/ACP99Manager.sol new file mode 100644 index 000000000..80b5e2d74 --- /dev/null +++ b/contracts/validator-manager/ACP99Manager.sol @@ -0,0 +1,212 @@ +// (c) 2025, Ava Labs, Inc. All rights reserved. +// See the file LICENSE for licensing terms. + +// SPDX-License-Identifier: Ecosystem + +pragma solidity 0.8.25; + +/** + * @notice Description of the conversion data used to convert + * a subnet to an L1 on the P-Chain. + * This data is the pre-image of a hash that is authenticated by the P-Chain + * and verified by the Validator Manager. + */ +struct ConversionData { + bytes32 subnetID; + bytes32 validatorManagerBlockchainID; + address validatorManagerAddress; + InitialValidator[] initialValidators; +} + +/// @notice Specifies an initial validator, used in the conversion data. +struct InitialValidator { + bytes nodeID; + bytes blsPublicKey; + uint64 weight; +} + +/// @notice L1 validator status +enum ValidatorStatus { + Unknown, + PendingAdded, + Active, + PendingRemoved, + Completed, + Invalidated +} + +/** + * @notice Specifies the owner of a validator's remaining balance or disable owner on the P-Chain. + * P-Chain addresses are also 20-bytes, so we use the address type to represent them. + */ +struct PChainOwner { + uint32 threshold; + address[] addresses; +} + +/** + * @notice Contains the active state of a Validator + * @param status The validator status + * @param nodeID The NodeID of the validator + * @param startingWeight The weight of the validator at the time of registration + * @param messageNonce The current weight update nonce + * @param weight The current weight of the validator + * @param startTime The start time of the validator + * @param endTime The end time of the validator + */ +struct Validator { + ValidatorStatus status; + bytes nodeID; + uint64 startingWeight; + uint64 sentNonce; + uint64 receivedNonce; + uint64 weightChangedAt; + uint64 weight; + uint64 startTime; + uint64 endTime; +} + +/* + * @title ACP99Manager + * @notice The ACP99Manager interface represents the functionality for sovereign L1 + * validator management, as specified in ACP-77 + */ +abstract contract ACP99Manager { + /// @notice Emitted when an initial validator is registered + event RegisteredInitialValidator( + bytes32 indexed nodeID, bytes32 indexed validationID, uint64 weight + ); + /// @notice Emitted when a validator registration to the L1 is initiated + event InitiatedValidatorRegistration( + bytes32 indexed nodeID, + bytes32 indexed validationID, + bytes32 registrationMessageID, + uint64 registrationExpiry, + uint64 weight + ); + /// @notice Emitted when a validator registration to the L1 is completed + event CompletedValidatorRegistration( + bytes32 indexed nodeID, bytes32 indexed validationID, uint64 weight + ); + /// @notice Emitted when a validator weight update is initiated + event InitiatedValidatorWeightUpdate( + bytes32 indexed nodeID, + bytes32 indexed validationID, + bytes32 weightUpdateMessageID, + uint64 weight + ); + /// @notice Emitted when a validator weight update is completed + event CompletedValidatorWeightUpdate( + bytes32 indexed nodeID, bytes32 indexed validationID, uint64 nonce, uint64 weight + ); + + /** + * @notice Verifies and sets the initial validator set for the chain through a P-Chain + * SubnetToL1ConversionMessage. + * + * Emits a {RegisteredInitialValidator} event for each initial validator in {conversionData}. + * + * @param conversionData The Subnet conversion message data used to recompute and verify against the ConversionID. + * @param messsageIndex The index that contains the SubnetToL1ConversionMessage Warp message containing the ConversionID to be verified against the provided {conversionData} + */ + function initializeValidatorSet( + ConversionData calldata conversionData, + uint32 messsageIndex + ) virtual public; + + /** + * @notice Completes the validator registration process by returning an acknowledgement of the registration of a + * validationID from the P-Chain. The validator should not be considered active until this method is successfully called. + * + * Emits a {CompletedValidatorRegistration} event on success. + * + * @param messageIndex The index of the Warp message to be received providing the acknowledgement. + */ + function completeValidatorRegistration( + uint32 messageIndex + ) virtual public returns (bytes32); + + /** + * @notice Initiates validator removal by issuing a L1ValidatorWeightMessage with the weight set to zero. + * The validator should be considered inactive as soon as this function is called. + * + * Emits an {InitiatedValidatorRemoval} on success. + * + * @param validationID The ID of the validator to remove. + */ + function _initiateValidatorRemoval( + bytes32 validationID + ) virtual internal; + + /** + * @notice Completes validator removal by consuming an RegisterL1ValidatorMessage from the P-Chain acknowledging + * that the validator has been removed. + * + * Emits a {CompletedValidatorRemoval} on success. + * + * @param messageIndex The index of the RegisterL1ValidatorMessage. + */ + function completeValidatorRemoval( + uint32 messageIndex + ) virtual public returns (bytes32 validationID); + + /** + * @notice Completes the validator weight update process by returning an acknowledgement of the weight update of a + * validationID from the P-Chain. The validator weight change should not have any effect until this method is successfully called. + * + * Emits a {CompletedValidatorWeightUpdate} event on success + * + * @param messageIndex The index of the Warp message to be received providing the acknowledgement. + */ + function completeValidatorWeightUpdate( + uint32 messageIndex + ) virtual public returns (bytes32); + + /// @notice Returns the ID of the Subnet tied to this manager + function subnetID() virtual public view returns (bytes32); + + /// @notice Returns the validation details for a given validation ID + function getValidator( + bytes32 validationID + ) virtual public view returns (Validator memory); + + /// @notice Returns the total weight of the current L1 validator set + function l1TotalWeight() virtual public view returns (uint64); + + /** + * @notice Initiate a validator registration by issuing a RegisterL1ValidatorTx Warp message. The validator should + * not be considered active until completeValidatorRegistration is called. + * + * Emits an {InitiatedValidatorRegistration} event on success. + * + * @param nodeID The ID of the node to add to the L1 + * @param blsPublicKey The BLS public key of the validator + * @param registrationExpiry The time after which this message is invalid + * @param remainingBalanceOwner The remaining balance owner of the validator + * @param disableOwner The disable owner of the validator + * @param weight The weight of the node on the L1 + */ + function _initiateValidatorRegistration( + bytes memory nodeID, + bytes memory blsPublicKey, + uint64 registrationExpiry, + PChainOwner memory remainingBalanceOwner, + PChainOwner memory disableOwner, + uint64 weight + ) virtual internal returns (bytes32); + + /** + * @notice Initiate a validator weight update by issuing a SetL1ValidatorWeightTx Warp message. + * If the weight is 0, this initiates the removal of the validator from the L1. The validator weight change + * should not have any effect until completeValidatorWeightUpdate is successfully called. + * + * Emits an {InitiatedValidatorWeightUpdate} event on success. + * + * @param validationID The ID of the validation period to modify + * @param weight The new weight of the validation + */ + function _initiateValidatorWeightUpdate( + bytes32 validationID, + uint64 weight + ) virtual internal returns (uint64, bytes32); +} \ No newline at end of file diff --git a/contracts/validator-manager/ExampleRewardCalculator.sol b/contracts/validator-manager/ExampleRewardCalculator.sol index 856158350..7ef351e62 100644 --- a/contracts/validator-manager/ExampleRewardCalculator.sol +++ b/contracts/validator-manager/ExampleRewardCalculator.sol @@ -31,7 +31,7 @@ contract ExampleRewardCalculator is IRewardCalculator { uint64 stakingEndTime, uint64 uptimeSeconds ) external view returns (uint256) { - // Equivalent to uptimeSeconds/(validator.endedAt - validator.startedAt) < UPTIME_REWARDS_THRESHOLD_PERCENTAGE/100 + // Equivalent to uptimeSeconds/(validator.endTime - validator.startTime) < UPTIME_REWARDS_THRESHOLD_PERCENTAGE/100 // Rearranged to prevent integer division truncation. if ( uptimeSeconds * 100 diff --git a/contracts/validator-manager/PoAValidatorManager.sol b/contracts/validator-manager/PoAValidatorManager.sol index b62a30557..6544f64fd 100644 --- a/contracts/validator-manager/PoAValidatorManager.sol +++ b/contracts/validator-manager/PoAValidatorManager.sol @@ -55,7 +55,14 @@ contract PoAValidatorManager is IPoAValidatorManager, ValidatorManager, OwnableU ValidatorRegistrationInput calldata registrationInput, uint64 weight ) external onlyOwner returns (bytes32 validationID) { - return _initializeValidatorRegistration(registrationInput, weight); + return _initiateValidatorRegistration({ + nodeID: registrationInput.nodeID, + blsPublicKey: registrationInput.blsPublicKey, + registrationExpiry: registrationInput.registrationExpiry, + remainingBalanceOwner: registrationInput.remainingBalanceOwner, + disableOwner: registrationInput.disableOwner, + weight: weight + }); } // solhint-enable ordering @@ -63,13 +70,14 @@ contract PoAValidatorManager is IPoAValidatorManager, ValidatorManager, OwnableU * @notice See {IPoAValidatorManager-initializeEndValidation}. */ function initializeEndValidation(bytes32 validationID) external override onlyOwner { - _initializeEndValidation(validationID); + _initiateValidatorRemoval(validationID); } /** * @notice See {IValidatorManager-completeEndValidation}. */ - function completeEndValidation(uint32 messageIndex) external { - _completeEndValidation(messageIndex); + function completeValidatorRemoval(uint32 messageIndex) virtual override public returns (bytes32) { + (bytes32 validationID,) = _completeEndValidation(messageIndex); + return validationID; } } diff --git a/contracts/validator-manager/PoSValidatorManager.sol b/contracts/validator-manager/PoSValidatorManager.sol index 22ebc6120..0a1e2f5b5 100644 --- a/contracts/validator-manager/PoSValidatorManager.sol +++ b/contracts/validator-manager/PoSValidatorManager.sol @@ -14,11 +14,13 @@ import { PoSValidatorInfo, PoSValidatorManagerSettings } from "./interfaces/IPoSValidatorManager.sol"; +import { + ValidatorRegistrationInput +} from "./interfaces/IValidatorManager.sol"; import { Validator, - ValidatorRegistrationInput, ValidatorStatus -} from "./interfaces/IValidatorManager.sol"; +} from "./ACP99Manager.sol"; import {IRewardCalculator} from "./interfaces/IRewardCalculator.sol"; import {WarpMessage} from "@avalabs/subnet-evm-contracts@1.2.0/contracts/interfaces/IWarpMessenger.sol"; @@ -85,7 +87,6 @@ abstract contract PoSValidatorManager is error InvalidDelegationFee(uint16 delegationFeeBips); error InvalidDelegationID(bytes32 delegationID); error InvalidDelegatorStatus(DelegatorStatus status); - error InvalidNonce(uint64 nonce); error InvalidRewardRecipient(address rewardRecipient); error InvalidStakeAmount(uint256 stakeAmount); error InvalidMinStakeDuration(uint64 minStakeDuration); @@ -327,7 +328,8 @@ abstract contract PoSValidatorManager is ) internal returns (bool) { PoSValidatorManagerStorage storage $ = _getPoSValidatorManagerStorage(); - Validator memory validator = _initializeEndValidation(validationID); + Validator memory validator = getValidator(validationID); + _initiateValidatorRemoval(validationID); // Non-PoS validators are required to boostrap the network, but are not eligible for rewards. if (!_isPoSValidator(validationID)) { @@ -341,10 +343,10 @@ abstract contract PoSValidatorManager is // Check that minimum stake duration has passed. if ( - validator.endedAt - < validator.startedAt + $._posValidatorInfo[validationID].minStakeDuration + validator.endTime + < validator.startTime + $._posValidatorInfo[validationID].minStakeDuration ) { - revert MinStakeDurationNotPassed(validator.endedAt); + revert MinStakeDurationNotPassed(validator.endTime); } // Uptime proofs include the absolute number of seconds the validator has been active. @@ -357,9 +359,9 @@ abstract contract PoSValidatorManager is uint256 reward = $._rewardCalculator.calculateReward({ stakeAmount: weightToValue(validator.startingWeight), - validatorStartTime: validator.startedAt, - stakingStartTime: validator.startedAt, - stakingEndTime: validator.endedAt, + validatorStartTime: validator.startTime, + stakingStartTime: validator.startTime, + stakingEndTime: validator.endTime, uptimeSeconds: uptimeSeconds }); @@ -374,9 +376,9 @@ abstract contract PoSValidatorManager is } /** - * @notice See {IValidatorManager-completeEndValidation}. + * @notice See {ACP99Manager-completeValidatorRemoval}. */ - function completeEndValidation(uint32 messageIndex) external nonReentrant { + function completeValidatorRemoval(uint32 messageIndex) virtual override public nonReentrant returns (bytes32) { PoSValidatorManagerStorage storage $ = _getPoSValidatorManagerStorage(); (bytes32 validationID, Validator memory validator) = _completeEndValidation(messageIndex); @@ -384,7 +386,7 @@ abstract contract PoSValidatorManager is // Return now if this was originally a PoA validator that was later migrated to this PoS manager, // or the validator was part of the initial validator set. if (!_isPoSValidator(validationID)) { - return; + return validationID; } address owner = $._posValidatorInfo[validationID].owner; @@ -403,6 +405,8 @@ abstract contract PoSValidatorManager is // The stake is unlocked whether the validation period is completed or invalidated. _unlock(owner, weightToValue(validator.startingWeight)); + + return validationID; } /** @@ -475,7 +479,14 @@ abstract contract PoSValidatorManager is uint256 lockedValue = _lock(stakeAmount); uint64 weight = valueToWeight(lockedValue); - bytes32 validationID = _initializeValidatorRegistration(registrationInput, weight); + bytes32 validationID = _initiateValidatorRegistration({ + nodeID: registrationInput.nodeID, + blsPublicKey: registrationInput.blsPublicKey, + registrationExpiry: registrationInput.registrationExpiry, + remainingBalanceOwner: registrationInput.remainingBalanceOwner, + disableOwner: registrationInput.disableOwner, + weight: weight + }); address owner = _msgSender(); @@ -545,7 +556,7 @@ abstract contract PoSValidatorManager is revert MaxWeightExceeded(newValidatorWeight); } - (uint64 nonce, bytes32 messageID) = _setValidatorWeight(validationID, newValidatorWeight); + (uint64 nonce, bytes32 messageID) = _initiateValidatorWeightUpdate(validationID, newValidatorWeight); bytes32 delegationID = keccak256(abi.encodePacked(validationID, nonce)); @@ -556,7 +567,7 @@ abstract contract PoSValidatorManager is $._delegatorStakes[delegationID].owner = delegatorAddress; $._delegatorStakes[delegationID].validationID = validationID; $._delegatorStakes[delegationID].weight = weight; - $._delegatorStakes[delegationID].startedAt = 0; + $._delegatorStakes[delegationID].startTime = 0; $._delegatorStakes[delegationID].startingNonce = nonce; $._delegatorStakes[delegationID].endingNonce = 0; @@ -595,25 +606,22 @@ abstract contract PoSValidatorManager is return _completeEndDelegation(delegationID); } - // Unpack the Warp message - (bytes32 messageValidationID, uint64 nonce,) = ValidatorMessages - .unpackL1ValidatorWeightMessage(_getPChainWarpMessage(messageIndex).payload); - - if (validationID != messageValidationID) { - revert InvalidValidationID(delegator.validationID); + // If we've already received a weight update with a nonce greater than the delegation's starting nonce, + // then there's no requirement to include an ICM message in this function call. + if (validator.receivedNonce < delegator.startingNonce) { + bytes32 messageValidationID = completeValidatorWeightUpdate(messageIndex); + if (validationID != messageValidationID) { + revert InvalidValidationID(delegator.validationID); + } + if (validator.receivedNonce < delegator.startingNonce) { + revert InvalidNonce(validator.receivedNonce); + } } - // The received nonce should be no greater than the highest sent nonce, and at least as high as - // the delegation's starting nonce. This allows a weight update using a higher nonce - // (which implicitly includes the delegation's weight update) to be used to complete delisting - // for an earlier delegation. This is necessary because the P-Chain is only willing to sign the latest weight update. - if (validator.messageNonce < nonce || delegator.startingNonce > nonce) { - revert InvalidNonce(nonce); - } // Update the delegation status $._delegatorStakes[delegationID].status = DelegatorStatus.Active; - $._delegatorStakes[delegationID].startedAt = uint64(block.timestamp); + $._delegatorStakes[delegationID].startTime = uint64(block.timestamp); emit DelegatorRegistered({ delegationID: delegationID, @@ -720,7 +728,7 @@ abstract contract PoSValidatorManager is if ( block.timestamp - < validator.startedAt + $._posValidatorInfo[validationID].minStakeDuration + < validator.startTime + $._posValidatorInfo[validationID].minStakeDuration ) { revert MinStakeDurationNotPassed(uint64(block.timestamp)); } @@ -728,7 +736,7 @@ abstract contract PoSValidatorManager is if (validator.status == ValidatorStatus.Active) { // Check that minimum stake duration has passed. - if (block.timestamp < delegator.startedAt + $._minimumStakeDuration) { + if (block.timestamp < delegator.startTime + $._minimumStakeDuration) { revert MinStakeDurationNotPassed(uint64(block.timestamp)); } @@ -743,7 +751,7 @@ abstract contract PoSValidatorManager is $._delegatorStakes[delegationID].status = DelegatorStatus.PendingRemoved; ($._delegatorStakes[delegationID].endingNonce,) = - _setValidatorWeight(validationID, validator.weight - delegator.weight); + _initiateValidatorWeightUpdate(validationID, validator.weight - delegator.weight); uint256 reward = _calculateAndSetDelegationReward(delegator, rewardRecipient, delegationID); @@ -780,7 +788,7 @@ abstract contract PoSValidatorManager is validator.status == ValidatorStatus.PendingRemoved || validator.status == ValidatorStatus.Completed ) { - delegationEndTime = validator.endedAt; + delegationEndTime = validator.endTime; } else if (validator.status == ValidatorStatus.Active) { delegationEndTime = uint64(block.timestamp); } else { @@ -789,14 +797,14 @@ abstract contract PoSValidatorManager is } // Only give rewards in the case that the delegation started before the validator exited. - if (delegationEndTime <= delegator.startedAt) { + if (delegationEndTime <= delegator.startTime) { return 0; } uint256 reward = $._rewardCalculator.calculateReward({ stakeAmount: weightToValue(delegator.weight), - validatorStartTime: validator.startedAt, - stakingStartTime: delegator.startedAt, + validatorStartTime: validator.startTime, + stakingStartTime: delegator.startTime, stakingEndTime: delegationEndTime, uptimeSeconds: $._posValidatorInfo[delegator.validationID].uptimeSeconds }); @@ -827,7 +835,7 @@ abstract contract PoSValidatorManager is } Validator memory validator = getValidator(delegator.validationID); - if (validator.messageNonce == 0) { + if (validator.sentNonce == 0) { // Should be unreachable. revert InvalidDelegationID(delegationID); } @@ -835,7 +843,7 @@ abstract contract PoSValidatorManager is // Submit the message to the Warp precompile. WARP_MESSENGER.sendWarpMessage( ValidatorMessages.packL1ValidatorWeightMessage( - delegator.validationID, validator.messageNonce, validator.weight + delegator.validationID, validator.sentNonce, validator.weight ) ); } @@ -843,6 +851,9 @@ abstract contract PoSValidatorManager is /** * @notice See {IPoSValidatorManager-completeEndDelegation}. */ + // TODONOW: Rather than ending on a delegation by delegation basis, + // Can we instead track the highest acknowledged nonce, delivered via completeValidatorWeightChange? + // We'd still have a delegation specific collectDelegationRewards or similar, but that would check against this nonce. function completeEndDelegation( bytes32 delegationID, uint32 messageIndex @@ -856,23 +867,22 @@ abstract contract PoSValidatorManager is if (delegator.status != DelegatorStatus.PendingRemoved) { revert InvalidDelegatorStatus(delegator.status); } + Validator memory validator = getValidator(delegator.validationID); - if (getValidator(delegator.validationID).status != ValidatorStatus.Completed) { - // Unpack the Warp message - WarpMessage memory warpMessage = _getPChainWarpMessage(messageIndex); - (bytes32 validationID, uint64 nonce,) = - ValidatorMessages.unpackL1ValidatorWeightMessage(warpMessage.payload); - + // We only expect an ICM message if we haven't received a weight update with a nonce greater than the delegation's ending nonce + if (getValidator(delegator.validationID).status != ValidatorStatus.Completed && validator.receivedNonce < delegator.endingNonce) { + bytes32 validationID = completeValidatorWeightUpdate(messageIndex); if (delegator.validationID != validationID) { revert InvalidValidationID(validationID); } + // The received nonce should be at least as high as the delegation's ending nonce. This allows a weight // update using a higher nonce (which implicitly includes the delegation's weight update) to be used to // complete delisting for an earlier delegation. This is necessary because the P-Chain is only willing // to sign the latest weight update. - if (delegator.endingNonce > nonce) { - revert InvalidNonce(nonce); + if (delegator.endingNonce > validator.receivedNonce) { + revert InvalidNonce(validator.receivedNonce); } } @@ -887,7 +897,8 @@ abstract contract PoSValidatorManager is // To prevent churn tracker abuse, check that one full churn period has passed, // so a delegator may not stake twice in the same churn period. - if (block.timestamp < delegator.startedAt + _getChurnPeriodSeconds()) { + // TODONOW: Is it safe to move this to initializeEndDelegation? + if (block.timestamp < delegator.startTime + _getChurnPeriodSeconds()) { revert MinStakeDurationNotPassed(uint64(block.timestamp)); } diff --git a/contracts/validator-manager/ValidatorManager.sol b/contracts/validator-manager/ValidatorManager.sol index f7f39653f..3579cfd56 100644 --- a/contracts/validator-manager/ValidatorManager.sol +++ b/contracts/validator-manager/ValidatorManager.sol @@ -7,16 +7,19 @@ pragma solidity 0.8.25; import {ValidatorMessages} from "./ValidatorMessages.sol"; import { - InitialValidator, IValidatorManager, + ValidatorChurnPeriod, + ValidatorManagerSettings, + ValidatorRegistrationInput +} from "./interfaces/IValidatorManager.sol"; +import { + ACP99Manager, + InitialValidator, PChainOwner, ConversionData, Validator, - ValidatorChurnPeriod, - ValidatorManagerSettings, - ValidatorRegistrationInput, ValidatorStatus -} from "./interfaces/IValidatorManager.sol"; +} from "./ACP99Manager.sol"; import { IWarpMessenger, WarpMessage @@ -31,7 +34,7 @@ import {Initializable} from * * @custom:security-contact https://github.com/ava-labs/icm-contracts/blob/main/SECURITY.md */ -abstract contract ValidatorManager is Initializable, ContextUpgradeable, IValidatorManager { +abstract contract ValidatorManager is Initializable, ContextUpgradeable, IValidatorManager, ACP99Manager { // solhint-disable private-vars-leading-underscore /// @custom:storage-location erc7201:avalanche-icm.storage.ValidatorManager @@ -78,6 +81,7 @@ abstract contract ValidatorManager is Initializable, ContextUpgradeable, IValida error InvalidTotalWeight(uint64 weight); error InvalidValidationID(bytes32 validationID); error InvalidValidatorStatus(ValidatorStatus status); + error InvalidNonce(uint64 nonce); error InvalidWarpMessage(); error MaxChurnRateExceeded(uint64 churnAmount); error NodeAlreadyRegistered(bytes nodeID); @@ -144,12 +148,12 @@ abstract contract ValidatorManager is Initializable, ContextUpgradeable, IValida } /** - * @notice See {IValidatorManager-initializeValidatorSet}. + * @notice See {ACP99Manager-initializeValidatorSet}. */ function initializeValidatorSet( ConversionData calldata conversionData, uint32 messageIndex - ) external { + ) virtual public override { ValidatorManagerStorage storage $ = _getValidatorManagerStorage(); if ($._initializedValidatorSet) { revert InvalidInitializationStatus(); @@ -182,10 +186,10 @@ abstract contract ValidatorManager is Initializable, ContextUpgradeable, IValida $._validationPeriods[validationID].status = ValidatorStatus.Active; $._validationPeriods[validationID].nodeID = initialValidator.nodeID; $._validationPeriods[validationID].startingWeight = initialValidator.weight; - $._validationPeriods[validationID].messageNonce = 0; + $._validationPeriods[validationID].sentNonce = 0; $._validationPeriods[validationID].weight = initialValidator.weight; - $._validationPeriods[validationID].startedAt = uint64(block.timestamp); - $._validationPeriods[validationID].endedAt = 0; + $._validationPeriods[validationID].startTime = uint64(block.timestamp); + $._validationPeriods[validationID].endTime = 0; totalWeight += initialValidator.weight; emit InitialValidatorCreated( @@ -213,7 +217,7 @@ abstract contract ValidatorManager is Initializable, ContextUpgradeable, IValida $._initializedValidatorSet = true; } - function _validatePChainOwner(PChainOwner calldata pChainOwner) internal pure { + function _validatePChainOwner(PChainOwner memory pChainOwner) internal pure { // If threshold is 0, addresses must be empty. if (pChainOwner.threshold == 0 && pChainOwner.addresses.length != 0) { revert InvalidPChainOwnerThreshold(pChainOwner.threshold, pChainOwner.addresses.length); @@ -235,20 +239,23 @@ abstract contract ValidatorManager is Initializable, ContextUpgradeable, IValida * @notice Begins the validator registration process, and sets the initial weight for the validator. * This is the only method related to validator registration and removal that needs the initializedValidatorSet * modifier. All others are guarded by checking the validator status changes initialized in this function. - * @param input The inputs for a validator registration. * @param weight The weight of the validator being registered. */ - function _initializeValidatorRegistration( - ValidatorRegistrationInput calldata input, + function _initiateValidatorRegistration( + bytes memory nodeID, + bytes memory blsPublicKey, + uint64 registrationExpiry, + PChainOwner memory remainingBalanceOwner, + PChainOwner memory disableOwner, uint64 weight - ) internal virtual initializedValidatorSet returns (bytes32) { + ) virtual override internal initializedValidatorSet returns (bytes32) { ValidatorManagerStorage storage $ = _getValidatorManagerStorage(); if ( - input.registrationExpiry <= block.timestamp - || input.registrationExpiry >= block.timestamp + MAXIMUM_REGISTRATION_EXPIRY_LENGTH + registrationExpiry <= block.timestamp + || registrationExpiry >= block.timestamp + MAXIMUM_REGISTRATION_EXPIRY_LENGTH ) { - revert InvalidRegistrationExpiry(input.registrationExpiry); + revert InvalidRegistrationExpiry(registrationExpiry); } // Ensure the new validator doesn't overflow the total weight @@ -256,19 +263,19 @@ abstract contract ValidatorManager is Initializable, ContextUpgradeable, IValida revert InvalidTotalWeight(weight); } - _validatePChainOwner(input.remainingBalanceOwner); - _validatePChainOwner(input.disableOwner); + _validatePChainOwner(remainingBalanceOwner); + _validatePChainOwner(disableOwner); // Ensure the nodeID is not the zero address, and is not already an active validator. - if (input.blsPublicKey.length != BLS_PUBLIC_KEY_LENGTH) { - revert InvalidBLSKeyLength(input.blsPublicKey.length); + if (blsPublicKey.length != BLS_PUBLIC_KEY_LENGTH) { + revert InvalidBLSKeyLength(blsPublicKey.length); } - if (input.nodeID.length == 0) { - revert InvalidNodeID(input.nodeID); + if (nodeID.length == 0) { + revert InvalidNodeID(nodeID); } - if ($._registeredValidators[input.nodeID] != bytes32(0)) { - revert NodeAlreadyRegistered(input.nodeID); + if ($._registeredValidators[nodeID] != bytes32(0)) { + revert NodeAlreadyRegistered(nodeID); } // Check that adding this validator would not exceed the maximum churn rate. @@ -278,29 +285,29 @@ abstract contract ValidatorManager is Initializable, ContextUpgradeable, IValida .packRegisterL1ValidatorMessage( ValidatorMessages.ValidationPeriod({ subnetID: $._subnetID, - nodeID: input.nodeID, - blsPublicKey: input.blsPublicKey, - remainingBalanceOwner: input.remainingBalanceOwner, - disableOwner: input.disableOwner, - registrationExpiry: input.registrationExpiry, + nodeID: nodeID, + blsPublicKey: blsPublicKey, + remainingBalanceOwner: remainingBalanceOwner, + disableOwner: disableOwner, + registrationExpiry: registrationExpiry, weight: weight }) ); $._pendingRegisterValidationMessages[validationID] = registerL1ValidatorMessage; - $._registeredValidators[input.nodeID] = validationID; + $._registeredValidators[nodeID] = validationID; // Submit the message to the Warp precompile. bytes32 messageID = WARP_MESSENGER.sendWarpMessage(registerL1ValidatorMessage); $._validationPeriods[validationID].status = ValidatorStatus.PendingAdded; - $._validationPeriods[validationID].nodeID = input.nodeID; + $._validationPeriods[validationID].nodeID = nodeID; $._validationPeriods[validationID].startingWeight = weight; - $._validationPeriods[validationID].messageNonce = 0; + $._validationPeriods[validationID].sentNonce = 0; $._validationPeriods[validationID].weight = weight; - $._validationPeriods[validationID].startedAt = 0; // The validation period only starts once the registration is acknowledged. - $._validationPeriods[validationID].endedAt = 0; + $._validationPeriods[validationID].startTime = 0; // The validation period only starts once the registration is acknowledged. + $._validationPeriods[validationID].endTime = 0; emit ValidationPeriodCreated( - validationID, input.nodeID, messageID, weight, input.registrationExpiry + validationID, nodeID, messageID, weight, registrationExpiry ); return validationID; @@ -324,9 +331,9 @@ abstract contract ValidatorManager is Initializable, ContextUpgradeable, IValida } /** - * @notice See {IValidatorManager-completeValidatorRegistration}. + * @notice See {ACP99Manager-completeValidatorRegistration}. */ - function completeValidatorRegistration(uint32 messageIndex) external { + function completeValidatorRegistration(uint32 messageIndex) virtual public override returns (bytes32) { ValidatorManagerStorage storage $ = _getValidatorManagerStorage(); (bytes32 validationID, bool validRegistration) = ValidatorMessages .unpackL1ValidatorRegistrationMessage(_getPChainWarpMessage(messageIndex).payload); @@ -344,10 +351,12 @@ abstract contract ValidatorManager is Initializable, ContextUpgradeable, IValida delete $._pendingRegisterValidationMessages[validationID]; $._validationPeriods[validationID].status = ValidatorStatus.Active; - $._validationPeriods[validationID].startedAt = uint64(block.timestamp); + $._validationPeriods[validationID].startTime = uint64(block.timestamp); emit ValidationPeriodRegistered( validationID, $._validationPeriods[validationID].weight, block.timestamp ); + + return validationID; } /** @@ -363,21 +372,49 @@ abstract contract ValidatorManager is Initializable, ContextUpgradeable, IValida * @notice Returns a validator registered to the given validationID * @param validationID ID of the validation period associated with the validator */ - function getValidator(bytes32 validationID) public view returns (Validator memory) { + function getValidator(bytes32 validationID) virtual override public view returns (Validator memory) { ValidatorManagerStorage storage $ = _getValidatorManagerStorage(); return $._validationPeriods[validationID]; } + function l1TotalWeight() virtual override public view returns (uint64) { + return _getValidatorManagerStorage()._churnTracker.totalWeight; + } + + function subnetID() virtual override public view returns (bytes32) { + return _getValidatorManagerStorage()._subnetID; + } + + function completeValidatorWeightUpdate( + uint32 messageIndex + ) virtual override public returns (bytes32) { + WarpMessage memory warpMessage = _getPChainWarpMessage(messageIndex); + (bytes32 validationID, uint64 nonce,) = + ValidatorMessages.unpackL1ValidatorWeightMessage(warpMessage.payload); + + ValidatorManagerStorage storage $ = _getValidatorManagerStorage(); + + // The received nonce should be no greater than the highest sent nonce to ensure + // that weight changes are only initiated by this contract. + if ($._validationPeriods[validationID].sentNonce < nonce) { + revert InvalidNonce(nonce); + } + + $._validationPeriods[validationID].receivedNonce = nonce; + + return validationID; + } + /** * @notice Begins the process of ending an active validation period. The validation period must have been previously * started by a successful call to {completeValidatorRegistration} with the given validationID. * Any rewards for this validation period will stop accruing when this function is called. * @param validationID The ID of the validation period being ended. */ - function _initializeEndValidation(bytes32 validationID) + function _initiateValidatorRemoval(bytes32 validationID) internal virtual - returns (Validator memory) + override { ValidatorManagerStorage storage $ = _getValidatorManagerStorage(); @@ -394,17 +431,15 @@ abstract contract ValidatorManager is Initializable, ContextUpgradeable, IValida // Set the end time of the validation period, since it is no longer known to be an active validator // on the P-Chain. - validator.endedAt = uint64(block.timestamp); + validator.endTime = uint64(block.timestamp); // Save the validator updates. $._validationPeriods[validationID] = validator; - (, bytes32 messageID) = _setValidatorWeight(validationID, 0); + (, bytes32 messageID) = _initiateValidatorWeightUpdate(validationID, 0); // Emit the event to signal the start of the validator removal process. emit ValidatorRemovalInitialized(validationID, messageID, validator.weight, block.timestamp); - - return validator; } /** @@ -420,7 +455,7 @@ abstract contract ValidatorManager is Initializable, ContextUpgradeable, IValida } WARP_MESSENGER.sendWarpMessage( - ValidatorMessages.packL1ValidatorWeightMessage(validationID, validator.messageNonce, 0) + ValidatorMessages.packL1ValidatorWeightMessage(validationID, validator.sentNonce, 0) ); } @@ -474,9 +509,9 @@ abstract contract ValidatorManager is Initializable, ContextUpgradeable, IValida return (validationID, validator); } - function _incrementAndGetNonce(bytes32 validationID) internal returns (uint64) { + function _incrementSentNonce(bytes32 validationID) internal returns (uint64) { ValidatorManagerStorage storage $ = _getValidatorManagerStorage(); - return ++$._validationPeriods[validationID].messageNonce; + return ++$._validationPeriods[validationID].sentNonce; } function _getPChainWarpMessage(uint32 messageIndex) @@ -500,17 +535,17 @@ abstract contract ValidatorManager is Initializable, ContextUpgradeable, IValida return warpMessage; } - function _setValidatorWeight( + function _initiateValidatorWeightUpdate( bytes32 validationID, uint64 newWeight - ) internal returns (uint64, bytes32) { + ) virtual override internal returns (uint64, bytes32) { ValidatorManagerStorage storage $ = _getValidatorManagerStorage(); uint64 validatorWeight = $._validationPeriods[validationID].weight; // Check that changing the validator weight would not exceed the maximum churn rate. _checkAndUpdateChurnTracker(newWeight, validatorWeight); - uint64 nonce = _incrementAndGetNonce(validationID); + uint64 nonce = _incrementSentNonce(validationID); $._validationPeriods[validationID].weight = newWeight; @@ -555,11 +590,11 @@ abstract contract ValidatorManager is Initializable, ContextUpgradeable, IValida ValidatorChurnPeriod memory churnTracker = $._churnTracker; if ( - churnTracker.startedAt == 0 - || currentTime >= churnTracker.startedAt + $._churnPeriodSeconds + churnTracker.startTime == 0 + || currentTime >= churnTracker.startTime + $._churnPeriodSeconds ) { churnTracker.churnAmount = weightChange; - churnTracker.startedAt = currentTime; + churnTracker.startTime = currentTime; churnTracker.initialWeight = churnTracker.totalWeight; } else { // Churn is always additive whether the weight is being added or removed. diff --git a/contracts/validator-manager/interfaces/IPoSValidatorManager.sol b/contracts/validator-manager/interfaces/IPoSValidatorManager.sol index 841f7d362..59cd0b311 100644 --- a/contracts/validator-manager/interfaces/IPoSValidatorManager.sol +++ b/contracts/validator-manager/interfaces/IPoSValidatorManager.sol @@ -52,7 +52,7 @@ struct Delegator { address owner; bytes32 validationID; uint64 weight; - uint64 startedAt; + uint64 startTime; uint64 startingNonce; uint64 endingNonce; } diff --git a/contracts/validator-manager/interfaces/IValidatorManager.sol b/contracts/validator-manager/interfaces/IValidatorManager.sol index 9ee1ed6ab..37001c44a 100644 --- a/contracts/validator-manager/interfaces/IValidatorManager.sol +++ b/contracts/validator-manager/interfaces/IValidatorManager.sol @@ -5,45 +5,13 @@ pragma solidity 0.8.25; -/** - * @dev Validator status - */ -enum ValidatorStatus { - Unknown, - PendingAdded, - Active, - PendingRemoved, - Completed, - Invalidated -} - -/** - * @dev Specifies the owner of a validator's remaining balance or disable owner on the P-Chain. - * P-Chain addresses are also 20-bytes, so we use the address type to represent them. - */ -struct PChainOwner { - uint32 threshold; - address[] addresses; -} - -/** - * @dev Contains the active state of a Validator - */ -struct Validator { - ValidatorStatus status; - bytes nodeID; - uint64 startingWeight; - uint64 messageNonce; - uint64 weight; - uint64 startedAt; - uint64 endedAt; -} +import {ValidatorStatus, ConversionData, PChainOwner} from "../ACP99Manager.sol"; /** * @dev Describes the current churn period */ struct ValidatorChurnPeriod { - uint256 startedAt; + uint256 startTime; uint64 initialWeight; uint64 totalWeight; uint64 churnAmount; @@ -61,28 +29,6 @@ struct ValidatorManagerSettings { uint8 maximumChurnPercentage; } -/** - * @dev Description of the conversion data used to convert - * a subnet to an L1 on the P-Chain. - * This data is the pre-image of a hash that is authenticated by the P-Chain - * and verified by the Validator Manager. - */ -struct ConversionData { - bytes32 subnetID; - bytes32 validatorManagerBlockchainID; - address validatorManagerAddress; - InitialValidator[] initialValidators; -} - -/** - * @dev Specifies an initial validator, used in the conversion data. - */ -struct InitialValidator { - bytes nodeID; - bytes blsPublicKey; - uint64 weight; -} - /** * @dev Specifies a validator to register. */ @@ -171,16 +117,6 @@ interface IValidatorManager { bytes32 setWeightMessageID ); - /** - * @notice Verifies and sets the initial validator set for the chain through a P-Chain SubnetToL1ConversionMessage. - * @param conversionData The subnet conversion message data used to recompute and verify against the conversionID. - * @param messsageIndex The index that contains the SubnetToL1ConversionMessage ICM message containing the conversionID to be verified against the provided {ConversionData} - */ - function initializeValidatorSet( - ConversionData calldata conversionData, - uint32 messsageIndex - ) external; - /** * @notice Resubmits a validator registration message to be sent to the P-Chain. * Only necessary if the original message can't be delivered due to validator churn. @@ -188,28 +124,10 @@ interface IValidatorManager { */ function resendRegisterValidatorMessage(bytes32 validationID) external; - /** - * @notice Completes the validator registration process by returning an acknowledgement of the registration of a - * validationID from the P-Chain. - * @param messageIndex The index of the ICM message to be received providing the acknowledgement. - */ - function completeValidatorRegistration(uint32 messageIndex) external; - /** * @notice Resubmits a validator end message to be sent to the P-Chain. * Only necessary if the original message can't be delivered due to validator churn. * @param validationID The ID of the validation period being ended. */ function resendEndValidatorMessage(bytes32 validationID) external; - - /** - * @notice Completes the process of ending a validation period by receiving an acknowledgement from the P-Chain - * that the validation ID is not active and will never be active in the future. Returns the the stake associated - * with the validation. - * Note: This function can be used for successful validation periods that have been explicitly ended by calling - * {initializeEndValidation} or for validation periods that never began on the P-Chain due to the {registrationExpiry} being reached. - * @param messageIndex The index of the ICM message to be received providing the proof the validation is not active - * and never will be active on the P-Chain. - */ - function completeEndValidation(uint32 messageIndex) external; }