diff --git a/solidity/contracts/avs/ECDSAStakeRegistry.sol b/solidity/contracts/avs/ECDSAStakeRegistry.sol index 0a4e32a011..6148c3c4e0 100644 --- a/solidity/contracts/avs/ECDSAStakeRegistry.sol +++ b/solidity/contracts/avs/ECDSAStakeRegistry.sol @@ -44,13 +44,14 @@ contract ECDSAStakeRegistry is __ECDSAStakeRegistry_init(_serviceManager, _thresholdWeight, _quorum); } - /// @notice Registers a new operator using a provided signature + /// @notice Registers a new operator using a provided signature and signing key /// @param _operatorSignature Contains the operator's signature, salt, and expiry + /// @param _signingKey The signing key to add to the operator's history function registerOperatorWithSignature( - address _operator, - ISignatureUtils.SignatureWithSaltAndExpiry memory _operatorSignature + ISignatureUtils.SignatureWithSaltAndExpiry memory _operatorSignature, + address _signingKey ) external { - _registerOperatorWithSig(_operator, _operatorSignature); + _registerOperatorWithSig(msg.sender, _operatorSignature, _signingKey); } /// @notice Deregisters an existing operator @@ -58,6 +59,18 @@ contract ECDSAStakeRegistry is _deregisterOperator(msg.sender); } + /** + * @notice Updates the signing key for an operator + * @dev Only callable by the operator themselves + * @param _newSigningKey The new signing key to set for the operator + */ + function updateOperatorSigningKey(address _newSigningKey) external { + if (!_operatorRegistered[msg.sender]) { + revert OperatorNotRegistered(); + } + _updateOperatorSigningKey(msg.sender, _newSigningKey); + } + /** * @notice Updates the StakeRegistry's view of one or more operators' stakes adding a new entry in their history of stake checkpoints, * @dev Queries stakes from the Eigenlayer core DelegationManager contract @@ -107,18 +120,18 @@ contract ECDSAStakeRegistry is /// @notice Verifies if the provided signature data is valid for the given data hash. /// @param _dataHash The hash of the data that was signed. - /// @param _signatureData Encoded signature data consisting of an array of signers, an array of signatures, and a reference block number. + /// @param _signatureData Encoded signature data consisting of an array of operators, an array of signatures, and a reference block number. /// @return The function selector that indicates the signature is valid according to ERC1271 standard. function isValidSignature( bytes32 _dataHash, bytes memory _signatureData ) external view returns (bytes4) { ( - address[] memory signers, + address[] memory operators, bytes[] memory signatures, uint32 referenceBlock ) = abi.decode(_signatureData, (address[], bytes[], uint32)); - _checkSignatures(_dataHash, signers, signatures, referenceBlock); + _checkSignatures(_dataHash, operators, signatures, referenceBlock); return IERC1271Upgradeable.isValidSignature.selector; } @@ -128,6 +141,37 @@ contract ECDSAStakeRegistry is return _quorum; } + /** + * @notice Retrieves the latest signing key for a given operator. + * @param _operator The address of the operator. + * @return The latest signing key of the operator. + */ + function getLastestOperatorSigningKey( + address _operator + ) external view returns (address) { + return address(uint160(_operatorSigningKeyHistory[_operator].latest())); + } + + /** + * @notice Retrieves the latest signing key for a given operator at a specific block number. + * @param _operator The address of the operator. + * @param _blockNumber The block number to get the operator's signing key. + * @return The signing key of the operator at the given block. + */ + function getOperatorSigningKeyAtBlock( + address _operator, + uint256 _blockNumber + ) external view returns (address) { + return + address( + uint160( + _operatorSigningKeyHistory[_operator].getAtBlock( + _blockNumber + ) + ) + ); + } + /// @notice Retrieves the last recorded weight for a given operator. /// @param _operator The address of the operator. /// @return uint256 - The latest weight of the operator. @@ -313,9 +357,11 @@ contract ECDSAStakeRegistry is /// @dev registers an operator through a provided signature /// @param _operatorSignature Contains the operator's signature, salt, and expiry + /// @param _signingKey The signing key to add to the operator's history function _registerOperatorWithSig( address _operator, - ISignatureUtils.SignatureWithSaltAndExpiry memory _operatorSignature + ISignatureUtils.SignatureWithSaltAndExpiry memory _operatorSignature, + address _signingKey ) internal virtual { if (_operatorRegistered[_operator]) { revert OperatorAlreadyRegistered(); @@ -324,6 +370,7 @@ contract ECDSAStakeRegistry is _operatorRegistered[_operator] = true; int256 delta = _updateOperatorWeight(_operator); _updateTotalWeight(delta); + _updateOperatorSigningKey(_operator, _signingKey); IServiceManager(_serviceManager).registerOperatorToAVS( _operator, _operatorSignature @@ -331,6 +378,28 @@ contract ECDSAStakeRegistry is emit OperatorRegistered(_operator, _serviceManager); } + /// @dev Internal function to update an operator's signing key + /// @param _operator The address of the operator to update the signing key for + /// @param _newSigningKey The new signing key to set for the operator + function _updateOperatorSigningKey( + address _operator, + address _newSigningKey + ) internal { + address oldSigningKey = address( + uint160(_operatorSigningKeyHistory[_operator].latest()) + ); + if (_newSigningKey == oldSigningKey) { + return; + } + _operatorSigningKeyHistory[_operator].push(uint160(_newSigningKey)); + emit SigningKeyUpdate( + _operator, + block.number, + _newSigningKey, + oldSigningKey + ); + } + /// @notice Updates the weight of an operator and returns the previous and current weights. /// @param _operator The address of the operator to update the weight of. function _updateOperatorWeight( @@ -401,30 +470,33 @@ contract ECDSAStakeRegistry is /** * @notice Common logic to verify a batch of ECDSA signatures against a hash, using either last stake weight or at a specific block. * @param _dataHash The hash of the data the signers endorsed. - * @param _signers A collection of addresses that endorsed the data hash. + * @param _operators A collection of addresses that endorsed the data hash. * @param _signatures A collection of signatures matching the signers. * @param _referenceBlock The block number for evaluating stake weight; use max uint32 for latest weight. */ function _checkSignatures( bytes32 _dataHash, - address[] memory _signers, + address[] memory _operators, bytes[] memory _signatures, uint32 _referenceBlock ) internal view { - uint256 signersLength = _signers.length; - address lastSigner; + uint256 signersLength = _operators.length; + address currentOperator; + address lastOperator; + address signer; uint256 signedWeight; _validateSignaturesLength(signersLength, _signatures.length); for (uint256 i; i < signersLength; i++) { - address currentSigner = _signers[i]; + currentOperator = _operators[i]; + signer = _getOperatorSigningKey(currentOperator, _referenceBlock); - _validateSortedSigners(lastSigner, currentSigner); - _validateSignature(currentSigner, _dataHash, _signatures[i]); + _validateSortedSigners(lastOperator, currentOperator); + _validateSignature(signer, _dataHash, _signatures[i]); - lastSigner = currentSigner; + lastOperator = currentOperator; uint256 operatorWeight = _getOperatorWeight( - currentSigner, + currentOperator, _referenceBlock ); signedWeight += operatorWeight; @@ -474,6 +546,27 @@ contract ECDSAStakeRegistry is } } + /// @notice Retrieves the operator weight for a signer, either at the last checkpoint or a specified block. + /// @param _operator The operator to query their signing key history for + /// @param _referenceBlock The block number to query the operator's weight at, or the maximum uint32 value for the last checkpoint. + /// @return The weight of the operator. + function _getOperatorSigningKey( + address _operator, + uint32 _referenceBlock + ) internal view returns (address) { + if (_referenceBlock >= block.number) { + revert InvalidReferenceBlock(); + } + return + address( + uint160( + _operatorSigningKeyHistory[_operator].getAtBlock( + _referenceBlock + ) + ) + ); + } + /// @notice Retrieves the operator weight for a signer, either at the last checkpoint or a specified block. /// @param _signer The address of the signer whose weight is returned. /// @param _referenceBlock The block number to query the operator's weight at, or the maximum uint32 value for the last checkpoint. @@ -482,11 +575,10 @@ contract ECDSAStakeRegistry is address _signer, uint32 _referenceBlock ) internal view returns (uint256) { - if (_referenceBlock == type(uint32).max) { - return _operatorWeightHistory[_signer].latest(); - } else { - return _operatorWeightHistory[_signer].getAtBlock(_referenceBlock); + if (_referenceBlock >= block.number) { + revert InvalidReferenceBlock(); } + return _operatorWeightHistory[_signer].getAtBlock(_referenceBlock); } /// @notice Retrieve the total stake weight at a specific block or the latest if not specified. @@ -496,11 +588,10 @@ contract ECDSAStakeRegistry is function _getTotalWeight( uint32 _referenceBlock ) internal view returns (uint256) { - if (_referenceBlock == type(uint32).max) { - return _totalWeightHistory.latest(); - } else { - return _totalWeightHistory.getAtBlock(_referenceBlock); + if (_referenceBlock >= block.number) { + revert InvalidReferenceBlock(); } + return _totalWeightHistory.getAtBlock(_referenceBlock); } /// @notice Retrieves the threshold stake for a given reference block. @@ -510,11 +601,10 @@ contract ECDSAStakeRegistry is function _getThresholdStake( uint32 _referenceBlock ) internal view returns (uint256) { - if (_referenceBlock == type(uint32).max) { - return _thresholdWeightHistory.latest(); - } else { - return _thresholdWeightHistory.getAtBlock(_referenceBlock); + if (_referenceBlock >= block.number) { + revert InvalidReferenceBlock(); } + return _thresholdWeightHistory.getAtBlock(_referenceBlock); } /// @notice Validates that the cumulative stake of signed messages meets or exceeds the required threshold. diff --git a/solidity/contracts/avs/ECDSAStakeRegistryStorage.sol b/solidity/contracts/avs/ECDSAStakeRegistryStorage.sol index 9e59fd8f63..28a584ca5e 100644 --- a/solidity/contracts/avs/ECDSAStakeRegistryStorage.sol +++ b/solidity/contracts/avs/ECDSAStakeRegistryStorage.sol @@ -30,6 +30,10 @@ abstract contract ECDSAStakeRegistryStorage is /// @notice Defines the duration after which the stake's weight expires. uint256 internal _stakeExpiry; + /// @notice Maps an operator to their signing key history using checkpoints + mapping(address => CheckpointsUpgradeable.History) + internal _operatorSigningKeyHistory; + /// @notice Tracks the total stake history over time using checkpoints CheckpointsUpgradeable.History internal _totalWeightHistory; @@ -51,5 +55,5 @@ abstract contract ECDSAStakeRegistryStorage is // slither-disable-next-line shadowing-state /// @dev Reserves storage slots for future upgrades // solhint-disable-next-line - uint256[42] private __gap; + uint256[40] private __gap; } diff --git a/solidity/contracts/interfaces/avs/vendored/IECDSAStakeRegistryEventsAndErrors.sol b/solidity/contracts/interfaces/avs/vendored/IECDSAStakeRegistryEventsAndErrors.sol index 021d34db40..07f6323da0 100644 --- a/solidity/contracts/interfaces/avs/vendored/IECDSAStakeRegistryEventsAndErrors.sol +++ b/solidity/contracts/interfaces/avs/vendored/IECDSAStakeRegistryEventsAndErrors.sol @@ -12,8 +12,6 @@ struct Quorum { StrategyParams[] strategies; // An array of strategy parameters to define the quorum } -/// part of mock interfaces for vendoring necessary Eigenlayer contracts for the hyperlane AVS -/// @author Layr Labs, Inc. interface IECDSAStakeRegistryEventsAndErrors { /// @notice Emitted when the system registers an operator /// @param _operator The address of the registered operator @@ -61,7 +59,19 @@ interface IECDSAStakeRegistryEventsAndErrors { /// @notice Emits when setting a new threshold weight. event ThresholdWeightUpdated(uint256 _thresholdWeight); + /// @notice Emitted when an operator's signing key is updated + /// @param operator The address of the operator whose signing key was updated + /// @param updateBlock The block number at which the signing key was updated + /// @param newSigningKey The operator's signing key after the update + /// @param oldSigningKey The operator's signing key before the update + event SigningKeyUpdate( + address indexed operator, + uint256 indexed updateBlock, + address indexed newSigningKey, + address oldSigningKey + ); /// @notice Indicates when the lengths of the signers array and signatures array do not match. + error LengthMismatch(); /// @notice Indicates encountering an invalid length for the signers or signatures array. @@ -76,6 +86,9 @@ interface IECDSAStakeRegistryEventsAndErrors { /// @notice Thrown when missing operators in an update error MustUpdateAllOperators(); + /// @notice Reference blocks must be for blocks that have already been confirmed + error InvalidReferenceBlock(); + /// @notice Indicates operator weights were out of sync and the signed weight exceed the total error InvalidSignedWeight(); diff --git a/solidity/test/avs/HyperlaneServiceManager.t.sol b/solidity/test/avs/HyperlaneServiceManager.t.sol index 4ea9ce8f1e..80d49f9749 100644 --- a/solidity/test/avs/HyperlaneServiceManager.t.sol +++ b/solidity/test/avs/HyperlaneServiceManager.t.sol @@ -29,6 +29,7 @@ contract HyperlaneServiceManagerTest is EigenlayerBase { // Operator info uint256 operatorPrivateKey = 0xdeadbeef; address operator; + address avsSigningKey = address(0xc0ffee); bytes32 emptySalt; uint256 maxExpiry = type(uint256).max; @@ -97,9 +98,11 @@ contract HyperlaneServiceManagerTest is EigenlayerBase { emptySalt, maxExpiry ); + + vm.prank(operator); _ecdsaStakeRegistry.registerOperatorWithSignature( - operator, - operatorSignature + operatorSignature, + avsSigningKey ); // assert @@ -122,12 +125,13 @@ contract HyperlaneServiceManagerTest is EigenlayerBase { maxExpiry ); + vm.prank(operator); vm.expectRevert( "EIP1271SignatureUtils.checkSignature_EIP1271: signature not from signer" ); _ecdsaStakeRegistry.registerOperatorWithSignature( - operator, - operatorSignature + operatorSignature, + avsSigningKey ); // assert @@ -409,9 +413,10 @@ contract HyperlaneServiceManagerTest is EigenlayerBase { maxExpiry ); + vm.prank(operator); _ecdsaStakeRegistry.registerOperatorWithSignature( - operator, - operatorSignature + operatorSignature, + avsSigningKey ); }