feat: support attesting signing key for Hyperlane AVS (#3847)

### Description


- Adding support for attesting signing key and key rotation from here
https://github.com/Layr-Labs/eigenlayer-middleware/pull/252

### Drive-by changes

None

### Related issues

None

### Backward compatibility

Yes

### Testing

Tested by Eigenlayer here:
https://github.com/Layr-Labs/eigenlayer-middleware/blob/dev/test/unit/ECDSAStakeRegistryUnit.t.sol
pull/3854/head
Kunal Arora 6 months ago committed by GitHub
parent 0cf692e731
commit d0ce9081dd
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
  1. 148
      solidity/contracts/avs/ECDSAStakeRegistry.sol
  2. 6
      solidity/contracts/avs/ECDSAStakeRegistryStorage.sol
  3. 17
      solidity/contracts/interfaces/avs/vendored/IECDSAStakeRegistryEventsAndErrors.sol
  4. 17
      solidity/test/avs/HyperlaneServiceManager.t.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.

@ -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;
}

@ -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();

@ -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
);
}

Loading…
Cancel
Save