Skip to content

Commit

Permalink
feat: support attesting signing key for Hyperlane AVS (#3847)
Browse files Browse the repository at this point in the history
### Description


- Adding support for attesting signing key and key rotation from here
Layr-Labs/eigenlayer-middleware#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
  • Loading branch information
aroralanuk authored May 27, 2024
1 parent 0cf692e commit d0ce908
Show file tree
Hide file tree
Showing 4 changed files with 150 additions and 38 deletions.
148 changes: 119 additions & 29 deletions solidity/contracts/avs/ECDSAStakeRegistry.sol
Original file line number Diff line number Diff line change
Expand Up @@ -44,20 +44,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 @@ -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;
}

Expand All @@ -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.
Expand Down Expand Up @@ -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();
Expand All @@ -324,13 +370,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 Down Expand Up @@ -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;
Expand Down Expand Up @@ -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.
Expand All @@ -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.
Expand All @@ -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.
Expand All @@ -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.
Expand Down
6 changes: 5 additions & 1 deletion solidity/contracts/avs/ECDSAStakeRegistryStorage.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand All @@ -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;
}
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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.
Expand All @@ -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();

Expand Down
17 changes: 11 additions & 6 deletions solidity/test/avs/HyperlaneServiceManager.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -97,9 +98,11 @@ contract HyperlaneServiceManagerTest is EigenlayerBase {
emptySalt,
maxExpiry
);

vm.prank(operator);
_ecdsaStakeRegistry.registerOperatorWithSignature(
operator,
operatorSignature
operatorSignature,
avsSigningKey
);

// assert
Expand All @@ -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
Expand Down Expand Up @@ -409,9 +413,10 @@ contract HyperlaneServiceManagerTest is EigenlayerBase {
maxExpiry
);

vm.prank(operator);
_ecdsaStakeRegistry.registerOperatorWithSignature(
operator,
operatorSignature
operatorSignature,
avsSigningKey
);
}

Expand Down

0 comments on commit d0ce908

Please sign in to comment.