Skip to content

Commit

Permalink
feat: ecdsa key rotation (#252)
Browse files Browse the repository at this point in the history
* feat: add operator key rotation

* test: update existing tests to account for signing key

* fix: frontrunning with different signing key

* test: verify the checkpoint logic for the signing keys

* feat: improve event for signing key update

* chore: clean up test function names and order functions

* fix: storage layout gap

* feat: prevent signing at current block

* test: add two more test cases for RBN

* fix: typo in function signature

* chore: remove unnecessary test contract

* fix: typo for invalid quorum

* fix: invalidQuorum -> validQuorum for NotOwner test
  • Loading branch information
stevennevins authored May 23, 2024
1 parent 0351419 commit afbcba8
Show file tree
Hide file tree
Showing 7 changed files with 748 additions and 181 deletions.
27 changes: 24 additions & 3 deletions src/interfaces/IECDSAStakeRegistryEventsAndErrors.sol
Original file line number Diff line number Diff line change
Expand Up @@ -36,13 +36,20 @@ interface ECDSAStakeRegistryEventsAndErrors {
/// @notice Emitted when the weight required to be an operator changes
/// @param oldMinimumWeight The previous weight
/// @param newMinimumWeight The updated weight
event UpdateMinimumWeight(uint256 oldMinimumWeight, uint256 newMinimumWeight);
event UpdateMinimumWeight(
uint256 oldMinimumWeight,
uint256 newMinimumWeight
);

/// @notice Emitted when the system updates an operator's weight
/// @param _operator The address of the operator updated
/// @param oldWeight The operator's weight before the update
/// @param newWeight The operator's weight after the update
event OperatorWeightUpdated(address indexed _operator, uint256 oldWeight, uint256 newWeight);
event OperatorWeightUpdated(
address indexed _operator,
uint256 oldWeight,
uint256 newWeight
);

/// @notice Emitted when the system updates the total weight
/// @param oldTotalWeight The total weight before the update
Expand All @@ -52,6 +59,17 @@ interface ECDSAStakeRegistryEventsAndErrors {
/// @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();

Expand All @@ -64,9 +82,12 @@ interface ECDSAStakeRegistryEventsAndErrors {
/// @notice Thrown when the threshold update is greater than BPS
error InvalidThreshold();

/// @notice Thrown when missing operators in an update
/// @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();

Expand Down
150 changes: 120 additions & 30 deletions src/unaudited/ECDSAStakeRegistry.sol
Original file line number Diff line number Diff line change
Expand Up @@ -43,20 +43,33 @@ 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
function deregisterOperator() external {
_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
Expand Down Expand Up @@ -106,18 +119,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;
}

Expand All @@ -127,6 +140,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.
Expand Down Expand Up @@ -312,9 +356,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();
Expand All @@ -323,13 +369,36 @@ contract ECDSAStakeRegistry is
_operatorRegistered[_operator] = true;
int256 delta = _updateOperatorWeight(_operator);
_updateTotalWeight(delta);
_updateOperatorSigningKey(_operator, _signingKey);
IServiceManager(_serviceManager).registerOperatorToAVS(
_operator,
_operatorSignature
);
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(
Expand All @@ -339,7 +408,7 @@ contract ECDSAStakeRegistry is
uint256 newWeight;
uint256 oldWeight = _operatorWeightHistory[_operator].latest();
if (!_operatorRegistered[_operator]) {
delta -= int(oldWeight);
delta -= int256(oldWeight);
if (delta == 0) {
return delta;
}
Expand Down Expand Up @@ -400,30 +469,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;
Expand Down Expand Up @@ -473,6 +545,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.
Expand All @@ -481,11 +574,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.
Expand All @@ -495,11 +587,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.
Expand All @@ -509,11 +600,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.
Expand Down
13 changes: 10 additions & 3 deletions src/unaudited/ECDSAStakeRegistryStorage.sol
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,9 @@ import {IDelegationManager} from "eigenlayer-contracts/src/contracts/interfaces/
import {CheckpointsUpgradeable} from "@openzeppelin-upgrades/contracts/utils/CheckpointsUpgradeable.sol";
import {ECDSAStakeRegistryEventsAndErrors, Quorum, StrategyParams} from "../interfaces/IECDSAStakeRegistryEventsAndErrors.sol";

abstract contract ECDSAStakeRegistryStorage is ECDSAStakeRegistryEventsAndErrors {
abstract contract ECDSAStakeRegistryStorage is
ECDSAStakeRegistryEventsAndErrors
{
/// @notice Manages staking delegations through the DelegationManager interface
IDelegationManager internal immutable DELEGATION_MANAGER;

Expand All @@ -27,14 +29,19 @@ abstract contract ECDSAStakeRegistryStorage is ECDSAStakeRegistryEventsAndErrors
/// @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;

/// @notice Tracks the threshold bps history using checkpoints
CheckpointsUpgradeable.History internal _thresholdWeightHistory;

/// @notice Maps operator addresses to their respective stake histories using checkpoints
mapping(address => CheckpointsUpgradeable.History) internal _operatorWeightHistory;
mapping(address => CheckpointsUpgradeable.History)
internal _operatorWeightHistory;

/// @notice Maps an operator to their registration status
mapping(address => bool) internal _operatorRegistered;
Expand All @@ -47,5 +54,5 @@ abstract contract ECDSAStakeRegistryStorage is ECDSAStakeRegistryEventsAndErrors
// slither-disable-next-line shadowing-state
/// @dev Reserves storage slots for future upgrades
// solhint-disable-next-line
uint256[42] private __gap;
uint256[39] private __gap;
}
Loading

0 comments on commit afbcba8

Please sign in to comment.