diff --git a/.gitignore b/.gitignore index 32a56dc..6ea9c5f 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,5 @@ /Node/target .vscode .env -tools/tx_spammer/venv \ No newline at end of file +tools/tx_spammer/venv +.DS_Store \ No newline at end of file diff --git a/SmartContracts/src/avs/PreconfRegistry.sol b/SmartContracts/src/avs/PreconfRegistry.sol index 9b3e94d..6410c4f 100644 --- a/SmartContracts/src/avs/PreconfRegistry.sol +++ b/SmartContracts/src/avs/PreconfRegistry.sol @@ -1,6 +1,7 @@ // SPDX-License-Identifier: UNLICENSED pragma solidity 0.8.25; +import {PreconfConstants} from "./libraries/PreconfConstants.sol"; import {BLS12381} from "../libraries/BLS12381.sol"; import {BLSSignatureChecker} from "./utils/BLSSignatureChecker.sol"; import {IPreconfRegistry} from "../interfaces/IPreconfRegistry.sol"; @@ -15,14 +16,16 @@ contract PreconfRegistry is IPreconfRegistry, ISignatureUtils, BLSSignatureCheck uint256 internal nextPreconferIndex; // Maps the preconfer's address to an index that may change over the lifetime of a preconfer - mapping(address => uint256) public preconferToIndex; + mapping(address preconfer => uint256 index) internal preconferToIndex; - // Maps the preconfer's address to an incrementing nonce used for validator signatures - mapping(address => uint256) public preconferToNonce; + // Maps an index to the preconfer's address + // We need this mapping to deregister a preconfer in O(1) time. + // While it may also be done by just using the above map and sending a "witness" that is calculated offchain, + // we ideally do not want the node to maintain historical state. + mapping(uint256 index => address preconfer) internal indexToPreconfer; - // Maps the preconfer's ecsda and one associated BLS public key hash to the timestamp - // at which the key hash was added - mapping(address => mapping(bytes32 => uint256)) public preconferToPubKeyHashToTimestamp; + // Maps a validator's BLS pub key hash to the validator's details + mapping(bytes32 publicKeyHash => Validator) internal validators; constructor(IServiceManager _preconfServiceManager) { preconfServiceManager = _preconfServiceManager; @@ -30,8 +33,9 @@ contract PreconfRegistry is IPreconfRegistry, ISignatureUtils, BLSSignatureCheck } /** - * @notice Registers a preconfer by giving them a non-zero registry index - * @param operatorSignature The signature of the preconfer in the format expected by Eigenlayer registry + * @notice Registers a preconfer in the registry by giving it a non-zero index + * @dev This function internally accesses Eigenlayer via the AVS service manager + * @param operatorSignature The signature of the operator in the format expected by Eigenlayer */ function registerPreconfer(SignatureWithSaltAndExpiry calldata operatorSignature) external { // Preconfer must not have registered already @@ -42,7 +46,11 @@ contract PreconfRegistry is IPreconfRegistry, ISignatureUtils, BLSSignatureCheck uint256 _nextPreconferIndex = nextPreconferIndex; preconferToIndex[msg.sender] = _nextPreconferIndex; - nextPreconferIndex = _nextPreconferIndex + 1; + indexToPreconfer[_nextPreconferIndex] = msg.sender; + + unchecked { + nextPreconferIndex = _nextPreconferIndex + 1; + } emit PreconferRegistered(msg.sender, _nextPreconferIndex); @@ -50,29 +58,30 @@ contract PreconfRegistry is IPreconfRegistry, ISignatureUtils, BLSSignatureCheck } /** - * @notice Deregisters a preconfer from the registry - * @dev The preconfer that has the last index must be provided as a witness to save gas - * @param lastIndexWitness The address of the preconfer that has the last index + * @notice Deregisters a preconfer from the registry by setting its index to zero + * @dev It assigns the index of the last preconfer to the preconfer being removed and + * decrements the global index counter. */ - function deregisterPreconfer(address lastIndexWitness) external { + function deregisterPreconfer() external { // Preconfer must have registered already if (preconferToIndex[msg.sender] == 0) { revert PreconferNotRegistered(); } - // Ensure that provided witness is the preconfer that has the last index - uint256 _nextPreconferIndex = nextPreconferIndex - 1; - if (preconferToIndex[lastIndexWitness] != _nextPreconferIndex) { - revert LastIndexWitnessIncorrect(); - } + unchecked { + uint256 _nextPreconferIndex = nextPreconferIndex - 1; - // Update to the decremented index to account for the removed preconfer - nextPreconferIndex = _nextPreconferIndex; + // Update to the decremented index to account for the removed preconfer + nextPreconferIndex = _nextPreconferIndex; - // Remove the preconfer and exchange its index with the last preconfer - uint256 removedPreconferIndex = preconferToIndex[msg.sender]; - preconferToIndex[msg.sender] = 0; - preconferToIndex[lastIndexWitness] = removedPreconferIndex; + uint256 removedPreconferIndex = preconferToIndex[msg.sender]; + address lastPreconfer = indexToPreconfer[_nextPreconferIndex]; + + // Remove the preconfer and exchange its index with the last preconfer + preconferToIndex[msg.sender] = 0; + preconferToIndex[lastPreconfer] = removedPreconferIndex; + indexToPreconfer[removedPreconferIndex] = lastPreconfer; + } emit PreconferDeregistered(msg.sender); @@ -80,62 +89,140 @@ contract PreconfRegistry is IPreconfRegistry, ISignatureUtils, BLSSignatureCheck } /** - * @notice Associates a batch of validators with a preconfer - * @param pubkeys The public keys of the validators - * @param signatures The BLS signatures of the validators + * @notice Assigns a validator to a preconfer + * @dev The function allows different validators to be assigned to different preconfers, but + * generally, it will be called by a preconfer to assign validators to itself. + * @param addValidatorParams Contains the public key, signature, expiry, and preconfer */ - function addValidators(BLS12381.G1Point[] calldata pubkeys, BLS12381.G2Point[] calldata signatures) external { - if (pubkeys.length != signatures.length) { - revert ArrayLengthMismatch(); - } + function addValidators(AddValidatorParam[] calldata addValidatorParams) external { + for (uint256 i; i < addValidatorParams.length; ++i) { + // Revert if preconfer is not registered + if (preconferToIndex[addValidatorParams[i].preconfer] == 0) { + revert PreconferNotRegistered(); + } + + bytes memory message = + _createMessage(ValidatorOp.ADD, addValidatorParams[i].signatureExpiry, addValidatorParams[i].preconfer); - uint256 preconferNonce = preconferToNonce[msg.sender]; - for (uint256 i; i < pubkeys.length; ++i) { // Revert if any signature is invalid - if (!verifySignature(_createMessage(preconferNonce), signatures[i], pubkeys[i])) { + if (!verifySignature(message, addValidatorParams[i].signature, addValidatorParams[i].pubkey)) { revert InvalidValidatorSignature(); } + // Revert if the signature has expired + if (block.timestamp > addValidatorParams[i].signatureExpiry) { + revert ValidatorSignatureExpired(); + } + // Point compress the public key just how it is done on the consensus layer - uint256[2] memory compressedPubKey = pubkeys[i].compress(); + uint256[2] memory compressedPubKey = addValidatorParams[i].pubkey.compress(); // Use the hash for ease of mapping bytes32 pubKeyHash = keccak256(abi.encodePacked(compressedPubKey)); - preconferToPubKeyHashToTimestamp[msg.sender][pubKeyHash] = block.timestamp; - - emit ValidatorAdded(msg.sender, compressedPubKey); - - unchecked { - ++preconferNonce; + Validator memory validator = validators[pubKeyHash]; + + // Update the validator if it has no preconfer assigned, or if it has stopped proposing + // for the former preconfer + if ( + validator.preconfer == address(0) + || (validator.stopProposingAt != 0 && block.timestamp > validator.stopProposingAt) + ) { + unchecked { + validators[pubKeyHash] = Validator({ + preconfer: addValidatorParams[i].preconfer, + // The delay is crucial in order to not contradict the lookahead + startProposingAt: uint40(block.timestamp + PreconfConstants.TWO_EPOCHS), + stopProposingAt: uint40(0) + }); + } + } else { + // Validator is already proposing for a preconfer + revert ValidatorAlreadyActive(); } - } - preconferToNonce[msg.sender] = preconferNonce; + emit ValidatorAdded(pubKeyHash, addValidatorParams[i].preconfer); + } } /** - * @notice Removes a batch of validators for a preconfer - * @param validatorPubKeyHashes The hashes of the public keys of the validators + * @notice Unassigns a validator from a preconfer + * @dev Instead of removing the validator immediately, we delay the removal by two epochs, + * & set the `stopProposingAt` timestamp. + * @param removeValidatorParams Contains the public key, signature and expiry */ - function removeValidators(bytes32[] memory validatorPubKeyHashes) external { - for (uint256 i; i < validatorPubKeyHashes.length; ++i) { - if (preconferToPubKeyHashToTimestamp[msg.sender][validatorPubKeyHashes[i]] == 0) { - revert InvalidValidatorPubKeyHash(); + function removeValidators(RemoveValidatorParam[] calldata removeValidatorParams) external { + for (uint256 i; i < removeValidatorParams.length; ++i) { + // Point compress the public key just how it is done on the consensus layer + uint256[2] memory compressedPubKey = removeValidatorParams[i].pubkey.compress(); + // Use the hash for ease of mapping + bytes32 pubKeyHash = keccak256(abi.encodePacked(compressedPubKey)); + + Validator memory validator = validators[pubKeyHash]; + + // Revert if the validator is not active (or already removed, but waiting to stop proposing) + if (validator.preconfer == address(0) || validator.stopProposingAt != 0) { + revert ValidatorAlreadyInactive(); + } + + bytes memory message = + _createMessage(ValidatorOp.REMOVE, removeValidatorParams[i].signatureExpiry, validator.preconfer); + + // Revert if any signature is invalid + if (!verifySignature(message, removeValidatorParams[i].signature, removeValidatorParams[i].pubkey)) { + revert InvalidValidatorSignature(); } - preconferToPubKeyHashToTimestamp[msg.sender][validatorPubKeyHashes[i]] = 0; - emit ValidatorRemoved(msg.sender, validatorPubKeyHashes[i]); + + // Revert if the signature has expired + if (block.timestamp > removeValidatorParams[i].signatureExpiry) { + revert ValidatorSignatureExpired(); + } + + unchecked { + // We also need to delay the removal by two epochs to avoid contradicting the lookahead + validators[pubKeyHash].stopProposingAt = uint40(block.timestamp + PreconfConstants.TWO_EPOCHS); + } + + emit ValidatorRemoved(pubKeyHash, validator.preconfer); } } + //======= + // Views + //======= + + function getMessageToSign(ValidatorOp validatorOp, uint256 expiry, address preconfer) + external + view + returns (bytes memory) + { + return _createMessage(validatorOp, expiry, preconfer); + } + + function getNextPreconferIndex() external view returns (uint256) { + return nextPreconferIndex; + } + + function getPreconferIndex(address preconfer) external view returns (uint256) { + return preconferToIndex[preconfer]; + } + + function getPreconferAtIndex(uint256 index) external view returns (address) { + return indexToPreconfer[index]; + } + + function getValidator(bytes32 pubKeyHash) external view returns (Validator memory) { + return validators[pubKeyHash]; + } + //========= // Helpers //========= - /** - * @notice Returns the message to be signed by the preconfer - * @param nonce The nonce of the preconfer - */ - function _createMessage(uint256 nonce) internal view returns (bytes memory) { - return abi.encodePacked(block.chainid, msg.sender, nonce); + function _createMessage(ValidatorOp validatorOp, uint256 expiry, address preconfer) + internal + view + returns (bytes memory) + { + return abi.encodePacked(block.chainid, validatorOp, expiry, preconfer); } } diff --git a/SmartContracts/src/avs/libraries/PreconfConstants.sol b/SmartContracts/src/avs/libraries/PreconfConstants.sol new file mode 100644 index 0000000..a60e349 --- /dev/null +++ b/SmartContracts/src/avs/libraries/PreconfConstants.sol @@ -0,0 +1,6 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity 0.8.25; + +library PreconfConstants { + uint256 internal constant TWO_EPOCHS = 384; +} diff --git a/SmartContracts/src/interfaces/IPreconfRegistry.sol b/SmartContracts/src/interfaces/IPreconfRegistry.sol index daaa83e..67116f0 100644 --- a/SmartContracts/src/interfaces/IPreconfRegistry.sol +++ b/SmartContracts/src/interfaces/IPreconfRegistry.sol @@ -5,42 +5,88 @@ import {BLS12381} from "../libraries/BLS12381.sol"; import {ISignatureUtils} from "eigenlayer-middleware/interfaces/IServiceManagerUI.sol"; interface IPreconfRegistry { + struct Validator { + // Preconfer that the validator proposer blocks for + address preconfer; + // Timestamp at which the preconfer may start proposing for the preconfer + // 2 epochs from validator addition timestamp + uint40 startProposingAt; + // Timestamp at which the preconfer must stop proposing for the preconfer + // 2 epochs from validator removal timestamp + uint40 stopProposingAt; + } + // ^ Note: 40 bits are enough for UNIX timestamp. This way we also compress the data to a single slot. + + struct AddValidatorParam { + // The public key of the validator + BLS12381.G1Point pubkey; + // The signature of the validator + BLS12381.G2Point signature; + // The timestamp at which the above signature expires + uint256 signatureExpiry; + // The preconfer that the validator will be proposing for + address preconfer; + } + + struct RemoveValidatorParam { + // The public key of the validator + BLS12381.G1Point pubkey; + // The signature of the validator + BLS12381.G2Point signature; + // The timestamp at which the above signature expires + uint256 signatureExpiry; + } + + enum ValidatorOp { + REMOVE, + ADD + } + event PreconferRegistered(address indexed preconfer, uint256 indexed index); event PreconferDeregistered(address indexed preconfer); - event ValidatorAdded(address indexed preconfer, uint256[2] compressedPubKey); - event ValidatorRemoved(address indexed preconfer, bytes32 validatorPubKeyHash); + event ValidatorAdded(bytes32 indexed pubKeyHash, address indexed preconfer); + event ValidatorRemoved(bytes32 indexed pubKeyHash, address indexed preconfer); /// @dev The preconfer is already registered in the registry error PreconferAlreadyRegistered(); /// @dev The preconfer is not registered in the registry error PreconferNotRegistered(); - /// @dev The length of the public keys and signatures arrays do not match - error ArrayLengthMismatch(); /// @dev The signature is invalid error InvalidValidatorSignature(); - /// @dev The address provided as witness of the preconfer that has the last index is incorrect - error LastIndexWitnessIncorrect(); - /// @dev The public key hash is not associated with the preconfer - error InvalidValidatorPubKeyHash(); + /// @dev The signature has expired + error ValidatorSignatureExpired(); + /// @dev The validator is already proposing for a preconfer and cannot be added again without removal + error ValidatorAlreadyActive(); + /// @dev The validator is already removed or waiting to stop proposing for a preconfer + error ValidatorAlreadyInactive(); /// @dev Registers a preconfer by giving them a non-zero registry index function registerPreconfer(ISignatureUtils.SignatureWithSaltAndExpiry calldata operatorSignature) external; /// @dev Deregisters a preconfer from the registry - function deregisterPreconfer(address lastIndexWitness) external; + function deregisterPreconfer() external; + + /// @dev Adds consensus layer validators to the system by assigning preconfers to them + function addValidators(AddValidatorParam[] calldata addValidatorParams) external; + + /// @dev Removes active validators who are proposing for a preconfer + function removeValidators(RemoveValidatorParam[] calldata removeValidatorParams) external; - /// @dev Associates a batch of validators with a preconfer - function addValidators(BLS12381.G1Point[] calldata pubkeys, BLS12381.G2Point[] calldata signatures) external; + /// @dev Returns the message that the validator must sign to add or remove themselves from a preconfer + function getMessageToSign(ValidatorOp validatorOp, uint256 expiry, address preconfer) + external + view + returns (bytes memory); - /// @dev Removes a batch of validators for a preconfer - function removeValidators(bytes32[] calldata validatorPubKeyHashes) external; + /// @dev Returns the index of the next preconfer + function getNextPreconferIndex() external view returns (uint256); /// @dev Returns the index of the preconfer - function preconferToIndex(address preconfer) external view returns (uint256); + function getPreconferIndex(address preconfer) external view returns (uint256); - /// @dev Returns the nonce of the preconfer - function preconferToNonce(address preconfer) external view returns (uint256); + /// @dev Returns the preconfer at the given index + function getPreconferAtIndex(uint256 index) external view returns (address); - /// @dev Returns the timestamp at which the public key hash was added - function preconferToPubKeyHashToTimestamp(address preconfer, bytes32 pubKeyHash) external view returns (uint256); + /// @dev Returns a validator who is proposing for a registered preconfer + function getValidator(bytes32 pubKeyHash) external view returns (Validator memory); }