From 2eb591d87f3efa5286c8443c448f953d31929a57 Mon Sep 17 00:00:00 2001 From: ChaoticWalrus <93558947+ChaoticWalrus@users.noreply.github.com> Date: Wed, 20 Dec 2023 13:46:27 -0800 Subject: [PATCH 01/48] feat: add tree diagram for RegistryManager --- test/tree/RegistryManagerUnit.tree | 158 +++++++++++++++++++++++++++++ 1 file changed, 158 insertions(+) create mode 100644 test/tree/RegistryManagerUnit.tree diff --git a/test/tree/RegistryManagerUnit.tree b/test/tree/RegistryManagerUnit.tree new file mode 100644 index 00000000..10238e2e --- /dev/null +++ b/test/tree/RegistryManagerUnit.tree @@ -0,0 +1,158 @@ +├── RegistryCoordinatorUnit.t.sol (*** denotes that integration tests are needed to validate path) +│ +├── initialize +│ ├── given that initialize has been called previously +│ │ └── it should revert +│ ├── given that the variable-length array input lengths don't all match +│ │ └── it should revert +│ └── it should set the storage variables correctly (owner, pauserRegistry, paused status, churnApprover, ejector, quorum params) +│ +├── registerOperator() +│ ├── given that operator registration is paused +│ │ └── it should revert +│ ├── given that the current number of operators for any of the quorums to register for already meets or exceeds the quorum's operator cap +│ │ └── it should revert +│ ├── given the operator has *not* previously registered a pubkey +│ │ └── ***it should attempt to register the provided pubkey with the BLSApkRegistry +│ ├── given that *has* previously registered a pubkey +│ │ └── ***it should fetch the operator's pubkey from the BLSApkRegistry +│ └── ***it should attempt to register the operator, via the `_registerOperator` function (see below) +│ +├── registerOperatorWithChurn() +│ ├── given that operator registration is paused +│ │ └── it should revert +│ ├── given that the provided operatorKickParams are not the same length as the provided quorumNumbers +│ │ └── it should revert +│ ├── given that the churnApprover did not sign the operatorKickParams and the caller's operatorID +│ │ └── it should revert +│ ├── given that the current number of operators for any of the quorums to register for already meets or exceeds the quorum's operator cap +│ │ └── for each quorum, it should check that the new and to-be-kicked operators' stakes meet the configured requirements +│ │ ├─ given that the configured requirements are not met +│ │ │ └── it should revert +│ │ └── it should deregister the to-be-kicked operator +│ ├── given the operator has *not* previously registered a pubkey +│ │ └── ***it should attempt to register the provided pubkey with the BLSApkRegistry +│ ├── given that *has* previously registered a pubkey +│ │ └── ***it should fetch the operator's pubkey from the BLSApkRegistry +│ └── ***it should attempt to register the operator, via the `_registerOperator` function (see below) +│ +├── deregisterOperator() +│ ├── given that operator deregistration is paused +│ │ └── it should revert +│ └── ***it should attempt to deregister the caller, via the `_deregisterOperator` function (see below) +│ +├── ejectOperator() +│ ├── given that caller is not the ejector +│ │ └── it should revert +│ └── ***it should attempt to deregister the operator, via the `_deregisterOperator` function (see below) +│ +├── updateOperators() +│ ├── given that operator updates are paused +│ │ └── it should revert +│ └── ***for each operator, it should attempt to update the operator's stake information for all quorums +│ that the operator is currently registered for, via the `_updateOperator` function (see below) +│ +├── updateOperatorsForQuorum() +│ ├── given that operator updates are paused +│ │ └── it should revert +│ ├── given that any of the provided quorum numbers is for a non-existant quorum +│ │ └── it should revert +│ ├── given that the length of the provided array of operator lists does not match the length of the provided quorum numbers +│ │ └── it should revert +│ ├── given that the length of any provided list of operators does not match the current number of operators in that quorum +│ │ └── it should revert +│ ├── given that of any of the provided lists of operators contains an operator who is not currently registered for the quorum +│ │ └── it should revert +│ ├── given that of any of the provided lists of operators contains a duplicate +│ │ └── it should revert +│ ├── given that of any of the provided lists of operators is not in ascending address order +│ │ └── it should revert +│ └── ***for each operator, it should attempt to update the operator's stake information, via the `_updateOperator` function (see below) +│ for each quorum, it should increase the quorumUpdateBlockNumber to the current block number +│ +├── updateSocket() +│ ├── given that the caller is not a registered operator +│ │ └── it should revert +│ └── it should emit an OperatorSocketUpdate event +│ +├── createQuorum() +│ ├── given that the caller is not a registered operator +│ │ └── it should revert +│ ├── given that the current quorum count meets or exceeds the MAX_QUORUM_COUNT +│ │ └── it should revert +│ └── it should set the OperatorSetParams for the new quorum +│ *** it should initialize the new quorum on the StakeRegistry, IndexRegistry, and BLSApkRegistry +│ +├── setOperatorSetParams() +│ ├── given that the caller is not the owner +│ │ └── it should revert +│ ├── given that the quorum does not already exist +│ │ └── it should revert +│ └── it should set the OperatorSetParams for the quorum, and emit an event +│ +├── setChurnApprover() +│ ├── given that the caller is not the owner +│ │ └── it should revert +│ └── it should update the churnApprover address and emit an event +│ +├── setEjector() +│ ├── given that the caller is not the owner +│ │ └── it should revert +│ └── it should update the ejector address and emit an event +│ +│ +├── getQuorumBitmapIndicesAtBlockNumber() +│ ├── given that any of the operatorIDs was not registered at the block number +│ │ └── it should revert +│ └── it should return the proper index of the entry in each operatorID's quorum bitmap history +│ +├── getQuorumBitmapAtBlockNumberByIndex() +│ ├── given that the operatorID was not registered at the block number +│ │ └── it should revert +│ ├── given that the index specifies a bitmap that became invalid prior to the block number +│ │ └── it should revert +│ ├── given that the index specifies a bitmap that became valid after the block number +│ │ └── it should revert +│ └── it should return the quorum bitmap of the operatorID at the block number +│ +├── _registerOperator() (internal function -- see mentions above) +│ ├── given that no quorums are being registered for +│ │ └── it should revert +│ ├── given that any quorums being registered for do not (yet) exist +│ │ └── it should revert +│ ├── given that the operator is already registered for any of quorums being registered for +│ │ └── it should revert +│ ├── given that the operator is not already registered at the AVS level +│ │ └── it should mark the operator as registered +│ │ *** and call the serviceManager to register the operator on the EigenLayer level +│ └── it should update the operator's bitmap history, via the `_updateOperatorBitmap` function (see below), +│ with a new entry that includes the added quorums +│ *** and call the BLSApkRegistry, StakeRegistry, and IndexRegistry to complete operator registration +│ +├── _deregisterOperator() (internal function -- see mentions above) +│ ├── given that the operator is not registered for the AVS +│ │ └── it should revert +│ ├── given that no quorums are being removed +│ │ └── it should revert +│ ├── given that any quorums being removed do not (yet) exist +│ │ └── it should revert +│ ├── given that the operator is *not* currently registered for any of quorums being removed +│ │ └── it should revert +│ ├── given that the operator is being removed from all quorums that they were registered for +│ │ └── it should mark the operator as no longer registered +│ │ *** and call the serviceManager to deregister the operator on the EigenLayer level +│ └── it should update the operator's bitmap history, via the `_updateOperatorBitmap` function (see below), +│ with a new entry that excludes the removed quorums +│ *** and call the BLSApkRegistry, StakeRegistry, and IndexRegistry to complete operator deregistration +│ +├── _updateOperator() (internal function -- see mentions above) +│ ├── given that the operator is not actively registered +│ │ └── it should do nothing ("no-op") +│ └── *** it should call the StakeRegistry to make it perform a stake update on the operator +│ *** and remove the operator from any quorums where the StakeRegistry returns that the operator +│ no longer meets the requirements, via the `_deregisterOperator` function (see above) +│ +└── _updateOperatorBitmap() (internal function -- see mentions above) + ├── given that the operator has no previous entries in their bitmap history OR given that the latest entry in the operator's bitmap history occurred in the current block + │ └── it should push a new entry with an "unset" (i.e. zero) nextUpdateBlock, and an updateBlockNumber of the current block + └── otherwise, it should only update the bitmap in the latest entry \ No newline at end of file From 0a6d3cd3fbf198df29d33a7639da8e4b9cc3f8cf Mon Sep 17 00:00:00 2001 From: ChaoticWalrus <93558947+ChaoticWalrus@users.noreply.github.com> Date: Thu, 21 Dec 2023 15:28:04 -0800 Subject: [PATCH 02/48] chore: reorder and rename tests --- test/unit/RegistryCoordinatorUnit.t.sol | 415 ++++++++++++------------ 1 file changed, 213 insertions(+), 202 deletions(-) diff --git a/test/unit/RegistryCoordinatorUnit.t.sol b/test/unit/RegistryCoordinatorUnit.t.sol index 0ab7584b..c5a43311 100644 --- a/test/unit/RegistryCoordinatorUnit.t.sol +++ b/test/unit/RegistryCoordinatorUnit.t.sol @@ -3,7 +3,7 @@ pragma solidity =0.8.12; import "../utils/MockAVSDeployer.sol"; -contract RegistryCoordinatorUnit is MockAVSDeployer { +contract RegistryCoordinatorUnitTests is MockAVSDeployer { using BN254 for BN254.G1Point; uint8 internal constant PAUSED_REGISTER_OPERATOR = 0; @@ -43,7 +43,52 @@ contract RegistryCoordinatorUnit is MockAVSDeployer { _deployMockEigenLayerAndAVS(); } - function testCorrectConstruction() public { + function _testRegisterOperatorWithChurn_SetUp(uint256 pseudoRandomNumber, bytes memory quorumNumbers, uint96 operatorToKickStake) internal returns(address operatorToRegister, BN254.G1Point memory operatorToRegisterPubKey, IRegistryCoordinator.OperatorKickParam[] memory operatorKickParams) { + uint32 kickRegistrationBlockNumber = 100; + + uint256 quorumBitmap = BitmapUtils.orderedBytesArrayToBitmap(quorumNumbers); + + cheats.roll(kickRegistrationBlockNumber); + + for (uint i = 0; i < defaultMaxOperatorCount - 1; i++) { + BN254.G1Point memory pubKey = BN254.hashToG1(keccak256(abi.encodePacked(pseudoRandomNumber, i))); + address operator = _incrementAddress(defaultOperator, i); + + _registerOperatorWithCoordinator(operator, quorumBitmap, pubKey); + } + + operatorToRegister = _incrementAddress(defaultOperator, defaultMaxOperatorCount); + operatorToRegisterPubKey = BN254.hashToG1(keccak256(abi.encodePacked(pseudoRandomNumber, defaultMaxOperatorCount))); + bytes32 operatorToRegisterId = BN254.hashG1Point(operatorToRegisterPubKey); + bytes32 operatorToKickId; + address operatorToKick; + + // register last operator before kick + operatorKickParams = new IRegistryCoordinator.OperatorKickParam[](1); + { + BN254.G1Point memory pubKey = BN254.hashToG1(keccak256(abi.encodePacked(pseudoRandomNumber, defaultMaxOperatorCount - 1))); + operatorToKickId = BN254.hashG1Point(pubKey); + operatorToKick = _incrementAddress(defaultOperator, defaultMaxOperatorCount - 1); + + // register last operator with much more than the kickBIPsOfTotalStake stake + _registerOperatorWithCoordinator(operatorToKick, quorumBitmap, pubKey, operatorToKickStake); + + bytes32[] memory operatorIdsToSwap = new bytes32[](1); + // operatorIdsToSwap[0] = operatorToRegisterId + operatorIdsToSwap[0] = operatorToRegisterId; + + operatorKickParams[0] = IRegistryCoordinator.OperatorKickParam({ + quorumNumber: uint8(quorumNumbers[0]), + operator: operatorToKick + }); + } + + blsApkRegistry.setBLSPublicKey(operatorToRegister, operatorToRegisterPubKey); + } +} + +contract RegistryCoordinatorUnitTests_Initialization_Setters is RegistryCoordinatorUnitTests { + function test_initialization() public { assertEq(address(registryCoordinator.stakeRegistry()), address(stakeRegistry)); assertEq(address(registryCoordinator.blsApkRegistry()), address(blsApkRegistry)); assertEq(address(registryCoordinator.indexRegistry()), address(indexRegistry)); @@ -70,27 +115,20 @@ contract RegistryCoordinatorUnit is MockAVSDeployer { ); } - function testSetOperatorSetParams_NotOwner_Reverts() public { - cheats.expectRevert("Ownable: caller is not the owner"); - cheats.prank(defaultOperator); - registryCoordinator.setOperatorSetParams(0, operatorSetParams[0]); - } - - function testSetOperatorSetParams_Valid() public { + function test_setOperatorSetParams() public { cheats.prank(registryCoordinatorOwner); cheats.expectEmit(true, true, true, true, address(registryCoordinator)); emit OperatorSetParamsUpdated(0, operatorSetParams[1]); registryCoordinator.setOperatorSetParams(0, operatorSetParams[1]); } - function testSetChurnApprover_NotOwner_Reverts() public { - address newChurnApprover = address(uint160(uint256(keccak256("newChurnApprover")))); + function test_setOperatorSetParams_revert_notOwner() public { cheats.expectRevert("Ownable: caller is not the owner"); cheats.prank(defaultOperator); - registryCoordinator.setChurnApprover(newChurnApprover); + registryCoordinator.setOperatorSetParams(0, operatorSetParams[0]); } - function testSetChurnApprover_Valid() public { + function test_setChurnApprover() public { address newChurnApprover = address(uint160(uint256(keccak256("newChurnApprover")))); cheats.prank(registryCoordinatorOwner); cheats.expectEmit(true, true, true, true, address(registryCoordinator)); @@ -98,14 +136,14 @@ contract RegistryCoordinatorUnit is MockAVSDeployer { registryCoordinator.setChurnApprover(newChurnApprover); } - function testSetEjector_NotOwner_Reverts() public { - address newEjector = address(uint160(uint256(keccak256("newEjector")))); + function test_setChurnApprover_revert_notOwner() public { + address newChurnApprover = address(uint160(uint256(keccak256("newChurnApprover")))); cheats.expectRevert("Ownable: caller is not the owner"); cheats.prank(defaultOperator); - registryCoordinator.setEjector(newEjector); + registryCoordinator.setChurnApprover(newChurnApprover); } - function testSetEjector_Valid() public { + function testSetEjector() public { address newEjector = address(uint160(uint256(keccak256("newEjector")))); cheats.prank(registryCoordinatorOwner); cheats.expectEmit(true, true, true, true, address(registryCoordinator)); @@ -114,7 +152,37 @@ contract RegistryCoordinatorUnit is MockAVSDeployer { assertEq(registryCoordinator.ejector(), newEjector); } - function testRegisterOperatorWithCoordinator_WhenPaused_Reverts() public { + function testSetEjector_revert_notOwner() public { + address newEjector = address(uint160(uint256(keccak256("newEjector")))); + cheats.expectRevert("Ownable: caller is not the owner"); + cheats.prank(defaultOperator); + registryCoordinator.setEjector(newEjector); + } + + function test_updateSocket() public { + bytes memory quorumNumbers = new bytes(1); + quorumNumbers[0] = bytes1(defaultQuorumNumber); + + uint256 quorumBitmap = BitmapUtils.orderedBytesArrayToBitmap(quorumNumbers); + _registerOperatorWithCoordinator(defaultOperator, quorumBitmap, defaultPubKey); + + cheats.prank(defaultOperator); + cheats.expectEmit(true, true, true, true, address(registryCoordinator)); + emit OperatorSocketUpdate(defaultOperatorId, "localhost:32004"); + registryCoordinator.updateSocket("localhost:32004"); + + } + + function test_updateSocket_revert_notRegistered() public { + cheats.prank(defaultOperator); + cheats.expectRevert("RegistryCoordinator.updateSocket: operator is not registered"); + registryCoordinator.updateSocket("localhost:32004"); + } +} + +contract RegistryCoordinatorUnitTests_RegisterOperator is RegistryCoordinatorUnitTests { + + function testRegisterOperator_revert_paused() public { bytes memory emptyQuorumNumbers = new bytes(0); ISignatureUtils.SignatureWithSaltAndExpiry memory emptySig; @@ -127,7 +195,7 @@ contract RegistryCoordinatorUnit is MockAVSDeployer { registryCoordinator.registerOperator(emptyQuorumNumbers, defaultSocket, pubkeyRegistrationParams, emptySig); } - function testRegisterOperatorWithCoordinator_EmptyQuorumNumbers_Reverts() public { + function testRegisterOperator_revert_emptyQuorumNumbers() public { bytes memory emptyQuorumNumbers = new bytes(0); ISignatureUtils.SignatureWithSaltAndExpiry memory emptySig; @@ -136,7 +204,7 @@ contract RegistryCoordinatorUnit is MockAVSDeployer { registryCoordinator.registerOperator(emptyQuorumNumbers, defaultSocket, pubkeyRegistrationParams, emptySig); } - function testRegisterOperatorWithCoordinator_QuorumNumbersTooLarge_Reverts() public { + function testRegisterOperator_revert_invalidQUorum() public { bytes memory quorumNumbersTooLarge = new bytes(1); ISignatureUtils.SignatureWithSaltAndExpiry memory emptySig; @@ -147,7 +215,7 @@ contract RegistryCoordinatorUnit is MockAVSDeployer { registryCoordinator.registerOperator(quorumNumbersTooLarge, defaultSocket, pubkeyRegistrationParams, emptySig); } - function testRegisterOperatorWithCoordinator_QuorumNotCreated_Reverts() public { + function test_registerOperator_revert_NonexistentQuorum() public { _deployMockEigenLayerAndAVS(10); bytes memory quorumNumbersNotCreated = new bytes(1); ISignatureUtils.SignatureWithSaltAndExpiry memory emptySig; @@ -159,7 +227,7 @@ contract RegistryCoordinatorUnit is MockAVSDeployer { registryCoordinator.registerOperator(quorumNumbersNotCreated, defaultSocket, pubkeyRegistrationParams, emptySig); } - function testRegisterOperatorWithCoordinatorForSingleQuorum_Valid() public { + function test_registerOperator_singleQuorum() public { bytes memory quorumNumbers = new bytes(1); ISignatureUtils.SignatureWithSaltAndExpiry memory emptySig; quorumNumbers[0] = bytes1(defaultQuorumNumber); @@ -202,7 +270,7 @@ contract RegistryCoordinatorUnit is MockAVSDeployer { ); } - function testRegisterOperatorWithCoordinatorForFuzzedQuorums_Valid(uint256 quorumBitmap) public { + function testFuzz_registerOperator(uint256 quorumBitmap) public { quorumBitmap = quorumBitmap & MAX_QUORUM_BITMAP; ISignatureUtils.SignatureWithSaltAndExpiry memory emptySig; cheats.assume(quorumBitmap != 0); @@ -254,7 +322,7 @@ contract RegistryCoordinatorUnit is MockAVSDeployer { ); } - function testRegisterOperatorWithCoordinator_RegisteredOperatorForNewQuorums_Valid() public { + function test_registerOperator_addingQuorumsAfterInitialRegistration() public { uint256 registrationBlockNumber = block.number + 100; uint256 nextRegistrationBlockNumber = registrationBlockNumber + 100; ISignatureUtils.SignatureWithSaltAndExpiry memory emptySig; @@ -312,7 +380,7 @@ contract RegistryCoordinatorUnit is MockAVSDeployer { ); } - function testRegisterOperatorWithCoordinator_OverFilledQuorum_Reverts(uint256 pseudoRandomNumber) public { + function test_registerOperator_revert_overFilledQuorum(uint256 pseudoRandomNumber) public { uint32 numOperators = defaultMaxOperatorCount; uint32 registrationBlockNumber = 200; ISignatureUtils.SignatureWithSaltAndExpiry memory emptySig; @@ -343,7 +411,7 @@ contract RegistryCoordinatorUnit is MockAVSDeployer { registryCoordinator.registerOperator(quorumNumbers, defaultSocket, pubkeyRegistrationParams, emptySig); } - function testRegisterOperatorWithCoordinator_RegisteredOperatorForSameQuorums_Reverts() public { + function test_registerOperator_revert_operatorAlreadyRegisteredForQuorum() public { uint256 registrationBlockNumber = block.number + 100; uint256 nextRegistrationBlockNumber = registrationBlockNumber + 100; ISignatureUtils.SignatureWithSaltAndExpiry memory emptySig; @@ -363,8 +431,10 @@ contract RegistryCoordinatorUnit is MockAVSDeployer { registryCoordinator.registerOperator(quorumNumbers, defaultSocket, pubkeyRegistrationParams, emptySig); } +} - function testDeregisterOperatorWithCoordinator_WhenPaused_Reverts() public { +contract RegistryCoordinatorUnitTests_DeregisterOperator_EjectOperator is RegistryCoordinatorUnitTests { + function test_deregisterOperator_revert_paused() public { bytes memory quorumNumbers = new bytes(1); quorumNumbers[0] = bytes1(defaultQuorumNumber); uint256 quorumBitmap = BitmapUtils.orderedBytesArrayToBitmap(quorumNumbers); @@ -380,7 +450,7 @@ contract RegistryCoordinatorUnit is MockAVSDeployer { registryCoordinator.deregisterOperator(quorumNumbers); } - function testDeregisterOperatorWithCoordinator_NotRegistered_Reverts() public { + function test_deregisterOperator_revert_notRegistered() public { bytes memory quorumNumbers = new bytes(1); quorumNumbers[0] = bytes1(defaultQuorumNumber); @@ -389,7 +459,7 @@ contract RegistryCoordinatorUnit is MockAVSDeployer { registryCoordinator.deregisterOperator(quorumNumbers); } - function testDeregisterOperatorWithCoordinator_IncorrectQuorums_Reverts() public { + function test_deregisterOperator_revert_incorrectQuorums() public { bytes memory quorumNumbers = new bytes(1); quorumNumbers[0] = bytes1(defaultQuorumNumber); uint256 quorumBitmap = BitmapUtils.orderedBytesArrayToBitmap(quorumNumbers); @@ -405,7 +475,7 @@ contract RegistryCoordinatorUnit is MockAVSDeployer { registryCoordinator.deregisterOperator(quorumNumbers); } - function testDeregisterOperatorWithCoordinatorForSingleQuorumAndSingleOperator_Valid() public { + function test_deregisterOperator_singleQuorumAndSingleOperator() public { ISignatureUtils.SignatureWithSaltAndExpiry memory emptySig; uint32 registrationBlockNumber = 100; uint32 deregistrationBlockNumber = 200; @@ -453,7 +523,7 @@ contract RegistryCoordinatorUnit is MockAVSDeployer { ); } - function testDeregisterOperatorWithCoordinatorForFuzzedQuorumAndSingleOperator_Valid(uint256 quorumBitmap) public { + function testFuzz_deregisterOperator_fuzzedQuorumAndSingleOperator(uint256 quorumBitmap) public { ISignatureUtils.SignatureWithSaltAndExpiry memory emptySig; uint32 registrationBlockNumber = 100; uint32 deregistrationBlockNumber = 200; @@ -505,7 +575,7 @@ contract RegistryCoordinatorUnit is MockAVSDeployer { ); } - function testDeregisterOperatorWithCoordinatorForFuzzedQuorumAndManyOperators_Valid(uint256 pseudoRandomNumber) public { + function testFuzz_deregisterOperator_manyOperators(uint256 pseudoRandomNumber) public { uint32 numOperators = defaultMaxOperatorCount; uint32 registrationBlockNumber = 100; @@ -524,7 +594,7 @@ contract RegistryCoordinatorUnit is MockAVSDeployer { for (uint i = 0; i < numOperators; i++) { emit log_named_uint("i", i); BN254.G1Point memory pubKey = BN254.hashToG1(keccak256(abi.encodePacked(pseudoRandomNumber, i))); - bytes32 operatorId = pubKey.hashG1Point(); + bytes32 operatorId = BN254.hashG1Point(pubKey); address operator = _incrementAddress(defaultOperator, i); _registerOperatorWithCoordinator(operator, quorumBitmaps[i], pubKey); @@ -539,7 +609,7 @@ contract RegistryCoordinatorUnit is MockAVSDeployer { uint256 indexOfOperatorToDerigister = pseudoRandomNumber % numOperators; address operatorToDerigister = _incrementAddress(defaultOperator, indexOfOperatorToDerigister); BN254.G1Point memory operatorToDeregisterPubKey = BN254.hashToG1(keccak256(abi.encodePacked(pseudoRandomNumber, indexOfOperatorToDerigister))); - bytes32 operatorToDerigisterId = operatorToDeregisterPubKey.hashG1Point(); + bytes32 operatorToDerigisterId = BN254.hashG1Point(operatorToDeregisterPubKey); uint256 operatorToDeregisterQuorumBitmap = quorumBitmaps[indexOfOperatorToDerigister]; bytes memory operatorToDeregisterQuorumNumbers = BitmapUtils.bitmapToBytesArray(operatorToDeregisterQuorumBitmap); @@ -580,8 +650,8 @@ contract RegistryCoordinatorUnit is MockAVSDeployer { } // @notice verify that it is possible for an operator to register, deregister, and then register again! - function testReregisterOperatorWithCoordinator_Valid() public { - testDeregisterOperatorWithCoordinatorForSingleQuorumAndSingleOperator_Valid(); + function test_reregisterOperator() public { + test_deregisterOperator_singleQuorumAndSingleOperator(); ISignatureUtils.SignatureWithSaltAndExpiry memory emptySig; uint32 reregistrationBlockNumber = 201; @@ -630,7 +700,99 @@ contract RegistryCoordinatorUnit is MockAVSDeployer { ); } - function testRegisterOperatorWithCoordinatorWithKicks_Valid(uint256 pseudoRandomNumber) public { + function test_ejectOperator_allQuorums() public { + // register operator with default stake with default quorum number + bytes memory quorumNumbers = new bytes(1); + quorumNumbers[0] = bytes1(defaultQuorumNumber); + ISignatureUtils.SignatureWithSaltAndExpiry memory emptySig; + + stakeRegistry.setOperatorWeight(uint8(quorumNumbers[0]), defaultOperator, defaultStake); + + cheats.prank(defaultOperator); + registryCoordinator.registerOperator(quorumNumbers, defaultSocket, pubkeyRegistrationParams, emptySig); + + cheats.expectEmit(true, true, true, true, address(blsApkRegistry)); + emit OperatorRemovedFromQuorums(defaultOperator, quorumNumbers); + + cheats.expectEmit(true, true, true, true, address(stakeRegistry)); + emit OperatorStakeUpdate(defaultOperatorId, uint8(quorumNumbers[0]), 0); + + // eject + cheats.prank(ejector); + registryCoordinator.ejectOperator(defaultOperator, quorumNumbers); + + // make sure the operator is deregistered + assertEq( + keccak256(abi.encode(registryCoordinator.getOperator(defaultOperator))), + keccak256(abi.encode(IRegistryCoordinator.OperatorInfo({ + operatorId: defaultOperatorId, + status: IRegistryCoordinator.OperatorStatus.DEREGISTERED + }))) + ); + // make sure the operator is not in any quorums + assertEq(registryCoordinator.getCurrentQuorumBitmap(defaultOperatorId), 0); + } + + function test_ejectOperator_subsetOfQuorums() public { + // register operator with default stake with 2 quorums + bytes memory quorumNumbers = new bytes(2); + quorumNumbers[0] = bytes1(defaultQuorumNumber); + quorumNumbers[1] = bytes1(defaultQuorumNumber + 1); + ISignatureUtils.SignatureWithSaltAndExpiry memory emptySig; + + for (uint i = 0; i < quorumNumbers.length; i++) { + stakeRegistry.setOperatorWeight(uint8(quorumNumbers[i]), defaultOperator, defaultStake); + } + + cheats.prank(defaultOperator); + registryCoordinator.registerOperator(quorumNumbers, defaultSocket, pubkeyRegistrationParams, emptySig); + + // eject from only first quorum + bytes memory quorumNumbersToEject = new bytes(1); + quorumNumbersToEject[0] = quorumNumbers[0]; + + cheats.expectEmit(true, true, true, true, address(blsApkRegistry)); + emit OperatorRemovedFromQuorums(defaultOperator, quorumNumbersToEject); + + cheats.expectEmit(true, true, true, true, address(stakeRegistry)); + emit OperatorStakeUpdate(defaultOperatorId, uint8(quorumNumbersToEject[0]), 0); + + cheats.prank(ejector); + registryCoordinator.ejectOperator(defaultOperator, quorumNumbersToEject); + + // make sure the operator is registered + assertEq( + keccak256(abi.encode(registryCoordinator.getOperator(defaultOperator))), + keccak256(abi.encode(IRegistryCoordinator.OperatorInfo({ + operatorId: defaultOperatorId, + status: IRegistryCoordinator.OperatorStatus.REGISTERED + }))) + ); + // make sure the operator is not in any quorums + assertEq( + registryCoordinator.getCurrentQuorumBitmap(defaultOperatorId), + BitmapUtils.orderedBytesArrayToBitmap(quorumNumbers) & ~BitmapUtils.orderedBytesArrayToBitmap(quorumNumbersToEject) // quorumsRegisteredFor & ~quorumsEjectedFrom + ); + } + + function test_ejectOperator_revert_notEjector() public { + bytes memory quorumNumbers = new bytes(1); + quorumNumbers[0] = bytes1(defaultQuorumNumber); + ISignatureUtils.SignatureWithSaltAndExpiry memory emptySig; + + stakeRegistry.setOperatorWeight(uint8(quorumNumbers[0]), defaultOperator, defaultStake); + + cheats.prank(defaultOperator); + registryCoordinator.registerOperator(quorumNumbers, defaultSocket, pubkeyRegistrationParams, emptySig); + + cheats.expectRevert("RegistryCoordinator.onlyEjector: caller is not the ejector"); + cheats.prank(defaultOperator); + registryCoordinator.ejectOperator(defaultOperator, quorumNumbers); + } +} + +contract RegistryCoordinatorUnitTests_RegisterOperatorWithChurn is RegistryCoordinatorUnitTests { + function testFuzz_registerOperatorWithChurn(uint256 pseudoRandomNumber) public { uint32 numOperators = defaultMaxOperatorCount; uint32 kickRegistrationBlockNumber = 100; uint32 registrationBlockNumber = 200; @@ -651,7 +813,7 @@ contract RegistryCoordinatorUnit is MockAVSDeployer { address operatorToRegister = _incrementAddress(defaultOperator, numOperators); BN254.G1Point memory operatorToRegisterPubKey = BN254.hashToG1(keccak256(abi.encodePacked(pseudoRandomNumber, numOperators))); - bytes32 operatorToRegisterId = operatorToRegisterPubKey.hashG1Point(); + bytes32 operatorToRegisterId = BN254.hashG1Point(operatorToRegisterPubKey); bytes32 operatorToKickId; address operatorToKick; @@ -659,7 +821,7 @@ contract RegistryCoordinatorUnit is MockAVSDeployer { IRegistryCoordinator.OperatorKickParam[] memory operatorKickParams = new IRegistryCoordinator.OperatorKickParam[](1); { BN254.G1Point memory pubKey = BN254.hashToG1(keccak256(abi.encodePacked(pseudoRandomNumber, numOperators - 1))); - operatorToKickId = pubKey.hashG1Point(); + operatorToKickId = BN254.hashG1Point(pubKey); operatorToKick = _incrementAddress(defaultOperator, numOperators - 1); _registerOperatorWithCoordinator(operatorToKick, quorumBitmap, pubKey); @@ -735,7 +897,7 @@ contract RegistryCoordinatorUnit is MockAVSDeployer { ); } - function testRegisterOperatorWithCoordinatorWithKicks_LessThanKickBIPsOfOperatorStake_Reverts(uint256 pseudoRandomNumber) public { + function test_registerOperatorWithChurn_revert_lessThanKickBIPsOfOperatorStake(uint256 pseudoRandomNumber) public { bytes memory quorumNumbers = new bytes(1); quorumNumbers[0] = bytes1(defaultQuorumNumber); ISignatureUtils.SignatureWithSaltAndExpiry memory emptyAVSRegSig; @@ -744,8 +906,8 @@ contract RegistryCoordinatorUnit is MockAVSDeployer { address operatorToRegister, BN254.G1Point memory operatorToRegisterPubKey, IRegistryCoordinator.OperatorKickParam[] memory operatorKickParams - ) = _testRegisterOperatorWithKicks_SetUp(pseudoRandomNumber, quorumNumbers, defaultStake); - bytes32 operatorToRegisterId = operatorToRegisterPubKey.hashG1Point(); + ) = _testRegisterOperatorWithChurn_SetUp(pseudoRandomNumber, quorumNumbers, defaultStake); + bytes32 operatorToRegisterId = BN254.hashG1Point(operatorToRegisterPubKey); stakeRegistry.setOperatorWeight(defaultQuorumNumber, operatorToRegister, defaultStake); @@ -763,7 +925,7 @@ contract RegistryCoordinatorUnit is MockAVSDeployer { ); } - function testRegisterOperatorWithCoordinatorWithKicks_LessThanKickBIPsOfTotalStake_Reverts(uint256 pseudoRandomNumber) public { + function test_registerOperatorWithChurn_revert_lessThanKickBIPsOfTotalStake(uint256 pseudoRandomNumber) public { bytes memory quorumNumbers = new bytes(1); quorumNumbers[0] = bytes1(defaultQuorumNumber); ISignatureUtils.SignatureWithSaltAndExpiry memory emptyAVSRegSig; @@ -773,8 +935,8 @@ contract RegistryCoordinatorUnit is MockAVSDeployer { address operatorToRegister, BN254.G1Point memory operatorToRegisterPubKey, IRegistryCoordinator.OperatorKickParam[] memory operatorKickParams - ) = _testRegisterOperatorWithKicks_SetUp(pseudoRandomNumber, quorumNumbers, operatorToKickStake); - bytes32 operatorToRegisterId = operatorToRegisterPubKey.hashG1Point(); + ) = _testRegisterOperatorWithChurn_SetUp(pseudoRandomNumber, quorumNumbers, operatorToKickStake); + bytes32 operatorToRegisterId = BN254.hashG1Point(operatorToRegisterPubKey); // set the stake of the operator to register to the defaultKickBIPsOfOperatorStake multiple of the operatorToKickStake @@ -794,7 +956,7 @@ contract RegistryCoordinatorUnit is MockAVSDeployer { ); } - function testRegisterOperatorWithCoordinatorWithKicks_InvalidSignatures_Reverts(uint256 pseudoRandomNumber) public { + function test_registerOperatorWithChurn_revert_invalidChurnApproverSignature(uint256 pseudoRandomNumber) public { bytes memory quorumNumbers = new bytes(1); quorumNumbers[0] = bytes1(defaultQuorumNumber); ISignatureUtils.SignatureWithSaltAndExpiry memory emptyAVSRegSig; @@ -803,7 +965,7 @@ contract RegistryCoordinatorUnit is MockAVSDeployer { address operatorToRegister, , IRegistryCoordinator.OperatorKickParam[] memory operatorKickParams - ) = _testRegisterOperatorWithKicks_SetUp(pseudoRandomNumber, quorumNumbers, defaultStake); + ) = _testRegisterOperatorWithChurn_SetUp(pseudoRandomNumber, quorumNumbers, defaultStake); uint96 registeringStake = defaultKickBIPsOfOperatorStake * defaultStake; stakeRegistry.setOperatorWeight(defaultQuorumNumber, operatorToRegister, registeringStake); @@ -825,7 +987,7 @@ contract RegistryCoordinatorUnit is MockAVSDeployer { ); } - function testRegisterOperatorWithCoordinatorWithKicks_ExpiredSignatures_Reverts(uint256 pseudoRandomNumber) public { + function test_registerOperatorWithChurn_revert_expiredChurnApproverSignature(uint256 pseudoRandomNumber) public { bytes memory quorumNumbers = new bytes(1); quorumNumbers[0] = bytes1(defaultQuorumNumber); ISignatureUtils.SignatureWithSaltAndExpiry memory emptyAVSRegSig; @@ -834,8 +996,8 @@ contract RegistryCoordinatorUnit is MockAVSDeployer { address operatorToRegister, BN254.G1Point memory operatorToRegisterPubKey, IRegistryCoordinator.OperatorKickParam[] memory operatorKickParams - ) = _testRegisterOperatorWithKicks_SetUp(pseudoRandomNumber, quorumNumbers, defaultStake); - bytes32 operatorToRegisterId = operatorToRegisterPubKey.hashG1Point(); + ) = _testRegisterOperatorWithChurn_SetUp(pseudoRandomNumber, quorumNumbers, defaultStake); + bytes32 operatorToRegisterId = BN254.hashG1Point(operatorToRegisterPubKey); uint96 registeringStake = defaultKickBIPsOfOperatorStake * defaultStake; stakeRegistry.setOperatorWeight(defaultQuorumNumber, operatorToRegister, registeringStake); @@ -854,156 +1016,5 @@ contract RegistryCoordinatorUnit is MockAVSDeployer { ); } - function testEjectOperatorFromCoordinator_AllQuorums_Valid() public { - // register operator with default stake with default quorum number - bytes memory quorumNumbers = new bytes(1); - quorumNumbers[0] = bytes1(defaultQuorumNumber); - ISignatureUtils.SignatureWithSaltAndExpiry memory emptySig; - - stakeRegistry.setOperatorWeight(uint8(quorumNumbers[0]), defaultOperator, defaultStake); - - cheats.prank(defaultOperator); - registryCoordinator.registerOperator(quorumNumbers, defaultSocket, pubkeyRegistrationParams, emptySig); - - cheats.expectEmit(true, true, true, true, address(blsApkRegistry)); - emit OperatorRemovedFromQuorums(defaultOperator, quorumNumbers); - - cheats.expectEmit(true, true, true, true, address(stakeRegistry)); - emit OperatorStakeUpdate(defaultOperatorId, uint8(quorumNumbers[0]), 0); - - // eject - cheats.prank(ejector); - registryCoordinator.ejectOperator(defaultOperator, quorumNumbers); - - // make sure the operator is deregistered - assertEq( - keccak256(abi.encode(registryCoordinator.getOperator(defaultOperator))), - keccak256(abi.encode(IRegistryCoordinator.OperatorInfo({ - operatorId: defaultOperatorId, - status: IRegistryCoordinator.OperatorStatus.DEREGISTERED - }))) - ); - // make sure the operator is not in any quorums - assertEq(registryCoordinator.getCurrentQuorumBitmap(defaultOperatorId), 0); - } - - function testEjectOperatorFromCoordinator_SubsetOfQuorums_Valid() public { - // register operator with default stake with 2 quorums - bytes memory quorumNumbers = new bytes(2); - quorumNumbers[0] = bytes1(defaultQuorumNumber); - quorumNumbers[1] = bytes1(defaultQuorumNumber + 1); - ISignatureUtils.SignatureWithSaltAndExpiry memory emptySig; - - for (uint i = 0; i < quorumNumbers.length; i++) { - stakeRegistry.setOperatorWeight(uint8(quorumNumbers[i]), defaultOperator, defaultStake); - } - - cheats.prank(defaultOperator); - registryCoordinator.registerOperator(quorumNumbers, defaultSocket, pubkeyRegistrationParams, emptySig); - - // eject from only first quorum - bytes memory quorumNumbersToEject = new bytes(1); - quorumNumbersToEject[0] = quorumNumbers[0]; - - cheats.expectEmit(true, true, true, true, address(blsApkRegistry)); - emit OperatorRemovedFromQuorums(defaultOperator, quorumNumbersToEject); - - cheats.expectEmit(true, true, true, true, address(stakeRegistry)); - emit OperatorStakeUpdate(defaultOperatorId, uint8(quorumNumbersToEject[0]), 0); - - cheats.prank(ejector); - registryCoordinator.ejectOperator(defaultOperator, quorumNumbersToEject); - - // make sure the operator is registered - assertEq( - keccak256(abi.encode(registryCoordinator.getOperator(defaultOperator))), - keccak256(abi.encode(IRegistryCoordinator.OperatorInfo({ - operatorId: defaultOperatorId, - status: IRegistryCoordinator.OperatorStatus.REGISTERED - }))) - ); - // make sure the operator is not in any quorums - assertEq( - registryCoordinator.getCurrentQuorumBitmap(defaultOperatorId), - BitmapUtils.orderedBytesArrayToBitmap(quorumNumbers) & ~BitmapUtils.orderedBytesArrayToBitmap(quorumNumbersToEject) // quorumsRegisteredFor & ~quorumsEjectedFrom - ); - } - - function testEjectOperatorFromCoordinator_NotEjector_Reverts() public { - bytes memory quorumNumbers = new bytes(1); - quorumNumbers[0] = bytes1(defaultQuorumNumber); - ISignatureUtils.SignatureWithSaltAndExpiry memory emptySig; - - stakeRegistry.setOperatorWeight(uint8(quorumNumbers[0]), defaultOperator, defaultStake); - - cheats.prank(defaultOperator); - registryCoordinator.registerOperator(quorumNumbers, defaultSocket, pubkeyRegistrationParams, emptySig); - - cheats.expectRevert("RegistryCoordinator.onlyEjector: caller is not the ejector"); - cheats.prank(defaultOperator); - registryCoordinator.ejectOperator(defaultOperator, quorumNumbers); - } - - function testUpdateSocket() public { - bytes memory quorumNumbers = new bytes(1); - quorumNumbers[0] = bytes1(defaultQuorumNumber); - - uint256 quorumBitmap = BitmapUtils.orderedBytesArrayToBitmap(quorumNumbers); - _registerOperatorWithCoordinator(defaultOperator, quorumBitmap, defaultPubKey); - - cheats.prank(defaultOperator); - cheats.expectEmit(true, true, true, true, address(registryCoordinator)); - emit OperatorSocketUpdate(defaultOperatorId, "localhost:32004"); - registryCoordinator.updateSocket("localhost:32004"); - - } - - function testUpdateSocket_NotRegistered_Reverts() public { - cheats.prank(defaultOperator); - cheats.expectRevert("RegistryCoordinator.updateSocket: operator is not registered"); - registryCoordinator.updateSocket("localhost:32004"); - } - - function _testRegisterOperatorWithKicks_SetUp(uint256 pseudoRandomNumber, bytes memory quorumNumbers, uint96 operatorToKickStake) internal returns(address operatorToRegister, BN254.G1Point memory operatorToRegisterPubKey, IRegistryCoordinator.OperatorKickParam[] memory operatorKickParams) { - uint32 kickRegistrationBlockNumber = 100; - - uint256 quorumBitmap = BitmapUtils.orderedBytesArrayToBitmap(quorumNumbers); - - cheats.roll(kickRegistrationBlockNumber); - - for (uint i = 0; i < defaultMaxOperatorCount - 1; i++) { - BN254.G1Point memory pubKey = BN254.hashToG1(keccak256(abi.encodePacked(pseudoRandomNumber, i))); - address operator = _incrementAddress(defaultOperator, i); - - _registerOperatorWithCoordinator(operator, quorumBitmap, pubKey); - } - - operatorToRegister = _incrementAddress(defaultOperator, defaultMaxOperatorCount); - operatorToRegisterPubKey = BN254.hashToG1(keccak256(abi.encodePacked(pseudoRandomNumber, defaultMaxOperatorCount))); - bytes32 operatorToRegisterId = operatorToRegisterPubKey.hashG1Point(); - bytes32 operatorToKickId; - address operatorToKick; - - // register last operator before kick - operatorKickParams = new IRegistryCoordinator.OperatorKickParam[](1); - { - BN254.G1Point memory pubKey = BN254.hashToG1(keccak256(abi.encodePacked(pseudoRandomNumber, defaultMaxOperatorCount - 1))); - operatorToKickId = pubKey.hashG1Point(); - operatorToKick = _incrementAddress(defaultOperator, defaultMaxOperatorCount - 1); - - // register last operator with much more than the kickBIPsOfTotalStake stake - _registerOperatorWithCoordinator(operatorToKick, quorumBitmap, pubKey, operatorToKickStake); - - bytes32[] memory operatorIdsToSwap = new bytes32[](1); - // operatorIdsToSwap[0] = operatorToRegisterId - operatorIdsToSwap[0] = operatorToRegisterId; - - operatorKickParams[0] = IRegistryCoordinator.OperatorKickParam({ - quorumNumber: uint8(quorumNumbers[0]), - operator: operatorToKick - }); - } - - blsApkRegistry.setBLSPublicKey(operatorToRegister, operatorToRegisterPubKey); - } } + From f9448fa9073833891bd86d653c4184da3142093d Mon Sep 17 00:00:00 2001 From: ChaoticWalrus <93558947+ChaoticWalrus@users.noreply.github.com> Date: Thu, 21 Dec 2023 15:49:01 -0800 Subject: [PATCH 03/48] feat: add a couple simple tests note that the `test_createQuorum` test is currently failing because initialization already sets up the max number of quorums something will need to be adjusted here --- test/unit/RegistryCoordinatorUnit.t.sol | 34 +++++++++++++++++++++++++ 1 file changed, 34 insertions(+) diff --git a/test/unit/RegistryCoordinatorUnit.t.sol b/test/unit/RegistryCoordinatorUnit.t.sol index c5a43311..44997660 100644 --- a/test/unit/RegistryCoordinatorUnit.t.sol +++ b/test/unit/RegistryCoordinatorUnit.t.sol @@ -8,6 +8,7 @@ contract RegistryCoordinatorUnitTests is MockAVSDeployer { uint8 internal constant PAUSED_REGISTER_OPERATOR = 0; uint8 internal constant PAUSED_DEREGISTER_OPERATOR = 1; + uint8 internal constant MAX_QUORUM_COUNT = 192; event OperatorSocketUpdate(bytes32 indexed operatorId, string socket); @@ -178,6 +179,39 @@ contract RegistryCoordinatorUnitTests_Initialization_Setters is RegistryCoordina cheats.expectRevert("RegistryCoordinator.updateSocket: operator is not registered"); registryCoordinator.updateSocket("localhost:32004"); } + + function test_createQuorum_revert_notOwner() public { + IRegistryCoordinator.OperatorSetParam memory operatorSetParams; + uint96 minimumStake; + IStakeRegistry.StrategyParams[] memory strategyParams; + + cheats.expectRevert("Ownable: caller is not the owner"); + cheats.prank(defaultOperator); + registryCoordinator.createQuorum(operatorSetParams, minimumStake, strategyParams); + } + + function test_createQuorum() public { + IRegistryCoordinator.OperatorSetParam memory operatorSetParams; + uint96 minimumStake; + IStakeRegistry.StrategyParams[] memory strategyParams; + + uint8 quorumCountBefore = registryCoordinator.quorumCount(); + + cheats.expectEmit(true, true, true, true, address(registryCoordinator)); + emit OperatorSetParamsUpdated(quorumCountBefore, operatorSetParams); + cheats.prank(registryCoordinatorOwner); + registryCoordinator.createQuorum(operatorSetParams, minimumStake, strategyParams); + + uint8 quorumCountAfter = registryCoordinator.quorumCount(); + assertEq(quorumCountAfter, quorumCountBefore + 1, "quorum count did not increase properly"); + assertLe(quorumCountAfter, MAX_QUORUM_COUNT, "quorum count exceeded max"); + + assertEq( + keccak256(abi.encode(operatorSetParams)), + keccak256(abi.encode(registryCoordinator.getOperatorSetParams(quorumCountBefore))), + "OperatorSetParams not stored properly" + ); + } } contract RegistryCoordinatorUnitTests_RegisterOperator is RegistryCoordinatorUnitTests { From ebe657e1167e59dc7959dfe11c8194b72bf9a347 Mon Sep 17 00:00:00 2001 From: Alex <18387287+wadealexc@users.noreply.github.com> Date: Thu, 28 Dec 2023 15:03:01 -0500 Subject: [PATCH 04/48] =?UTF-8?q?docs:=20update=20reg=20coord=20to=20inclu?= =?UTF-8?q?de=20pubkey=20registration=20and=20service=20man=E2=80=A6=20(#1?= =?UTF-8?q?16)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * docs: update reg coord to include pubkey registration and service manager usage * docs: add service manager to tech docs intro * fix: update table of contents * docs: add docs for BLSSignatureChecker and OperatorStateRetriever * docs: address feedback --- docs/BLSSignatureChecker.md | 201 ++++++++++++++++++++++++++++- docs/README.md | 15 ++- docs/RegistryCoordinator.md | 17 ++- docs/ServiceManagerBase.md | 68 ++++++++++ docs/old/BLSPublicKeyCompendium.md | 27 ---- docs/registries/BLSApkRegistry.md | 14 ++ 6 files changed, 309 insertions(+), 33 deletions(-) create mode 100644 docs/ServiceManagerBase.md delete mode 100644 docs/old/BLSPublicKeyCompendium.md diff --git a/docs/BLSSignatureChecker.md b/docs/BLSSignatureChecker.md index bc860a6f..31e8d09e 100644 --- a/docs/BLSSignatureChecker.md +++ b/docs/BLSSignatureChecker.md @@ -1 +1,200 @@ -TODO! \ No newline at end of file +[core-docs-m2]: https://github.com/Layr-Labs/eigenlayer-contracts/tree/m2-mainnet/docs +[core-dmgr-docs]: https://github.com/Layr-Labs/eigenlayer-contracts/blob/m2-mainnet/docs/core/DelegationManager.md +[core-dmgr-register]: https://github.com/Layr-Labs/eigenlayer-contracts/blob/m2-mainnet/docs/core/DelegationManager.md#registeroperatortoavs +[core-dmgr-deregister]: https://github.com/Layr-Labs/eigenlayer-contracts/blob/m2-mainnet/docs/core/DelegationManager.md#deregisteroperatorfromavs + +[eigenda-service-manager]: https://github.com/Layr-Labs/eigenda/blob/m2-mainnet-contracts/contracts/src/core/EigenDAServiceManager.sol + +## BLSSignatureChecker + +| File | Type | Proxy | +| -------- | -------- | -------- | +| [`BLSSignatureChecker.sol`](../src/BLSSignatureChecker.sol) | Singleton | Transparent proxy | +| [`OperatorStateRetriever.sol`](../src/OperatorStateRetriever.sol) | Singleton | None | + +`BLSSignatureChecker` and `OperatorStateRetriever` perform (respectively) the onchain and offchain portions of BLS signature validation for the aggregate of a quorum's registered Operators. + +The `OperatorStateRetriever` has various view methods intended to be called by offchain infrastructure in order to prepare a call to `BLSSignatureChecker.checkSignatures`. These methods traverse the state histories kept by the various registry contracts (see [./RegistryCoordinator.md](./RegistryCoordinator.md)) to query states at specific block numbers. + +These historical states are then used within `BLSSignatureChecker` to validate a BLS signature formed from an aggregated subset of the Operators registered for one or more quorums at some specific block number. + +#### High-level Concepts + +This document organizes methods according to the following themes (click each to be taken to the relevant section): +* [Onchain](#onchain) +* [Offchain](#offchain) +* [System Configuration](#system-configuration) + +--- + +### Onchain + +#### `BLSSignatureChecker.checkSignatures` + +```solidity +function checkSignatures( + bytes32 msgHash, + bytes calldata quorumNumbers, + uint32 referenceBlockNumber, + NonSignerStakesAndSignature memory params +) + public + view + returns (QuorumStakeTotals memory, bytes32) + +struct NonSignerStakesAndSignature { + uint32[] nonSignerQuorumBitmapIndices; + BN254.G1Point[] nonSignerPubkeys; + BN254.G1Point[] quorumApks; + BN254.G2Point apkG2; + BN254.G1Point sigma; + uint32[] quorumApkIndices; + uint32[] totalStakeIndices; + uint32[][] nonSignerStakeIndices; +} + +struct QuorumStakeTotals { + uint96[] signedStakeForQuorum; + uint96[] totalStakeForQuorum; +} +``` + +The goal of this method is to allow an AVS to validate a BLS signature formed from the aggregate pubkey ("apk") of Operators registered in one or more quorums at some `referenceBlockNumber`. + +Some notes on method parameters: +* `referenceBlockNumber` is the reason each registry contract keeps historical states: so that lookups can be performed on each registry's info at a particular block. This is important because Operators may sign some data on behalf of an AVS, then deregister from one or more of the AVS's quorums. Historical states allow signature validation to be performed against a "fixed point" in AVS/quorum history. +* `quorumNumbers` is used to perform signature validation across one *or more* quorums. Also, Operators may be registered for more than one quorum - and for each quorum an Operator is registered for, that Operator's pubkey is included in that quorum's apk within the `BLSApkRegistry`. This means that, when calculating an apk across multiple `quorumNumbers`, Operators registered for more than one of these quorums will have their pubkey included more than once in the total apk. +* `params` contains both a signature from all signing Operators, as well as several fields that identify registered, non-signing Operators. While non-signing Operators haven't contributed to the signature, but need to be accounted for because, as Operators registered for one or more signing quorums, their public keys are included in that quorum's apk. Essentially, in order to validate the signature, nonsigners' public keys need to be subtracted out from the total apk to derive the apk that actually signed the message. + +This method performs the following steps. Note that each step involves lookups of historical state from `referenceBlockNumber`, but the writing in this section will use the present tense because adding "at the `referenceBlockNumber`" everywhere gets confusing. Steps follow: +1. Calculate the *total nonsigner apk*, an aggregate pubkey composed of all nonsigner pubkeys. For each nonsigner: + * Query the `RegistryCoordinator` to get the nonsigner's registered quorums. + * Multiply the nonsigner's pubkey by the number of quorums in `quorumNumbers` the nonsigner is registered for. + * Add the result to the *total nonsigner apk*. +2. Calculate the negative of the *total nonsigner apk*. +3. For each quorum: + * Query the `BLSApkRegistry` to get the *quorum apk*: the aggregate pubkey of all Operators registered for that quorum. + * Add the *quorum apk* to the *total nonsigner apk*. This effectively subtracts out any pubkeys belonging to nonsigning Operators in the quorum, leaving only pubkeys of signing Operators. We'll call the result the *total signing apk*. + * Query the `StakeRegistry` to get the total stake for the quorum. + * For each nonsigner, if the nonsigner is registered for the quorum, query the `StakeRegistry` for their stake and subtract it from the total. This leaves only stake belonging to signing Operators. +4. Use the `msgHash`, the *total signing apk*, `params.apkG2`, and `params.sigma` to validate the BLS signature. +5. Return the total stake and signing stakes for each quorum, along with a hash identifying the `referenceBlockNumber` and non-signers + +*Entry Points* (EigenDA): +* Called by [`EigenDAServiceManager.confirmBatch`][eigenda-service-manager] + +*Requirements*: +* Input validation: + * Quorum-related fields MUST have equal lengths: `quorumNumbers`, `params.quorumApks`, `params.quorumApkIndices`, `params.totalStakeIndices`, `params.nonSignerStakeIndices` + * Nonsigner-related fields MUST have equal lengths: `params.nonSignerPubkeys`, `params.nonSignerQuorumBitmapIndices` + * `referenceBlockNumber` MUST NOT be greater than `block.number` + * `quorumNumbers` MUST be an ordered list of valid, initialized quorums + * `params.nonSignerPubkeys` MUST ONLY contain unique pubkeys, in ascending order of their pubkey hash +* For each quorum: + * If stale stakes are forbidden (see [`BLSSignatureChecker.setStaleStakesForbidden`](#blssignaturecheckersetstalestakesforbidden)), check the last `quorumUpdateBlockNumber` is within `DelegationManager.withdrawalDelayBlocks` of `referenceBlockNumber`. This references a value in the EigenLayer core contracts - see [EigenLayer core docs][core-docs-m2] for more info. + * Validate that each `params.quorumApks` corresponds to the quorum's apk at the `referenceBlockNumber` +* For each historical state lookup, the `referenceBlockNumber` and provided index MUST point to a valid historical entry: + * `referenceBlockNumber` MUST come after the entry's `updateBlockNumber` + * The entry's `nextUpdateBlockNumber` MUST EITHER be 0, OR greater than `referenceBlockNumber` + +--- + +### Offchain + +These methods perform very gas-heavy lookups through various registry states, and are called by offchain infrastructure to construct calldata for a call to `BLSSignatureChecker.checkSignatures`: +* [`OperatorStateRetriever.getOperatorState (operatorId)`](#operatorstateretrievergetoperatorstate-operatorid) +* [`OperatorStateRetriever.getOperatorState (quorumNumbers)`](#operatorstateretrievergetoperatorstate-quorumnumbers) +* [`OperatorStateRetriever.getCheckSignaturesIndices`](#operatorstateretrievergetchecksignaturesindices) + +#### `OperatorStateRetriever.getOperatorState (operatorId)` + +```solidity +function getOperatorState( + IRegistryCoordinator registryCoordinator, + bytes32 operatorId, + uint32 blockNumber +) + external + view + returns (uint256, Operator[][] memory) + +struct Operator { + bytes32 operatorId; + uint96 stake; +} +``` + +Traverses history in the `RegistryCoordinator`, `IndexRegistry`, and `StakeRegistry` to retrieve information on an Operator (given by `operatorId`) and the quorums they are registered for at a specific `blockNumber`. Returns: +* `uint256`: a bitmap of the quorums the Operator was registered for at `blockNumber` +* `Operator[][]`: For each of the quorums mentioned above, this is a list of the Operators registered for that quorum at `blockNumber`, containing each Operator's `operatorId` and `stake`. + +#### `OperatorStateRetriever.getOperatorState (quorumNumbers)` + +```solidity +function getOperatorState( + IRegistryCoordinator registryCoordinator, + bytes memory quorumNumbers, + uint32 blockNumber +) + public + view + returns(Operator[][] memory) +``` + +Traverses history in the `RegistryCoordinator`, `IndexRegistry`, and `StakeRegistry` to retrieve information on the Operator set registered for each quorum in `quorumNumbers` at `blockNumber`. Returns: +* `Operator[][]`: For each quorum in `quorumNumbers`, this is a list of the Operators registered for that quorum at `blockNumber`, containing each Operator's `operatorId` and `stake`. + +#### `OperatorStateRetriever.getCheckSignaturesIndices` + +```solidity +function getCheckSignaturesIndices( + IRegistryCoordinator registryCoordinator, + uint32 referenceBlockNumber, + bytes calldata quorumNumbers, + bytes32[] calldata nonSignerOperatorIds +) + external + view + returns (CheckSignaturesIndices memory) + +struct CheckSignaturesIndices { + uint32[] nonSignerQuorumBitmapIndices; + uint32[] quorumApkIndices; + uint32[] totalStakeIndices; + uint32[][] nonSignerStakeIndices; // nonSignerStakeIndices[quorumNumberIndex][nonSignerIndex] +} +``` + +Traverses histories in the `RegistryCoordinator`, `IndexRegistry`, `StakeRegistry`, and `BLSApkRegistry` to retrieve information on one or more quorums' Operator sets and nonsigning Operators at a given `referenceBlockNumber`. + +The return values are all "indices," because of the linear historical state each registry keeps. Offchain code calls this method to compute indices into historical state, which later is leveraged for cheap lookups in `BLSSignatureChecker.checkSignatures` (rather than traversing over the history during an onchain operation). + +For each quorum, this returns: +* `uint32[] nonSignerQuorumBitmapIndices`: The indices in `RegistryCoordinator._operatorBitmapHistory` where each nonsigner's registered quorum bitmap can be found at `referenceBlockNumber`. Length is equal to the number of nonsigners included in `nonSignerOperatorIds` +* `uint32[] quorumApkIndices`: The indices in `BLSApkRegistry.apkHistory` where the quorum's apk can be found at `referenceBlockNumber`. Length is equal to the number of quorums in `quorumNumbers`. +* `uint32[] totalStakeIndices`: The indices in `StakeRegistry._totalStakeHistory` where each quorum's total stake can be found at `referenceBlockNumber`. Length is equal to the number of quorums in `quorumNumbers`. +* `uint32[][] nonSignerStakeIndices`: For each quorum, a list of the indices of each nonsigner's `StakeRegistry.operatorStakeHistory` entry at `referenceBlockNumber`. Length is equal to the number of quorums in `quorumNumbers`, and each sub-list is equal in length to the number of nonsigners in `nonSignerOperatorIds` registered for that quorum at `referenceBlockNumber` + +--- + +### System Configuration + +#### `BLSSignatureChecker.setStaleStakesForbidden` + +```solidity +function setStaleStakesForbidden( + bool value +) + external + onlyCoordinatorOwner +``` + +This method allows the `RegistryCoordinator` Owner to update `staleStakesForbidden` in the `BLSSignatureChecker`. If stale stakes are forbidden, `BLSSignatureChecker.checkSignatures` will perform an additional check when querying each quorum's apk, Operator stakes, and total stakes. + +This additional check requires that each quorum was updated within a certain block window of the `referenceBlockNumber` passed into `BLSSignatureChecker.checkSignatures`. + +*Effects*: +* Sets `staleStakesForbidden` to `value` + +*Requirements*: +* Caller MUST be the `RegistryCoordinator` Owner \ No newline at end of file diff --git a/docs/README.md b/docs/README.md index 54bf44ec..e0017c24 100644 --- a/docs/README.md +++ b/docs/README.md @@ -34,6 +34,7 @@ For more information on EigenDA, check out the repo: [Layr-Labs/eigenda][eigenda * [State Histories](#state-histories) * [Hooking Into EigenLayer Core](#hooking-into-eigenlayer-core) * [System Components](#system-components) + * [Service Manager](#service-manager) * [Registries](#registries) * [BLSSignatureChecker](#blssignaturechecker) @@ -105,6 +106,18 @@ Eventually, operator slashing and payment for services will be part of the middl ### System Components +#### Service Manager + +| Code | Type | Proxy | +| -------- | -------- | -------- | +| [`ServiceManagerBase.sol`](../src/ServiceManagerBase.sol) | Singleton | Transparent proxy | + +The Service Manager contract serves as the AVS's address relative to EigenLayer core contracts. When operators register for/deregister from the AVS, the Service Manager forwards this request to the DelegationManager (see [Hooking Into EigenLayer Core](#hooking-into-eigenlayer-core) above). + +It also contains a few convenience methods used to query operator information by the frontend. + +See full documentation in [`ServiceManagerBase.md`](./ServiceManagerBase.md). + #### Registries | Code | Type | Proxy | @@ -136,4 +149,4 @@ The BLSSignatureChecker verifies signatures made by the aggregate pubkeys ("Apk" The `OperatorStateRetriever` is used by offchain code to query the `RegistryCoordinator` (and its registries) for information that will ultimately be passed into `BLSSignatureChecker.checkSignatures`. -See full documentation in [`BLSSignatureChecker.md`](./BLSSignatureChecker.md). \ No newline at end of file +See full documentation for both of these contracts in [`BLSSignatureChecker.md`](./BLSSignatureChecker.md). \ No newline at end of file diff --git a/docs/RegistryCoordinator.md b/docs/RegistryCoordinator.md index ae82c64b..7f65b7db 100644 --- a/docs/RegistryCoordinator.md +++ b/docs/RegistryCoordinator.md @@ -42,6 +42,7 @@ These methods allow operators to register for/deregister from one or more quorum function registerOperator( bytes calldata quorumNumbers, string calldata socket, + IBLSApkRegistry.PubkeyRegistrationParams calldata params, SignatureWithSaltAndExpiry memory operatorSignature ) external @@ -53,12 +54,16 @@ Registers the caller as an Operator for one or more quorums, as long as registra * `StakeRegistry.registerOperator` * `IndexRegistry.registerOperator` -If the Operator was not currently registered for any quorums, this method will register the Operator to the AVS in the EigenLayer core contracts (`DelegationManager.registerOperatorToAVS`), passing in the provided `operatorSignature`. See the [`DelegationManager` docs][core-dmgr-docs] for more details. +If the Operator has never registered for any of this AVS's quorums before, they need to register a BLS public key to participate in AVS signing events. In this case, this method will automatically pass `params` to the `BLSApkRegistry` to perform public key registration. The registered pubkey hash becomes the Operator's unique operator id, used to identify them in many places in the middleware contracts. + +If the Operator was not currently registered for any quorums, this method will register the Operator to the AVS in the EigenLayer core contracts via the `ServiceManagerBase`. *Effects*: +* If the Operator has never registered for the AVS before: + * Registers their BLS pubkey in the `BLSApkRegistry` (see [`BLSApkRegistry.registerBLSPublicKey`](./registries/BLSApkRegistry.md#registerblspublickey)) * If the Operator was not currently registered for any quorums: * Updates their status to `REGISTERED` - * Registers them in the core contracts (see [`DelegationManager.registerOperatorToAVS`][core-dmgr-register]) + * Registers them in the core contracts (see [`ServiceManagerBase.registerOperatorToAVS`](./ServiceManagerBase.md#registeroperatortoavs)) * Adds the new quorums to the Operator's current registered quorums, and updates the Operator's bitmap history * See [`BLSApkRegistry.registerOperator`](./registries/BLSApkRegistry.md#registeroperator) * See [`StakeRegistry.registerOperator`](./registries/StakeRegistry.md#registeroperator) @@ -71,7 +76,7 @@ If the Operator was not currently registered for any quorums, this method will r * `quorumNumbers` MUST contain at least one valid quorum * `quorumNumbers` MUST NOT contain any quorums the Operator is already registered for * If the Operator was not currently registered for any quorums: - * See [`DelegationManager.registerOperatorToAVS`][core-dmgr-register] + * See [`ServiceManagerBase.registerOperatorToAVS`](./ServiceManagerBase.md#registeroperatortoavs) * See [`BLSApkRegistry.registerOperator`](./registries/BLSApkRegistry.md#registeroperator) * See [`StakeRegistry.registerOperator`](./registries/StakeRegistry.md#registeroperator) * See [`IndexRegistry.registerOperator`](./registries/IndexRegistry.md#registeroperator) @@ -83,6 +88,7 @@ If the Operator was not currently registered for any quorums, this method will r function registerOperatorWithChurn( bytes calldata quorumNumbers, string calldata socket, + IBLSApkRegistry.PubkeyRegistrationParams calldata params, OperatorKickParam[] calldata operatorKickParams, SignatureWithSaltAndExpiry memory churnApproverSignature, SignatureWithSaltAndExpiry memory operatorSignature @@ -121,7 +127,9 @@ function deregisterOperator( Allows an Operator to deregister themselves from one or more quorums. *Effects*: -* If the Operator is no longer registered for any quorums, updates their status to `DEREGISTERED` +* If the Operator is no longer registered for any quorums: + * Updates their status to `DEREGISTERED` + * Deregisters them in the core contracts (see [`ServiceManagerBase.deregisterOperatorFromAVS`](./ServiceManagerBase.md#deregisteroperatorfromavs)) * Removes the new quorums from the Operator's current registered quorums, and updates the Operator's bitmap history * See [`BLSApkRegistry.deregisterOperator`](./registries/BLSApkRegistry.md#deregisteroperator) * See [`StakeRegistry.deregisterOperator`](./registries/StakeRegistry.md#deregisteroperator) @@ -133,6 +141,7 @@ Allows an Operator to deregister themselves from one or more quorums. * `quorumNumbers` MUST be an ordered array of quorum numbers, with no entry exceeding the current `quorumCount` * `quorumNumbers` MUST contain at least one valid quorum * `quorumNumbers` MUST ONLY contain bits that are also set in the Operator's current registered quorum bitmap +* See [`ServiceManagerBase.deregisterOperatorFromAVS`](./ServiceManagerBase.md#deregisteroperatorfromavs) * See [`BLSApkRegistry.deregisterOperator`](./registries/BLSApkRegistry.md#deregisteroperator) * See [`StakeRegistry.deregisterOperator`](./registries/StakeRegistry.md#deregisteroperator) * See [`IndexRegistry.deregisterOperator`](./registries/IndexRegistry.md#deregisteroperator) diff --git a/docs/ServiceManagerBase.md b/docs/ServiceManagerBase.md new file mode 100644 index 00000000..e42e10c5 --- /dev/null +++ b/docs/ServiceManagerBase.md @@ -0,0 +1,68 @@ +[core-dmgr-docs]: https://github.com/Layr-Labs/eigenlayer-contracts/blob/m2-mainnet/docs/core/DelegationManager.md +[core-dmgr-register]: https://github.com/Layr-Labs/eigenlayer-contracts/blob/m2-mainnet/docs/core/DelegationManager.md#registeroperatortoavs +[core-dmgr-deregister]: https://github.com/Layr-Labs/eigenlayer-contracts/blob/m2-mainnet/docs/core/DelegationManager.md#deregisteroperatorfromavs + +## ServiceManagerBase + +| File | Type | Proxy | +| -------- | -------- | -------- | +| [`ServiceManagerBase.sol`](../src/ServiceManagerBase.sol) | Singleton | Transparent proxy | + +The `ServiceManagerBase` represents the AVS's address relative to EigenLayer core. When registering or deregistering an operator from an AVS, the AVS's `ServiceManagerBase` communicates this change to the core contracts, allowing the core contracts to maintain an up-to-date view on operator registration status with various AVSs. + +*As of M2*: +* Currently, this contract is used by the `DelegationManager` to keep track of operator registration and deregistration. Eventually, this relationship will be expanded to allow operators to opt in to slashing and payments for services. + +--- + +#### `registerOperatorToAVS` + +```solidity +function registerOperatorToAVS( + address operator, + ISignatureUtils.SignatureWithSaltAndExpiry memory operatorSignature +) + public + virtual + onlyRegistryCoordinator +``` + +When the `RegistryCoordinator` registers an operator for an AVS and they were not previously registered, it calls this method on `ServiceManagerBase`, which forwards the call to the EigenLayer core contract, the `DelegationManager`. + +*Entry Points*: +* `RegistryCoordinator.registerOperator` +* `RegistryCoordinator.registerOperatorWithChurn` + +*Effects*: +* See EigenLayer core: [`DelegationManager.registerOperatorToAVS`][core-dmgr-register] + +*Requirements*: +* Caller MUST be the `RegistryCoordinator` +* See EigenLayer core: [`DelegationManager.registerOperatorToAVS`][core-dmgr-register] + +#### `deregisterOperatorFromAVS` + +```solidity +function deregisterOperatorFromAVS( + address operator +) + public + virtual + onlyRegistryCoordinator +``` + +When the `RegistryCoordinator` deregisters an operator from an AVS, it calls this method on `ServiceManagerBase`, which forwards the call to the EigenLayer core contract, the `DelegationManager`. + +*Entry Points*: +* `RegistryCoordinator.registerOperatorWithChurn` +* `RegistryCoordinator.deregisterOperator` +* `RegistryCoordinator.ejectOperator` +* `RegistryCoordinator.updateOperators` +* `RegistryCoordinator.updateOperatorsForQuorum` + +*Effects*: +* See EigenLayer core: [`DelegationManager.deregisterOperatorFromAVS`][core-dmgr-deregister] + +*Requirements*: +* Caller MUST be the `RegistryCoordinator` +* See EigenLayer core: [`DelegationManager.deregisterOperatorFromAVS`][core-dmgr-deregister] \ No newline at end of file diff --git a/docs/old/BLSPublicKeyCompendium.md b/docs/old/BLSPublicKeyCompendium.md deleted file mode 100644 index c97adc32..00000000 --- a/docs/old/BLSPublicKeyCompendium.md +++ /dev/null @@ -1,27 +0,0 @@ -# BLSPublicKeyCompendium - -This contract is shared by all AVSs and serves as a single place for operators to connect their execution layer address to a bn254 public key. This contract also prevents [rogue key attacks](https://xn--2-umb.com/22/bls-signatures/#rogue-key-attack). - -## Flows - -There is only one flow for this contract, which is a call from an operator to register a bn254 public key as controlled by their execution layer address. - -### Cryptographic Verification - -Operators provide the contract with -- A BLS signature $\sigma \in \mathbb{G}_1$ of $M = (msg.sender, chain.id, \text{"EigenLayer\_BN254\_Pubkey\_Registration"})$ -- Their public keys $pk_1 \in \mathbb{G}_1$ and $pk_2 \in \mathbb{G}_2$ - -The contract then -- Calculates $\gamma = keccak256(\sigma, pk_1, pk_2, M)$ -- Verifies the paring $e(\sigma + \gamma pk_1, [1]_2) = e(H(m) + \gamma[1]_1, pk_2)$ - -This verifies that the operator owns the secret key corresponding to the public keys and that the $pk_1$ and $pk_2$ have the same discrete logarithm according to their respective curve's generators. - -We do this particular verification because aggregation of public keys and hashing to the curve is cheap in $\mathbb{G}_1$ on ethereum, and the above scheme allows for both! (aggregation to be done in the [BLSSignatureChecker](./BLSSignatureChecker.md)) More detailed notes exist [here](https://geometry.xyz/notebook/Optimized-BLS-multisignatures-on-EVM). - -The contract then stores a map from the execution layer address to the hash of the operator's $\mathbb{G}_1$ public key and the other way around. - -### Upstream Dependencies - -The [BLSPubkeyRegistry](./BLSPubkeyRegistry.md) looks up the public key hashes in this contract when operators register with a certain public key. diff --git a/docs/registries/BLSApkRegistry.md b/docs/registries/BLSApkRegistry.md index 4c707ca3..935ce4a2 100644 --- a/docs/registries/BLSApkRegistry.md +++ b/docs/registries/BLSApkRegistry.md @@ -71,6 +71,20 @@ TODO *Requirements*: * +#### `registerBLSPublicKey` + +```solidity + +``` + +TODO + +*Effects*: +* + +*Requirements*: +* + #### `initializeQuorum` ```solidity From 98f884454d9e9de1e344bb6fba9a2cd3915e5b57 Mon Sep 17 00:00:00 2001 From: Alex <18387287+wadealexc@users.noreply.github.com> Date: Mon, 1 Jan 2024 10:31:52 -0500 Subject: [PATCH 05/48] docs: add documentation for each registry (#125) * docs: standardize capitalization of Operator since thats what we do everywhere else * docs: add BLSApkRegistry docs * chore: remove old files - i've incorporated all the info from these files into the current docs, so i'm removing them * docs: add wip for IndexRegistry and StakeRegistry, and fix spacing in StakeRegistry * docs: add IndexRegistry docs * docs: Add StakeRegistry * docs: clarify wording --- docs/README.md | 46 +-- docs/old/BLSSignatureChecker.md | 44 --- docs/old/Old_BLSPubkeyRegistry.md | 36 --- .../Old_BLSRegistryCoordinatorWithIndices.md | 86 ----- docs/old/Old_IndexRegistry.md | 48 --- docs/old/Old_README.md | 101 ------ docs/old/Old_StakeRegistry.md | 56 ---- docs/old/OperatorStateRetriever.md | 21 -- docs/registries/BLSApkRegistry.md | 157 ++++++--- docs/registries/IndexRegistry.md | 156 ++++++--- docs/registries/StakeRegistry.md | 299 ++++++++++++++---- src/BLSApkRegistry.sol | 2 +- src/StakeRegistry.sol | 16 +- 13 files changed, 497 insertions(+), 571 deletions(-) delete mode 100644 docs/old/BLSSignatureChecker.md delete mode 100644 docs/old/Old_BLSPubkeyRegistry.md delete mode 100644 docs/old/Old_BLSRegistryCoordinatorWithIndices.md delete mode 100644 docs/old/Old_IndexRegistry.md delete mode 100644 docs/old/Old_README.md delete mode 100644 docs/old/Old_StakeRegistry.md delete mode 100644 docs/old/OperatorStateRetriever.md diff --git a/docs/README.md b/docs/README.md index e0017c24..919e1dcf 100644 --- a/docs/README.md +++ b/docs/README.md @@ -11,12 +11,12 @@ Reference Links: ## EigenLayer Middleware Docs -EigenLayer AVSs ("actively validated services") are protocols that make use of EigenLayer's restaking primitives. AVSs are validated by EigenLayer operators, who are backed by delegated restaked assets via the [EigenLayer core contracts][core-contracts-repo]. Each AVS will deploy or modify instances of the contracts in this repo to hook into the EigenLayer core contracts and ensure their service has an up-to-date view of its currently-registered operators. +EigenLayer AVSs ("actively validated services") are protocols that make use of EigenLayer's restaking primitives. AVSs are validated by EigenLayer Operators, who are backed by delegated restaked assets via the [EigenLayer core contracts][core-contracts-repo]. Each AVS will deploy or modify instances of the contracts in this repo to hook into the EigenLayer core contracts and ensure their service has an up-to-date view of its currently-registered Operators. -**Currently, each AVS needs to implement one thing on-chain:** registration/deregistration conditions that define how an operator registers for/deregisters from the AVS. This repo provides building blocks to support these functions. +**Currently, each AVS needs to implement one thing on-chain:** registration/deregistration conditions that define how an Operator registers for/deregisters from the AVS. This repo provides building blocks to support these functions. *Eventually,* the core contracts and this repo will be extended to cover other conditions, including: -* payment conditions that define how an operator is paid for the services it provides +* payment conditions that define how an Operator is paid for the services it provides * slashing conditions that define "malicious behavior" in the context of the AVS, and the punishments for this behavior *... however, the design for these conditions is still in progress.* @@ -44,11 +44,11 @@ For more information on EigenDA, check out the repo: [Layr-Labs/eigenda][eigenda ##### Quorums -A quorum is a grouping and configuration of specific kinds of stake that an AVS considers when interacting with operators. When operators register for an AVS, they select one or more quorums within the AVS to register for. Depending on its configuration, each quorum evaluates a specific subset of the operator's restaked tokens and uses this to determine a specific weight for the operator for that quorum. This weight is ultimately used to determine when an AVS has reached consensus. +A quorum is a grouping and configuration of specific kinds of stake that an AVS considers when interacting with Operators. When Operators register for an AVS, they select one or more quorums within the AVS to register for. Depending on its configuration, each quorum evaluates a specific subset of the Operator's restaked tokens and uses this to determine a specific weight for the Operator for that quorum. This weight is ultimately used to determine when an AVS has reached consensus. The purpose of having a quorum is that an AVS can customize the makeup of its security offering by choosing which kinds of stake/security it would like to utilize. -As an example, an AVS might want to support primarily native ETH stakers. It would do so by configuring a quorum to only weigh operators that control shares belonging to the native eth strategy (defined in the core contracts). +As an example, an AVS might want to support primarily native ETH stakers. It would do so by configuring a quorum to only weigh Operators that control shares belonging to the native eth strategy (defined in the core contracts). The Owner initializes quorums in the `RegistryCoordinator`, and may configure them further in both the `RegistryCoordinator` and `StakeRegistry` contracts. When quorums are initialized, they are assigned a unique, sequential quorum number. @@ -59,26 +59,26 @@ The Owner initializes quorums in the `RegistryCoordinator`, and may configure th Each quorum has an associated list of `StrategyParams`, which the Owner can configure via the `StakeRegistry`. `StrategyParams` define pairs of strategies and multipliers for the quorum: -* Strategies refer to the `DelegationManager` in the EigenLayer core contracts, which tracks shares delegated to each operator for each supported strategy. Basically, a strategy is a wrapper around an underlying token - either an LST or Native ETH. +* Strategies refer to the `DelegationManager` in the EigenLayer core contracts, which tracks shares delegated to each Operator for each supported strategy. Basically, a strategy is a wrapper around an underlying token - either an LST or Native ETH. * Multipliers determine the relative weight given to shares belonging to the corresponding strategy. -When the `StakeRegistry` updates its view of an operator's stake for a given quorum, it queries the `DelegationManager` to get the operator's shares in each of the quorum's strategies and applies the multiplier to the returned share count. +When the `StakeRegistry` updates its view of an Operator's stake for a given quorum, it queries the `DelegationManager` to get the Operator's shares in each of the quorum's strategies and applies the multiplier to the returned share count. For more information on the `DelegationManager`, see the [EigenLayer core docs][core-docs-m2]. ##### Operator Sets and Churn -Quorums define a maximum operator count as well as parameters that determine when a new operator can replace an existing operator when this max count is reached. The process of replacing an existing operator when the max count is reached is called "churn," and requires a signature from the Churn Approver. +Quorums define a maximum Operator count as well as parameters that determine when a new Operator can replace an existing Operator when this max count is reached. The process of replacing an existing Operator when the max count is reached is called "churn," and requires a signature from the Churn Approver. -These definitions are contained in a quorum's `OperatorSetParam`, which the Owner can configure via the `RegistryCoordinator`. A quorum's `OperatorSetParam` defines both a max operator count, as well as stake thresholds that the incoming and existing operators need to meet to qualify for churn. +These definitions are contained in a quorum's `OperatorSetParam`, which the Owner can configure via the `RegistryCoordinator`. A quorum's `OperatorSetParam` defines both a max Operator count, as well as stake thresholds that the incoming and existing Operators need to meet to qualify for churn. *Additional context*: -Currently for EigenDA, the max operator count is 200. This maximum exists because EigenDA requires that completed "jobs" validate a signature by the aggregate BLS pubkey of the operator set over some job parameters. Although an aggregate BLS pubkey's signature should have a fixed cost no matter the number of operators, it may be the case that not all operators sign off on a job. +Currently for EigenDA, the max Operator count is 200. This maximum exists because EigenDA requires that completed "jobs" validate a signature by the aggregate BLS pubkey of the Operator set over some job parameters. Although an aggregate BLS pubkey's signature should have a fixed cost no matter the number of Operators, it may be the case that not all Operators sign off on a job. -When this happens, EigenDA needs to provide a list of the pubkeys of the non-signers to subtract them out from the quorum's aggregate pubkey ("Apk"). The limit of 200 operators keeps the gas costs reasonable in a worst case scenario. See `BLSSignatureChecker.checkSignatures` for this part of the implementation. +When this happens, EigenDA needs to provide a list of the pubkeys of the non-signers to subtract them out from the quorum's aggregate pubkey ("Apk"). The limit of 200 Operators keeps the gas costs reasonable in a worst case scenario. See `BLSSignatureChecker.checkSignatures` for this part of the implementation. -In order to prevent the operator set from getting calcified, the churn mechanism was introduced to allow operators to be replaced in some cases. Future work is being done to increase the max operator count and refine the churn mechanism. +In order to prevent the Operator set from getting calcified, the churn mechanism was introduced to allow Operators to be replaced in some cases. Future work is being done to increase the max Operator count and refine the churn mechanism. ##### State Histories @@ -96,13 +96,13 @@ These histories are used by offchain code to query state at particular blocks, a ##### Hooking Into EigenLayer Core -The main thing that links an AVS to the EigenLayer core contracts is that when EigenLayer operators register/deregister with an AVS, the AVS calls these functions in EigenLayer core: +The main thing that links an AVS to the EigenLayer core contracts is that when EigenLayer Operators register/deregister with an AVS, the AVS calls these functions in EigenLayer core: * [`DelegationManager.registerOperatorToAVS`][core-registerToAVS] * [`DelegationManager.deregisterOperatorFromAVS`][core-deregisterFromAVS] -These methods ensure that the operator registering with the AVS is also registered as an operator in EigenLayer core. In this repo, these methods are called by the `ServiceManagerBase`. +These methods ensure that the Operator registering with the AVS is also registered as an Operator in EigenLayer core. In this repo, these methods are called by the `ServiceManagerBase`. -Eventually, operator slashing and payment for services will be part of the middleware/core relationship, but these features aren't implemented yet and their design is a work in progress. +Eventually, Operator slashing and payment for services will be part of the middleware/core relationship, but these features aren't implemented yet and their design is a work in progress. ### System Components @@ -112,9 +112,9 @@ Eventually, operator slashing and payment for services will be part of the middl | -------- | -------- | -------- | | [`ServiceManagerBase.sol`](../src/ServiceManagerBase.sol) | Singleton | Transparent proxy | -The Service Manager contract serves as the AVS's address relative to EigenLayer core contracts. When operators register for/deregister from the AVS, the Service Manager forwards this request to the DelegationManager (see [Hooking Into EigenLayer Core](#hooking-into-eigenlayer-core) above). +The Service Manager contract serves as the AVS's address relative to EigenLayer core contracts. When Operators register for/deregister from the AVS, the Service Manager forwards this request to the DelegationManager (see [Hooking Into EigenLayer Core](#hooking-into-eigenlayer-core) above). -It also contains a few convenience methods used to query operator information by the frontend. +It also contains a few convenience methods used to query Operator information by the frontend. See full documentation in [`ServiceManagerBase.md`](./ServiceManagerBase.md). @@ -127,12 +127,12 @@ See full documentation in [`ServiceManagerBase.md`](./ServiceManagerBase.md). | [`StakeRegistry.sol`](../src/StakeRegistry.sol) | Singleton | Transparent proxy | | [`IndexRegistry.sol`](../src/IndexRegistry.sol) | Singleton | Transparent proxy | -The `RegistryCoordinator` keeps track of which quorums exist and have been initialized. It is also the primary entry point for operators as they register for and deregister from an AVS's quorums. +The `RegistryCoordinator` keeps track of which quorums exist and have been initialized. It is also the primary entry point for Operators as they register for and deregister from an AVS's quorums. -When operators register or deregister, the registry coordinator updates that operator's currently-registered quorums, and pushes the registration/deregistration to each of the three registries it controls: -* `BLSApkRegistry`: tracks the aggregate BLS pubkey hash for the operators registered to each quorum. Also maintains a history of these aggregate pubkey hashes. -* `StakeRegistry`: interfaces with the EigenLayer core contracts to determine the weight of operators according to their stake and each quorum's configuration. Also maintains a history of these weights. -* `IndexRegistry`: assigns indices to operators within each quorum, and tracks historical indices and operators per quorum. Used primarily by offchain infrastructure to fetch ordered lists of operators in quorums. +When Operators register or deregister, the registry coordinator updates that Operator's currently-registered quorums, and pushes the registration/deregistration to each of the three registries it controls: +* `BLSApkRegistry`: tracks the aggregate BLS pubkey hash for the Operators registered to each quorum. Also maintains a history of these aggregate pubkey hashes. +* `StakeRegistry`: interfaces with the EigenLayer core contracts to determine the weight of Operators according to their stake and each quorum's configuration. Also maintains a history of these weights. +* `IndexRegistry`: assigns indices to Operators within each quorum, and tracks historical indices and Operators per quorum. Used primarily by offchain infrastructure to fetch ordered lists of Operators in quorums. Both the registry coordinator and each of the registries maintain historical state for the specific information they track. This historical state tracking can be used to query state at a particular block, which is primarily used in offchain infrastructure. @@ -145,7 +145,7 @@ See full documentation for the registry coordinator in [`RegistryCoordinator.md` | [`BLSSignatureChecker.sol`](../src/BLSSignatureChecker.sol) | Singleton | Transparent proxy | | [`OperatorStateRetriever.sol`](../src/OperatorStateRetriever.sol) | Singleton | Transparent proxy | -The BLSSignatureChecker verifies signatures made by the aggregate pubkeys ("Apk") of operators in one or more quorums. The primary function, `checkSignatures`, is called by an AVS when confirming that a given message hash is signed by operators belonging to one or more quorums. +The BLSSignatureChecker verifies signatures made by the aggregate pubkeys ("Apk") of Operators in one or more quorums. The primary function, `checkSignatures`, is called by an AVS when confirming that a given message hash is signed by Operators belonging to one or more quorums. The `OperatorStateRetriever` is used by offchain code to query the `RegistryCoordinator` (and its registries) for information that will ultimately be passed into `BLSSignatureChecker.checkSignatures`. diff --git a/docs/old/BLSSignatureChecker.md b/docs/old/BLSSignatureChecker.md deleted file mode 100644 index 1d1ec4cc..00000000 --- a/docs/old/BLSSignatureChecker.md +++ /dev/null @@ -1,44 +0,0 @@ -# BLSSignatureChecker - -This contract is deployed per AVS. It verifies the signatures of operators in an efficient way given the rest of the registry architecture. A lot of EigenLayer AVSs can be summarized as a quorum signature on a message and slashing if some quality of that message and other state is true. - -## Flows - -### checkSignatures -``` -function checkSignatures( - bytes32 msgHash, - bytes calldata quorumNumbers, - uint32 referenceBlockNumber, - NonSignerStakesAndSignature memory nonSignerStakesAndSignature - ) - -struct NonSignerStakesAndSignature { - uint32[] nonSignerQuorumBitmapIndices; - BN254.G1Point[] nonSignerPubkeys; - BN254.G1Point[] quorumApks; - BN254.G2Point apkG2; - BN254.G1Point sigma; - uint32[] quorumApkIndices; - uint32[] totalStakeIndices; - uint32[][] nonSignerStakeIndices; // nonSignerStakeIndices[quorumNumberIndex][nonSignerIndex] - } -``` -This function is called by an AVS aggregator when confirming that a `msgHash` is signed by certain `quorumNumbers`. - -The function calculates the sum (`apk`) of the aggregate public keys for all the quorums in question using the provided `quorumApkIndices` which points to hashes of the quorum aggregate public keys at the `referenceBlockNumber` of the signature of which `quorumApks` are the preimages. Since there may be nodes from the quorums that don't sign, the `nonSignerPubkeys` are subtracted from `apk`. There is a detail here that since an operator may serve more than one of the quorums in question, the number of quorums in `quorumNumbers` that the nonsigner served at the `referenceBlockNumber` is calculated using the provided `nonSignerQuorumBitmapIndices` and their public key is multiplied by the number before it is subtracted from `apk`. This gets rid of the duplicate additions of their public key because their public key is in more than one of the added `quorumApks`. Now the contract has `apk` set to the claimed aggregate public key of the signers. - -Next, the contract fetches the total stakes of each of the `quorumNumbers` at the `referenceBlockNumber` using the provided `totalStakeIndices`. The stakes of each of the nonsigners for each of the quorums are fetched using `nonSignerStakeIndices` and subtracted from the total stakes. Now the contract has the claimed signing stake for each of the quorums. - -Finally, the contract does a similar check to the [BLSPublicKeyCompendium](./BLSPublicKeyCompendium.md): - -- Calculates $\gamma = keccak256(apk, apkG2, sigma)$ -- Verifies the paring $e(\sigma + \gamma apk, [1]_2) = e(H(msgHash) + \gamma[1]_1, apkG2)$ - -More detailed notes exist on the signature check [here](https://geometry.xyz/notebook/Optimized-BLS-multisignatures-on-EVM). - -If it checks out, the contract returns the stake that signed the message for each quorum and the hash of the reference block number and the list of public key hashes of the nonsigners for future use. - -## Upstream Dependencies - -AVSs are expected to use this contract's method for their specific tasks. For example, EigenDA uses this function in their contracts when confirming batches of blobs on their DA layer onchain. diff --git a/docs/old/Old_BLSPubkeyRegistry.md b/docs/old/Old_BLSPubkeyRegistry.md deleted file mode 100644 index 27a788f9..00000000 --- a/docs/old/Old_BLSPubkeyRegistry.md +++ /dev/null @@ -1,36 +0,0 @@ -# BLSPubkeyRegistry - -This contract is a registry that keeps track of aggregate public key hashes of the quorums of an AVS over time. AVSs that want access to the aggregate public keys of their operator set over time should integrate this registry with their RegistryCoordinator. - -## Flows - -### registerOperator - -The RegistryCoordinator for the AVS makes a call to the BLSPubkeyRegistry to register an operator with a certain public key for a certain set of quorums. The BLSPubkeyRegistry verifies that the operator in fact owns the public key by [making a call to the BLSPublicKeyCompendium](./BLSPublicKeyCompendium.md#integrations). It then, for each quorum the operator is registering for, adds the operator's public key to the aggregate quorum public key, ends the active block range for the previous aggregate public key, and begins the active block range for the new aggregate quorum public key. Updates are stored using the following struct: -```solidity -/// @notice Data structure used to track the history of the Aggregate Public Key of all operators -struct ApkUpdate { - // first 24 bytes of keccak256(apk_x, apk_y) - bytes24 apkHash; - // block number at which the update occurred - uint32 updateBlockNumber; - // block number at which the next update occurred - uint32 nextUpdateBlockNumber; -} -``` - -The aggregate quorum public key stored in the contract is also overwritten with the new aggregate quorum public key. - -The function also returns the hash of the operator's public key as it may be used as is for the operator in the AVS. The [BLSRegistryCoordinator](./BLSRegistryCoordinatorWithIndices.md) uses the hash of the operator's public key as the operator's identifier (operator id) since it lowers gas costs in the [BLSSignatureChecker](./BLSSignatureChecker.md). - -### deregisterOperator - -The RegistryCoordinator for the AVS makes a call to the BLSPubkeyRegistry to deregister an operator with a certain public key for a certain set of quorums. The BLSPubkeyRegistry verifies that the operator in fact owns the public key by [making a call to the BLSPublicKeyCompendium](./BLSPublicKeyCompendium.md#integrations). It then, for each quorum the operator is registering for, subtracts the operator's public key from the aggregate quorum public key, ends the active block range for the previous aggregate public key, and begins the active block range for the new aggregate quorum public key. - -The aggregate quorum public key stored in the contract is also overwritten with the new aggregate quorum public key. - -Note that the contract does not check that the quorums that the operator's public key is being subtracted from are a subset of the quorums the operator is registered for, that logic is expected to be done in the RegistryCoordinator. - -## Upstream Dependencies - -The main integration with the BLSPublicKeyRegistry is used by the AVSs [BLSSignatureChecker](./BLSSignatureChecker.md). An offchain actor provides a public key, a quorum id, and an index in the array of aggregate quorum public key hashes, and the AVS's signature checker verifies that a certain quorum's aggregate public key hash at a certain block number was in fact the hash of the provided public key. Look at `getApkHashForQuorumAtBlockNumberFromIndex`. diff --git a/docs/old/Old_BLSRegistryCoordinatorWithIndices.md b/docs/old/Old_BLSRegistryCoordinatorWithIndices.md deleted file mode 100644 index 5acd89e1..00000000 --- a/docs/old/Old_BLSRegistryCoordinatorWithIndices.md +++ /dev/null @@ -1,86 +0,0 @@ -# BLSRegistryCoordinatorWithIndices - -This contract is deployed for every AVS and serves as the main entrypoint for operators to register and deregister from the AVS. In addition, it is where the AVS defines its operator churn parameters. - -## Flows - -### registerOperator - -When registering the operator must provide -1. The quorums they are registering for -2. Their BLS public key that they registered with the [BLSPubkeyCompendium](./BLSPublicKeyCompendium.md) -3. The socket (ip:port) at which AVS offchain actors should make requests - -The RegistryCoordinator then -1. Registers the operator's BLS public key with the [BLSPubkeyRegistry](BLSPubkeyRegistry.md) and notes the hash of their public key as their operator id -2. Registers the operator with the [StakeRegistry](./StakeRegistry.md) -3. Registers the operator with the [IndexRegistry](./IndexRegistry.md) -4. Stores the quorum bitmap of the operator using the following struct: -``` -/** - * @notice Data structure for storing info on quorum bitmap updates where the `quorumBitmap` is the bitmap of the - * quorums the operator is registered for starting at (inclusive)`updateBlockNumber` and ending at (exclusive) `nextUpdateBlockNumber` - * @dev nextUpdateBlockNumber is initialized to 0 for the latest update - */ -struct QuorumBitmapUpdate { - uint32 updateBlockNumber; - uint32 nextUpdateBlockNumber; - uint192 quorumBitmap; -} -``` - -Operators can be registered for certain quorums and later register for other (non-overlapping) quorums. - -### If quorum full - -The following struct is defined for each quorum -``` -/** - * @notice Data structure for storing operator set params for a given quorum. Specifically the - * `maxOperatorCount` is the maximum number of operators that can be registered for the quorum - * `kickBIPsOfOperatorStake` is the multiple (in basis points) of stake that a new operator must have, as compared the operator that they are kicking out of the quorum - * `kickBIPsOfTotalStake` is the fraction (in basis points) of the total stake of the quorum that an operator needs to be below to be kicked. - */ -struct OperatorSetParam { - uint32 maxOperatorCount; - uint16 kickBIPsOfOperatorStake; - uint16 kickBIPsOfTotalStake; -} -``` - -If any of the quorums is full (number of operators in it is `maxOperatorCount`), the newly registering operator must provide the public key of another operator to be kicked. The new and kicked operator must satisfy the two conditions: -1. the new operator has an amount of stake that is at least `kickBIPsOfOperatorStake` multiple of the kicked operator's stake -2. the kicked operator has less than `kickBIPsOfTotalStake` fraction of the quorum's total stake - -The provided operators are deregistered from the respective quorums that are full which the registering operator is registering for. Since the operators being kicked may not be the operators with the least stake, the RegistryCoordinator requires that the provided operators are signed off by a permissioned address called a `churnApprover`. Note that the quorum operator caps are due to the cost of BLS signature (dis)aggregation onchain. - -Operators register with a list of -``` -/** -* @notice Data structure for the parameters needed to kick an operator from a quorum with number `quorumNumber`, used during registration churn. -* Specifically the `operator` is the address of the operator to kick, `pubkey` is the BLS public key of the operator, -*/ -struct OperatorKickParam { - uint8 quorumNumber; - address operator; - BN254.G1Point pubkey; -} -``` -For each quorum they need to kick operators from. This list, along with the id of the registering operator needs to be signed (along with a salt and expiry) by an actor known as the *churnApprover*. Operators will make a request to the churnApprover offchain before registering for their signature, if needed. - -### deregisterOperator - -When deregistering, an operator provides -1. The quorums they registered for -2. Their BLS public key -3. The ids of the operators that must swap indices with the [deregistering operator in the IndexRegistry](./IndexRegistry.md#deregisteroperator). - -The RegistryCoordinator then deregisters the operator with the BLSPubkeyRegistry, StakeRegistry, and IndexRegistry. It then ends the block range for its stored quorum bitmap for the operator. - -Operators can deregister from a subset of quorums that they are registered for. - -## Upstream Dependencies - -Operators register and deregister with the AVS for certain quorums through this contract. - -EigenLabs intends to run the EigenDA churnApprover. \ No newline at end of file diff --git a/docs/old/Old_IndexRegistry.md b/docs/old/Old_IndexRegistry.md deleted file mode 100644 index ad7c3f1e..00000000 --- a/docs/old/Old_IndexRegistry.md +++ /dev/null @@ -1,48 +0,0 @@ -# IndexRegistry - -This contract assigns each operator an index (0 indexed) within each of its quorums. If a quorum has $n$ operators, each operator will be assigned an index $0$ through $n-1$. This contract is used for AVSs that need a common ordering among all operators in a quorum that is accessible onchain. For example, this will be used in proofs of custody by EigenDA. This contract also keeps a list of all operators that have ever joined the AVS for convenience purposes in offchain software that are out of scope for this document. - -## Flows - -### registerOperator - -The RegistryCoordinator for the AVS makes call to the IndexRegistry to register an operator for a certain set of quorums. The IndexRegistry will assign the next index in each of the quorums the operator is registering for to the operator storing the following struct: -```solidity -// struct used to give definitive ordering to operators at each blockNumber. -struct OperatorIndexUpdate { - // blockNumber number from which `index` was the operators index - // the operator's index is the first entry such that `blockNumber >= entry.fromBlockNumber` - uint32 fromBlockNumber; - // index of the operator in array of operators - // index = type(uint32).max = OPERATOR_DEREGISTERED_INDEX implies the operator was deregistered - uint32 index; -} -``` - -The IndexRegistry also adds the operator's id to the append only list of operators that have registered for the middleware and it stores the total number of operators after the registering operator has registered for each of the quorums the operator is registering for by pushing the below struct to a growing array. - -```solidity -// struct used to denote the number of operators in a quorum at a given blockNumber -struct QuorumUpdate { - // The total number of operators at a `blockNumber` is the first entry such that `blockNumber >= entry.fromBlockNumber` - uint32 fromBlockNumber; - // The number of operators at `fromBlockNumber` - uint32 numOperators; -} -``` - -### deregisterOperator - -The RegistryCoordinator for the AVS makes call to the IndexRegistry to deregister an operator for a certain set of quorums. The RegistryCoordinator provides a witness of the ids of the operators that have the greatest index in each of the quorums that the operator is deregistering from. The IndexRegistry then, for each quorum the operator is deregistering from, -1. Decrements the total number of operators in the quorum -2. Makes sure the provided "greatest index operator id" in fact has the greatest index in the quorum by checking it against the total number of operators in the quorum -3. Sets the index of the "greatest index operator" to the index of the deregistering operator -4. Sets the index of the deregistering operator to `OPERATOR_DEREGISTERED_INDEX = type(uint32).max` - -Steps 3 and 4 are done via pushing the above struct to a growing array that is kept track of for each operator. - -Note that the contract does not check that the quorums that the operator is being deregistered from are a subset of the quorums the operator is registered for, that logic is expected to be done in the RegistryCoordinator. - -## Upstream Dependencies - -The [BLSOperatorStateRetriever](./BLSOperatorStateRetriever.md) uses the globally ordered list of all operators every registered for the AVS to serve information about the active operator set for the AVS to offchain nodes. \ No newline at end of file diff --git a/docs/old/Old_README.md b/docs/old/Old_README.md deleted file mode 100644 index a6901cb2..00000000 --- a/docs/old/Old_README.md +++ /dev/null @@ -1,101 +0,0 @@ -# EigenLayer Middleware - -## Introduction - -EigenLayer AVSs are a new type of protocol that makes use of EigenLayer’s restaking primitive. AVSs are different from current chains and other smart contract protocols in that they are validated by EigenLayer operators. There are 3 specific types of conditions that AVSs implement in smart contracts onchain: - -- Registration/Deregistration conditions: What requirements do operators need to have in order to register for/deregister from the AVS? -- Payment conditions: How much does a certain operator deserve to be paid for their validation? In what form are they paid? -- Slashing conditions: What behavior is not allowed of operators by the AVS? What exact mechanism should be used to determine this behavior onchain? - -The EigenLabs dev team has been building out a smart contract architecture for AVSs to provide a base for AVSs to build on top of for their specific use case. They have been using it internally for the first AVS on EigenLayer: EigenDA. - -## The Registry Coordinator and Registries - -![Registry Architecture](./docs/images/registry_architecture.png) - -There are two things that matter in terms of operators’ onchain interaction with AVS contracts: - -- What qualities of operators need to be kept track of onchain? (e.g. stake, BLS pubkeys, etc.) -- Registration/Deregistration conditions - -These two points have been addressed through the Registry Coordinator/Registry Architecture. - -### Definitions - -#### Quorums - -Quorums are the different divisions of the operator set for an AVS. One can think of a quorum being defined by the token staked for that quorum, although [it is slightly more complicated than that](./docs/StakeRegistry.md#definitions). One often wants to make trust assumptions on quorums, but wants many quorums for the same AVS. - -One example of the quorum concept is in EigenDA, where we have a single ETH quorum for which LSTs and native beacon chain ETH are accepted as stake and another quorum for each rollup that wants to stake their own token for security. - -### RegistryCoordinator - -The Registry Coordinator is a contract that is deployed by each AVS. It handles - -- Keeping track of what quorums operators are a part of -- Handling operator churn (registration/deregistration) -- Communicating to registries - The current implementation of this contract is the [BLSRegistryCoordinatorWithIndices](./docs/BLSRegistryCoordinatorWithIndices.md). - -### Registries - -The registries are contracts that keep track of the attributes of individual operators. For example, we have initially built the - -- [StakeRegistry](./docs/StakeRegistry.md) which keeps track of the stakes of different operators for different quorums at different times -- [BLSPubkeyRegistry](./docs/BLSPubkeyRegistry.md) which keeps track of the aggregate public key of different quorums at different times. Note that the AVS contracts use [BLS aggregate signatures](#bls-signature-checker) due to their favorable scalability to large operator sets. -- [IndexRegistry](./docs/IndexRegistry.md) which keeps track of an ordered list of the operators in each quorum at different times. (note that this behavior is likely only needed for EigenDA) - Registries are meant to be read from and indexed by offchain AVS actors (operators and AVS coordinators). - -A registry coordinator has 1 or more registries connected to it and all of them are called when an operator registers or deregisters. They are a plug and play system that are meant to be added to the RegistryCoordinator as needed. AVSs should create registries they need for their purpose. - -### Note on (active) block ranges - -Note that the registry contract implementations use lists structs similar to the following type (with the `Value` type altered): - -```solidity -struct ValueUpdateA { - Value value; - uint32 updateBlockNumber; // when the value started being valid - uint32 nextUpdateBlockNumber; // then the value stopped being valid or 0, if the value is still valid -} -``` - -or - -```solidity -struct ValueUpdateB { - Value value; - uint32 toBlockNumber; // when the value expired -} -``` - -These structs, consecutively, are a history of the `Value` over certain ranges of blocks. These are (aptly) called block ranges, and _active_ block ranges for the `ValueUpdate` that contains the current block number. - -## TODO: Service Manager - -## BLS Signature Checker - -At the core of many AVSs on EigenLayer (almost all except those that affect Ethereum block production) is the verification of a quorum signature of an AVS's operator set on a certain message and slashing if some quality of that message and other state is true. The registry architecture is optimized for making this signature as cheap as possible to verify (it is still relatively expensive). - -The current implementation of this contract is the [BLSSignatureChecker](./docs/BLSSignatureChecker.md). - -## Deployments - -### M2 Testnet (Current Goerli Deployment) - -| Name | Solidity | Contract | Notes | -| ------------------------- | ----------------------------- | ------------------------------------------------------------------------------------------------- | ----- | -| BLSOperatorStateRetriever | BLSOperatorStateRetriever.sol | [`0x737D...A3a3`](https://goerli.etherscan.io/address/0x737Dd62816a9392e84Fa21C531aF77C00816A3a3) | | -| BLSPubkeyCompendium | BLSPubkeyCompendium.sol | [`0xc81d...1b19`](https://goerli.etherscan.io/address/0xc81d3963087Fe09316cd1E032457989C7aC91b19) | | - -## Further reading - -More detailed functional docs have been written on the AVS architecture implemented in the middleware contracts. The recommended order for reading the other docs in this folder is - -1. [BLSRegistryCoordinatorWithIndices](./docs/BLSRegistryCoordinatorWithIndices.md) -2. [BLSPublicKeyCompendium](./docs/BLSPublicKeyCompendium.md) and [BLSPublicKeyRegistry](./docs/BLSPubkeyRegistry.md) -3. [StakeRegistry](./docs/StakeRegistry.md) -4. [IndexRegistry](./docs/IndexRegistry.md) -5. [BLSOperatorStateRetriever](./docs/BLSOperatorStateRetriever.md) -6. [BLSSignatureChecker](./docs/BLSSignatureChecker.md) diff --git a/docs/old/Old_StakeRegistry.md b/docs/old/Old_StakeRegistry.md deleted file mode 100644 index 3ca598f9..00000000 --- a/docs/old/Old_StakeRegistry.md +++ /dev/null @@ -1,56 +0,0 @@ -# StakeRegistry - -This contract is deployed for every AVS and keeps track of the AVS's operators' stakes over time and the total stakes for each quorum. In addition, this contract also handles the addition and modification of quorum. - -# Definitions - -A **quorum** is defined by list of the following structs: -``` -struct StrategyAndWeightingMultiplier { - IStrategy strategy; - uint96 multiplier; -} -``` - -## Flows - -### createQuorum - -The owner of the StakeRegistry can create a quorum by providing the list of `StrategyAndWeightingMultiplier`s. Quorums cannot be removed. - -### modifyQuorum - -The owner of the StakeRegistry can modify the set of strategies and they multipliers for a certain quorum. - -### registerOperator - -The RegistryCoordinator for the AVS makes a call to the StakeRegistry to register an operator for a certain set of quorums. For each of the quorums being registered for, the StakeRegistry calculates a linear combination of the operator's delegated shares of each `strategy` in the quorum and their corresponding `multiplier` to get a `stake`. The contract then stores the stake in the following struct: -``` -/// @notice struct used to store the stakes of an individual operator or the sum of all operators' stakes, for storage -struct OperatorStakeUpdate { - // the block number at which the stake amounts were updated and stored - uint32 updateBlockNumber; - // the block number at which the *next update* occurred. - /// @notice This entry has the value **0** until another update takes place. - uint32 nextUpdateBlockNumber; - // stake weight for the quorum - uint96 stake; -} -``` -For each quorum the operator is a part of. - -### deregisterOperator - -The RegistryCoordinator for the AVS calls the StakeRegistry to deregister an operator for a certain set of quorums. For each of the quorums being registered for, the StakeRegistry ends the block range of the current `OperatorStakeUpdate` for the operator for the quorum. - -Note that the contract does not check that the quorums that the operator is being deregistered from are a subset of the quorums the operator is registered for, that logic is expected to be done in the RegistryCoordinator. - -### updateStakes - -An offchain actor can provide a list of operator ids, their corresponding addresses, and a few other witnesses in order to recalculate the stakes of the provided operators for all of the quorums each operator is registered for. This ends block range of the current `OperatorStakeUpdate`s for each of the quorums for each of the provided operators and pushes a new update for each of them. - -This has more implications after slashing is enabled... TODO - -## Upstream Dependencies - -The main integration with the StakeRegistry is used by the AVSs [BLSSignatureChecker](./BLSSignatureChecker.md). An offchain actor provides an operator id, a quorum id, and an index in the array of the operator's stake updates to verify the stake of an operator at a particular block number. They also provide a quorum id and an index in the array of total stake updates to verify the stake of the entire quorum at a particular block number. diff --git a/docs/old/OperatorStateRetriever.md b/docs/old/OperatorStateRetriever.md deleted file mode 100644 index 9dbffe38..00000000 --- a/docs/old/OperatorStateRetriever.md +++ /dev/null @@ -1,21 +0,0 @@ -# BLSOperatorStateRetriever - -This contract is deployed once and is intended to be used by all AVSs that use the provided registry contracts. It is used as a utility for offchain AVS actors to fetch the details of AVSs operators, their stakes in the StakeRegistry, and their indices in the IndexRegistry. - -## Flows - -This contract is a bunch of view functions that are very gas expensive, they are meant to only be called by offchain actors. - -### getOperatorState - -This gets the ordered list of operators and their stakes for the provided quorum numbers for the AVS with the provided registry coordinator at the provided block number - -There is an overloaded version of this function that takes an operator id and uses the quorum numbers they were registered for instead of having the caller provide quorum numbers. - -### getCheckSignaturesIndices - -This function is particularly called by AVS aggregators, who get BLS signatures from AVS operators to confirm their signature onchain. Using the provided block number, registry coordinator, quorum numbers, and non signing operator ids, it returns the correct various indices required to confirm signatures with the [BLSSignatureChecker](./BLSSignatureChecker.md). - -## Upstream Dependencies - -Again, this is called by offchain actors during requests from AVS offchain actors. For example, in EigenDA, a disperser sends data to operators, and those operators are expected to call functions in this contract to make sure they have received the correct amount of data, among other things. \ No newline at end of file diff --git a/docs/registries/BLSApkRegistry.md b/docs/registries/BLSApkRegistry.md index 935ce4a2..031936b8 100644 --- a/docs/registries/BLSApkRegistry.md +++ b/docs/registries/BLSApkRegistry.md @@ -1,102 +1,161 @@ ## BLSApkRegistry -| File | Type | Proxy? | +| File | Type | Proxy | | -------- | -------- | -------- | | [`BLSApkRegistry.sol`](../../src/BLSApkRegistry.sol) | Singleton | Transparent proxy | -TODO +The `BLSApkRegistry` tracks the current aggregate BLS pubkey for all Operators registered to each quorum, and keeps a historical record of each quorum's aggregate BLS pubkey hash. This contract makes heavy use of the `BN254` library to perform various operations on the BN254 elliptic curve (see [`BN254.sol`](../../src/libraries/BN254.sol)). -#### High-level Concepts +Each time an Operator registers for a quorum, its BLS pubkey is added to that quorum's `currentApk`. Each time an Operator deregisters from a quorum, its BLS pubkey is subtracted from that quorum's `currentApk`. This contract maintains a history of the hash of each quorum's apk over time, which is used by the `BLSSignatureChecker` to fetch the "total signing key" for a quorum at a specific block number. -TODO +#### High-level Concepts This document organizes methods according to the following themes (click each to be taken to the relevant section): - -TODO - - -#### Important State Variables - -TODO - - --- -### Theme - -TODO +### Registering and Deregistering - - -#### `registerOperator` +#### `registerBLSPublicKey` ```solidity - +function registerBLSPublicKey( + address operator, + PubkeyRegistrationParams calldata params, + BN254.G1Point calldata pubkeyRegistrationMessageHash +) + external + onlyRegistryCoordinator + returns (bytes32 operatorId) + +struct PubkeyRegistrationParams { + BN254.G1Point pubkeyRegistrationSignature; + BN254.G1Point pubkeyG1; + BN254.G2Point pubkeyG2; +} ``` -TODO +This method is ONLY callable by the `RegistryCoordinator`. It is called when an Operator registers for the AVS for the first time. + +This method validates a BLS signature over the `pubkeyRegistrationMessageHash`, then permanently assigns the pubkey to the Operator. The hash of `params.pubkeyG1` becomes the Operator's unique `operatorId`, which identifies the Operator throughout the registry contracts. + +*Entry Points*: +* `RegistryCoordinator.registerOperator` +* `RegistryCoordinator.registerOperatorWithChurn` *Effects*: -* +* Registers the Operator's BLS pubkey for the first time, updating the following mappings: + * `operatorToPubkey[operator]` + * `operatorToPubkeyHash[operator]` + * `pubkeyHashToOperator[pubkeyHash]` *Requirements*: -* +* Caller MUST be the `RegistryCoordinator` +* `params.pubkeyG1` MUST NOT hash to the `ZERO_PK_HASH` +* `operator` MUST NOT have already registered a pubkey: + * `operatorToPubkeyHash[operator]` MUST be zero + * `pubkeyHashToOperator[pubkeyHash]` MUST be zero +* `params.pubkeyRegistrationSignature` MUST be a valid signature over `pubkeyRegistrationMessageHash` -#### `deregisterOperator` +#### `registerOperator` ```solidity - +function registerOperator( + address operator, + bytes memory quorumNumbers +) + public + virtual + onlyRegistryCoordinator ``` -TODO +`registerOperator` fetches the Operator's registered BLS pubkey (see `registerBLSPublicKey` above). Then, for each quorum in `quorumNumbers`, the Operator's pubkey is added to that quorum's `currentApk`. The `apkHistory` for the `quorumNumber` is also updated to reflect this change. + +This method is ONLY callable by the `RegistryCoordinator`, and is called when an Operator registers for one or more quorums. This method *assumes* that `operator` is not already registered for any of `quorumNumbers`, and that there are no duplicates in `quorumNumbers`. These properties are enforced by the `RegistryCoordinator`. + +*Entry Points*: +* `RegistryCoordinator.registerOperator` +* `RegistryCoordinator.registerOperatorWithChurn` *Effects*: -* +* For each `quorum` in `quorumNumbers`: + * Add the Operator's pubkey to the quorum's apk in `currentApk[quorum]` + * Updates the quorum's `apkHistory`, pushing a new `ApkUpdate` for the current block number and setting its `apkHash` to the new hash of `currentApk[quorum]`. + * *Note:* If the most recent entry in `apkHistory[quorum]` was made during the current block, this method updates the most recent entry rather than pushing a new one. *Requirements*: -* +* Caller MUST be the `RegistryCoordinator` +* `operator` MUST already have a registered BLS pubkey (see `registerBLSPublicKey` above) +* Each quorum in `quorumNumbers` MUST be initialized (see `initializeQuorum` below) -#### `registerBLSPublicKey` +#### `deregisterOperator` ```solidity - +function deregisterOperator( + address operator, + bytes memory quorumNumbers +) + public + virtual + onlyRegistryCoordinator ``` -TODO +`deregisterOperator` fetches the Operator's registered BLS pubkey (see `registerBLSPublicKey` above). For each quorum in `quorumNumbers`, `deregisterOperator` performs the same steps as `registerOperator` above - except that the Operator's pubkey is negated. Whereas `registerOperator` "adds" a pubkey to each quorum's apk, `deregisterOperator` "subtracts" a pubkey from each quorum's apk. + +This method is ONLY callable by the `RegistryCoordinator`, and is called when an Operator deregisters from one or more quorums. This method *assumes* that `operator` is registered for all quorums in `quorumNumbers`, and that there are no duplicates in `quorumNumbers`. These properties are enforced by the `RegistryCoordinator`. + +*Entry Points*: +* `RegistryCoordinator.registerOperatorWithChurn` +* `RegistryCoordinator.deregisterOperator` +* `RegistryCoordinator.ejectOperator` +* `RegistryCoordinator.updateOperators` +* `RegistryCoordinator.updateOperatorsForQuorum` *Effects*: -* +* For each `quorum` in `quorumNumbers`: + * Negate the Operator's pubkey, then subtract it from the quorum's apk in `currentApk[quorum]` + * Updates the quorum's `apkHistory`, pushing a new `ApkUpdate` for the current block number and setting its `apkHash` to the new hash of `currentApk[quorum]`. + * *Note:* If the most recent entry in `apkHistory[quorum]` was made during the current block, this method updates the most recent entry rather than pushing a new one. *Requirements*: -* +* Caller MUST be the `RegistryCoordinator` +* `operator` MUST already have a registered BLS pubkey (see `registerBLSPublicKey` above) +* Each quorum in `quorumNumbers` MUST be initialized (see `initializeQuorum` below) + +--- + +### System Configuration #### `initializeQuorum` ```solidity - +function initializeQuorum( + uint8 quorumNumber +) + public + virtual + onlyRegistryCoordinator ``` -TODO +This method is ONLY callable by the `RegistryCoordinator`. It is called when the `RegistryCoordinator` Owner creates a new quorum. + +`initializeQuorum` initializes a new quorum by pushing an initial `ApkUpdate` to `apkHistory[quorumNumber]`. Other methods can validate that a quorum exists by checking whether `apkHistory[quorumNumber]` has a nonzero length. + +*Entry Points*: +* `RegistryCoordinator.createQuorum` *Effects*: -* +* Pushes an `ApkUpdate` to `apkHistory[quorumNumber]`. The update has a zeroed out `apkHash`, and its `updateBlockNumber` is set to the current block. *Requirements*: -* +* Caller MUST be the `RegistryCoordinator` +* `apkHistory[quorumNumber].length` MUST be zero --- \ No newline at end of file diff --git a/docs/registries/IndexRegistry.md b/docs/registries/IndexRegistry.md index 8f3aa89d..a7db94a0 100644 --- a/docs/registries/IndexRegistry.md +++ b/docs/registries/IndexRegistry.md @@ -4,85 +4,163 @@ | -------- | -------- | -------- | | [`IndexRegistry.sol`](../../src/IndexRegistry.sol) | Singleton | Transparent proxy | -TODO +The `IndexRegistry` provides an index for every registered Operator in every quorum. For example, if a quorum has `n` Operators, every Operator registered for that quorum will have an index in the range `[0:n-1]`. The role of this contract is to provide an AVS with a common, on-chain ordering of Operators within a quorum. -#### High-level Concepts +*In EigenDA*, the Operator ordering properties of the `IndexRegistry` will eventually be used in proofs of custody, though this feature is not implemented yet. -TODO +#### Important State Variables -This document organizes methods according to the following themes (click each to be taken to the relevant section): +```solidity +/// @notice maps quorumNumber => operator id => current index +mapping(uint8 => mapping(bytes32 => uint32)) public currentOperatorIndex; + +/// @notice maps quorumNumber => index => historical operator ids at that index +mapping(uint8 => mapping(uint32 => OperatorUpdate[])) internal _indexHistory; + +/// @notice maps quorumNumber => historical number of unique registered operators +mapping(uint8 => QuorumUpdate[]) internal _operatorCountHistory; + +struct OperatorUpdate { + uint32 fromBlockNumber; + bytes32 operatorId; +} + +struct QuorumUpdate { + uint32 fromBlockNumber; + uint32 numOperators; +} +``` -TODO - +Operators are assigned a unique index in each quorum they're registered for. If a quorum has `n` registered Operators, every Operator in that quorum will have an index in the range `[0:n-1]`. To accomplish this, the `IndexRegistry` uses the three mappings listed above: +* `currentOperatorIndex` is a straightforward mapping of an Operator's current index in a specific quorum. It is updated when an Operator registers for a quorum. +* `_indexHistory` keeps track of the `operatorIds` assigned to an index at various points in time. This is used by offchain code to determine what `operatorId` belonged to an index at a specific block. +* `_operatorCountHistory` keeps track of the number of Operators registered to each quorum over time. Note that a quorum's Operator count is also its "max index". Paired with `_indexHistory`, this allows offchain code to query the entire Operator set registered for a quorum at a given block number. For an example of this in the code, see `IndexRegistry.getOperatorListAtBlockNumber`. -#### Important State Variables +*Note*: `currentOperatorIndex` is ONLY updated when an Operator is *assigned* to an index. When an Operator deregisters and is removed, we don't update `currentOperatorIndex` because their index is not "0" - that's held by another Operator. Their index is also not the index they currently have. There's not really a "right answer" for this - see https://github.com/Layr-Labs/eigenlayer-middleware/issues/126 for more details. -TODO +#### High-level Concepts - +This document organizes methods according to the following themes (click each to be taken to the relevant section): +* [Registering and Deregistering](#registering-and-deregistering) +* [System Configuration](#system-configuration) --- -### Theme - -TODO - - +These methods are ONLY called through the `RegistryCoordinator` - when an Operator registers for or deregisters from one or more quorums: +* [`registerOperator`](#registeroperator) +* [`deregisterOperator`](#deregisteroperator) #### `registerOperator` ```solidity - +function registerOperator( + bytes32 operatorId, + bytes calldata quorumNumbers +) + public + virtual + onlyRegistryCoordinator + returns(uint32[] memory) ``` -TODO +When an Operator registers for a quorum, the following things happen: +1. The current Operator count for the quorum is increased. + * This updates `_operatorCountHistory[quorum]`. The quorum's new "max index" is equal to the previous Operator count. + * Additionally, if the `_indexHistory` for the quorum indicates that this is the first time the quorum has reached a given Operator count, an initial `OperatorUpdate` is pushed to `_indexHistory` for the new operator count. This is to maintain an invariant: that existing indices have nonzero history. +2. The quorum's max index (previous Operator count) is assigned to the registering Operator as their current index. + * This updates `currentOperatorIndex[quorum][operatorId]` + * This also updates `_indexHistory[quorum][prevOperatorCount]`, recording the `operatorId` as the latest holder of the index in question. + +This method is ONLY callable by the `RegistryCoordinator`, and is called when an Operator registers for one or more quorums. This method *assumes* that the `operatorId` is not already registered for any of `quorumNumbers`, and that there are no duplicates in `quorumNumbers`. These properties are enforced by the `RegistryCoordinator`. + +*Entry Points*: +* `RegistryCoordinator.registerOperator` +* `RegistryCoordinator.registerOperatorWithChurn` *Effects*: -* +* For each `quorum` in `quorumNumbers`: + * Updates `_operatorCountHistory[quorum]`, increasing the quorum's `numOperators` by 1. + * Note that if the most recent update for the quorum is from the current block number, the entry is updated. Otherwise, a new entry is pushed. + * Updates `_indexHistory[quorum][newOperatorCount - 1]`, recording the `operatorId` as the latest holder of the new max index. + * Note that if the most recent update for the quorum's index is from the current block number, the entry is updated. Otherwise, a new entry is pushed. + * Updates `currentOperatorIndex[quorum][operatorId]`, assigning the `operatorId` to the new max index. *Requirements*: -* +* Caller MUST be the `RegistryCoordinator` +* Each quorum in `quorumNumbers` MUST be initialized (see `initializeQuorum` below) #### `deregisterOperator` ```solidity - +function deregisterOperator( + bytes32 operatorId, + bytes calldata quorumNumbers +) + public + virtual + onlyRegistryCoordinator ``` -TODO +When an Operator deregisters from a quorum, the following things happen: +1. The current Operator count for the quorum is decreased, updating `_operatorCountHistory[quorum]`. The new "max index" is equal to the new Operator count (minus 1). +2. The Operator currently assigned to the now-invalid index is "popped". + * This updates `_indexHistory[quorum][newOperatorCount]`, recording that the Operator assigned to this index is `OPERATOR_DOES_NOT_EXIST_ID` +3. If the deregistering Operator and the popped Operator are not the same, the popped Operator is assigned a new index: the deregistering Operator's previous index. + * This updates `_indexHistory[quorum][removedOperatorIndex]`, recording that the popped Operator is assigned to this index. + * This also updates `currentOperatorIndex[quorum][removedOperator]`, assigning the popped Operator to the old Operator's index. + +This method is ONLY callable by the `RegistryCoordinator`, and is called when an Operator deregisters from one or more quorums. This method *assumes* that the `operatorId` is currently registered for each quorum in `quorumNumbers`, and that there are no duplicates in `quorumNumbers`. These properties are enforced by the `RegistryCoordinator`. + +*Entry Points*: +* `RegistryCoordinator.registerOperatorWithChurn` +* `RegistryCoordinator.deregisterOperator` +* `RegistryCoordinator.ejectOperator` +* `RegistryCoordinator.updateOperators` +* `RegistryCoordinator.updateOperatorsForQuorum` *Effects*: -* +* For each `quorum` in `quorumNumbers`: + * Updates `_operatorCountHistory[quorum]`, decreasing the quorum's `numOperators` by 1. + * Note that if the most recent update for the quorum is from the current block number, the entry is updated. Otherwise, a new entry is pushed. + * Updates `_indexHistory[quorum][newOperatorCount]`, "popping" the Operator that currently holds this index, and marking it as assigned to `OPERATOR_DOES_NOT_EXIST_ID`. + * Note that if the most recent update for the quorum's index is from the current block number, the entry is updated. Otherwise, a new entry is pushed. + * If `operatorId` is NOT the popped Operator, the popped Operator is assigned to `operatorId's` current index. (Updates `_indexHistory` and `currentOperatorIndex`) *Requirements*: -* +* Caller MUST be the `RegistryCoordinator` +* Each quorum in `quorumNumbers` MUST be initialized (see `initializeQuorum` below) + +--- + +### System Configuration #### `initializeQuorum` ```solidity - +function initializeQuorum( + uint8 quorumNumber +) + public + virtual + onlyRegistryCoordinator ``` -TODO +This method is ONLY callable by the `RegistryCoordinator`. It is called when the `RegistryCoordinator` Owner creates a new quorum. + +`initializeQuorum` initializes a new quorum by pushing an initial `QuorumUpdate` to `_operatorCountHistory[quorumNumber]`, setting the initial `numOperators` for the quorum to 0. + +Other methods can validate that a quorum exists by checking whether `_operatorCountHistory[quorumNumber]` has a nonzero length. + +*Entry Points*: +* `RegistryCoordinator.createQuorum` *Effects*: -* +* Pushes a `QuorumUpdate` to `_operatorCountHistory[quorumNumber]`. The update's `updateBlockNumber` is set to the current block, and `numOperators` is set to 0. *Requirements*: -* +* Caller MUST be the `RegistryCoordinator` +* `_operatorCountHistory[quorumNumber].length` MUST be zero --- \ No newline at end of file diff --git a/docs/registries/StakeRegistry.md b/docs/registries/StakeRegistry.md index ba0e67b4..1764975d 100644 --- a/docs/registries/StakeRegistry.md +++ b/docs/registries/StakeRegistry.md @@ -1,158 +1,339 @@ +[core-docs-m2]: https://github.com/Layr-Labs/eigenlayer-contracts/tree/m2-mainnet/docs + ## StakeRegistry -| File | Type | Proxy? | +| File | Type | Proxy | | -------- | -------- | -------- | | [`StakeRegistry.sol`](../src/StakeRegistry.sol) | Singleton | Transparent proxy | -TODO +The `StakeRegistry` interfaces with the EigenLayer core contracts to determine the individual and collective stake weight of each Operator registered for each quorum. These weights are used to determine an Operator's relative weight for each of an AVS's quorums. And in the `RegistryCoordinator` specifically, they play an important role in *churn*: determining whether an Operator is eligible to replace another Operator in a quorum. -#### High-level Concepts +#### Calculating Stake Weight -TODO +Stake weight is primarily a function of the number of shares an Operator has been delegated within the EigenLayer core contracts, along with a per-quorum configuration maintained by the `RegistryCoordinator` Owner (see [System Configuration](#system-configuration) below). This configuration determines, for a given quorum, which Strategies "count" towards an Operator's total stake weight, as well as "how much" each Strategy counts for: -This document organizes methods according to the following themes (click each to be taken to the relevant section): +```solidity +/// @notice maps quorumNumber => list of strategies considered (and each strategy's multiplier) +mapping(uint8 => StrategyParams[]) public strategyParams; -TODO - +struct StrategyParams { + IStrategy strategy; + uint96 multiplier; +} +``` -#### Important State Variables +For a given quorum, an Operator's stake weight is determined by iterating over the quorum's list of `StrategyParams` and querying `DelegationManager.operatorShares(operator, strategy)`. The result is multiplied by the corresponding `multiplier` (and divided by the `WEIGHTING_DIVISOR`) to calculate the Operator's weight for that strategy. Then, this result is added to a growing sum of stake weights -- and after the quorum's `StrategyParams` have all been considered, the Operator's total stake weight is calculated. -TODO +Note that the `RegistryCoordinator` Owner also configures a "minimum stake" for each quorum, which an Operator must meet in order to register for (or remain registered for) a quorum. - +For more information on the `DelegationManager`, strategies, and shares, see the [EigenLayer core docs][core-docs-m2]. ---- +#### High-level Concepts -### Theme +This document organizes methods according to the following themes (click each to be taken to the relevant section): +* [Registering and Deregistering](#registering-and-deregistering) +* [Updating Registered Operators](#updating-registered-operators) +* [System Configuration](#system-configuration) -TODO +--- - +These methods are ONLY called through the `RegistryCoordinator` - when an Operator registers for or deregisters from one or more quorums: +* [`registerOperator`](#registeroperator) +* [`deregisterOperator`](#deregisteroperator) #### `registerOperator` ```solidity - +function registerOperator( + address operator, + bytes32 operatorId, + bytes calldata quorumNumbers +) + public + virtual + onlyRegistryCoordinator + returns (uint96[] memory, uint96[] memory) ``` -TODO +When an Operator registers for a quorum, the `StakeRegistry` first calculates the Operator's current weighted stake. If the Operator meets the quorum's configured minimum stake, the Operator's `operatorStakeHistory` is updated to reflect the Operator's current stake. + +Additionally, the Operator's stake is added to the `_totalStakeHistory` for that quorum. + +This method is ONLY callable by the `RegistryCoordinator`, and is called when an Operator registers for one or more quorums. This method *assumes* that: +* `operatorId` belongs to the `operator` +* `operatorId` is not already registered for any of `quorumNumbers` +* There are no duplicates in `quorumNumbers` + +These properties are enforced by the `RegistryCoordinator`. + +*Entry Points*: +* `RegistryCoordinator.registerOperator` +* `RegistryCoordinator.registerOperatorWithChurn` *Effects*: -* +* For each `quorum` in `quorumNumbers`: + * The Operator's total stake weight is calculated, and the result is recorded in `operatorStakeHistory[operatorId][quorum]`. + * Note that if the most recent update is from the current block number, the entry is updated. Otherwise, a new entry is pushed. + * The Operator's total stake weight is added to the quorum's total stake weight in `_totalStakeHistory[quorum]`. + * Note that if the most recent update is from the current block number, the entry is updated. Otherwise, a new entry is pushed. *Requirements*: -* +* Caller MUST be the `RegistryCoordinator` +* Each quorum in `quorumNumbers` MUST be initialized (see `initializeQuorum` below) +* For each `quorum` in `quorumNumbers`: + * The calculated total stake weight for the Operator MUST NOT be less than that quorum's minimum stake #### `deregisterOperator` ```solidity - +function deregisterOperator( + bytes32 operatorId, + bytes calldata quorumNumbers +) + public + virtual + onlyRegistryCoordinator ``` -TODO +When an Operator deregisters from a quorum, the `StakeRegistry` sets their stake to 0 and subtracts their stake from the quorum's total stake, updating `operatorStakeHistory` and `_totalStakeHistory`, respectively. + +This method is ONLY callable by the `RegistryCoordinator`, and is called when an Operator deregisters from one or more quorums. This method *assumes* that: +* `operatorId` is currently registered for each quorum in `quorumNumbers` +* There are no duplicates in `quorumNumbers` + +These properties are enforced by the `RegistryCoordinator`. + +*Entry Points*: +* `RegistryCoordinator.registerOperatorWithChurn` +* `RegistryCoordinator.deregisterOperator` +* `RegistryCoordinator.ejectOperator` +* `RegistryCoordinator.updateOperators` +* `RegistryCoordinator.updateOperatorsForQuorum` *Effects*: -* +* For each `quorum` in `quorumNumbers`: + * The Operator's stake weight in `operatorStakeHistory[operatorId][quorum]` is set to 0. + * Note that if the most recent update is from the current block number, the entry is updated. Otherwise, a new entry is pushed. + * The Operator's stake weight is removed from the quorum's total stake weight in `_totalStakeHistory[quorum]`. + * Note that if the most recent update is from the current block number, the entry is updated. Otherwise, a new entry is pushed. *Requirements*: -* +* Caller MUST be the `RegistryCoordinator` +* Each quorum in `quorumNumbers` MUST be initialized (see `initializeQuorum` below) + +--- + +### Updating Registered Operators #### `updateOperatorStake` ```solidity - +function updateOperatorStake( + address operator, + bytes32 operatorId, + bytes calldata quorumNumbers +) + external + onlyRegistryCoordinator + returns (uint192) ``` -TODO +AVSs will require up-to-date views on an Operator's stake. When an Operator's shares change in the EigenLayer core contracts (due to additional delegation, undelegation, withdrawals, etc), this change is not automatically pushed to middleware contracts. This is because middleware contracts are unique to each AVS, and core contract share updates would become prohibitively expensive if they needed to update each AVS every time an Operator's shares changed. + +Rather than *pushing* updates, `RegistryCoordinator.updateOperators` and `updateOperatorsForQuorum` can be called by anyone to *pull* updates from the core contracts. Those `RegistryCoordinator` methods act as entry points for this method, which performs the same stake weight calculation as `registerOperator`, updating the Operator's `operatorStakeHistory` and the quorum's `_totalStakeHistory`. + +*Note*: there is one major difference between `updateOperatorStake` and `registerOperator` - if an Operator does NOT meet the minimum stake for a quorum, their stake weight is set to 0 and removed from the quorum's total stake weight, mimicing the behavior of `deregisterOperator`. For each quorum where this occurs, that quorum's number is added to a bitmap, `uint192 quorumsToRemove`, which is returned to the `RegistryCoordinator`. The `RegistryCoordinator` uses this returned bitmap to completely deregister Operators, maintaining an invariant that if an Operator's stake weight for a quorum is 0, they are NOT registered for that quorum. + +This method is ONLY callable by the `RegistryCoordinator`, and is called when an Operator registers for one or more quorums. This method *assumes* that: +* `operatorId` belongs to the `operator` +* `operatorId` is currently registered for each quorum in `quorumNumbers` +* There are no duplicates in `quorumNumbers` + +These properties are enforced by the `RegistryCoordinator`. + +*Entry Points*: +* `RegistryCoordinator.updateOperators` +* `RegistryCoordinator.updateOperatorsForQuorum` *Effects*: -* +* For each `quorum` in `quorumNumbers`: + * The Operator's total stake weight is calculated, and the result is recorded in `operatorStakeHistory[operatorId][quorum]`. If the Operator does NOT meet the quorum's configured minimum stake, their stake weight is set to 0 instead. + * Note that if the most recent update is from the current block number, the entry is updated. Otherwise, a new entry is pushed. + * The Operator's stake weight delta is applied to the quorum's total stake weight in `_totalStakeHistory[quorum]`. + * Note that if the most recent update is from the current block number, the entry is updated. Otherwise, a new entry is pushed. *Requirements*: -* +* Caller MUST be the `RegistryCoordinator` +* Each quorum in `quorumNumbers` MUST be initialized (see `initializeQuorum` below) + +--- + +### System Configuration + +This method is used by the `RegistryCoordinator` to initialize new quorums in the `StakeRegistry`: +* [`initializeQuorum`](#initializequorum) + +These methods are used by the `RegistryCoordinator's` Owner to configure initialized quorums in the `StakeRegistry`. They are not expected to be called very often, and will require updating Operator stakes via `RegistryCoordinator.updateOperatorsForQuorum` to maintain up-to-date views on Operator stake weights. Methods follow: +* [`setMinimumStakeForQuorum`](#setminimumstakeforquorum) +* [`addStrategies`](#addstrategies) +* [`removeStrategies`](#removestrategies) +* [`modifyStrategyParams`](#modifystrategyparams) #### `initializeQuorum` ```solidity - +function initializeQuorum( + uint8 quorumNumber, + uint96 minimumStake, + StrategyParams[] memory _strategyParams +) + public + virtual + onlyRegistryCoordinator + +struct StrategyParams { + IStrategy strategy; + uint96 multiplier; +} ``` -TODO +This method is ONLY callable by the `RegistryCoordinator`, and is called when the `RegistryCoordinator` Owner creates a new quorum. + +`initializeQuorum` initializes a new quorum by pushing an initial `StakeUpdate` to `_totalStakeHistory[quorumNumber]`, with an initial stake of 0. Other methods can validate that a quorum exists by checking whether `_totalStakeHistory[quorumNumber]` has a nonzero length. + +Additionally, this method configures a `minimumStake` for the quorum, as well as the `StrategyParams` it considers when calculating stake weight. + +*Entry Points*: +* `RegistryCoordinator.createQuorum` *Effects*: -* +* See `addStrategies` below +* See `setMinimumStakeForQuorum` below +* Pushes a `StakeUpdate` to `_totalStakeHistory[quorumNumber]`. The update's `updateBlockNumber` is set to the current block, and `stake` is set to 0. *Requirements*: -* +* Caller MUST be the `RegistryCoordinator` +* `quorumNumber` MUST NOT belong to an existing, initialized quorum +* See `addStrategies` below +* See `setMinimumStakeForQuorum` below #### `setMinimumStakeForQuorum` ```solidity - +function setMinimumStakeForQuorum( + uint8 quorumNumber, + uint96 minimumStake +) + public + virtual + onlyCoordinatorOwner + quorumExists(quorumNumber) ``` -TODO +Allows the `RegistryCoordinator` Owner to configure the `minimumStake` for an existing quorum. This value is used to determine whether an Operator has sufficient stake to register for (or stay registered for) a quorum. + +There is no lower or upper bound on a quorum's minimum stake. *Effects*: -* +* Set `minimumStakeForQuorum[quorum]` to `minimumStake` *Requirements*: -* +* Caller MUST be `RegistryCoordinator.owner()` +* `quorumNumber` MUST belong to an existing, initialized quorum #### `addStrategies` ```solidity - +function addStrategies( + uint8 quorumNumber, + StrategyParams[] memory _strategyParams +) + public + virtual + onlyCoordinatorOwner + quorumExists(quorumNumber) + +struct StrategyParams { + IStrategy strategy; + uint96 multiplier; +} ``` -TODO +Allows the `RegistryCoordinator` Owner to add `StrategyParams` to a quorum, which effect how Operators' stake weights are calculated. + +For each `StrategyParams` added, this method checks that the incoming `strategy` has not already been added to the quorum. This is done via a relatively expensive loop over storage, but this function isn't expected to be called very often. *Effects*: -* +* Each added `_strategyParams` is pushed to the quorum's stored `strategyParams[quorumNumber]` *Requirements*: -* +* Caller MUST be `RegistryCoordinator.owner()` +* `quorumNumber` MUST belong to an existing, initialized quorum +* `_strategyParams` MUST NOT be empty +* The quorum's current `StrategyParams` count plus the new `_strategyParams` MUST NOT exceed `MAX_WEIGHING_FUNCTION_LENGTH` +* `_strategyParams` MUST NOT contain duplicates, and MUST NOT contain strategies that are already being considered by the quorum +* For each `_strategyParams` being added, the `multiplier` MUST NOT be 0 #### `removeStrategies` ```solidity - +function removeStrategies( + uint8 quorumNumber, + uint256[] memory indicesToRemove +) + public + virtual + onlyCoordinatorOwner + quorumExists(quorumNumber) + +struct StrategyParams { + IStrategy strategy; + uint96 multiplier; +} ``` -TODO +Allows the `RegistryCoordinator` Owner to remove `StrategyParams` from a quorum, which effect how Operators' stake weights are calculated. Removals are processed by removing specific indices passed in by the caller. + +For each `StrategyParams` removed, this method replaces `strategyParams[quorumNumber][indicesToRemove[i]]` with the last item in `strategyParams[quorumNumber]`, then pops the last element of `strategyParams[quorumNumber]`. *Effects*: -* +* Removes the specified `StrategyParams` according to their index in the quorum's `strategyParams` list. *Requirements*: -* +* Caller MUST be `RegistryCoordinator.owner()` +* `quorumNumber` MUST belong to an existing, initialized quorum +* `indicesToRemove` MUST NOT be empty #### `modifyStrategyParams` ```solidity - +function modifyStrategyParams( + uint8 quorumNumber, + uint256[] calldata strategyIndices, + uint96[] calldata newMultipliers +) + public + virtual + onlyCoordinatorOwner + quorumExists(quorumNumber) + +struct StrategyParams { + IStrategy strategy; + uint96 multiplier; +} ``` -TODO +Allows the `RegistryCoordinator` Owner to modify the multipliers specified in a quorum's configured `StrategyParams`. *Effects*: -* +* The quorum's `StrategyParams` at the specified `strategyIndices` are given a new multiplier *Requirements*: -* +* Caller MUST be `RegistryCoordinator.owner()` +* `quorumNumber` MUST belong to an existing, initialized quorum +* `strategyIndices` MUST NOT be empty +* `strategyIndices` and `newMultipliers` MUST have equal lengths --- \ No newline at end of file diff --git a/src/BLSApkRegistry.sol b/src/BLSApkRegistry.sol index bd140036..d6c892e8 100644 --- a/src/BLSApkRegistry.sol +++ b/src/BLSApkRegistry.sol @@ -200,7 +200,7 @@ contract BLSApkRegistry is BLSApkRegistryStorage { * @notice Returns the indices of the quorumApks index at `blockNumber` for the provided `quorumNumbers` * @dev Returns the current indices if `blockNumber >= block.number` */ - function getApkIndicesAtBlockNumber( + function getApkIndicesAtBlockNumber( bytes calldata quorumNumbers, uint256 blockNumber ) external view returns (uint32[] memory) { diff --git a/src/StakeRegistry.sol b/src/StakeRegistry.sol index 964501a9..059cd344 100644 --- a/src/StakeRegistry.sol +++ b/src/StakeRegistry.sol @@ -394,7 +394,7 @@ contract StakeRegistry is StakeRegistryStorage { * @dev This function has no check to make sure that the strategies for a single quorum have the same underlying asset. This is a concious choice, * since a middleware may want, e.g., a stablecoin quorum that accepts USDC, USDT, DAI, etc. as underlying assets and trades them as "equivalent". */ - function _addStrategyParams( + function _addStrategyParams( uint8 quorumNumber, StrategyParams[] memory _strategyParams ) internal { @@ -544,7 +544,7 @@ contract StakeRegistry is StakeRegistryStorage { * @param operatorId The id of the operator of interest. * @param quorumNumber The quorum number to get the stake for. */ - function getStakeHistory( + function getStakeHistory( bytes32 operatorId, uint8 quorumNumber ) external view returns (StakeUpdate[] memory) { @@ -555,7 +555,7 @@ contract StakeRegistry is StakeRegistryStorage { * @notice Returns the most recent stake weight for the `operatorId` for quorum `quorumNumber` * @dev Function returns weight of **0** in the event that the operator has no stake history */ - function getCurrentStake(bytes32 operatorId, uint8 quorumNumber) external view returns (uint96) { + function getCurrentStake(bytes32 operatorId, uint8 quorumNumber) external view returns (uint96) { StakeUpdate memory operatorStakeUpdate = getLatestStakeUpdate(operatorId, quorumNumber); return operatorStakeUpdate.stake; } @@ -585,7 +585,7 @@ contract StakeRegistry is StakeRegistryStorage { * @param index Array index for lookup, within the dynamic array `operatorStakeHistory[operatorId][quorumNumber]`. * @dev Function will revert if `index` is out-of-bounds. */ - function getStakeUpdateAtIndex( + function getStakeUpdateAtIndex( uint8 quorumNumber, bytes32 operatorId, uint256 index @@ -624,7 +624,7 @@ contract StakeRegistry is StakeRegistryStorage { * @param blockNumber Block number to make sure the stake is from. * @dev Function will revert if `index` is out-of-bounds. */ - function getStakeAtBlockNumberAndIndex( + function getStakeAtBlockNumberAndIndex( uint8 quorumNumber, uint32 blockNumber, bytes32 operatorId, @@ -659,7 +659,7 @@ contract StakeRegistry is StakeRegistryStorage { * @param quorumNumber The quorum number to get the stake for. * @param index Array index for lookup, within the dynamic array `_totalStakeHistory[quorumNumber]`. */ - function getTotalStakeUpdateAtIndex( + function getTotalStakeUpdateAtIndex( uint8 quorumNumber, uint256 index ) external view returns (StakeUpdate memory) { @@ -674,7 +674,7 @@ contract StakeRegistry is StakeRegistryStorage { * @param blockNumber Block number to make sure the stake is from. * @dev Function will revert if `index` is out-of-bounds. */ - function getTotalStakeAtBlockNumberFromIndex( + function getTotalStakeAtBlockNumberFromIndex( uint8 quorumNumber, uint32 blockNumber, uint256 index @@ -690,7 +690,7 @@ contract StakeRegistry is StakeRegistryStorage { * @param quorumNumbers The quorum numbers to get the stake indices for. * @dev Function will revert if there are no indices for the given `blockNumber` */ - function getTotalStakeIndicesAtBlockNumber( + function getTotalStakeIndicesAtBlockNumber( uint32 blockNumber, bytes calldata quorumNumbers ) external view returns (uint32[] memory) { From d2f6b631b6cfda6fa88f823f3fc78096fcde2c2d Mon Sep 17 00:00:00 2001 From: ChaoticWalrus <93558947+ChaoticWalrus@users.noreply.github.com> Date: Tue, 2 Jan 2024 15:54:52 -0800 Subject: [PATCH 06/48] chore: fix tree file name --- .../{RegistryManagerUnit.tree => RegistryCoordinatorUnit.tree} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename test/tree/{RegistryManagerUnit.tree => RegistryCoordinatorUnit.tree} (100%) diff --git a/test/tree/RegistryManagerUnit.tree b/test/tree/RegistryCoordinatorUnit.tree similarity index 100% rename from test/tree/RegistryManagerUnit.tree rename to test/tree/RegistryCoordinatorUnit.tree From ba24f6e2f952dc57a5033409fa4801f352ca4291 Mon Sep 17 00:00:00 2001 From: ChaoticWalrus <93558947+ChaoticWalrus@users.noreply.github.com> Date: Tue, 2 Jan 2024 16:05:35 -0800 Subject: [PATCH 07/48] chore: add a couple post-checks on state also fix a couple test names --- test/unit/RegistryCoordinatorUnit.t.sol | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/test/unit/RegistryCoordinatorUnit.t.sol b/test/unit/RegistryCoordinatorUnit.t.sol index 44997660..3c1a66d8 100644 --- a/test/unit/RegistryCoordinatorUnit.t.sol +++ b/test/unit/RegistryCoordinatorUnit.t.sol @@ -121,6 +121,8 @@ contract RegistryCoordinatorUnitTests_Initialization_Setters is RegistryCoordina cheats.expectEmit(true, true, true, true, address(registryCoordinator)); emit OperatorSetParamsUpdated(0, operatorSetParams[1]); registryCoordinator.setOperatorSetParams(0, operatorSetParams[1]); + assertEq(keccak256(abi.encode(registryCoordinator.getOperatorSetParams(0))),keccak256(abi.encode(operatorSetParams[1])), + "operator set params not updated correctly"); } function test_setOperatorSetParams_revert_notOwner() public { @@ -135,6 +137,7 @@ contract RegistryCoordinatorUnitTests_Initialization_Setters is RegistryCoordina cheats.expectEmit(true, true, true, true, address(registryCoordinator)); emit ChurnApproverUpdated(churnApprover, newChurnApprover); registryCoordinator.setChurnApprover(newChurnApprover); + assertEq(registryCoordinator.churnApprover(), newChurnApprover); } function test_setChurnApprover_revert_notOwner() public { @@ -144,7 +147,7 @@ contract RegistryCoordinatorUnitTests_Initialization_Setters is RegistryCoordina registryCoordinator.setChurnApprover(newChurnApprover); } - function testSetEjector() public { + function test_setEjector() public { address newEjector = address(uint160(uint256(keccak256("newEjector")))); cheats.prank(registryCoordinatorOwner); cheats.expectEmit(true, true, true, true, address(registryCoordinator)); @@ -153,7 +156,7 @@ contract RegistryCoordinatorUnitTests_Initialization_Setters is RegistryCoordina assertEq(registryCoordinator.ejector(), newEjector); } - function testSetEjector_revert_notOwner() public { + function test_setEjector_revert_notOwner() public { address newEjector = address(uint160(uint256(keccak256("newEjector")))); cheats.expectRevert("Ownable: caller is not the owner"); cheats.prank(defaultOperator); From 0bbd0897757de4ae34924b62457843a07fcf6391 Mon Sep 17 00:00:00 2001 From: ChaoticWalrus <93558947+ChaoticWalrus@users.noreply.github.com> Date: Tue, 2 Jan 2024 21:30:30 -0800 Subject: [PATCH 08/48] chore: fix breaking test set up one less than the max number of quorums in a single test's setup, rather than the full max number, so that the test will properly allow the creation of a new quorum also fix a typo in a test name --- test/unit/RegistryCoordinatorUnit.t.sol | 24 +++++++++++++++++++----- 1 file changed, 19 insertions(+), 5 deletions(-) diff --git a/test/unit/RegistryCoordinatorUnit.t.sol b/test/unit/RegistryCoordinatorUnit.t.sol index 3c1a66d8..d42852c0 100644 --- a/test/unit/RegistryCoordinatorUnit.t.sol +++ b/test/unit/RegistryCoordinatorUnit.t.sol @@ -41,7 +41,7 @@ contract RegistryCoordinatorUnitTests is MockAVSDeployer { event EjectorUpdated(address prevEjector, address newEjector); function setUp() virtual public { - _deployMockEigenLayerAndAVS(); + _deployMockEigenLayerAndAVS(numQuorums); } function _testRegisterOperatorWithChurn_SetUp(uint256 pseudoRandomNumber, bytes memory quorumNumbers, uint96 operatorToKickStake) internal returns(address operatorToRegister, BN254.G1Point memory operatorToRegisterPubKey, IRegistryCoordinator.OperatorKickParam[] memory operatorKickParams) { @@ -194,9 +194,23 @@ contract RegistryCoordinatorUnitTests_Initialization_Setters is RegistryCoordina } function test_createQuorum() public { - IRegistryCoordinator.OperatorSetParam memory operatorSetParams; - uint96 minimumStake; - IStakeRegistry.StrategyParams[] memory strategyParams; + // re-run setup, but setting up zero quorums + // this is necessary since the default setup already configures the max number of quorums, preventing adding more + _deployMockEigenLayerAndAVS(0); + + IRegistryCoordinator.OperatorSetParam memory operatorSetParams = + IRegistryCoordinator.OperatorSetParam({ + maxOperatorCount: defaultMaxOperatorCount, + kickBIPsOfOperatorStake: defaultKickBIPsOfOperatorStake, + kickBIPsOfTotalStake: defaultKickBIPsOfTotalStake + }); + uint96 minimumStake = 1; + IStakeRegistry.StrategyParams[] memory strategyParams = new IStakeRegistry.StrategyParams[](1); + strategyParams[0] = + IStakeRegistry.StrategyParams({ + strategy: IStrategy(address(1000)), + multiplier: 1e16 + }); uint8 quorumCountBefore = registryCoordinator.quorumCount(); @@ -241,7 +255,7 @@ contract RegistryCoordinatorUnitTests_RegisterOperator is RegistryCoordinatorUni registryCoordinator.registerOperator(emptyQuorumNumbers, defaultSocket, pubkeyRegistrationParams, emptySig); } - function testRegisterOperator_revert_invalidQUorum() public { + function testRegisterOperator_revert_invalidQuorum() public { bytes memory quorumNumbersTooLarge = new bytes(1); ISignatureUtils.SignatureWithSaltAndExpiry memory emptySig; From fb36bffc19478b12298f6fe5827b471835bab0b2 Mon Sep 17 00:00:00 2001 From: ChaoticWalrus <93558947+ChaoticWalrus@users.noreply.github.com> Date: Wed, 20 Dec 2023 13:46:27 -0800 Subject: [PATCH 09/48] feat: add tree diagram for RegistryManager --- test/tree/RegistryManagerUnit.tree | 158 +++++++++++++++++++++++++++++ 1 file changed, 158 insertions(+) create mode 100644 test/tree/RegistryManagerUnit.tree diff --git a/test/tree/RegistryManagerUnit.tree b/test/tree/RegistryManagerUnit.tree new file mode 100644 index 00000000..10238e2e --- /dev/null +++ b/test/tree/RegistryManagerUnit.tree @@ -0,0 +1,158 @@ +├── RegistryCoordinatorUnit.t.sol (*** denotes that integration tests are needed to validate path) +│ +├── initialize +│ ├── given that initialize has been called previously +│ │ └── it should revert +│ ├── given that the variable-length array input lengths don't all match +│ │ └── it should revert +│ └── it should set the storage variables correctly (owner, pauserRegistry, paused status, churnApprover, ejector, quorum params) +│ +├── registerOperator() +│ ├── given that operator registration is paused +│ │ └── it should revert +│ ├── given that the current number of operators for any of the quorums to register for already meets or exceeds the quorum's operator cap +│ │ └── it should revert +│ ├── given the operator has *not* previously registered a pubkey +│ │ └── ***it should attempt to register the provided pubkey with the BLSApkRegistry +│ ├── given that *has* previously registered a pubkey +│ │ └── ***it should fetch the operator's pubkey from the BLSApkRegistry +│ └── ***it should attempt to register the operator, via the `_registerOperator` function (see below) +│ +├── registerOperatorWithChurn() +│ ├── given that operator registration is paused +│ │ └── it should revert +│ ├── given that the provided operatorKickParams are not the same length as the provided quorumNumbers +│ │ └── it should revert +│ ├── given that the churnApprover did not sign the operatorKickParams and the caller's operatorID +│ │ └── it should revert +│ ├── given that the current number of operators for any of the quorums to register for already meets or exceeds the quorum's operator cap +│ │ └── for each quorum, it should check that the new and to-be-kicked operators' stakes meet the configured requirements +│ │ ├─ given that the configured requirements are not met +│ │ │ └── it should revert +│ │ └── it should deregister the to-be-kicked operator +│ ├── given the operator has *not* previously registered a pubkey +│ │ └── ***it should attempt to register the provided pubkey with the BLSApkRegistry +│ ├── given that *has* previously registered a pubkey +│ │ └── ***it should fetch the operator's pubkey from the BLSApkRegistry +│ └── ***it should attempt to register the operator, via the `_registerOperator` function (see below) +│ +├── deregisterOperator() +│ ├── given that operator deregistration is paused +│ │ └── it should revert +│ └── ***it should attempt to deregister the caller, via the `_deregisterOperator` function (see below) +│ +├── ejectOperator() +│ ├── given that caller is not the ejector +│ │ └── it should revert +│ └── ***it should attempt to deregister the operator, via the `_deregisterOperator` function (see below) +│ +├── updateOperators() +│ ├── given that operator updates are paused +│ │ └── it should revert +│ └── ***for each operator, it should attempt to update the operator's stake information for all quorums +│ that the operator is currently registered for, via the `_updateOperator` function (see below) +│ +├── updateOperatorsForQuorum() +│ ├── given that operator updates are paused +│ │ └── it should revert +│ ├── given that any of the provided quorum numbers is for a non-existant quorum +│ │ └── it should revert +│ ├── given that the length of the provided array of operator lists does not match the length of the provided quorum numbers +│ │ └── it should revert +│ ├── given that the length of any provided list of operators does not match the current number of operators in that quorum +│ │ └── it should revert +│ ├── given that of any of the provided lists of operators contains an operator who is not currently registered for the quorum +│ │ └── it should revert +│ ├── given that of any of the provided lists of operators contains a duplicate +│ │ └── it should revert +│ ├── given that of any of the provided lists of operators is not in ascending address order +│ │ └── it should revert +│ └── ***for each operator, it should attempt to update the operator's stake information, via the `_updateOperator` function (see below) +│ for each quorum, it should increase the quorumUpdateBlockNumber to the current block number +│ +├── updateSocket() +│ ├── given that the caller is not a registered operator +│ │ └── it should revert +│ └── it should emit an OperatorSocketUpdate event +│ +├── createQuorum() +│ ├── given that the caller is not a registered operator +│ │ └── it should revert +│ ├── given that the current quorum count meets or exceeds the MAX_QUORUM_COUNT +│ │ └── it should revert +│ └── it should set the OperatorSetParams for the new quorum +│ *** it should initialize the new quorum on the StakeRegistry, IndexRegistry, and BLSApkRegistry +│ +├── setOperatorSetParams() +│ ├── given that the caller is not the owner +│ │ └── it should revert +│ ├── given that the quorum does not already exist +│ │ └── it should revert +│ └── it should set the OperatorSetParams for the quorum, and emit an event +│ +├── setChurnApprover() +│ ├── given that the caller is not the owner +│ │ └── it should revert +│ └── it should update the churnApprover address and emit an event +│ +├── setEjector() +│ ├── given that the caller is not the owner +│ │ └── it should revert +│ └── it should update the ejector address and emit an event +│ +│ +├── getQuorumBitmapIndicesAtBlockNumber() +│ ├── given that any of the operatorIDs was not registered at the block number +│ │ └── it should revert +│ └── it should return the proper index of the entry in each operatorID's quorum bitmap history +│ +├── getQuorumBitmapAtBlockNumberByIndex() +│ ├── given that the operatorID was not registered at the block number +│ │ └── it should revert +│ ├── given that the index specifies a bitmap that became invalid prior to the block number +│ │ └── it should revert +│ ├── given that the index specifies a bitmap that became valid after the block number +│ │ └── it should revert +│ └── it should return the quorum bitmap of the operatorID at the block number +│ +├── _registerOperator() (internal function -- see mentions above) +│ ├── given that no quorums are being registered for +│ │ └── it should revert +│ ├── given that any quorums being registered for do not (yet) exist +│ │ └── it should revert +│ ├── given that the operator is already registered for any of quorums being registered for +│ │ └── it should revert +│ ├── given that the operator is not already registered at the AVS level +│ │ └── it should mark the operator as registered +│ │ *** and call the serviceManager to register the operator on the EigenLayer level +│ └── it should update the operator's bitmap history, via the `_updateOperatorBitmap` function (see below), +│ with a new entry that includes the added quorums +│ *** and call the BLSApkRegistry, StakeRegistry, and IndexRegistry to complete operator registration +│ +├── _deregisterOperator() (internal function -- see mentions above) +│ ├── given that the operator is not registered for the AVS +│ │ └── it should revert +│ ├── given that no quorums are being removed +│ │ └── it should revert +│ ├── given that any quorums being removed do not (yet) exist +│ │ └── it should revert +│ ├── given that the operator is *not* currently registered for any of quorums being removed +│ │ └── it should revert +│ ├── given that the operator is being removed from all quorums that they were registered for +│ │ └── it should mark the operator as no longer registered +│ │ *** and call the serviceManager to deregister the operator on the EigenLayer level +│ └── it should update the operator's bitmap history, via the `_updateOperatorBitmap` function (see below), +│ with a new entry that excludes the removed quorums +│ *** and call the BLSApkRegistry, StakeRegistry, and IndexRegistry to complete operator deregistration +│ +├── _updateOperator() (internal function -- see mentions above) +│ ├── given that the operator is not actively registered +│ │ └── it should do nothing ("no-op") +│ └── *** it should call the StakeRegistry to make it perform a stake update on the operator +│ *** and remove the operator from any quorums where the StakeRegistry returns that the operator +│ no longer meets the requirements, via the `_deregisterOperator` function (see above) +│ +└── _updateOperatorBitmap() (internal function -- see mentions above) + ├── given that the operator has no previous entries in their bitmap history OR given that the latest entry in the operator's bitmap history occurred in the current block + │ └── it should push a new entry with an "unset" (i.e. zero) nextUpdateBlock, and an updateBlockNumber of the current block + └── otherwise, it should only update the bitmap in the latest entry \ No newline at end of file From 007cc924daa60173443b41b8127d7bfec0213350 Mon Sep 17 00:00:00 2001 From: ChaoticWalrus <93558947+ChaoticWalrus@users.noreply.github.com> Date: Thu, 21 Dec 2023 15:28:04 -0800 Subject: [PATCH 10/48] chore: reorder and rename tests --- test/unit/RegistryCoordinatorUnit.t.sol | 415 ++++++++++++------------ 1 file changed, 213 insertions(+), 202 deletions(-) diff --git a/test/unit/RegistryCoordinatorUnit.t.sol b/test/unit/RegistryCoordinatorUnit.t.sol index 0ab7584b..c5a43311 100644 --- a/test/unit/RegistryCoordinatorUnit.t.sol +++ b/test/unit/RegistryCoordinatorUnit.t.sol @@ -3,7 +3,7 @@ pragma solidity =0.8.12; import "../utils/MockAVSDeployer.sol"; -contract RegistryCoordinatorUnit is MockAVSDeployer { +contract RegistryCoordinatorUnitTests is MockAVSDeployer { using BN254 for BN254.G1Point; uint8 internal constant PAUSED_REGISTER_OPERATOR = 0; @@ -43,7 +43,52 @@ contract RegistryCoordinatorUnit is MockAVSDeployer { _deployMockEigenLayerAndAVS(); } - function testCorrectConstruction() public { + function _testRegisterOperatorWithChurn_SetUp(uint256 pseudoRandomNumber, bytes memory quorumNumbers, uint96 operatorToKickStake) internal returns(address operatorToRegister, BN254.G1Point memory operatorToRegisterPubKey, IRegistryCoordinator.OperatorKickParam[] memory operatorKickParams) { + uint32 kickRegistrationBlockNumber = 100; + + uint256 quorumBitmap = BitmapUtils.orderedBytesArrayToBitmap(quorumNumbers); + + cheats.roll(kickRegistrationBlockNumber); + + for (uint i = 0; i < defaultMaxOperatorCount - 1; i++) { + BN254.G1Point memory pubKey = BN254.hashToG1(keccak256(abi.encodePacked(pseudoRandomNumber, i))); + address operator = _incrementAddress(defaultOperator, i); + + _registerOperatorWithCoordinator(operator, quorumBitmap, pubKey); + } + + operatorToRegister = _incrementAddress(defaultOperator, defaultMaxOperatorCount); + operatorToRegisterPubKey = BN254.hashToG1(keccak256(abi.encodePacked(pseudoRandomNumber, defaultMaxOperatorCount))); + bytes32 operatorToRegisterId = BN254.hashG1Point(operatorToRegisterPubKey); + bytes32 operatorToKickId; + address operatorToKick; + + // register last operator before kick + operatorKickParams = new IRegistryCoordinator.OperatorKickParam[](1); + { + BN254.G1Point memory pubKey = BN254.hashToG1(keccak256(abi.encodePacked(pseudoRandomNumber, defaultMaxOperatorCount - 1))); + operatorToKickId = BN254.hashG1Point(pubKey); + operatorToKick = _incrementAddress(defaultOperator, defaultMaxOperatorCount - 1); + + // register last operator with much more than the kickBIPsOfTotalStake stake + _registerOperatorWithCoordinator(operatorToKick, quorumBitmap, pubKey, operatorToKickStake); + + bytes32[] memory operatorIdsToSwap = new bytes32[](1); + // operatorIdsToSwap[0] = operatorToRegisterId + operatorIdsToSwap[0] = operatorToRegisterId; + + operatorKickParams[0] = IRegistryCoordinator.OperatorKickParam({ + quorumNumber: uint8(quorumNumbers[0]), + operator: operatorToKick + }); + } + + blsApkRegistry.setBLSPublicKey(operatorToRegister, operatorToRegisterPubKey); + } +} + +contract RegistryCoordinatorUnitTests_Initialization_Setters is RegistryCoordinatorUnitTests { + function test_initialization() public { assertEq(address(registryCoordinator.stakeRegistry()), address(stakeRegistry)); assertEq(address(registryCoordinator.blsApkRegistry()), address(blsApkRegistry)); assertEq(address(registryCoordinator.indexRegistry()), address(indexRegistry)); @@ -70,27 +115,20 @@ contract RegistryCoordinatorUnit is MockAVSDeployer { ); } - function testSetOperatorSetParams_NotOwner_Reverts() public { - cheats.expectRevert("Ownable: caller is not the owner"); - cheats.prank(defaultOperator); - registryCoordinator.setOperatorSetParams(0, operatorSetParams[0]); - } - - function testSetOperatorSetParams_Valid() public { + function test_setOperatorSetParams() public { cheats.prank(registryCoordinatorOwner); cheats.expectEmit(true, true, true, true, address(registryCoordinator)); emit OperatorSetParamsUpdated(0, operatorSetParams[1]); registryCoordinator.setOperatorSetParams(0, operatorSetParams[1]); } - function testSetChurnApprover_NotOwner_Reverts() public { - address newChurnApprover = address(uint160(uint256(keccak256("newChurnApprover")))); + function test_setOperatorSetParams_revert_notOwner() public { cheats.expectRevert("Ownable: caller is not the owner"); cheats.prank(defaultOperator); - registryCoordinator.setChurnApprover(newChurnApprover); + registryCoordinator.setOperatorSetParams(0, operatorSetParams[0]); } - function testSetChurnApprover_Valid() public { + function test_setChurnApprover() public { address newChurnApprover = address(uint160(uint256(keccak256("newChurnApprover")))); cheats.prank(registryCoordinatorOwner); cheats.expectEmit(true, true, true, true, address(registryCoordinator)); @@ -98,14 +136,14 @@ contract RegistryCoordinatorUnit is MockAVSDeployer { registryCoordinator.setChurnApprover(newChurnApprover); } - function testSetEjector_NotOwner_Reverts() public { - address newEjector = address(uint160(uint256(keccak256("newEjector")))); + function test_setChurnApprover_revert_notOwner() public { + address newChurnApprover = address(uint160(uint256(keccak256("newChurnApprover")))); cheats.expectRevert("Ownable: caller is not the owner"); cheats.prank(defaultOperator); - registryCoordinator.setEjector(newEjector); + registryCoordinator.setChurnApprover(newChurnApprover); } - function testSetEjector_Valid() public { + function testSetEjector() public { address newEjector = address(uint160(uint256(keccak256("newEjector")))); cheats.prank(registryCoordinatorOwner); cheats.expectEmit(true, true, true, true, address(registryCoordinator)); @@ -114,7 +152,37 @@ contract RegistryCoordinatorUnit is MockAVSDeployer { assertEq(registryCoordinator.ejector(), newEjector); } - function testRegisterOperatorWithCoordinator_WhenPaused_Reverts() public { + function testSetEjector_revert_notOwner() public { + address newEjector = address(uint160(uint256(keccak256("newEjector")))); + cheats.expectRevert("Ownable: caller is not the owner"); + cheats.prank(defaultOperator); + registryCoordinator.setEjector(newEjector); + } + + function test_updateSocket() public { + bytes memory quorumNumbers = new bytes(1); + quorumNumbers[0] = bytes1(defaultQuorumNumber); + + uint256 quorumBitmap = BitmapUtils.orderedBytesArrayToBitmap(quorumNumbers); + _registerOperatorWithCoordinator(defaultOperator, quorumBitmap, defaultPubKey); + + cheats.prank(defaultOperator); + cheats.expectEmit(true, true, true, true, address(registryCoordinator)); + emit OperatorSocketUpdate(defaultOperatorId, "localhost:32004"); + registryCoordinator.updateSocket("localhost:32004"); + + } + + function test_updateSocket_revert_notRegistered() public { + cheats.prank(defaultOperator); + cheats.expectRevert("RegistryCoordinator.updateSocket: operator is not registered"); + registryCoordinator.updateSocket("localhost:32004"); + } +} + +contract RegistryCoordinatorUnitTests_RegisterOperator is RegistryCoordinatorUnitTests { + + function testRegisterOperator_revert_paused() public { bytes memory emptyQuorumNumbers = new bytes(0); ISignatureUtils.SignatureWithSaltAndExpiry memory emptySig; @@ -127,7 +195,7 @@ contract RegistryCoordinatorUnit is MockAVSDeployer { registryCoordinator.registerOperator(emptyQuorumNumbers, defaultSocket, pubkeyRegistrationParams, emptySig); } - function testRegisterOperatorWithCoordinator_EmptyQuorumNumbers_Reverts() public { + function testRegisterOperator_revert_emptyQuorumNumbers() public { bytes memory emptyQuorumNumbers = new bytes(0); ISignatureUtils.SignatureWithSaltAndExpiry memory emptySig; @@ -136,7 +204,7 @@ contract RegistryCoordinatorUnit is MockAVSDeployer { registryCoordinator.registerOperator(emptyQuorumNumbers, defaultSocket, pubkeyRegistrationParams, emptySig); } - function testRegisterOperatorWithCoordinator_QuorumNumbersTooLarge_Reverts() public { + function testRegisterOperator_revert_invalidQUorum() public { bytes memory quorumNumbersTooLarge = new bytes(1); ISignatureUtils.SignatureWithSaltAndExpiry memory emptySig; @@ -147,7 +215,7 @@ contract RegistryCoordinatorUnit is MockAVSDeployer { registryCoordinator.registerOperator(quorumNumbersTooLarge, defaultSocket, pubkeyRegistrationParams, emptySig); } - function testRegisterOperatorWithCoordinator_QuorumNotCreated_Reverts() public { + function test_registerOperator_revert_NonexistentQuorum() public { _deployMockEigenLayerAndAVS(10); bytes memory quorumNumbersNotCreated = new bytes(1); ISignatureUtils.SignatureWithSaltAndExpiry memory emptySig; @@ -159,7 +227,7 @@ contract RegistryCoordinatorUnit is MockAVSDeployer { registryCoordinator.registerOperator(quorumNumbersNotCreated, defaultSocket, pubkeyRegistrationParams, emptySig); } - function testRegisterOperatorWithCoordinatorForSingleQuorum_Valid() public { + function test_registerOperator_singleQuorum() public { bytes memory quorumNumbers = new bytes(1); ISignatureUtils.SignatureWithSaltAndExpiry memory emptySig; quorumNumbers[0] = bytes1(defaultQuorumNumber); @@ -202,7 +270,7 @@ contract RegistryCoordinatorUnit is MockAVSDeployer { ); } - function testRegisterOperatorWithCoordinatorForFuzzedQuorums_Valid(uint256 quorumBitmap) public { + function testFuzz_registerOperator(uint256 quorumBitmap) public { quorumBitmap = quorumBitmap & MAX_QUORUM_BITMAP; ISignatureUtils.SignatureWithSaltAndExpiry memory emptySig; cheats.assume(quorumBitmap != 0); @@ -254,7 +322,7 @@ contract RegistryCoordinatorUnit is MockAVSDeployer { ); } - function testRegisterOperatorWithCoordinator_RegisteredOperatorForNewQuorums_Valid() public { + function test_registerOperator_addingQuorumsAfterInitialRegistration() public { uint256 registrationBlockNumber = block.number + 100; uint256 nextRegistrationBlockNumber = registrationBlockNumber + 100; ISignatureUtils.SignatureWithSaltAndExpiry memory emptySig; @@ -312,7 +380,7 @@ contract RegistryCoordinatorUnit is MockAVSDeployer { ); } - function testRegisterOperatorWithCoordinator_OverFilledQuorum_Reverts(uint256 pseudoRandomNumber) public { + function test_registerOperator_revert_overFilledQuorum(uint256 pseudoRandomNumber) public { uint32 numOperators = defaultMaxOperatorCount; uint32 registrationBlockNumber = 200; ISignatureUtils.SignatureWithSaltAndExpiry memory emptySig; @@ -343,7 +411,7 @@ contract RegistryCoordinatorUnit is MockAVSDeployer { registryCoordinator.registerOperator(quorumNumbers, defaultSocket, pubkeyRegistrationParams, emptySig); } - function testRegisterOperatorWithCoordinator_RegisteredOperatorForSameQuorums_Reverts() public { + function test_registerOperator_revert_operatorAlreadyRegisteredForQuorum() public { uint256 registrationBlockNumber = block.number + 100; uint256 nextRegistrationBlockNumber = registrationBlockNumber + 100; ISignatureUtils.SignatureWithSaltAndExpiry memory emptySig; @@ -363,8 +431,10 @@ contract RegistryCoordinatorUnit is MockAVSDeployer { registryCoordinator.registerOperator(quorumNumbers, defaultSocket, pubkeyRegistrationParams, emptySig); } +} - function testDeregisterOperatorWithCoordinator_WhenPaused_Reverts() public { +contract RegistryCoordinatorUnitTests_DeregisterOperator_EjectOperator is RegistryCoordinatorUnitTests { + function test_deregisterOperator_revert_paused() public { bytes memory quorumNumbers = new bytes(1); quorumNumbers[0] = bytes1(defaultQuorumNumber); uint256 quorumBitmap = BitmapUtils.orderedBytesArrayToBitmap(quorumNumbers); @@ -380,7 +450,7 @@ contract RegistryCoordinatorUnit is MockAVSDeployer { registryCoordinator.deregisterOperator(quorumNumbers); } - function testDeregisterOperatorWithCoordinator_NotRegistered_Reverts() public { + function test_deregisterOperator_revert_notRegistered() public { bytes memory quorumNumbers = new bytes(1); quorumNumbers[0] = bytes1(defaultQuorumNumber); @@ -389,7 +459,7 @@ contract RegistryCoordinatorUnit is MockAVSDeployer { registryCoordinator.deregisterOperator(quorumNumbers); } - function testDeregisterOperatorWithCoordinator_IncorrectQuorums_Reverts() public { + function test_deregisterOperator_revert_incorrectQuorums() public { bytes memory quorumNumbers = new bytes(1); quorumNumbers[0] = bytes1(defaultQuorumNumber); uint256 quorumBitmap = BitmapUtils.orderedBytesArrayToBitmap(quorumNumbers); @@ -405,7 +475,7 @@ contract RegistryCoordinatorUnit is MockAVSDeployer { registryCoordinator.deregisterOperator(quorumNumbers); } - function testDeregisterOperatorWithCoordinatorForSingleQuorumAndSingleOperator_Valid() public { + function test_deregisterOperator_singleQuorumAndSingleOperator() public { ISignatureUtils.SignatureWithSaltAndExpiry memory emptySig; uint32 registrationBlockNumber = 100; uint32 deregistrationBlockNumber = 200; @@ -453,7 +523,7 @@ contract RegistryCoordinatorUnit is MockAVSDeployer { ); } - function testDeregisterOperatorWithCoordinatorForFuzzedQuorumAndSingleOperator_Valid(uint256 quorumBitmap) public { + function testFuzz_deregisterOperator_fuzzedQuorumAndSingleOperator(uint256 quorumBitmap) public { ISignatureUtils.SignatureWithSaltAndExpiry memory emptySig; uint32 registrationBlockNumber = 100; uint32 deregistrationBlockNumber = 200; @@ -505,7 +575,7 @@ contract RegistryCoordinatorUnit is MockAVSDeployer { ); } - function testDeregisterOperatorWithCoordinatorForFuzzedQuorumAndManyOperators_Valid(uint256 pseudoRandomNumber) public { + function testFuzz_deregisterOperator_manyOperators(uint256 pseudoRandomNumber) public { uint32 numOperators = defaultMaxOperatorCount; uint32 registrationBlockNumber = 100; @@ -524,7 +594,7 @@ contract RegistryCoordinatorUnit is MockAVSDeployer { for (uint i = 0; i < numOperators; i++) { emit log_named_uint("i", i); BN254.G1Point memory pubKey = BN254.hashToG1(keccak256(abi.encodePacked(pseudoRandomNumber, i))); - bytes32 operatorId = pubKey.hashG1Point(); + bytes32 operatorId = BN254.hashG1Point(pubKey); address operator = _incrementAddress(defaultOperator, i); _registerOperatorWithCoordinator(operator, quorumBitmaps[i], pubKey); @@ -539,7 +609,7 @@ contract RegistryCoordinatorUnit is MockAVSDeployer { uint256 indexOfOperatorToDerigister = pseudoRandomNumber % numOperators; address operatorToDerigister = _incrementAddress(defaultOperator, indexOfOperatorToDerigister); BN254.G1Point memory operatorToDeregisterPubKey = BN254.hashToG1(keccak256(abi.encodePacked(pseudoRandomNumber, indexOfOperatorToDerigister))); - bytes32 operatorToDerigisterId = operatorToDeregisterPubKey.hashG1Point(); + bytes32 operatorToDerigisterId = BN254.hashG1Point(operatorToDeregisterPubKey); uint256 operatorToDeregisterQuorumBitmap = quorumBitmaps[indexOfOperatorToDerigister]; bytes memory operatorToDeregisterQuorumNumbers = BitmapUtils.bitmapToBytesArray(operatorToDeregisterQuorumBitmap); @@ -580,8 +650,8 @@ contract RegistryCoordinatorUnit is MockAVSDeployer { } // @notice verify that it is possible for an operator to register, deregister, and then register again! - function testReregisterOperatorWithCoordinator_Valid() public { - testDeregisterOperatorWithCoordinatorForSingleQuorumAndSingleOperator_Valid(); + function test_reregisterOperator() public { + test_deregisterOperator_singleQuorumAndSingleOperator(); ISignatureUtils.SignatureWithSaltAndExpiry memory emptySig; uint32 reregistrationBlockNumber = 201; @@ -630,7 +700,99 @@ contract RegistryCoordinatorUnit is MockAVSDeployer { ); } - function testRegisterOperatorWithCoordinatorWithKicks_Valid(uint256 pseudoRandomNumber) public { + function test_ejectOperator_allQuorums() public { + // register operator with default stake with default quorum number + bytes memory quorumNumbers = new bytes(1); + quorumNumbers[0] = bytes1(defaultQuorumNumber); + ISignatureUtils.SignatureWithSaltAndExpiry memory emptySig; + + stakeRegistry.setOperatorWeight(uint8(quorumNumbers[0]), defaultOperator, defaultStake); + + cheats.prank(defaultOperator); + registryCoordinator.registerOperator(quorumNumbers, defaultSocket, pubkeyRegistrationParams, emptySig); + + cheats.expectEmit(true, true, true, true, address(blsApkRegistry)); + emit OperatorRemovedFromQuorums(defaultOperator, quorumNumbers); + + cheats.expectEmit(true, true, true, true, address(stakeRegistry)); + emit OperatorStakeUpdate(defaultOperatorId, uint8(quorumNumbers[0]), 0); + + // eject + cheats.prank(ejector); + registryCoordinator.ejectOperator(defaultOperator, quorumNumbers); + + // make sure the operator is deregistered + assertEq( + keccak256(abi.encode(registryCoordinator.getOperator(defaultOperator))), + keccak256(abi.encode(IRegistryCoordinator.OperatorInfo({ + operatorId: defaultOperatorId, + status: IRegistryCoordinator.OperatorStatus.DEREGISTERED + }))) + ); + // make sure the operator is not in any quorums + assertEq(registryCoordinator.getCurrentQuorumBitmap(defaultOperatorId), 0); + } + + function test_ejectOperator_subsetOfQuorums() public { + // register operator with default stake with 2 quorums + bytes memory quorumNumbers = new bytes(2); + quorumNumbers[0] = bytes1(defaultQuorumNumber); + quorumNumbers[1] = bytes1(defaultQuorumNumber + 1); + ISignatureUtils.SignatureWithSaltAndExpiry memory emptySig; + + for (uint i = 0; i < quorumNumbers.length; i++) { + stakeRegistry.setOperatorWeight(uint8(quorumNumbers[i]), defaultOperator, defaultStake); + } + + cheats.prank(defaultOperator); + registryCoordinator.registerOperator(quorumNumbers, defaultSocket, pubkeyRegistrationParams, emptySig); + + // eject from only first quorum + bytes memory quorumNumbersToEject = new bytes(1); + quorumNumbersToEject[0] = quorumNumbers[0]; + + cheats.expectEmit(true, true, true, true, address(blsApkRegistry)); + emit OperatorRemovedFromQuorums(defaultOperator, quorumNumbersToEject); + + cheats.expectEmit(true, true, true, true, address(stakeRegistry)); + emit OperatorStakeUpdate(defaultOperatorId, uint8(quorumNumbersToEject[0]), 0); + + cheats.prank(ejector); + registryCoordinator.ejectOperator(defaultOperator, quorumNumbersToEject); + + // make sure the operator is registered + assertEq( + keccak256(abi.encode(registryCoordinator.getOperator(defaultOperator))), + keccak256(abi.encode(IRegistryCoordinator.OperatorInfo({ + operatorId: defaultOperatorId, + status: IRegistryCoordinator.OperatorStatus.REGISTERED + }))) + ); + // make sure the operator is not in any quorums + assertEq( + registryCoordinator.getCurrentQuorumBitmap(defaultOperatorId), + BitmapUtils.orderedBytesArrayToBitmap(quorumNumbers) & ~BitmapUtils.orderedBytesArrayToBitmap(quorumNumbersToEject) // quorumsRegisteredFor & ~quorumsEjectedFrom + ); + } + + function test_ejectOperator_revert_notEjector() public { + bytes memory quorumNumbers = new bytes(1); + quorumNumbers[0] = bytes1(defaultQuorumNumber); + ISignatureUtils.SignatureWithSaltAndExpiry memory emptySig; + + stakeRegistry.setOperatorWeight(uint8(quorumNumbers[0]), defaultOperator, defaultStake); + + cheats.prank(defaultOperator); + registryCoordinator.registerOperator(quorumNumbers, defaultSocket, pubkeyRegistrationParams, emptySig); + + cheats.expectRevert("RegistryCoordinator.onlyEjector: caller is not the ejector"); + cheats.prank(defaultOperator); + registryCoordinator.ejectOperator(defaultOperator, quorumNumbers); + } +} + +contract RegistryCoordinatorUnitTests_RegisterOperatorWithChurn is RegistryCoordinatorUnitTests { + function testFuzz_registerOperatorWithChurn(uint256 pseudoRandomNumber) public { uint32 numOperators = defaultMaxOperatorCount; uint32 kickRegistrationBlockNumber = 100; uint32 registrationBlockNumber = 200; @@ -651,7 +813,7 @@ contract RegistryCoordinatorUnit is MockAVSDeployer { address operatorToRegister = _incrementAddress(defaultOperator, numOperators); BN254.G1Point memory operatorToRegisterPubKey = BN254.hashToG1(keccak256(abi.encodePacked(pseudoRandomNumber, numOperators))); - bytes32 operatorToRegisterId = operatorToRegisterPubKey.hashG1Point(); + bytes32 operatorToRegisterId = BN254.hashG1Point(operatorToRegisterPubKey); bytes32 operatorToKickId; address operatorToKick; @@ -659,7 +821,7 @@ contract RegistryCoordinatorUnit is MockAVSDeployer { IRegistryCoordinator.OperatorKickParam[] memory operatorKickParams = new IRegistryCoordinator.OperatorKickParam[](1); { BN254.G1Point memory pubKey = BN254.hashToG1(keccak256(abi.encodePacked(pseudoRandomNumber, numOperators - 1))); - operatorToKickId = pubKey.hashG1Point(); + operatorToKickId = BN254.hashG1Point(pubKey); operatorToKick = _incrementAddress(defaultOperator, numOperators - 1); _registerOperatorWithCoordinator(operatorToKick, quorumBitmap, pubKey); @@ -735,7 +897,7 @@ contract RegistryCoordinatorUnit is MockAVSDeployer { ); } - function testRegisterOperatorWithCoordinatorWithKicks_LessThanKickBIPsOfOperatorStake_Reverts(uint256 pseudoRandomNumber) public { + function test_registerOperatorWithChurn_revert_lessThanKickBIPsOfOperatorStake(uint256 pseudoRandomNumber) public { bytes memory quorumNumbers = new bytes(1); quorumNumbers[0] = bytes1(defaultQuorumNumber); ISignatureUtils.SignatureWithSaltAndExpiry memory emptyAVSRegSig; @@ -744,8 +906,8 @@ contract RegistryCoordinatorUnit is MockAVSDeployer { address operatorToRegister, BN254.G1Point memory operatorToRegisterPubKey, IRegistryCoordinator.OperatorKickParam[] memory operatorKickParams - ) = _testRegisterOperatorWithKicks_SetUp(pseudoRandomNumber, quorumNumbers, defaultStake); - bytes32 operatorToRegisterId = operatorToRegisterPubKey.hashG1Point(); + ) = _testRegisterOperatorWithChurn_SetUp(pseudoRandomNumber, quorumNumbers, defaultStake); + bytes32 operatorToRegisterId = BN254.hashG1Point(operatorToRegisterPubKey); stakeRegistry.setOperatorWeight(defaultQuorumNumber, operatorToRegister, defaultStake); @@ -763,7 +925,7 @@ contract RegistryCoordinatorUnit is MockAVSDeployer { ); } - function testRegisterOperatorWithCoordinatorWithKicks_LessThanKickBIPsOfTotalStake_Reverts(uint256 pseudoRandomNumber) public { + function test_registerOperatorWithChurn_revert_lessThanKickBIPsOfTotalStake(uint256 pseudoRandomNumber) public { bytes memory quorumNumbers = new bytes(1); quorumNumbers[0] = bytes1(defaultQuorumNumber); ISignatureUtils.SignatureWithSaltAndExpiry memory emptyAVSRegSig; @@ -773,8 +935,8 @@ contract RegistryCoordinatorUnit is MockAVSDeployer { address operatorToRegister, BN254.G1Point memory operatorToRegisterPubKey, IRegistryCoordinator.OperatorKickParam[] memory operatorKickParams - ) = _testRegisterOperatorWithKicks_SetUp(pseudoRandomNumber, quorumNumbers, operatorToKickStake); - bytes32 operatorToRegisterId = operatorToRegisterPubKey.hashG1Point(); + ) = _testRegisterOperatorWithChurn_SetUp(pseudoRandomNumber, quorumNumbers, operatorToKickStake); + bytes32 operatorToRegisterId = BN254.hashG1Point(operatorToRegisterPubKey); // set the stake of the operator to register to the defaultKickBIPsOfOperatorStake multiple of the operatorToKickStake @@ -794,7 +956,7 @@ contract RegistryCoordinatorUnit is MockAVSDeployer { ); } - function testRegisterOperatorWithCoordinatorWithKicks_InvalidSignatures_Reverts(uint256 pseudoRandomNumber) public { + function test_registerOperatorWithChurn_revert_invalidChurnApproverSignature(uint256 pseudoRandomNumber) public { bytes memory quorumNumbers = new bytes(1); quorumNumbers[0] = bytes1(defaultQuorumNumber); ISignatureUtils.SignatureWithSaltAndExpiry memory emptyAVSRegSig; @@ -803,7 +965,7 @@ contract RegistryCoordinatorUnit is MockAVSDeployer { address operatorToRegister, , IRegistryCoordinator.OperatorKickParam[] memory operatorKickParams - ) = _testRegisterOperatorWithKicks_SetUp(pseudoRandomNumber, quorumNumbers, defaultStake); + ) = _testRegisterOperatorWithChurn_SetUp(pseudoRandomNumber, quorumNumbers, defaultStake); uint96 registeringStake = defaultKickBIPsOfOperatorStake * defaultStake; stakeRegistry.setOperatorWeight(defaultQuorumNumber, operatorToRegister, registeringStake); @@ -825,7 +987,7 @@ contract RegistryCoordinatorUnit is MockAVSDeployer { ); } - function testRegisterOperatorWithCoordinatorWithKicks_ExpiredSignatures_Reverts(uint256 pseudoRandomNumber) public { + function test_registerOperatorWithChurn_revert_expiredChurnApproverSignature(uint256 pseudoRandomNumber) public { bytes memory quorumNumbers = new bytes(1); quorumNumbers[0] = bytes1(defaultQuorumNumber); ISignatureUtils.SignatureWithSaltAndExpiry memory emptyAVSRegSig; @@ -834,8 +996,8 @@ contract RegistryCoordinatorUnit is MockAVSDeployer { address operatorToRegister, BN254.G1Point memory operatorToRegisterPubKey, IRegistryCoordinator.OperatorKickParam[] memory operatorKickParams - ) = _testRegisterOperatorWithKicks_SetUp(pseudoRandomNumber, quorumNumbers, defaultStake); - bytes32 operatorToRegisterId = operatorToRegisterPubKey.hashG1Point(); + ) = _testRegisterOperatorWithChurn_SetUp(pseudoRandomNumber, quorumNumbers, defaultStake); + bytes32 operatorToRegisterId = BN254.hashG1Point(operatorToRegisterPubKey); uint96 registeringStake = defaultKickBIPsOfOperatorStake * defaultStake; stakeRegistry.setOperatorWeight(defaultQuorumNumber, operatorToRegister, registeringStake); @@ -854,156 +1016,5 @@ contract RegistryCoordinatorUnit is MockAVSDeployer { ); } - function testEjectOperatorFromCoordinator_AllQuorums_Valid() public { - // register operator with default stake with default quorum number - bytes memory quorumNumbers = new bytes(1); - quorumNumbers[0] = bytes1(defaultQuorumNumber); - ISignatureUtils.SignatureWithSaltAndExpiry memory emptySig; - - stakeRegistry.setOperatorWeight(uint8(quorumNumbers[0]), defaultOperator, defaultStake); - - cheats.prank(defaultOperator); - registryCoordinator.registerOperator(quorumNumbers, defaultSocket, pubkeyRegistrationParams, emptySig); - - cheats.expectEmit(true, true, true, true, address(blsApkRegistry)); - emit OperatorRemovedFromQuorums(defaultOperator, quorumNumbers); - - cheats.expectEmit(true, true, true, true, address(stakeRegistry)); - emit OperatorStakeUpdate(defaultOperatorId, uint8(quorumNumbers[0]), 0); - - // eject - cheats.prank(ejector); - registryCoordinator.ejectOperator(defaultOperator, quorumNumbers); - - // make sure the operator is deregistered - assertEq( - keccak256(abi.encode(registryCoordinator.getOperator(defaultOperator))), - keccak256(abi.encode(IRegistryCoordinator.OperatorInfo({ - operatorId: defaultOperatorId, - status: IRegistryCoordinator.OperatorStatus.DEREGISTERED - }))) - ); - // make sure the operator is not in any quorums - assertEq(registryCoordinator.getCurrentQuorumBitmap(defaultOperatorId), 0); - } - - function testEjectOperatorFromCoordinator_SubsetOfQuorums_Valid() public { - // register operator with default stake with 2 quorums - bytes memory quorumNumbers = new bytes(2); - quorumNumbers[0] = bytes1(defaultQuorumNumber); - quorumNumbers[1] = bytes1(defaultQuorumNumber + 1); - ISignatureUtils.SignatureWithSaltAndExpiry memory emptySig; - - for (uint i = 0; i < quorumNumbers.length; i++) { - stakeRegistry.setOperatorWeight(uint8(quorumNumbers[i]), defaultOperator, defaultStake); - } - - cheats.prank(defaultOperator); - registryCoordinator.registerOperator(quorumNumbers, defaultSocket, pubkeyRegistrationParams, emptySig); - - // eject from only first quorum - bytes memory quorumNumbersToEject = new bytes(1); - quorumNumbersToEject[0] = quorumNumbers[0]; - - cheats.expectEmit(true, true, true, true, address(blsApkRegistry)); - emit OperatorRemovedFromQuorums(defaultOperator, quorumNumbersToEject); - - cheats.expectEmit(true, true, true, true, address(stakeRegistry)); - emit OperatorStakeUpdate(defaultOperatorId, uint8(quorumNumbersToEject[0]), 0); - - cheats.prank(ejector); - registryCoordinator.ejectOperator(defaultOperator, quorumNumbersToEject); - - // make sure the operator is registered - assertEq( - keccak256(abi.encode(registryCoordinator.getOperator(defaultOperator))), - keccak256(abi.encode(IRegistryCoordinator.OperatorInfo({ - operatorId: defaultOperatorId, - status: IRegistryCoordinator.OperatorStatus.REGISTERED - }))) - ); - // make sure the operator is not in any quorums - assertEq( - registryCoordinator.getCurrentQuorumBitmap(defaultOperatorId), - BitmapUtils.orderedBytesArrayToBitmap(quorumNumbers) & ~BitmapUtils.orderedBytesArrayToBitmap(quorumNumbersToEject) // quorumsRegisteredFor & ~quorumsEjectedFrom - ); - } - - function testEjectOperatorFromCoordinator_NotEjector_Reverts() public { - bytes memory quorumNumbers = new bytes(1); - quorumNumbers[0] = bytes1(defaultQuorumNumber); - ISignatureUtils.SignatureWithSaltAndExpiry memory emptySig; - - stakeRegistry.setOperatorWeight(uint8(quorumNumbers[0]), defaultOperator, defaultStake); - - cheats.prank(defaultOperator); - registryCoordinator.registerOperator(quorumNumbers, defaultSocket, pubkeyRegistrationParams, emptySig); - - cheats.expectRevert("RegistryCoordinator.onlyEjector: caller is not the ejector"); - cheats.prank(defaultOperator); - registryCoordinator.ejectOperator(defaultOperator, quorumNumbers); - } - - function testUpdateSocket() public { - bytes memory quorumNumbers = new bytes(1); - quorumNumbers[0] = bytes1(defaultQuorumNumber); - - uint256 quorumBitmap = BitmapUtils.orderedBytesArrayToBitmap(quorumNumbers); - _registerOperatorWithCoordinator(defaultOperator, quorumBitmap, defaultPubKey); - - cheats.prank(defaultOperator); - cheats.expectEmit(true, true, true, true, address(registryCoordinator)); - emit OperatorSocketUpdate(defaultOperatorId, "localhost:32004"); - registryCoordinator.updateSocket("localhost:32004"); - - } - - function testUpdateSocket_NotRegistered_Reverts() public { - cheats.prank(defaultOperator); - cheats.expectRevert("RegistryCoordinator.updateSocket: operator is not registered"); - registryCoordinator.updateSocket("localhost:32004"); - } - - function _testRegisterOperatorWithKicks_SetUp(uint256 pseudoRandomNumber, bytes memory quorumNumbers, uint96 operatorToKickStake) internal returns(address operatorToRegister, BN254.G1Point memory operatorToRegisterPubKey, IRegistryCoordinator.OperatorKickParam[] memory operatorKickParams) { - uint32 kickRegistrationBlockNumber = 100; - - uint256 quorumBitmap = BitmapUtils.orderedBytesArrayToBitmap(quorumNumbers); - - cheats.roll(kickRegistrationBlockNumber); - - for (uint i = 0; i < defaultMaxOperatorCount - 1; i++) { - BN254.G1Point memory pubKey = BN254.hashToG1(keccak256(abi.encodePacked(pseudoRandomNumber, i))); - address operator = _incrementAddress(defaultOperator, i); - - _registerOperatorWithCoordinator(operator, quorumBitmap, pubKey); - } - - operatorToRegister = _incrementAddress(defaultOperator, defaultMaxOperatorCount); - operatorToRegisterPubKey = BN254.hashToG1(keccak256(abi.encodePacked(pseudoRandomNumber, defaultMaxOperatorCount))); - bytes32 operatorToRegisterId = operatorToRegisterPubKey.hashG1Point(); - bytes32 operatorToKickId; - address operatorToKick; - - // register last operator before kick - operatorKickParams = new IRegistryCoordinator.OperatorKickParam[](1); - { - BN254.G1Point memory pubKey = BN254.hashToG1(keccak256(abi.encodePacked(pseudoRandomNumber, defaultMaxOperatorCount - 1))); - operatorToKickId = pubKey.hashG1Point(); - operatorToKick = _incrementAddress(defaultOperator, defaultMaxOperatorCount - 1); - - // register last operator with much more than the kickBIPsOfTotalStake stake - _registerOperatorWithCoordinator(operatorToKick, quorumBitmap, pubKey, operatorToKickStake); - - bytes32[] memory operatorIdsToSwap = new bytes32[](1); - // operatorIdsToSwap[0] = operatorToRegisterId - operatorIdsToSwap[0] = operatorToRegisterId; - - operatorKickParams[0] = IRegistryCoordinator.OperatorKickParam({ - quorumNumber: uint8(quorumNumbers[0]), - operator: operatorToKick - }); - } - - blsApkRegistry.setBLSPublicKey(operatorToRegister, operatorToRegisterPubKey); - } } + From 891f76d3920d45eee37d72299687a9835cfa84fe Mon Sep 17 00:00:00 2001 From: ChaoticWalrus <93558947+ChaoticWalrus@users.noreply.github.com> Date: Thu, 21 Dec 2023 15:49:01 -0800 Subject: [PATCH 11/48] feat: add a couple simple tests note that the `test_createQuorum` test is currently failing because initialization already sets up the max number of quorums something will need to be adjusted here --- test/unit/RegistryCoordinatorUnit.t.sol | 34 +++++++++++++++++++++++++ 1 file changed, 34 insertions(+) diff --git a/test/unit/RegistryCoordinatorUnit.t.sol b/test/unit/RegistryCoordinatorUnit.t.sol index c5a43311..44997660 100644 --- a/test/unit/RegistryCoordinatorUnit.t.sol +++ b/test/unit/RegistryCoordinatorUnit.t.sol @@ -8,6 +8,7 @@ contract RegistryCoordinatorUnitTests is MockAVSDeployer { uint8 internal constant PAUSED_REGISTER_OPERATOR = 0; uint8 internal constant PAUSED_DEREGISTER_OPERATOR = 1; + uint8 internal constant MAX_QUORUM_COUNT = 192; event OperatorSocketUpdate(bytes32 indexed operatorId, string socket); @@ -178,6 +179,39 @@ contract RegistryCoordinatorUnitTests_Initialization_Setters is RegistryCoordina cheats.expectRevert("RegistryCoordinator.updateSocket: operator is not registered"); registryCoordinator.updateSocket("localhost:32004"); } + + function test_createQuorum_revert_notOwner() public { + IRegistryCoordinator.OperatorSetParam memory operatorSetParams; + uint96 minimumStake; + IStakeRegistry.StrategyParams[] memory strategyParams; + + cheats.expectRevert("Ownable: caller is not the owner"); + cheats.prank(defaultOperator); + registryCoordinator.createQuorum(operatorSetParams, minimumStake, strategyParams); + } + + function test_createQuorum() public { + IRegistryCoordinator.OperatorSetParam memory operatorSetParams; + uint96 minimumStake; + IStakeRegistry.StrategyParams[] memory strategyParams; + + uint8 quorumCountBefore = registryCoordinator.quorumCount(); + + cheats.expectEmit(true, true, true, true, address(registryCoordinator)); + emit OperatorSetParamsUpdated(quorumCountBefore, operatorSetParams); + cheats.prank(registryCoordinatorOwner); + registryCoordinator.createQuorum(operatorSetParams, minimumStake, strategyParams); + + uint8 quorumCountAfter = registryCoordinator.quorumCount(); + assertEq(quorumCountAfter, quorumCountBefore + 1, "quorum count did not increase properly"); + assertLe(quorumCountAfter, MAX_QUORUM_COUNT, "quorum count exceeded max"); + + assertEq( + keccak256(abi.encode(operatorSetParams)), + keccak256(abi.encode(registryCoordinator.getOperatorSetParams(quorumCountBefore))), + "OperatorSetParams not stored properly" + ); + } } contract RegistryCoordinatorUnitTests_RegisterOperator is RegistryCoordinatorUnitTests { From bf3bf38eabf378c44ff4d3cad42ff7f333c1049a Mon Sep 17 00:00:00 2001 From: ChaoticWalrus <93558947+ChaoticWalrus@users.noreply.github.com> Date: Tue, 2 Jan 2024 15:54:52 -0800 Subject: [PATCH 12/48] chore: fix tree file name --- .../{RegistryManagerUnit.tree => RegistryCoordinatorUnit.tree} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename test/tree/{RegistryManagerUnit.tree => RegistryCoordinatorUnit.tree} (100%) diff --git a/test/tree/RegistryManagerUnit.tree b/test/tree/RegistryCoordinatorUnit.tree similarity index 100% rename from test/tree/RegistryManagerUnit.tree rename to test/tree/RegistryCoordinatorUnit.tree From eb9a8da0a2a95b599e41a46a5ea2d627feccf61e Mon Sep 17 00:00:00 2001 From: ChaoticWalrus <93558947+ChaoticWalrus@users.noreply.github.com> Date: Tue, 2 Jan 2024 16:05:35 -0800 Subject: [PATCH 13/48] chore: add a couple post-checks on state also fix a couple test names --- test/unit/RegistryCoordinatorUnit.t.sol | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/test/unit/RegistryCoordinatorUnit.t.sol b/test/unit/RegistryCoordinatorUnit.t.sol index 44997660..3c1a66d8 100644 --- a/test/unit/RegistryCoordinatorUnit.t.sol +++ b/test/unit/RegistryCoordinatorUnit.t.sol @@ -121,6 +121,8 @@ contract RegistryCoordinatorUnitTests_Initialization_Setters is RegistryCoordina cheats.expectEmit(true, true, true, true, address(registryCoordinator)); emit OperatorSetParamsUpdated(0, operatorSetParams[1]); registryCoordinator.setOperatorSetParams(0, operatorSetParams[1]); + assertEq(keccak256(abi.encode(registryCoordinator.getOperatorSetParams(0))),keccak256(abi.encode(operatorSetParams[1])), + "operator set params not updated correctly"); } function test_setOperatorSetParams_revert_notOwner() public { @@ -135,6 +137,7 @@ contract RegistryCoordinatorUnitTests_Initialization_Setters is RegistryCoordina cheats.expectEmit(true, true, true, true, address(registryCoordinator)); emit ChurnApproverUpdated(churnApprover, newChurnApprover); registryCoordinator.setChurnApprover(newChurnApprover); + assertEq(registryCoordinator.churnApprover(), newChurnApprover); } function test_setChurnApprover_revert_notOwner() public { @@ -144,7 +147,7 @@ contract RegistryCoordinatorUnitTests_Initialization_Setters is RegistryCoordina registryCoordinator.setChurnApprover(newChurnApprover); } - function testSetEjector() public { + function test_setEjector() public { address newEjector = address(uint160(uint256(keccak256("newEjector")))); cheats.prank(registryCoordinatorOwner); cheats.expectEmit(true, true, true, true, address(registryCoordinator)); @@ -153,7 +156,7 @@ contract RegistryCoordinatorUnitTests_Initialization_Setters is RegistryCoordina assertEq(registryCoordinator.ejector(), newEjector); } - function testSetEjector_revert_notOwner() public { + function test_setEjector_revert_notOwner() public { address newEjector = address(uint160(uint256(keccak256("newEjector")))); cheats.expectRevert("Ownable: caller is not the owner"); cheats.prank(defaultOperator); From f4f9cf1d5b99fcc0a7f179efab8feda6b128f088 Mon Sep 17 00:00:00 2001 From: ChaoticWalrus <93558947+ChaoticWalrus@users.noreply.github.com> Date: Tue, 2 Jan 2024 21:30:30 -0800 Subject: [PATCH 14/48] chore: fix breaking test set up one less than the max number of quorums in a single test's setup, rather than the full max number, so that the test will properly allow the creation of a new quorum also fix a typo in a test name --- test/unit/RegistryCoordinatorUnit.t.sol | 24 +++++++++++++++++++----- 1 file changed, 19 insertions(+), 5 deletions(-) diff --git a/test/unit/RegistryCoordinatorUnit.t.sol b/test/unit/RegistryCoordinatorUnit.t.sol index 3c1a66d8..d42852c0 100644 --- a/test/unit/RegistryCoordinatorUnit.t.sol +++ b/test/unit/RegistryCoordinatorUnit.t.sol @@ -41,7 +41,7 @@ contract RegistryCoordinatorUnitTests is MockAVSDeployer { event EjectorUpdated(address prevEjector, address newEjector); function setUp() virtual public { - _deployMockEigenLayerAndAVS(); + _deployMockEigenLayerAndAVS(numQuorums); } function _testRegisterOperatorWithChurn_SetUp(uint256 pseudoRandomNumber, bytes memory quorumNumbers, uint96 operatorToKickStake) internal returns(address operatorToRegister, BN254.G1Point memory operatorToRegisterPubKey, IRegistryCoordinator.OperatorKickParam[] memory operatorKickParams) { @@ -194,9 +194,23 @@ contract RegistryCoordinatorUnitTests_Initialization_Setters is RegistryCoordina } function test_createQuorum() public { - IRegistryCoordinator.OperatorSetParam memory operatorSetParams; - uint96 minimumStake; - IStakeRegistry.StrategyParams[] memory strategyParams; + // re-run setup, but setting up zero quorums + // this is necessary since the default setup already configures the max number of quorums, preventing adding more + _deployMockEigenLayerAndAVS(0); + + IRegistryCoordinator.OperatorSetParam memory operatorSetParams = + IRegistryCoordinator.OperatorSetParam({ + maxOperatorCount: defaultMaxOperatorCount, + kickBIPsOfOperatorStake: defaultKickBIPsOfOperatorStake, + kickBIPsOfTotalStake: defaultKickBIPsOfTotalStake + }); + uint96 minimumStake = 1; + IStakeRegistry.StrategyParams[] memory strategyParams = new IStakeRegistry.StrategyParams[](1); + strategyParams[0] = + IStakeRegistry.StrategyParams({ + strategy: IStrategy(address(1000)), + multiplier: 1e16 + }); uint8 quorumCountBefore = registryCoordinator.quorumCount(); @@ -241,7 +255,7 @@ contract RegistryCoordinatorUnitTests_RegisterOperator is RegistryCoordinatorUni registryCoordinator.registerOperator(emptyQuorumNumbers, defaultSocket, pubkeyRegistrationParams, emptySig); } - function testRegisterOperator_revert_invalidQUorum() public { + function testRegisterOperator_revert_invalidQuorum() public { bytes memory quorumNumbersTooLarge = new bytes(1); ISignatureUtils.SignatureWithSaltAndExpiry memory emptySig; From 798c4617ec40344475442100189540f06cecaef2 Mon Sep 17 00:00:00 2001 From: ChaoticWalrus <93558947+ChaoticWalrus@users.noreply.github.com> Date: Wed, 3 Jan 2024 11:10:05 -0800 Subject: [PATCH 15/48] feat: add a test for partial deregistration also improve some formatting + fix some typos, and add a touch more documentation --- test/unit/RegistryCoordinatorUnit.t.sol | 166 ++++++++++++++++++++---- 1 file changed, 138 insertions(+), 28 deletions(-) diff --git a/test/unit/RegistryCoordinatorUnit.t.sol b/test/unit/RegistryCoordinatorUnit.t.sol index d42852c0..946db5f5 100644 --- a/test/unit/RegistryCoordinatorUnit.t.sol +++ b/test/unit/RegistryCoordinatorUnit.t.sol @@ -44,7 +44,15 @@ contract RegistryCoordinatorUnitTests is MockAVSDeployer { _deployMockEigenLayerAndAVS(numQuorums); } - function _testRegisterOperatorWithChurn_SetUp(uint256 pseudoRandomNumber, bytes memory quorumNumbers, uint96 operatorToKickStake) internal returns(address operatorToRegister, BN254.G1Point memory operatorToRegisterPubKey, IRegistryCoordinator.OperatorKickParam[] memory operatorKickParams) { + function _test_registerOperatorWithChurn_SetUp( + uint256 pseudoRandomNumber, + bytes memory quorumNumbers, + uint96 operatorToKickStake + ) internal returns( + address operatorToRegister, + BN254.G1Point memory operatorToRegisterPubKey, + IRegistryCoordinator.OperatorKickParam[] memory operatorKickParams + ) { uint32 kickRegistrationBlockNumber = 100; uint256 quorumBitmap = BitmapUtils.orderedBytesArrayToBitmap(quorumNumbers); @@ -233,7 +241,7 @@ contract RegistryCoordinatorUnitTests_Initialization_Setters is RegistryCoordina contract RegistryCoordinatorUnitTests_RegisterOperator is RegistryCoordinatorUnitTests { - function testRegisterOperator_revert_paused() public { + function test_registerOperator_revert_paused() public { bytes memory emptyQuorumNumbers = new bytes(0); ISignatureUtils.SignatureWithSaltAndExpiry memory emptySig; @@ -246,7 +254,7 @@ contract RegistryCoordinatorUnitTests_RegisterOperator is RegistryCoordinatorUni registryCoordinator.registerOperator(emptyQuorumNumbers, defaultSocket, pubkeyRegistrationParams, emptySig); } - function testRegisterOperator_revert_emptyQuorumNumbers() public { + function test_registerOperator_revert_emptyQuorumNumbers() public { bytes memory emptyQuorumNumbers = new bytes(0); ISignatureUtils.SignatureWithSaltAndExpiry memory emptySig; @@ -255,7 +263,7 @@ contract RegistryCoordinatorUnitTests_RegisterOperator is RegistryCoordinatorUni registryCoordinator.registerOperator(emptyQuorumNumbers, defaultSocket, pubkeyRegistrationParams, emptySig); } - function testRegisterOperator_revert_invalidQuorum() public { + function test_registerOperator_revert_invalidQuorum() public { bytes memory quorumNumbersTooLarge = new bytes(1); ISignatureUtils.SignatureWithSaltAndExpiry memory emptySig; @@ -298,7 +306,7 @@ contract RegistryCoordinatorUnitTests_RegisterOperator is RegistryCoordinatorUni cheats.prank(defaultOperator); registryCoordinator.registerOperator(quorumNumbers, defaultSocket, pubkeyRegistrationParams, emptySig); uint256 gasAfter = gasleft(); - emit log_named_uint("gasUsed", gasBefore - gasAfter); + emit log_named_uint("gasUsed, register for single quorum", gasBefore - gasAfter); uint256 quorumBitmap = BitmapUtils.orderedBytesArrayToBitmap(quorumNumbers); @@ -321,7 +329,9 @@ contract RegistryCoordinatorUnitTests_RegisterOperator is RegistryCoordinatorUni ); } + // @notice tests registering an operator for a fuzzed assortment of quorums function testFuzz_registerOperator(uint256 quorumBitmap) public { + // filter the fuzzed input down to only valid quorums quorumBitmap = quorumBitmap & MAX_QUORUM_BITMAP; ISignatureUtils.SignatureWithSaltAndExpiry memory emptySig; cheats.assume(quorumBitmap != 0); @@ -373,6 +383,7 @@ contract RegistryCoordinatorUnitTests_RegisterOperator is RegistryCoordinatorUni ); } + // @notice tests registering an operator for a single quorum and later registering them for an additional quorum function test_registerOperator_addingQuorumsAfterInitialRegistration() public { uint256 registrationBlockNumber = block.number + 100; uint256 nextRegistrationBlockNumber = registrationBlockNumber + 100; @@ -526,6 +537,7 @@ contract RegistryCoordinatorUnitTests_DeregisterOperator_EjectOperator is Regist registryCoordinator.deregisterOperator(quorumNumbers); } + // @notice verifies that an operator who was registered for a single quorum can be deregistered function test_deregisterOperator_singleQuorumAndSingleOperator() public { ISignatureUtils.SignatureWithSaltAndExpiry memory emptySig; uint32 registrationBlockNumber = 100; @@ -574,11 +586,14 @@ contract RegistryCoordinatorUnitTests_DeregisterOperator_EjectOperator is Regist ); } + // @notice verifies that an operator who was registered for a fuzzed set of quorums can be deregistered + // @dev deregisters the operator from *all* quorums for which they we registered. function testFuzz_deregisterOperator_fuzzedQuorumAndSingleOperator(uint256 quorumBitmap) public { ISignatureUtils.SignatureWithSaltAndExpiry memory emptySig; uint32 registrationBlockNumber = 100; uint32 deregistrationBlockNumber = 200; + // filter down fuzzed input to only valid quorums quorumBitmap = quorumBitmap & MAX_QUORUM_BITMAP; cheats.assume(quorumBitmap != 0); bytes memory quorumNumbers = BitmapUtils.bitmapToBytesArray(quorumBitmap); @@ -625,7 +640,95 @@ contract RegistryCoordinatorUnitTests_DeregisterOperator_EjectOperator is Regist }))) ); } + // @notice verifies that an operator who was registered for a fuzzed set of quorums can be deregistered from a subset of those quorums + // @dev deregisters the operator from a fuzzed subset of the quorums for which they we registered. + function testFuzz_deregisterOperator_singleOperator_partialDeregistration( + uint256 registrationQuorumBitmap, + uint256 deregistrationQuorumBitmap + ) public { + ISignatureUtils.SignatureWithSaltAndExpiry memory emptySig; + uint32 registrationBlockNumber = 100; + uint32 deregistrationBlockNumber = 200; + + // filter down fuzzed input to only valid quorums + registrationQuorumBitmap = registrationQuorumBitmap & MAX_QUORUM_BITMAP; + cheats.assume(registrationQuorumBitmap != 0); + // filter the other fuzzed input to a subset of the first fuzzed input + deregistrationQuorumBitmap = deregistrationQuorumBitmap & registrationQuorumBitmap; + cheats.assume(deregistrationQuorumBitmap != 0); + bytes memory registrationquorumNumbers = BitmapUtils.bitmapToBytesArray(registrationQuorumBitmap); + + for (uint i = 0; i < registrationquorumNumbers.length; i++) { + stakeRegistry.setOperatorWeight(uint8(registrationquorumNumbers[i]), defaultOperator, defaultStake); + } + + cheats.startPrank(defaultOperator); + + cheats.roll(registrationBlockNumber); + + registryCoordinator.registerOperator(registrationquorumNumbers, defaultSocket, pubkeyRegistrationParams, emptySig); + + bytes memory deregistrationquorumNumbers = BitmapUtils.bitmapToBytesArray(deregistrationQuorumBitmap); + + cheats.expectEmit(true, true, true, true, address(blsApkRegistry)); + emit OperatorRemovedFromQuorums(defaultOperator, deregistrationquorumNumbers); + for (uint i = 0; i < deregistrationquorumNumbers.length; i++) { + cheats.expectEmit(true, true, true, true, address(stakeRegistry)); + emit OperatorStakeUpdate(defaultOperatorId, uint8(deregistrationquorumNumbers[i]), 0); + } + cheats.roll(deregistrationBlockNumber); + + uint256 gasBefore = gasleft(); + registryCoordinator.deregisterOperator(deregistrationquorumNumbers); + uint256 gasAfter = gasleft(); + emit log_named_uint("gasUsed", gasBefore - gasAfter); + emit log_named_uint("numQuorums", deregistrationquorumNumbers.length); + + // check that the operator is marked as 'degregistered' only if deregistered from *all* quorums + if (deregistrationQuorumBitmap == registrationQuorumBitmap) { + assertEq( + keccak256(abi.encode(registryCoordinator.getOperator(defaultOperator))), + keccak256(abi.encode(IRegistryCoordinator.OperatorInfo({ + operatorId: defaultOperatorId, + status: IRegistryCoordinator.OperatorStatus.DEREGISTERED + }))) + ); + } else { + assertEq( + keccak256(abi.encode(registryCoordinator.getOperator(defaultOperator))), + keccak256(abi.encode(IRegistryCoordinator.OperatorInfo({ + operatorId: defaultOperatorId, + status: IRegistryCoordinator.OperatorStatus.REGISTERED + }))) + ); + } + // ensure that the operator's current quorum bitmap matches the expectation + uint256 expectedQuorumBitmap = BitmapUtils.minus(registrationQuorumBitmap, deregistrationQuorumBitmap); + assertEq(registryCoordinator.getCurrentQuorumBitmap(defaultOperatorId), expectedQuorumBitmap); + // check that the quorum bitmap history is as expected + assertEq( + keccak256(abi.encode(registryCoordinator.getQuorumBitmapUpdateByIndex(defaultOperatorId, 0))), + keccak256(abi.encode(IRegistryCoordinator.QuorumBitmapUpdate({ + quorumBitmap: uint192(registrationQuorumBitmap), + updateBlockNumber: registrationBlockNumber, + nextUpdateBlockNumber: deregistrationBlockNumber + }))) + ); + // note: there will be no second entry in the operator's bitmap history in the event that the operator has totally deregistered + if (deregistrationQuorumBitmap != registrationQuorumBitmap) { + assertEq( + keccak256(abi.encode(registryCoordinator.getQuorumBitmapUpdateByIndex(defaultOperatorId, 1))), + keccak256(abi.encode(IRegistryCoordinator.QuorumBitmapUpdate({ + quorumBitmap: uint192(expectedQuorumBitmap), + updateBlockNumber: deregistrationBlockNumber, + nextUpdateBlockNumber: 0 + }))) + ); + } + } + + // @notice registers the max number of operators with fuzzed bitmaps and then deregisters a pseudorandom operator (from all of their quorums) function testFuzz_deregisterOperator_manyOperators(uint256 pseudoRandomNumber) public { uint32 numOperators = defaultMaxOperatorCount; @@ -657,11 +760,11 @@ contract RegistryCoordinatorUnitTests_DeregisterOperator_EjectOperator is Regist } } - uint256 indexOfOperatorToDerigister = pseudoRandomNumber % numOperators; - address operatorToDerigister = _incrementAddress(defaultOperator, indexOfOperatorToDerigister); - BN254.G1Point memory operatorToDeregisterPubKey = BN254.hashToG1(keccak256(abi.encodePacked(pseudoRandomNumber, indexOfOperatorToDerigister))); - bytes32 operatorToDerigisterId = BN254.hashG1Point(operatorToDeregisterPubKey); - uint256 operatorToDeregisterQuorumBitmap = quorumBitmaps[indexOfOperatorToDerigister]; + uint256 indexOfOperatorToDeregister = pseudoRandomNumber % numOperators; + address operatorToDeregister = _incrementAddress(defaultOperator, indexOfOperatorToDeregister); + BN254.G1Point memory operatorToDeregisterPubKey = BN254.hashToG1(keccak256(abi.encodePacked(pseudoRandomNumber, indexOfOperatorToDeregister))); + bytes32 operatorToDeregisterId = BN254.hashG1Point(operatorToDeregisterPubKey); + uint256 operatorToDeregisterQuorumBitmap = quorumBitmaps[indexOfOperatorToDeregister]; bytes memory operatorToDeregisterQuorumNumbers = BitmapUtils.bitmapToBytesArray(operatorToDeregisterQuorumBitmap); bytes32[] memory operatorIdsToSwap = new bytes32[](operatorToDeregisterQuorumNumbers.length); @@ -670,28 +773,28 @@ contract RegistryCoordinatorUnitTests_DeregisterOperator_EjectOperator is Regist } cheats.expectEmit(true, true, true, true, address(blsApkRegistry)); - emit OperatorRemovedFromQuorums(operatorToDerigister, operatorToDeregisterQuorumNumbers); + emit OperatorRemovedFromQuorums(operatorToDeregister, operatorToDeregisterQuorumNumbers); for (uint i = 0; i < operatorToDeregisterQuorumNumbers.length; i++) { cheats.expectEmit(true, true, true, true, address(stakeRegistry)); - emit OperatorStakeUpdate(operatorToDerigisterId, uint8(operatorToDeregisterQuorumNumbers[i]), 0); + emit OperatorStakeUpdate(operatorToDeregisterId, uint8(operatorToDeregisterQuorumNumbers[i]), 0); } cheats.roll(deregistrationBlockNumber); - cheats.prank(operatorToDerigister); + cheats.prank(operatorToDeregister); registryCoordinator.deregisterOperator(operatorToDeregisterQuorumNumbers); assertEq( - keccak256(abi.encode(registryCoordinator.getOperator(operatorToDerigister))), + keccak256(abi.encode(registryCoordinator.getOperator(operatorToDeregister))), keccak256(abi.encode(IRegistryCoordinator.OperatorInfo({ - operatorId: operatorToDerigisterId, + operatorId: operatorToDeregisterId, status: IRegistryCoordinator.OperatorStatus.DEREGISTERED }))) ); assertEq(registryCoordinator.getCurrentQuorumBitmap(defaultOperatorId), 0); assertEq( - keccak256(abi.encode(registryCoordinator.getQuorumBitmapUpdateByIndex(operatorToDerigisterId, 0))), + keccak256(abi.encode(registryCoordinator.getQuorumBitmapUpdateByIndex(operatorToDeregisterId, 0))), keccak256(abi.encode(IRegistryCoordinator.QuorumBitmapUpdate({ quorumBitmap: uint192(operatorToDeregisterQuorumBitmap), updateBlockNumber: registrationBlockNumber, @@ -819,10 +922,11 @@ contract RegistryCoordinatorUnitTests_DeregisterOperator_EjectOperator is Regist status: IRegistryCoordinator.OperatorStatus.REGISTERED }))) ); - // make sure the operator is not in any quorums + // make sure the operator is properly removed from the quorums assertEq( - registryCoordinator.getCurrentQuorumBitmap(defaultOperatorId), - BitmapUtils.orderedBytesArrayToBitmap(quorumNumbers) & ~BitmapUtils.orderedBytesArrayToBitmap(quorumNumbersToEject) // quorumsRegisteredFor & ~quorumsEjectedFrom + registryCoordinator.getCurrentQuorumBitmap(defaultOperatorId), + // quorumsRegisteredFor & ~quorumsEjectedFrom + BitmapUtils.orderedBytesArrayToBitmap(quorumNumbers) & ~BitmapUtils.orderedBytesArrayToBitmap(quorumNumbersToEject) ); } @@ -843,6 +947,7 @@ contract RegistryCoordinatorUnitTests_DeregisterOperator_EjectOperator is Regist } contract RegistryCoordinatorUnitTests_RegisterOperatorWithChurn is RegistryCoordinatorUnitTests { + // @notice registers an operator for a single quorum, with a fuzzed pubkey, churning out another operator from the quorum function testFuzz_registerOperatorWithChurn(uint256 pseudoRandomNumber) public { uint32 numOperators = defaultMaxOperatorCount; uint32 kickRegistrationBlockNumber = 100; @@ -909,7 +1014,8 @@ contract RegistryCoordinatorUnitTests_RegisterOperatorWithChurn is RegistryCoord { ISignatureUtils.SignatureWithSaltAndExpiry memory emptyAVSRegSig; - ISignatureUtils.SignatureWithSaltAndExpiry memory signatureWithExpiry = _signOperatorChurnApproval(operatorToRegisterId, operatorKickParams, defaultSalt, block.timestamp + 10); + ISignatureUtils.SignatureWithSaltAndExpiry memory signatureWithExpiry = + _signOperatorChurnApproval(operatorToRegisterId, operatorKickParams, defaultSalt, block.timestamp + 10); cheats.prank(operatorToRegister); uint256 gasBefore = gasleft(); registryCoordinator.registerOperatorWithChurn( @@ -957,13 +1063,14 @@ contract RegistryCoordinatorUnitTests_RegisterOperatorWithChurn is RegistryCoord address operatorToRegister, BN254.G1Point memory operatorToRegisterPubKey, IRegistryCoordinator.OperatorKickParam[] memory operatorKickParams - ) = _testRegisterOperatorWithChurn_SetUp(pseudoRandomNumber, quorumNumbers, defaultStake); + ) = _test_registerOperatorWithChurn_SetUp(pseudoRandomNumber, quorumNumbers, defaultStake); bytes32 operatorToRegisterId = BN254.hashG1Point(operatorToRegisterPubKey); stakeRegistry.setOperatorWeight(defaultQuorumNumber, operatorToRegister, defaultStake); cheats.roll(registrationBlockNumber); - ISignatureUtils.SignatureWithSaltAndExpiry memory signatureWithExpiry = _signOperatorChurnApproval(operatorToRegisterId, operatorKickParams, defaultSalt, block.timestamp + 10); + ISignatureUtils.SignatureWithSaltAndExpiry memory signatureWithExpiry = + _signOperatorChurnApproval(operatorToRegisterId, operatorKickParams, defaultSalt, block.timestamp + 10); cheats.prank(operatorToRegister); cheats.expectRevert("RegistryCoordinator._validateChurn: incoming operator has insufficient stake for churn"); registryCoordinator.registerOperatorWithChurn( @@ -986,7 +1093,7 @@ contract RegistryCoordinatorUnitTests_RegisterOperatorWithChurn is RegistryCoord address operatorToRegister, BN254.G1Point memory operatorToRegisterPubKey, IRegistryCoordinator.OperatorKickParam[] memory operatorKickParams - ) = _testRegisterOperatorWithChurn_SetUp(pseudoRandomNumber, quorumNumbers, operatorToKickStake); + ) = _test_registerOperatorWithChurn_SetUp(pseudoRandomNumber, quorumNumbers, operatorToKickStake); bytes32 operatorToRegisterId = BN254.hashG1Point(operatorToRegisterPubKey); @@ -994,7 +1101,8 @@ contract RegistryCoordinatorUnitTests_RegisterOperatorWithChurn is RegistryCoord stakeRegistry.setOperatorWeight(defaultQuorumNumber, operatorToRegister, operatorToKickStake * defaultKickBIPsOfOperatorStake / 10000 + 1); cheats.roll(registrationBlockNumber); - ISignatureUtils.SignatureWithSaltAndExpiry memory signatureWithExpiry = _signOperatorChurnApproval(operatorToRegisterId, operatorKickParams, defaultSalt, block.timestamp + 10); + ISignatureUtils.SignatureWithSaltAndExpiry memory signatureWithExpiry = + _signOperatorChurnApproval(operatorToRegisterId, operatorKickParams, defaultSalt, block.timestamp + 10); cheats.prank(operatorToRegister); cheats.expectRevert("RegistryCoordinator._validateChurn: cannot kick operator with more than kickBIPsOfTotalStake"); registryCoordinator.registerOperatorWithChurn( @@ -1016,7 +1124,7 @@ contract RegistryCoordinatorUnitTests_RegisterOperatorWithChurn is RegistryCoord address operatorToRegister, , IRegistryCoordinator.OperatorKickParam[] memory operatorKickParams - ) = _testRegisterOperatorWithChurn_SetUp(pseudoRandomNumber, quorumNumbers, defaultStake); + ) = _test_registerOperatorWithChurn_SetUp(pseudoRandomNumber, quorumNumbers, defaultStake); uint96 registeringStake = defaultKickBIPsOfOperatorStake * defaultStake; stakeRegistry.setOperatorWeight(defaultQuorumNumber, operatorToRegister, registeringStake); @@ -1024,7 +1132,8 @@ contract RegistryCoordinatorUnitTests_RegisterOperatorWithChurn is RegistryCoord cheats.roll(registrationBlockNumber); ISignatureUtils.SignatureWithSaltAndExpiry memory signatureWithSaltAndExpiry; signatureWithSaltAndExpiry.expiry = block.timestamp + 10; - signatureWithSaltAndExpiry.signature = hex"000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001B"; + signatureWithSaltAndExpiry.signature = + hex"000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001B"; signatureWithSaltAndExpiry.salt = defaultSalt; cheats.prank(operatorToRegister); cheats.expectRevert("ECDSA: invalid signature"); @@ -1047,14 +1156,15 @@ contract RegistryCoordinatorUnitTests_RegisterOperatorWithChurn is RegistryCoord address operatorToRegister, BN254.G1Point memory operatorToRegisterPubKey, IRegistryCoordinator.OperatorKickParam[] memory operatorKickParams - ) = _testRegisterOperatorWithChurn_SetUp(pseudoRandomNumber, quorumNumbers, defaultStake); + ) = _test_registerOperatorWithChurn_SetUp(pseudoRandomNumber, quorumNumbers, defaultStake); bytes32 operatorToRegisterId = BN254.hashG1Point(operatorToRegisterPubKey); uint96 registeringStake = defaultKickBIPsOfOperatorStake * defaultStake; stakeRegistry.setOperatorWeight(defaultQuorumNumber, operatorToRegister, registeringStake); cheats.roll(registrationBlockNumber); - ISignatureUtils.SignatureWithSaltAndExpiry memory signatureWithSaltAndExpiry = _signOperatorChurnApproval(operatorToRegisterId, operatorKickParams, defaultSalt, block.timestamp - 1); + ISignatureUtils.SignatureWithSaltAndExpiry memory signatureWithSaltAndExpiry = + _signOperatorChurnApproval(operatorToRegisterId, operatorKickParams, defaultSalt, block.timestamp - 1); cheats.prank(operatorToRegister); cheats.expectRevert("RegistryCoordinator._verifyChurnApproverSignature: churnApprover signature expired"); registryCoordinator.registerOperatorWithChurn( From 42e683880f307d9717c6ad9e13f8935d4f1860d5 Mon Sep 17 00:00:00 2001 From: ChaoticWalrus <93558947+ChaoticWalrus@users.noreply.github.com> Date: Wed, 3 Jan 2024 15:13:45 -0800 Subject: [PATCH 16/48] feat: expose more internal functions in harnessed contract --- .../RegistryCoordinatorHarness.t.sol | 42 ++++++++++++++----- test/unit/BLSSignatureCheckerUnit.t.sol | 4 +- 2 files changed, 33 insertions(+), 13 deletions(-) diff --git a/test/harnesses/RegistryCoordinatorHarness.t.sol b/test/harnesses/RegistryCoordinatorHarness.t.sol index 70bd306b..d1af8be7 100644 --- a/test/harnesses/RegistryCoordinatorHarness.t.sol +++ b/test/harnesses/RegistryCoordinatorHarness.t.sol @@ -24,16 +24,36 @@ contract RegistryCoordinatorHarness is RegistryCoordinator, Test { _operatorInfo[operator].operatorId = operatorId; } - function recordOperatorQuorumBitmapUpdate(bytes32 operatorId, uint192 quorumBitmap) external { - uint256 operatorQuorumBitmapHistoryLength = _operatorBitmapHistory[operatorId].length; - if (operatorQuorumBitmapHistoryLength != 0) { - _operatorBitmapHistory[operatorId][operatorQuorumBitmapHistoryLength - 1].nextUpdateBlockNumber = uint32(block.number); - } - - _operatorBitmapHistory[operatorId].push(QuorumBitmapUpdate({ - updateBlockNumber: uint32(block.number), - nextUpdateBlockNumber: 0, - quorumBitmap: quorumBitmap - })); + // @notice exposes the internal `_registerOperator` function, overriding all access controls + function _registerOperatorExternal( + address operator, + bytes32 operatorId, + bytes calldata quorumNumbers, + string memory socket, + SignatureWithSaltAndExpiry memory operatorSignature + ) external returns (RegisterResults memory results) { + return _registerOperator(operator, operatorId, quorumNumbers, socket, operatorSignature); + } + + // @notice exposes the internal `_deregisterOperator` function, overriding all access controls + function _deregisterOperatorExternal( + address operator, + bytes calldata quorumNumbers + ) external { + _deregisterOperator(operator, quorumNumbers); + } + + // @notice exposes the internal `_updateOperator` function, overriding all access controls + function _updateOperatorExternal( + address operator, + OperatorInfo memory operatorInfo, + bytes memory quorumsToUpdate + ) external { + _updateOperator(operator, operatorInfo, quorumsToUpdate); + } + + // @notice exposes the internal `_updateOperatorBitmap` function, overriding all access controls + function _updateOperatorBitmapExternal(bytes32 operatorId, uint192 quorumBitmap) external { + _updateOperatorBitmap(operatorId, quorumBitmap); } } diff --git a/test/unit/BLSSignatureCheckerUnit.t.sol b/test/unit/BLSSignatureCheckerUnit.t.sol index 46974297..1ccd829a 100644 --- a/test/unit/BLSSignatureCheckerUnit.t.sol +++ b/test/unit/BLSSignatureCheckerUnit.t.sol @@ -82,8 +82,8 @@ contract BLSSignatureCheckerUnitTests is BLSMockAVSDeployer { (uint32 referenceBlockNumber, BLSSignatureChecker.NonSignerStakesAndSignature memory nonSignerStakesAndSignature) = _registerSignatoriesAndGetNonSignerStakeAndSignatureRandom(pseudoRandomNumber, numNonSigners, quorumBitmap); - // record a quorumBitmap update - registryCoordinator.recordOperatorQuorumBitmapUpdate(nonSignerStakesAndSignature.nonSignerPubkeys[0].hashG1Point(), uint192(quorumBitmap | 2)); + // record a quorumBitmap update via a harnessed function + registryCoordinator._updateOperatorBitmapExternal(nonSignerStakesAndSignature.nonSignerPubkeys[0].hashG1Point(), uint192(quorumBitmap | 2)); // set the nonSignerQuorumBitmapIndices to a different value nonSignerStakesAndSignature.nonSignerQuorumBitmapIndices[0] = 1; From 3e95552cf920b71121aee4a7be131cf714a6c5bf Mon Sep 17 00:00:00 2001 From: ChaoticWalrus <93558947+ChaoticWalrus@users.noreply.github.com> Date: Wed, 3 Jan 2024 15:30:23 -0800 Subject: [PATCH 17/48] feat: add some simple coverage for the internal `_registerOperator` fnc --- test/unit/RegistryCoordinatorUnit.t.sol | 70 +++++++++++++++++++++++++ 1 file changed, 70 insertions(+) diff --git a/test/unit/RegistryCoordinatorUnit.t.sol b/test/unit/RegistryCoordinatorUnit.t.sol index 946db5f5..63df11a3 100644 --- a/test/unit/RegistryCoordinatorUnit.t.sol +++ b/test/unit/RegistryCoordinatorUnit.t.sol @@ -493,6 +493,76 @@ contract RegistryCoordinatorUnitTests_RegisterOperator is RegistryCoordinatorUni registryCoordinator.registerOperator(quorumNumbers, defaultSocket, pubkeyRegistrationParams, emptySig); } + + // tests for the internal `_registerOperator` function: + function test_registerOperatorInternal_revert_noQuorums() public { + bytes memory emptyQuorumNumbers = new bytes(0); + ISignatureUtils.SignatureWithSaltAndExpiry memory emptySig; + + cheats.expectRevert("RegistryCoordinator._registerOperator: bitmap cannot be 0"); + registryCoordinator._registerOperatorExternal(defaultOperator, defaultOperatorId, emptyQuorumNumbers, defaultSocket, emptySig); + } + + function test_registerOperatorInternal_revert_nonexistentQuorum() public { + bytes memory quorumNumbersTooLarge = new bytes(1); + ISignatureUtils.SignatureWithSaltAndExpiry memory emptySig; + + quorumNumbersTooLarge[0] = 0xC0; + + cheats.expectRevert("BitmapUtils.orderedBytesArrayToBitmap: bitmap exceeds max value"); + registryCoordinator._registerOperatorExternal(defaultOperator, defaultOperatorId, quorumNumbersTooLarge, defaultSocket, emptySig); + } + + function test_registerOperatorInternal_revert_operatorAlreadyRegisteredForQuorum() public { + ISignatureUtils.SignatureWithSaltAndExpiry memory emptySig; + bytes memory quorumNumbers = new bytes(1); + quorumNumbers[0] = bytes1(defaultQuorumNumber); + + stakeRegistry.setOperatorWeight(uint8(quorumNumbers[0]), defaultOperator, defaultStake); + registryCoordinator._registerOperatorExternal(defaultOperator, defaultOperatorId, quorumNumbers, defaultSocket, emptySig); + + cheats.expectRevert("RegistryCoordinator._registerOperator: operator already registered for some quorums being registered for"); + registryCoordinator._registerOperatorExternal(defaultOperator, defaultOperatorId, quorumNumbers, defaultSocket, emptySig); + } + + function test_registerOperatorInternal() public { + bytes memory quorumNumbers = new bytes(1); + ISignatureUtils.SignatureWithSaltAndExpiry memory emptySig; + quorumNumbers[0] = bytes1(defaultQuorumNumber); + + stakeRegistry.setOperatorWeight(uint8(quorumNumbers[0]), defaultOperator, defaultStake); + + cheats.expectEmit(true, true, true, true, address(registryCoordinator)); + emit OperatorSocketUpdate(defaultOperatorId, defaultSocket); + cheats.expectEmit(true, true, true, true, address(blsApkRegistry)); + emit OperatorAddedToQuorums(defaultOperator, quorumNumbers); + cheats.expectEmit(true, true, true, true, address(stakeRegistry)); + emit OperatorStakeUpdate(defaultOperatorId, defaultQuorumNumber, defaultStake); + cheats.expectEmit(true, true, true, true, address(indexRegistry)); + emit QuorumIndexUpdate(defaultOperatorId, defaultQuorumNumber, 0); + + registryCoordinator._registerOperatorExternal(defaultOperator, defaultOperatorId, quorumNumbers, defaultSocket, emptySig); + + uint256 quorumBitmap = BitmapUtils.orderedBytesArrayToBitmap(quorumNumbers); + + assertEq(registryCoordinator.getOperatorId(defaultOperator), defaultOperatorId); + assertEq( + keccak256(abi.encode(registryCoordinator.getOperator(defaultOperator))), + keccak256(abi.encode(IRegistryCoordinator.OperatorInfo({ + operatorId: defaultOperatorId, + status: IRegistryCoordinator.OperatorStatus.REGISTERED + }))) + ); + assertEq(registryCoordinator.getCurrentQuorumBitmap(defaultOperatorId), quorumBitmap); + assertEq( + keccak256(abi.encode(registryCoordinator.getQuorumBitmapUpdateByIndex(defaultOperatorId, 0))), + keccak256(abi.encode(IRegistryCoordinator.QuorumBitmapUpdate({ + quorumBitmap: uint192(quorumBitmap), + updateBlockNumber: uint32(block.number), + nextUpdateBlockNumber: 0 + }))) + ); + } } contract RegistryCoordinatorUnitTests_DeregisterOperator_EjectOperator is RegistryCoordinatorUnitTests { From 83c5aa6f2e0d1e657c1836fb2ea4a476b51920ee Mon Sep 17 00:00:00 2001 From: ChaoticWalrus <93558947+ChaoticWalrus@users.noreply.github.com> Date: Wed, 3 Jan 2024 15:45:23 -0800 Subject: [PATCH 18/48] feat: add test coverage for the internal `deregisterOperator` fnc also clarify somewhat ambiguous wording in the tree file --- test/tree/RegistryCoordinatorUnit.tree | 2 +- test/unit/RegistryCoordinatorUnit.t.sol | 135 +++++++++++++++++++++++- 2 files changed, 135 insertions(+), 2 deletions(-) diff --git a/test/tree/RegistryCoordinatorUnit.tree b/test/tree/RegistryCoordinatorUnit.tree index 10238e2e..159fd6b6 100644 --- a/test/tree/RegistryCoordinatorUnit.tree +++ b/test/tree/RegistryCoordinatorUnit.tree @@ -136,7 +136,7 @@ │ │ └── it should revert │ ├── given that any quorums being removed do not (yet) exist │ │ └── it should revert -│ ├── given that the operator is *not* currently registered for any of quorums being removed +│ ├── given that the operator is *not* currently registered for any or all of quorums being removed │ │ └── it should revert │ ├── given that the operator is being removed from all quorums that they were registered for │ │ └── it should mark the operator as no longer registered diff --git a/test/unit/RegistryCoordinatorUnit.t.sol b/test/unit/RegistryCoordinatorUnit.t.sol index 63df11a3..e6e68936 100644 --- a/test/unit/RegistryCoordinatorUnit.t.sol +++ b/test/unit/RegistryCoordinatorUnit.t.sol @@ -274,7 +274,7 @@ contract RegistryCoordinatorUnitTests_RegisterOperator is RegistryCoordinatorUni registryCoordinator.registerOperator(quorumNumbersTooLarge, defaultSocket, pubkeyRegistrationParams, emptySig); } - function test_registerOperator_revert_NonexistentQuorum() public { + function test_registerOperator_revert_nonexistentQuorum() public { _deployMockEigenLayerAndAVS(10); bytes memory quorumNumbersNotCreated = new bytes(1); ISignatureUtils.SignatureWithSaltAndExpiry memory emptySig; @@ -924,6 +924,139 @@ contract RegistryCoordinatorUnitTests_DeregisterOperator_EjectOperator is Regist ); } + // tests for the internal `_deregisterOperator` function: + function test_deregisterOperatorExternal_revert_noQuorums() public { + ISignatureUtils.SignatureWithSaltAndExpiry memory emptySig; + uint32 registrationBlockNumber = 100; + uint32 deregistrationBlockNumber = 200; + + bytes memory quorumNumbers = new bytes(1); + quorumNumbers[0] = bytes1(defaultQuorumNumber); + + stakeRegistry.setOperatorWeight(uint8(quorumNumbers[0]), defaultOperator, defaultStake); + + cheats.roll(registrationBlockNumber); + cheats.startPrank(defaultOperator); + registryCoordinator.registerOperator(quorumNumbers, defaultSocket, pubkeyRegistrationParams, emptySig); + + bytes memory emptyQuorumNumbers = new bytes(0); + + cheats.roll(deregistrationBlockNumber); + cheats.expectRevert("RegistryCoordinator._deregisterOperator: bitmap cannot be 0"); + registryCoordinator._deregisterOperatorExternal(defaultOperator, emptyQuorumNumbers); + } + + function test_deregisterOperatorExternal_revert_notRegistered() public { + bytes memory emptyQuorumNumbers = new bytes(0); + cheats.expectRevert("RegistryCoordinator._deregisterOperator: operator is not registered"); + registryCoordinator._deregisterOperatorExternal(defaultOperator, emptyQuorumNumbers); + } + + function test_deregisterOperatorExternal_revert_incorrectQuorums() public { + ISignatureUtils.SignatureWithSaltAndExpiry memory emptySig; + uint32 registrationBlockNumber = 100; + uint32 deregistrationBlockNumber = 200; + + bytes memory quorumNumbers = new bytes(1); + quorumNumbers[0] = bytes1(defaultQuorumNumber); + + stakeRegistry.setOperatorWeight(uint8(quorumNumbers[0]), defaultOperator, defaultStake); + + cheats.roll(registrationBlockNumber); + cheats.startPrank(defaultOperator); + registryCoordinator.registerOperator(quorumNumbers, defaultSocket, pubkeyRegistrationParams, emptySig); + + bytes memory incorrectQuorum = new bytes(1); + incorrectQuorum[0] = bytes1(defaultQuorumNumber + 1); + + cheats.roll(deregistrationBlockNumber); + cheats.expectRevert("RegistryCoordinator._deregisterOperator: operator is not registered for specified quorums"); + registryCoordinator._deregisterOperatorExternal(defaultOperator, incorrectQuorum); + } + + // note: this is not possible to test, because there is no route to getting the operator registered for nonexistent quorums + // function test_deregisterOperatorExternal_revert_nonexistentQuorums() public { + + function testFuzz_deregisterOperatorInternal_partialDeregistration( + uint256 registrationQuorumBitmap, + uint256 deregistrationQuorumBitmap + ) public { + ISignatureUtils.SignatureWithSaltAndExpiry memory emptySig; + uint32 registrationBlockNumber = 100; + uint32 deregistrationBlockNumber = 200; + + // filter down fuzzed input to only valid quorums + registrationQuorumBitmap = registrationQuorumBitmap & MAX_QUORUM_BITMAP; + cheats.assume(registrationQuorumBitmap != 0); + // filter the other fuzzed input to a subset of the first fuzzed input + deregistrationQuorumBitmap = deregistrationQuorumBitmap & registrationQuorumBitmap; + cheats.assume(deregistrationQuorumBitmap != 0); + bytes memory registrationquorumNumbers = BitmapUtils.bitmapToBytesArray(registrationQuorumBitmap); + + for (uint i = 0; i < registrationquorumNumbers.length; i++) { + stakeRegistry.setOperatorWeight(uint8(registrationquorumNumbers[i]), defaultOperator, defaultStake); + } + + cheats.roll(registrationBlockNumber); + cheats.startPrank(defaultOperator); + registryCoordinator.registerOperator(registrationquorumNumbers, defaultSocket, pubkeyRegistrationParams, emptySig); + + bytes memory deregistrationquorumNumbers = BitmapUtils.bitmapToBytesArray(deregistrationQuorumBitmap); + + cheats.expectEmit(true, true, true, true, address(blsApkRegistry)); + emit OperatorRemovedFromQuorums(defaultOperator, deregistrationquorumNumbers); + for (uint i = 0; i < deregistrationquorumNumbers.length; i++) { + cheats.expectEmit(true, true, true, true, address(stakeRegistry)); + emit OperatorStakeUpdate(defaultOperatorId, uint8(deregistrationquorumNumbers[i]), 0); + } + + cheats.roll(deregistrationBlockNumber); + + registryCoordinator._deregisterOperatorExternal(defaultOperator, deregistrationquorumNumbers); + + // check that the operator is marked as 'degregistered' only if deregistered from *all* quorums + if (deregistrationQuorumBitmap == registrationQuorumBitmap) { + assertEq( + keccak256(abi.encode(registryCoordinator.getOperator(defaultOperator))), + keccak256(abi.encode(IRegistryCoordinator.OperatorInfo({ + operatorId: defaultOperatorId, + status: IRegistryCoordinator.OperatorStatus.DEREGISTERED + }))) + ); + } else { + assertEq( + keccak256(abi.encode(registryCoordinator.getOperator(defaultOperator))), + keccak256(abi.encode(IRegistryCoordinator.OperatorInfo({ + operatorId: defaultOperatorId, + status: IRegistryCoordinator.OperatorStatus.REGISTERED + }))) + ); + } + // ensure that the operator's current quorum bitmap matches the expectation + uint256 expectedQuorumBitmap = BitmapUtils.minus(registrationQuorumBitmap, deregistrationQuorumBitmap); + assertEq(registryCoordinator.getCurrentQuorumBitmap(defaultOperatorId), expectedQuorumBitmap); + // check that the quorum bitmap history is as expected + assertEq( + keccak256(abi.encode(registryCoordinator.getQuorumBitmapUpdateByIndex(defaultOperatorId, 0))), + keccak256(abi.encode(IRegistryCoordinator.QuorumBitmapUpdate({ + quorumBitmap: uint192(registrationQuorumBitmap), + updateBlockNumber: registrationBlockNumber, + nextUpdateBlockNumber: deregistrationBlockNumber + }))) + ); + // note: there will be no second entry in the operator's bitmap history in the event that the operator has totally deregistered + if (deregistrationQuorumBitmap != registrationQuorumBitmap) { + assertEq( + keccak256(abi.encode(registryCoordinator.getQuorumBitmapUpdateByIndex(defaultOperatorId, 1))), + keccak256(abi.encode(IRegistryCoordinator.QuorumBitmapUpdate({ + quorumBitmap: uint192(expectedQuorumBitmap), + updateBlockNumber: deregistrationBlockNumber, + nextUpdateBlockNumber: 0 + }))) + ); + } + } + function test_ejectOperator_allQuorums() public { // register operator with default stake with default quorum number bytes memory quorumNumbers = new bytes(1); From e43114def78109aa338f012a787dc225e6e0e981 Mon Sep 17 00:00:00 2001 From: ChaoticWalrus <93558947+ChaoticWalrus@users.noreply.github.com> Date: Wed, 3 Jan 2024 16:03:48 -0800 Subject: [PATCH 19/48] feat: add simple test coverage for the internal `_updateOperatorBitmap` fnc --- test/unit/RegistryCoordinatorUnit.t.sol | 57 +++++++++++++++++++++++++ 1 file changed, 57 insertions(+) diff --git a/test/unit/RegistryCoordinatorUnit.t.sol b/test/unit/RegistryCoordinatorUnit.t.sol index e6e68936..59c6d372 100644 --- a/test/unit/RegistryCoordinatorUnit.t.sol +++ b/test/unit/RegistryCoordinatorUnit.t.sol @@ -1379,6 +1379,63 @@ contract RegistryCoordinatorUnitTests_RegisterOperatorWithChurn is RegistryCoord emptyAVSRegSig ); } +} + +contract RegistryCoordinatorUnitTests_UpdateOperators is RegistryCoordinatorUnitTests { + // @notice tests that the internal `_updateOperatorBitmap` function works as expected, for fuzzed inputs + function testFuzz_updateOperatorBitmapInternal_noPreviousEntries(uint192 newBitmap) public { + registryCoordinator._updateOperatorBitmapExternal(defaultOperatorId, newBitmap); + assertEq( + keccak256(abi.encode(registryCoordinator.getQuorumBitmapUpdateByIndex(defaultOperatorId, 0))), + keccak256(abi.encode(IRegistryCoordinator.QuorumBitmapUpdate({ + quorumBitmap: uint192(newBitmap), + updateBlockNumber: uint32(block.number), + nextUpdateBlockNumber: 0 + }))) + ); + } + // @notice tests that the internal `_updateOperatorBitmap` function works as expected, for fuzzed inputs + function testFuzz_updateOperatorBitmapInternal_previousEntryInCurrentBlock(uint192 newBitmap) public { + uint192 pastBitmap = 1; + testFuzz_updateOperatorBitmapInternal_noPreviousEntries(pastBitmap); + + registryCoordinator._updateOperatorBitmapExternal(defaultOperatorId, newBitmap); + assertEq( + keccak256(abi.encode(registryCoordinator.getQuorumBitmapUpdateByIndex(defaultOperatorId, 0))), + keccak256(abi.encode(IRegistryCoordinator.QuorumBitmapUpdate({ + quorumBitmap: uint192(newBitmap), + updateBlockNumber: uint32(block.number), + nextUpdateBlockNumber: 0 + }))) + ); + } + + // @notice tests that the internal `_updateOperatorBitmap` function works as expected, for fuzzed inputs + function testFuzz_updateOperatorBitmapInternal_previousEntryInPastBlock(uint192 newBitmap) public { + uint192 pastBitmap = 1; + testFuzz_updateOperatorBitmapInternal_noPreviousEntries(pastBitmap); + // advance the block number + uint256 previousBlockNumber = block.number; + cheats.roll(previousBlockNumber + 1); + + registryCoordinator._updateOperatorBitmapExternal(defaultOperatorId, newBitmap); + assertEq( + keccak256(abi.encode(registryCoordinator.getQuorumBitmapUpdateByIndex(defaultOperatorId, 0))), + keccak256(abi.encode(IRegistryCoordinator.QuorumBitmapUpdate({ + quorumBitmap: uint192(pastBitmap), + updateBlockNumber: uint32(previousBlockNumber), + nextUpdateBlockNumber: uint32(block.number) + }))) + ); + assertEq( + keccak256(abi.encode(registryCoordinator.getQuorumBitmapUpdateByIndex(defaultOperatorId, 1))), + keccak256(abi.encode(IRegistryCoordinator.QuorumBitmapUpdate({ + quorumBitmap: uint192(newBitmap), + updateBlockNumber: uint32(block.number), + nextUpdateBlockNumber: 0 + }))) + ); + } } From 4dae4875116d9e33abfe1c4242ebcc4750e94d55 Mon Sep 17 00:00:00 2001 From: ChaoticWalrus <93558947+ChaoticWalrus@users.noreply.github.com> Date: Thu, 4 Jan 2024 09:17:39 -0800 Subject: [PATCH 20/48] chore: remove commitlint job that reviews all commits in PR from CI This was causing a *lot* of CI failures, including for merge commits With this commit, CI will still check the _latest_ commit for meeting conventions, it just won't run over all commits in a PR This may lead to a few more "unconventional" commits making it through, but the CI should still flag when someone is just not using conventional commits at all, which I think was the original goal. --- .github/workflows/commitlint.yml | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/.github/workflows/commitlint.yml b/.github/workflows/commitlint.yml index 352413f0..b2dfb166 100644 --- a/.github/workflows/commitlint.yml +++ b/.github/workflows/commitlint.yml @@ -16,8 +16,4 @@ jobs: - name: Validate current commit (last commit) with commitlint if: github.event_name == 'push' - run: npx commitlint --from HEAD~1 --to HEAD --verbose - - - name: Validate PR commits with commitlint - if: github.event_name == 'pull_request' - run: npx commitlint --from ${{ github.event.pull_request.head.sha }}~${{ github.event.pull_request.commits }} --to ${{ github.event.pull_request.head.sha }} --verbose + run: npx commitlint --from HEAD~1 --to HEAD --verbose \ No newline at end of file From 500d7ba9c86f18808fb46ae34f167631a39eb3b5 Mon Sep 17 00:00:00 2001 From: ChaoticWalrus <93558947+ChaoticWalrus@users.noreply.github.com> Date: Thu, 4 Jan 2024 11:08:58 -0800 Subject: [PATCH 21/48] feat: add some coverage for `updateOperators(ForQuorums)` fncs --- test/unit/RegistryCoordinatorUnit.t.sol | 69 +++++++++++++++++++++++++ 1 file changed, 69 insertions(+) diff --git a/test/unit/RegistryCoordinatorUnit.t.sol b/test/unit/RegistryCoordinatorUnit.t.sol index 59c6d372..6f71cf17 100644 --- a/test/unit/RegistryCoordinatorUnit.t.sol +++ b/test/unit/RegistryCoordinatorUnit.t.sol @@ -8,6 +8,7 @@ contract RegistryCoordinatorUnitTests is MockAVSDeployer { uint8 internal constant PAUSED_REGISTER_OPERATOR = 0; uint8 internal constant PAUSED_DEREGISTER_OPERATOR = 1; + uint8 internal constant PAUSED_UPDATE_OPERATOR = 2; uint8 internal constant MAX_QUORUM_COUNT = 192; event OperatorSocketUpdate(bytes32 indexed operatorId, string socket); @@ -1382,6 +1383,73 @@ contract RegistryCoordinatorUnitTests_RegisterOperatorWithChurn is RegistryCoord } contract RegistryCoordinatorUnitTests_UpdateOperators is RegistryCoordinatorUnitTests { + function test_updateOperators_revert_paused() public { + cheats.prank(pauser); + registryCoordinator.pause(2 ** PAUSED_UPDATE_OPERATOR); + + address[] memory operatorsToUpdate = new address[](1); + operatorsToUpdate[0] = defaultOperator; + + cheats.expectRevert(bytes("Pausable: index is paused")); + registryCoordinator.updateOperators(operatorsToUpdate); + } + + // @notice tests the `updateOperators` function with a single registered operator as input + function test_updateOperators_singleOperator() public { + // register the default operator + ISignatureUtils.SignatureWithSaltAndExpiry memory emptySig; + uint32 registrationBlockNumber = 100; + bytes memory quorumNumbers = new bytes(1); + quorumNumbers[0] = bytes1(defaultQuorumNumber); + stakeRegistry.setOperatorWeight(uint8(quorumNumbers[0]), defaultOperator, defaultStake); + cheats.startPrank(defaultOperator); + cheats.roll(registrationBlockNumber); + registryCoordinator.registerOperator(quorumNumbers, defaultSocket, pubkeyRegistrationParams, emptySig); + + address[] memory operatorsToUpdate = new address[](1); + operatorsToUpdate[0] = defaultOperator; + + // TODO: any additional checks here? mostly this just calls the StakeRegistry, so more appropriate for an integration test + registryCoordinator.updateOperators(operatorsToUpdate); + } + + // @notice tests the `updateOperators` function with a single *un*registered operator as input + function test_updateOperators_unregisteredOperator() public { + address[] memory operatorsToUpdate = new address[](1); + operatorsToUpdate[0] = defaultOperator; + + // force a staticcall to the `updateOperators` function -- this should *pass* because the call should be a strict no-op! + (bool success, ) = address(registryCoordinator).staticcall(abi.encodeWithSignature("updateOperators(address[])", operatorsToUpdate)); + require(success, "staticcall failed!"); + } + + function test_updateOperatorsForQuorum_revert_paused() public { + cheats.prank(pauser); + registryCoordinator.pause(2 ** PAUSED_UPDATE_OPERATOR); + + address[][] memory operatorsToUpdate = new address[][](1); + address[] memory operatorArray = new address[](1); + operatorArray[0] = defaultOperator; + operatorsToUpdate[0] = operatorArray; + bytes memory quorumNumbers = new bytes(1); + quorumNumbers[0] = bytes1(defaultQuorumNumber); + + cheats.expectRevert(bytes("Pausable: index is paused")); + registryCoordinator.updateOperatorsForQuorum(operatorsToUpdate, quorumNumbers); + } + + function test_updateOperatorsForQuorum_revert_nonexistentQuorum() public { + _deployMockEigenLayerAndAVS(10); + bytes memory quorumNumbersNotCreated = new bytes(1); + ISignatureUtils.SignatureWithSaltAndExpiry memory emptySig; + quorumNumbersNotCreated[0] = 0x0B; + address[][] memory operatorsToUpdate = new address[][](1); + + cheats.prank(defaultOperator); + cheats.expectRevert("BitmapUtils.orderedBytesArrayToBitmap: bitmap exceeds max value"); + registryCoordinator.updateOperatorsForQuorum(operatorsToUpdate, quorumNumbersNotCreated); + } + // @notice tests that the internal `_updateOperatorBitmap` function works as expected, for fuzzed inputs function testFuzz_updateOperatorBitmapInternal_noPreviousEntries(uint192 newBitmap) public { registryCoordinator._updateOperatorBitmapExternal(defaultOperatorId, newBitmap); @@ -1394,6 +1462,7 @@ contract RegistryCoordinatorUnitTests_UpdateOperators is RegistryCoordinatorUnit }))) ); } + // @notice tests that the internal `_updateOperatorBitmap` function works as expected, for fuzzed inputs function testFuzz_updateOperatorBitmapInternal_previousEntryInCurrentBlock(uint192 newBitmap) public { uint192 pastBitmap = 1; From 5e5115f54059b36e4999ccb282989be04d0a2f8e Mon Sep 17 00:00:00 2001 From: ChaoticWalrus <93558947+ChaoticWalrus@users.noreply.github.com> Date: Thu, 4 Jan 2024 15:36:17 -0800 Subject: [PATCH 22/48] feat: add testing for `updateOperatorsForQuorum` function --- test/unit/RegistryCoordinatorUnit.t.sol | 161 +++++++++++++++++++++++- 1 file changed, 159 insertions(+), 2 deletions(-) diff --git a/test/unit/RegistryCoordinatorUnit.t.sol b/test/unit/RegistryCoordinatorUnit.t.sol index 6f71cf17..2081a3bc 100644 --- a/test/unit/RegistryCoordinatorUnit.t.sol +++ b/test/unit/RegistryCoordinatorUnit.t.sol @@ -41,6 +41,8 @@ contract RegistryCoordinatorUnitTests is MockAVSDeployer { event EjectorUpdated(address prevEjector, address newEjector); + event QuorumBlockNumberUpdated(uint8 indexed quorumNumber, uint256 blocknumber); + function setUp() virtual public { _deployMockEigenLayerAndAVS(numQuorums); } @@ -1414,7 +1416,7 @@ contract RegistryCoordinatorUnitTests_UpdateOperators is RegistryCoordinatorUnit } // @notice tests the `updateOperators` function with a single *un*registered operator as input - function test_updateOperators_unregisteredOperator() public { + function test_updateOperators_unregisteredOperator() public view { address[] memory operatorsToUpdate = new address[](1); operatorsToUpdate[0] = defaultOperator; @@ -1441,7 +1443,6 @@ contract RegistryCoordinatorUnitTests_UpdateOperators is RegistryCoordinatorUnit function test_updateOperatorsForQuorum_revert_nonexistentQuorum() public { _deployMockEigenLayerAndAVS(10); bytes memory quorumNumbersNotCreated = new bytes(1); - ISignatureUtils.SignatureWithSaltAndExpiry memory emptySig; quorumNumbersNotCreated[0] = 0x0B; address[][] memory operatorsToUpdate = new address[][](1); @@ -1450,6 +1451,162 @@ contract RegistryCoordinatorUnitTests_UpdateOperators is RegistryCoordinatorUnit registryCoordinator.updateOperatorsForQuorum(operatorsToUpdate, quorumNumbersNotCreated); } + function test_updateOperatorsForQuorum_revert_inputLengthMismatch() public { + address[][] memory operatorsToUpdate = new address[][](2); + bytes memory quorumNumbers = new bytes(1); + quorumNumbers[0] = bytes1(defaultQuorumNumber); + + cheats.expectRevert(bytes("RegistryCoordinator.updateOperatorsForQuorum: input length mismatch")); + registryCoordinator.updateOperatorsForQuorum(operatorsToUpdate, quorumNumbers); + } + + function test_updateOperatorsForQuorum_revert_incorrectNumberOfOperators() public { + address[][] memory operatorsToUpdate = new address[][](1); + address[] memory operatorArray = new address[](1); + operatorArray[0] = defaultOperator; + operatorsToUpdate[0] = operatorArray; + bytes memory quorumNumbers = new bytes(1); + quorumNumbers[0] = bytes1(defaultQuorumNumber); + + cheats.expectRevert(bytes("RegistryCoordinator.updateOperatorsForQuorum: number of updated operators does not match quorum total")); + registryCoordinator.updateOperatorsForQuorum(operatorsToUpdate, quorumNumbers); + } + + function test_updateOperatorsForQuorum_revert_unregisteredOperator() public { + // register the default operator + ISignatureUtils.SignatureWithSaltAndExpiry memory emptySig; + uint32 registrationBlockNumber = 100; + bytes memory quorumNumbers = new bytes(1); + quorumNumbers[0] = bytes1(defaultQuorumNumber); + stakeRegistry.setOperatorWeight(uint8(quorumNumbers[0]), defaultOperator, defaultStake); + cheats.startPrank(defaultOperator); + cheats.roll(registrationBlockNumber); + registryCoordinator.registerOperator(quorumNumbers, defaultSocket, pubkeyRegistrationParams, emptySig); + + address[][] memory operatorsToUpdate = new address[][](1); + address[] memory operatorArray = new address[](1); + // use an unregistered operator address as input + operatorArray[0] = _incrementAddress(defaultOperator, 1); + operatorsToUpdate[0] = operatorArray; + + cheats.expectRevert(bytes("RegistryCoordinator.updateOperatorsForQuorum: operator not in quorum")); + registryCoordinator.updateOperatorsForQuorum(operatorsToUpdate, quorumNumbers); + } + + // note: there is not an explicit check for duplicates, as checking for explicit ordering covers this + function test_updateOperatorsForQuorum_revert_duplicateOperator(uint256 pseudoRandomNumber) public { + // register 2 operators + uint32 numOperators = 2; + uint32 registrationBlockNumber = 200; + bytes memory quorumNumbers = new bytes(1); + quorumNumbers[0] = bytes1(defaultQuorumNumber); + uint256 quorumBitmap = BitmapUtils.orderedBytesArrayToBitmap(quorumNumbers); + cheats.roll(registrationBlockNumber); + for (uint i = 0; i < numOperators; i++) { + BN254.G1Point memory pubKey = BN254.hashToG1(keccak256(abi.encodePacked(pseudoRandomNumber, i))); + address operator = _incrementAddress(defaultOperator, i); + + _registerOperatorWithCoordinator(operator, quorumBitmap, pubKey); + } + + address[][] memory operatorsToUpdate = new address[][](1); + address[] memory operatorArray = new address[](2); + // use the same operator address twice as input + operatorArray[0] = defaultOperator; + operatorArray[1] = defaultOperator; + operatorsToUpdate[0] = operatorArray; + + // note: there is not an explicit check for duplicates, as checking for explicit ordering covers this + cheats.expectRevert(bytes("RegistryCoordinator.updateOperatorsForQuorum: operators array must be sorted in ascending address order")); + registryCoordinator.updateOperatorsForQuorum(operatorsToUpdate, quorumNumbers); + } + + function test_updateOperatorsForQuorum_revert_incorrectListOrder(uint256 pseudoRandomNumber) public { + // register 2 operators + uint32 numOperators = 2; + uint32 registrationBlockNumber = 200; + bytes memory quorumNumbers = new bytes(1); + quorumNumbers[0] = bytes1(defaultQuorumNumber); + uint256 quorumBitmap = BitmapUtils.orderedBytesArrayToBitmap(quorumNumbers); + cheats.roll(registrationBlockNumber); + for (uint i = 0; i < numOperators; i++) { + BN254.G1Point memory pubKey = BN254.hashToG1(keccak256(abi.encodePacked(pseudoRandomNumber, i))); + address operator = _incrementAddress(defaultOperator, i); + + _registerOperatorWithCoordinator(operator, quorumBitmap, pubKey); + } + + address[][] memory operatorsToUpdate = new address[][](1); + address[] memory operatorArray = new address[](2); + // order the operator addresses in descending order, instead of ascending order + operatorArray[0] = _incrementAddress(defaultOperator, 1); + operatorArray[1] = defaultOperator; + operatorsToUpdate[0] = operatorArray; + + cheats.expectRevert(bytes("RegistryCoordinator.updateOperatorsForQuorum: operators array must be sorted in ascending address order")); + registryCoordinator.updateOperatorsForQuorum(operatorsToUpdate, quorumNumbers); + } + + function test_updateOperatorsForQuorum_singleOperator() public { + // register the default operator + ISignatureUtils.SignatureWithSaltAndExpiry memory emptySig; + uint32 registrationBlockNumber = 100; + bytes memory quorumNumbers = new bytes(1); + quorumNumbers[0] = bytes1(defaultQuorumNumber); + stakeRegistry.setOperatorWeight(uint8(quorumNumbers[0]), defaultOperator, defaultStake); + cheats.startPrank(defaultOperator); + cheats.roll(registrationBlockNumber); + registryCoordinator.registerOperator(quorumNumbers, defaultSocket, pubkeyRegistrationParams, emptySig); + + address[][] memory operatorsToUpdate = new address[][](1); + address[] memory operatorArray = new address[](1); + operatorArray[0] = defaultOperator; + operatorsToUpdate[0] = operatorArray; + + uint256 quorumUpdateBlockNumberBefore = registryCoordinator.quorumUpdateBlockNumber(defaultQuorumNumber); + require(quorumUpdateBlockNumberBefore != block.number, "bad test setup!"); + + cheats.expectEmit(true, true, true, true, address(registryCoordinator)); + emit QuorumBlockNumberUpdated(defaultQuorumNumber, block.number); + registryCoordinator.updateOperatorsForQuorum(operatorsToUpdate, quorumNumbers); + + uint256 quorumUpdateBlockNumberAfter = registryCoordinator.quorumUpdateBlockNumber(defaultQuorumNumber); + assertEq(quorumUpdateBlockNumberAfter, block.number, "quorumUpdateBlockNumber not set correctly"); + } + + function test_updateOperatorsForQuorum_twoOperators(uint256 pseudoRandomNumber) public { + // register 2 operators + uint32 numOperators = 2; + uint32 registrationBlockNumber = 200; + bytes memory quorumNumbers = new bytes(1); + quorumNumbers[0] = bytes1(defaultQuorumNumber); + uint256 quorumBitmap = BitmapUtils.orderedBytesArrayToBitmap(quorumNumbers); + cheats.roll(registrationBlockNumber); + for (uint i = 0; i < numOperators; i++) { + BN254.G1Point memory pubKey = BN254.hashToG1(keccak256(abi.encodePacked(pseudoRandomNumber, i))); + address operator = _incrementAddress(defaultOperator, i); + + _registerOperatorWithCoordinator(operator, quorumBitmap, pubKey); + } + + address[][] memory operatorsToUpdate = new address[][](1); + address[] memory operatorArray = new address[](2); + // order the operator addresses in descending order, instead of ascending order + operatorArray[0] = defaultOperator; + operatorArray[1] = _incrementAddress(defaultOperator, 1); + operatorsToUpdate[0] = operatorArray; + + uint256 quorumUpdateBlockNumberBefore = registryCoordinator.quorumUpdateBlockNumber(defaultQuorumNumber); + require(quorumUpdateBlockNumberBefore != block.number, "bad test setup!"); + + cheats.expectEmit(true, true, true, true, address(registryCoordinator)); + emit QuorumBlockNumberUpdated(defaultQuorumNumber, block.number); + registryCoordinator.updateOperatorsForQuorum(operatorsToUpdate, quorumNumbers); + + uint256 quorumUpdateBlockNumberAfter = registryCoordinator.quorumUpdateBlockNumber(defaultQuorumNumber); + assertEq(quorumUpdateBlockNumberAfter, block.number, "quorumUpdateBlockNumber not set correctly"); + } + // @notice tests that the internal `_updateOperatorBitmap` function works as expected, for fuzzed inputs function testFuzz_updateOperatorBitmapInternal_noPreviousEntries(uint192 newBitmap) public { registryCoordinator._updateOperatorBitmapExternal(defaultOperatorId, newBitmap); From 814855cba111a8c9ccea3216e1b58193cb62ed76 Mon Sep 17 00:00:00 2001 From: ChaoticWalrus <93558947+ChaoticWalrus@users.noreply.github.com> Date: Mon, 8 Jan 2024 13:11:26 -0800 Subject: [PATCH 23/48] feat: add some coverage for complex view functions note: this commit also adds a TODO around a currently-failing test. I plan to discuss the correct path forwards here and then push another commit. --- test/unit/RegistryCoordinatorUnit.t.sol | 84 ++++++++++++++++++++++++- 1 file changed, 82 insertions(+), 2 deletions(-) diff --git a/test/unit/RegistryCoordinatorUnit.t.sol b/test/unit/RegistryCoordinatorUnit.t.sol index 2081a3bc..719d17fe 100644 --- a/test/unit/RegistryCoordinatorUnit.t.sol +++ b/test/unit/RegistryCoordinatorUnit.t.sol @@ -568,6 +568,7 @@ contract RegistryCoordinatorUnitTests_RegisterOperator is RegistryCoordinatorUni } } +// @dev note that this contract also contains tests for the `getQuorumBitmapIndicesAtBlockNumber` and `getQuorumBitmapAtBlockNumberByIndex` view fncs contract RegistryCoordinatorUnitTests_DeregisterOperator_EjectOperator is RegistryCoordinatorUnitTests { function test_deregisterOperator_revert_paused() public { bytes memory quorumNumbers = new bytes(1); @@ -1150,6 +1151,86 @@ contract RegistryCoordinatorUnitTests_DeregisterOperator_EjectOperator is Regist cheats.prank(defaultOperator); registryCoordinator.ejectOperator(defaultOperator, quorumNumbers); } + + // TODO: this test currently fails. either need to document behavior + modify the test, or modify the code + function test_getQuorumBitmapIndicesAtBlockNumber_revert_notRegistered() public { + uint32 blockNumber; + bytes32[] memory operatorIds = new bytes32[](1); + cheats.expectRevert("RegistryCoordinator.getQuorumBitmapIndicesAtBlockNumber: operatorId has no quorumBitmaps at blockNumber"); + registryCoordinator.getQuorumBitmapIndicesAtBlockNumber(blockNumber, operatorIds); + } + + // @notice tests for correct reversion and return values in the event that an operator registers and later deregisters + function test_getQuorumBitmapIndicesAtBlockNumber_operatorDeregistered() public { + test_deregisterOperator_singleQuorumAndSingleOperator(); + uint32 registrationBlockNumber = 100; + uint32 deregistrationBlockNumber = 200; + uint32 blockNumber = 0; + bytes32[] memory operatorIds = new bytes32[](1); + operatorIds[0] = defaultOperatorId; + + uint32[] memory returnArray = registryCoordinator.getQuorumBitmapIndicesAtBlockNumber(blockNumber, operatorIds); + assertEq(returnArray[0], 0, "defaultOperator bitmap index at blockNumber 0 was not 0"); + + blockNumber = registrationBlockNumber; + returnArray = registryCoordinator.getQuorumBitmapIndicesAtBlockNumber(blockNumber, operatorIds); + assertEq(returnArray[0], 0, "defaultOperator bitmap index at blockNumber registrationBlockNumber was not 0"); + + blockNumber = registrationBlockNumber + 1; + returnArray = registryCoordinator.getQuorumBitmapIndicesAtBlockNumber(blockNumber, operatorIds); + assertEq(returnArray[0], 0, "defaultOperator bitmap index at blockNumber registrationBlockNumber + 1 was not 0"); + + blockNumber = deregistrationBlockNumber; + returnArray = registryCoordinator.getQuorumBitmapIndicesAtBlockNumber(blockNumber, operatorIds); + assertEq(returnArray[0], 1, "defaultOperator bitmap index at blockNumber deregistrationBlockNumber was not 1"); + + blockNumber = deregistrationBlockNumber + 1; + returnArray = registryCoordinator.getQuorumBitmapIndicesAtBlockNumber(blockNumber, operatorIds); + assertEq(returnArray[0], 1, "defaultOperator bitmap index at blockNumber deregistrationBlockNumber + 1 was not 1"); + } + + // @notice tests for correct reversion and return values in the event that an operator registers and later deregisters + function test_getQuorumBitmapAtBlockNumberByIndex_operatorDeregistered() public { + test_deregisterOperator_singleQuorumAndSingleOperator(); + uint32 registrationBlockNumber = 100; + uint32 deregistrationBlockNumber = 200; + uint32 blockNumber = 0; + bytes32 operatorId = defaultOperatorId; + uint256 index = 0; + + uint192 defaultQuorumBitmap = 1; + uint192 emptyBitmap = 0; + + // try an incorrect blockNumber input and confirm reversion + cheats.expectRevert("RegistryCoordinator.getQuorumBitmapAtBlockNumberByIndex: quorumBitmapUpdate is from after blockNumber"); + uint192 returnVal = registryCoordinator.getQuorumBitmapAtBlockNumberByIndex(operatorId, blockNumber, index); + + blockNumber = registrationBlockNumber; + returnVal = registryCoordinator.getQuorumBitmapAtBlockNumberByIndex(operatorId, blockNumber, index); + assertEq(returnVal, defaultQuorumBitmap, "defaultOperator bitmap index at blockNumber registrationBlockNumber was not defaultQuorumBitmap"); + + blockNumber = registrationBlockNumber + 1; + returnVal = registryCoordinator.getQuorumBitmapAtBlockNumberByIndex(operatorId, blockNumber, index); + assertEq(returnVal, defaultQuorumBitmap, "defaultOperator bitmap index at blockNumber registrationBlockNumber + 1 was not defaultQuorumBitmap"); + + // try an incorrect index input and confirm reversion + index = 1; + cheats.expectRevert("RegistryCoordinator.getQuorumBitmapAtBlockNumberByIndex: quorumBitmapUpdate is from after blockNumber"); + returnVal = registryCoordinator.getQuorumBitmapAtBlockNumberByIndex(operatorId, blockNumber, index); + + blockNumber = deregistrationBlockNumber; + returnVal = registryCoordinator.getQuorumBitmapAtBlockNumberByIndex(operatorId, blockNumber, index); + assertEq(returnVal, emptyBitmap, "defaultOperator bitmap index at blockNumber deregistrationBlockNumber was not emptyBitmap"); + + blockNumber = deregistrationBlockNumber + 1; + returnVal = registryCoordinator.getQuorumBitmapAtBlockNumberByIndex(operatorId, blockNumber, index); + assertEq(returnVal, emptyBitmap, "defaultOperator bitmap index at blockNumber deregistrationBlockNumber + 1 was not emptyBitmap"); + + // try an incorrect index input and confirm reversion + index = 0; + cheats.expectRevert("RegistryCoordinator.getQuorumBitmapAtBlockNumberByIndex: quorumBitmapUpdate is from before blockNumber"); + returnVal = registryCoordinator.getQuorumBitmapAtBlockNumberByIndex(operatorId, blockNumber, index); + } } contract RegistryCoordinatorUnitTests_RegisterOperatorWithChurn is RegistryCoordinatorUnitTests { @@ -1663,5 +1744,4 @@ contract RegistryCoordinatorUnitTests_UpdateOperators is RegistryCoordinatorUnit }))) ); } -} - +} \ No newline at end of file From 8882f7c84809cee78285adbdb3e47b2d6b051265 Mon Sep 17 00:00:00 2001 From: ChaoticWalrus <93558947+ChaoticWalrus@users.noreply.github.com> Date: Tue, 9 Jan 2024 10:53:19 -0800 Subject: [PATCH 24/48] chore: clarify NatSpec comments --- src/RegistryCoordinator.sol | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/RegistryCoordinator.sol b/src/RegistryCoordinator.sol index b96a32f1..39ff0267 100644 --- a/src/RegistryCoordinator.sol +++ b/src/RegistryCoordinator.sol @@ -783,8 +783,10 @@ contract RegistryCoordinator is } /** - * @notice Returns the quorum bitmap for the given `operatorId` at the given `blockNumber` via the `index` - * @dev reverts if `index` is incorrect + * @notice Returns the quorum bitmap for the given `operatorId` at the given `blockNumber` via the `index`, + * reverting if `index` is incorrect + * @dev This function is meant to be used in concert with `getQuorumBitmapIndicesAtBlockNumber`, which + * helps off-chain processes to fetch the correct `index` input */ function getQuorumBitmapAtBlockNumberByIndex( bytes32 operatorId, From 26565d61f93733ccbaf096f51620f6c9c6f242fa Mon Sep 17 00:00:00 2001 From: ChaoticWalrus <93558947+ChaoticWalrus@users.noreply.github.com> Date: Tue, 9 Jan 2024 12:34:15 -0800 Subject: [PATCH 25/48] fix: make `getQuorumBitmapIndicesAtBlockNumber` revert if operator was registered the logic is now more in-line with the logic in the StakeRegistry -- for reference, see: https://github.com/layr-labs/eigenlayer-middleware/blob/ 98f884454d9e9de1e344bb6fba9a2cd3915e5b57/src/StakeRegistry.sol#L297-L299 --- src/RegistryCoordinator.sol | 46 +++++++++++++++++-------- test/unit/RegistryCoordinatorUnit.t.sol | 7 ++-- 2 files changed, 36 insertions(+), 17 deletions(-) diff --git a/src/RegistryCoordinator.sol b/src/RegistryCoordinator.sol index 39ff0267..52eaa643 100644 --- a/src/RegistryCoordinator.sol +++ b/src/RegistryCoordinator.sol @@ -758,30 +758,48 @@ contract RegistryCoordinator is return _operatorInfo[operator].status; } - /// @notice Returns the indices of the quorumBitmaps for the provided `operatorIds` at the given `blockNumber` + /** + * @notice Returns the indices of the quorumBitmaps for the provided `operatorIds` at the given `blockNumber` + * @dev Reverts if any of the `operatorIds` was not (yet) registered at `blockNumber` + * @dev This function is designed to find proper inputs to the `getQuorumBitmapAtBlockNumberByIndex` function + */ function getQuorumBitmapIndicesAtBlockNumber( uint32 blockNumber, bytes32[] memory operatorIds ) external view returns (uint32[] memory) { uint32[] memory indices = new uint32[](operatorIds.length); for (uint256 i = 0; i < operatorIds.length; i++) { - uint256 length = _operatorBitmapHistory[operatorIds[i]].length; - for (uint256 j = 0; j < length; j++) { - if (_operatorBitmapHistory[operatorIds[i]][length - j - 1].updateBlockNumber <= blockNumber) { - uint32 nextUpdateBlockNumber = - _operatorBitmapHistory[operatorIds[i]][length - j - 1].nextUpdateBlockNumber; - require( - nextUpdateBlockNumber == 0 || nextUpdateBlockNumber > blockNumber, - "RegistryCoordinator.getQuorumBitmapIndicesAtBlockNumber: operatorId has no quorumBitmaps at blockNumber" - ); - indices[i] = uint32(length - j - 1); - break; - } - } + indices[i] = _getQuorumBitmapIndexAtBlockNumber(blockNumber, operatorIds[i]); } return indices; } + /** + * @notice Returns the index of the quorumBitmap for the provided `operatorId` at the given `blockNumber` + * @dev Reverts if the operator had not yet (ever) registered at `blockNumber` + * @dev This function is designed to find proper inputs to the `getQuorumBitmapAtBlockNumberByIndex` function + */ + function _getQuorumBitmapIndexAtBlockNumber( + uint32 blockNumber, + bytes32 operatorId + ) internal view returns (uint32 index) { + uint256 length = _operatorBitmapHistory[operatorId].length; + for (uint256 i = 0; i < length; i++) { + if (_operatorBitmapHistory[operatorId][length - i - 1].updateBlockNumber <= blockNumber) { + uint32 nextUpdateBlockNumber = + _operatorBitmapHistory[operatorId][length - i - 1].nextUpdateBlockNumber; + require( + nextUpdateBlockNumber == 0 || nextUpdateBlockNumber > blockNumber, + "RegistryCoordinator.getQuorumBitmapIndexAtBlockNumber: operatorId has no bitmap update at blockNumber" + ); + return uint32(length - i - 1); + } + } + revert( + "RegistryCoordinator.getQuorumBitmapIndexAtBlockNumber: no bitmap update found for operatorId at block number" + ); + } + /** * @notice Returns the quorum bitmap for the given `operatorId` at the given `blockNumber` via the `index`, * reverting if `index` is incorrect diff --git a/test/unit/RegistryCoordinatorUnit.t.sol b/test/unit/RegistryCoordinatorUnit.t.sol index 719d17fe..59578db9 100644 --- a/test/unit/RegistryCoordinatorUnit.t.sol +++ b/test/unit/RegistryCoordinatorUnit.t.sol @@ -1156,7 +1156,7 @@ contract RegistryCoordinatorUnitTests_DeregisterOperator_EjectOperator is Regist function test_getQuorumBitmapIndicesAtBlockNumber_revert_notRegistered() public { uint32 blockNumber; bytes32[] memory operatorIds = new bytes32[](1); - cheats.expectRevert("RegistryCoordinator.getQuorumBitmapIndicesAtBlockNumber: operatorId has no quorumBitmaps at blockNumber"); + cheats.expectRevert("RegistryCoordinator.getQuorumBitmapIndexAtBlockNumber: no bitmap update found for operatorId at block number"); registryCoordinator.getQuorumBitmapIndicesAtBlockNumber(blockNumber, operatorIds); } @@ -1169,8 +1169,9 @@ contract RegistryCoordinatorUnitTests_DeregisterOperator_EjectOperator is Regist bytes32[] memory operatorIds = new bytes32[](1); operatorIds[0] = defaultOperatorId; - uint32[] memory returnArray = registryCoordinator.getQuorumBitmapIndicesAtBlockNumber(blockNumber, operatorIds); - assertEq(returnArray[0], 0, "defaultOperator bitmap index at blockNumber 0 was not 0"); + uint32[] memory returnArray; + cheats.expectRevert("RegistryCoordinator.getQuorumBitmapIndexAtBlockNumber: no bitmap update found for operatorId at block number"); + registryCoordinator.getQuorumBitmapIndicesAtBlockNumber(blockNumber, operatorIds); blockNumber = registrationBlockNumber; returnArray = registryCoordinator.getQuorumBitmapIndicesAtBlockNumber(blockNumber, operatorIds); From 4e70e8126a1259ac0172c456512117cd25db6c79 Mon Sep 17 00:00:00 2001 From: ChaoticWalrus <93558947+ChaoticWalrus@users.noreply.github.com> Date: Tue, 9 Jan 2024 12:38:41 -0800 Subject: [PATCH 26/48] feat: add simple unit test for `getQuorumBitmapIndicesAtBlockNumber` also improve wording in the 'tree' file --- test/tree/RegistryCoordinatorUnit.tree | 2 +- test/unit/RegistryCoordinatorUnit.t.sol | 29 +++++++++++++++++++++++++ 2 files changed, 30 insertions(+), 1 deletion(-) diff --git a/test/tree/RegistryCoordinatorUnit.tree b/test/tree/RegistryCoordinatorUnit.tree index 159fd6b6..e089625d 100644 --- a/test/tree/RegistryCoordinatorUnit.tree +++ b/test/tree/RegistryCoordinatorUnit.tree @@ -102,7 +102,7 @@ │ │ ├── getQuorumBitmapIndicesAtBlockNumber() -│ ├── given that any of the operatorIDs was not registered at the block number +│ ├── given that any of the operatorIDs had not yet registered at the block number │ │ └── it should revert │ └── it should return the proper index of the entry in each operatorID's quorum bitmap history │ diff --git a/test/unit/RegistryCoordinatorUnit.t.sol b/test/unit/RegistryCoordinatorUnit.t.sol index 59578db9..97f29b45 100644 --- a/test/unit/RegistryCoordinatorUnit.t.sol +++ b/test/unit/RegistryCoordinatorUnit.t.sol @@ -1160,6 +1160,35 @@ contract RegistryCoordinatorUnitTests_DeregisterOperator_EjectOperator is Regist registryCoordinator.getQuorumBitmapIndicesAtBlockNumber(blockNumber, operatorIds); } + // @notice tests for correct reversion and return values in the event that an operator registers + function test_getQuorumBitmapIndicesAtBlockNumber_operatorRegistered() public { + // register the operator + ISignatureUtils.SignatureWithSaltAndExpiry memory emptySig; + uint32 registrationBlockNumber = 100; + bytes memory quorumNumbers = new bytes(1); + quorumNumbers[0] = bytes1(defaultQuorumNumber); + stakeRegistry.setOperatorWeight(uint8(quorumNumbers[0]), defaultOperator, defaultStake); + cheats.roll(registrationBlockNumber); + cheats.startPrank(defaultOperator); + registryCoordinator.registerOperator(quorumNumbers, defaultSocket, pubkeyRegistrationParams, emptySig); + + uint32 blockNumber = 0; + bytes32[] memory operatorIds = new bytes32[](1); + operatorIds[0] = defaultOperatorId; + + uint32[] memory returnArray; + cheats.expectRevert("RegistryCoordinator.getQuorumBitmapIndexAtBlockNumber: no bitmap update found for operatorId at block number"); + registryCoordinator.getQuorumBitmapIndicesAtBlockNumber(blockNumber, operatorIds); + + blockNumber = registrationBlockNumber; + returnArray = registryCoordinator.getQuorumBitmapIndicesAtBlockNumber(blockNumber, operatorIds); + assertEq(returnArray[0], 0, "defaultOperator bitmap index at blockNumber registrationBlockNumber was not 0"); + + blockNumber = registrationBlockNumber + 1; + returnArray = registryCoordinator.getQuorumBitmapIndicesAtBlockNumber(blockNumber, operatorIds); + assertEq(returnArray[0], 0, "defaultOperator bitmap index at blockNumber registrationBlockNumber + 1 was not 0"); + } + // @notice tests for correct reversion and return values in the event that an operator registers and later deregisters function test_getQuorumBitmapIndicesAtBlockNumber_operatorDeregistered() public { test_deregisterOperator_singleQuorumAndSingleOperator(); From ab44a8239660a5fb469922e76b1b5921e0d1b5e8 Mon Sep 17 00:00:00 2001 From: ChaoticWalrus <93558947+ChaoticWalrus@users.noreply.github.com> Date: Tue, 9 Jan 2024 13:47:05 -0800 Subject: [PATCH 27/48] chore: move `_getQuorumBitmapIndexAtBlockNumber` into the section with other internal fncs --- src/RegistryCoordinator.sol | 52 ++++++++++++++++++------------------- 1 file changed, 26 insertions(+), 26 deletions(-) diff --git a/src/RegistryCoordinator.sol b/src/RegistryCoordinator.sol index 52eaa643..9c7b6622 100644 --- a/src/RegistryCoordinator.sol +++ b/src/RegistryCoordinator.sol @@ -714,6 +714,32 @@ contract RegistryCoordinator is } } + /** + * @notice Returns the index of the quorumBitmap for the provided `operatorId` at the given `blockNumber` + * @dev Reverts if the operator had not yet (ever) registered at `blockNumber` + * @dev This function is designed to find proper inputs to the `getQuorumBitmapAtBlockNumberByIndex` function + */ + function _getQuorumBitmapIndexAtBlockNumber( + uint32 blockNumber, + bytes32 operatorId + ) internal view returns (uint32 index) { + uint256 length = _operatorBitmapHistory[operatorId].length; + for (uint256 i = 0; i < length; i++) { + if (_operatorBitmapHistory[operatorId][length - i - 1].updateBlockNumber <= blockNumber) { + uint32 nextUpdateBlockNumber = + _operatorBitmapHistory[operatorId][length - i - 1].nextUpdateBlockNumber; + require( + nextUpdateBlockNumber == 0 || nextUpdateBlockNumber > blockNumber, + "RegistryCoordinator.getQuorumBitmapIndexAtBlockNumber: operatorId has no bitmap update at blockNumber" + ); + return uint32(length - i - 1); + } + } + revert( + "RegistryCoordinator.getQuorumBitmapIndexAtBlockNumber: no bitmap update found for operatorId at block number" + ); + } + function _setOperatorSetParams(uint8 quorumNumber, OperatorSetParam memory operatorSetParams) internal { _quorumParams[quorumNumber] = operatorSetParams; emit OperatorSetParamsUpdated(quorumNumber, operatorSetParams); @@ -774,32 +800,6 @@ contract RegistryCoordinator is return indices; } - /** - * @notice Returns the index of the quorumBitmap for the provided `operatorId` at the given `blockNumber` - * @dev Reverts if the operator had not yet (ever) registered at `blockNumber` - * @dev This function is designed to find proper inputs to the `getQuorumBitmapAtBlockNumberByIndex` function - */ - function _getQuorumBitmapIndexAtBlockNumber( - uint32 blockNumber, - bytes32 operatorId - ) internal view returns (uint32 index) { - uint256 length = _operatorBitmapHistory[operatorId].length; - for (uint256 i = 0; i < length; i++) { - if (_operatorBitmapHistory[operatorId][length - i - 1].updateBlockNumber <= blockNumber) { - uint32 nextUpdateBlockNumber = - _operatorBitmapHistory[operatorId][length - i - 1].nextUpdateBlockNumber; - require( - nextUpdateBlockNumber == 0 || nextUpdateBlockNumber > blockNumber, - "RegistryCoordinator.getQuorumBitmapIndexAtBlockNumber: operatorId has no bitmap update at blockNumber" - ); - return uint32(length - i - 1); - } - } - revert( - "RegistryCoordinator.getQuorumBitmapIndexAtBlockNumber: no bitmap update found for operatorId at block number" - ); - } - /** * @notice Returns the quorum bitmap for the given `operatorId` at the given `blockNumber` via the `index`, * reverting if `index` is incorrect From 8cd21f4d8754453d5f90948b9be74fe2885d8230 Mon Sep 17 00:00:00 2001 From: ChaoticWalrus <93558947+ChaoticWalrus@users.noreply.github.com> Date: Tue, 9 Jan 2024 13:48:27 -0800 Subject: [PATCH 28/48] chore: remove unnecessary require statement + improve code clarity --- src/RegistryCoordinator.sol | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/src/RegistryCoordinator.sol b/src/RegistryCoordinator.sol index 9c7b6622..5f68b35a 100644 --- a/src/RegistryCoordinator.sol +++ b/src/RegistryCoordinator.sol @@ -726,13 +726,8 @@ contract RegistryCoordinator is uint256 length = _operatorBitmapHistory[operatorId].length; for (uint256 i = 0; i < length; i++) { if (_operatorBitmapHistory[operatorId][length - i - 1].updateBlockNumber <= blockNumber) { - uint32 nextUpdateBlockNumber = - _operatorBitmapHistory[operatorId][length - i - 1].nextUpdateBlockNumber; - require( - nextUpdateBlockNumber == 0 || nextUpdateBlockNumber > blockNumber, - "RegistryCoordinator.getQuorumBitmapIndexAtBlockNumber: operatorId has no bitmap update at blockNumber" - ); - return uint32(length - i - 1); + index = length - i - 1; + return index; } } revert( From 278b66094f8b194c42d474a164423094343b541f Mon Sep 17 00:00:00 2001 From: ChaoticWalrus <93558947+ChaoticWalrus@users.noreply.github.com> Date: Tue, 9 Jan 2024 14:02:58 -0800 Subject: [PATCH 29/48] fix: correct a compiler error for implicit type conversion --- src/RegistryCoordinator.sol | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/RegistryCoordinator.sol b/src/RegistryCoordinator.sol index 5f68b35a..348786c7 100644 --- a/src/RegistryCoordinator.sol +++ b/src/RegistryCoordinator.sol @@ -726,7 +726,7 @@ contract RegistryCoordinator is uint256 length = _operatorBitmapHistory[operatorId].length; for (uint256 i = 0; i < length; i++) { if (_operatorBitmapHistory[operatorId][length - i - 1].updateBlockNumber <= blockNumber) { - index = length - i - 1; + index = uint32(length - i - 1); return index; } } From 1f1a840d0b83c27e71a940f9e64ae4bb0a1863ba Mon Sep 17 00:00:00 2001 From: ChaoticWalrus <93558947+ChaoticWalrus@users.noreply.github.com> Date: Tue, 9 Jan 2024 14:42:15 -0800 Subject: [PATCH 30/48] feat: address TODOs in tests --- test/mocks/StakeRegistryMock.sol | 16 +++++++--- test/unit/RegistryCoordinatorUnit.t.sol | 40 +++++++++++++++++++++++-- 2 files changed, 50 insertions(+), 6 deletions(-) diff --git a/test/mocks/StakeRegistryMock.sol b/test/mocks/StakeRegistryMock.sol index 455163a1..9ff83503 100644 --- a/test/mocks/StakeRegistryMock.sol +++ b/test/mocks/StakeRegistryMock.sol @@ -9,6 +9,12 @@ import "../../src/interfaces/IRegistryCoordinator.sol"; * @author Layr Labs, Inc. */ contract StakeRegistryMock is IStakeRegistry { + // bitmap returned by the mocked `updateOperatorStake` function + uint192 updateOperatorStakeReturnBitmap; + + function set_updateOperatorStakeReturnBitmap(uint192 newValue) external { + updateOperatorStakeReturnBitmap = newValue; + } function registryCoordinator() external view returns (address) {} @@ -195,10 +201,12 @@ contract StakeRegistryMock is IStakeRegistry { * added to the */ function updateOperatorStake( - address operator, - bytes32 operatorId, - bytes calldata quorumNumbers - ) external returns (uint192) {} + address /*operator*/, + bytes32 /*operatorId*/, + bytes calldata /*quorumNumbers*/ + ) external returns (uint192) { + return updateOperatorStakeReturnBitmap; + } function getMockOperatorId(address operator) external pure returns(bytes32) { return bytes32(uint256(keccak256(abi.encodePacked(operator, "operatorId")))); diff --git a/test/unit/RegistryCoordinatorUnit.t.sol b/test/unit/RegistryCoordinatorUnit.t.sol index 97f29b45..bb0439dc 100644 --- a/test/unit/RegistryCoordinatorUnit.t.sol +++ b/test/unit/RegistryCoordinatorUnit.t.sol @@ -1152,7 +1152,6 @@ contract RegistryCoordinatorUnitTests_DeregisterOperator_EjectOperator is Regist registryCoordinator.ejectOperator(defaultOperator, quorumNumbers); } - // TODO: this test currently fails. either need to document behavior + modify the test, or modify the code function test_getQuorumBitmapIndicesAtBlockNumber_revert_notRegistered() public { uint32 blockNumber; bytes32[] memory operatorIds = new bytes32[](1); @@ -1522,10 +1521,47 @@ contract RegistryCoordinatorUnitTests_UpdateOperators is RegistryCoordinatorUnit address[] memory operatorsToUpdate = new address[](1); operatorsToUpdate[0] = defaultOperator; - // TODO: any additional checks here? mostly this just calls the StakeRegistry, so more appropriate for an integration test registryCoordinator.updateOperators(operatorsToUpdate); } + // @notice tests the `updateOperators` function with a single registered operator as input + // @dev also sets up return data from the StakeRegistry + function testFuzz_updateOperators_singleOperator(uint192 registrationBitmap, uint192 mockReturnData) public { + // filter fuzzed inputs to only valid inputs + cheats.assume(registrationBitmap != 0); + mockReturnData = (mockReturnData & registrationBitmap); + emit log_named_uint("mockReturnData", mockReturnData); + + // register the default operator + ISignatureUtils.SignatureWithSaltAndExpiry memory emptySig; + uint32 registrationBlockNumber = 100; + bytes memory quorumNumbers = BitmapUtils.bitmapToBytesArray(registrationBitmap); + for (uint256 i = 0; i < quorumNumbers.length; ++i) { + stakeRegistry.setOperatorWeight(uint8(quorumNumbers[i]), defaultOperator, defaultStake); + } + cheats.startPrank(defaultOperator); + cheats.roll(registrationBlockNumber); + registryCoordinator.registerOperator(quorumNumbers, defaultSocket, pubkeyRegistrationParams, emptySig); + + address[] memory operatorsToUpdate = new address[](1); + operatorsToUpdate[0] = defaultOperator; + + uint192 quorumBitmapBefore = registryCoordinator.getCurrentQuorumBitmap(defaultOperatorId); + assertEq(quorumBitmapBefore, registrationBitmap, "operator bitmap somehow incorrect"); + + // make the stake registry return info that the operator should be removed from quorums + uint192 quorumBitmapToRemove = mockReturnData; + bytes memory quorumNumbersToRemove = BitmapUtils.bitmapToBytesArray(quorumBitmapToRemove); + for (uint256 i = 0; i < quorumNumbersToRemove.length; ++i) { + stakeRegistry.setOperatorWeight(uint8(quorumNumbersToRemove[i]), defaultOperator, 0); + } + uint256 expectedQuorumBitmap = BitmapUtils.minus(quorumBitmapBefore, quorumBitmapToRemove); + + registryCoordinator.updateOperators(operatorsToUpdate); + uint192 quorumBitmapAfter = registryCoordinator.getCurrentQuorumBitmap(defaultOperatorId); + assertEq(expectedQuorumBitmap, quorumBitmapAfter, "quorum bitmap did not update correctly"); + } + // @notice tests the `updateOperators` function with a single *un*registered operator as input function test_updateOperators_unregisteredOperator() public view { address[] memory operatorsToUpdate = new address[](1); From 22fb8660387dc6996abc13b1a18b0350eac6ed50 Mon Sep 17 00:00:00 2001 From: ChaoticWalrus <93558947+ChaoticWalrus@users.noreply.github.com> Date: Wed, 20 Dec 2023 13:46:27 -0800 Subject: [PATCH 31/48] feat: add tree diagram for RegistryManager --- test/tree/RegistryManagerUnit.tree | 158 +++++++++++++++++++++++++++++ 1 file changed, 158 insertions(+) create mode 100644 test/tree/RegistryManagerUnit.tree diff --git a/test/tree/RegistryManagerUnit.tree b/test/tree/RegistryManagerUnit.tree new file mode 100644 index 00000000..10238e2e --- /dev/null +++ b/test/tree/RegistryManagerUnit.tree @@ -0,0 +1,158 @@ +├── RegistryCoordinatorUnit.t.sol (*** denotes that integration tests are needed to validate path) +│ +├── initialize +│ ├── given that initialize has been called previously +│ │ └── it should revert +│ ├── given that the variable-length array input lengths don't all match +│ │ └── it should revert +│ └── it should set the storage variables correctly (owner, pauserRegistry, paused status, churnApprover, ejector, quorum params) +│ +├── registerOperator() +│ ├── given that operator registration is paused +│ │ └── it should revert +│ ├── given that the current number of operators for any of the quorums to register for already meets or exceeds the quorum's operator cap +│ │ └── it should revert +│ ├── given the operator has *not* previously registered a pubkey +│ │ └── ***it should attempt to register the provided pubkey with the BLSApkRegistry +│ ├── given that *has* previously registered a pubkey +│ │ └── ***it should fetch the operator's pubkey from the BLSApkRegistry +│ └── ***it should attempt to register the operator, via the `_registerOperator` function (see below) +│ +├── registerOperatorWithChurn() +│ ├── given that operator registration is paused +│ │ └── it should revert +│ ├── given that the provided operatorKickParams are not the same length as the provided quorumNumbers +│ │ └── it should revert +│ ├── given that the churnApprover did not sign the operatorKickParams and the caller's operatorID +│ │ └── it should revert +│ ├── given that the current number of operators for any of the quorums to register for already meets or exceeds the quorum's operator cap +│ │ └── for each quorum, it should check that the new and to-be-kicked operators' stakes meet the configured requirements +│ │ ├─ given that the configured requirements are not met +│ │ │ └── it should revert +│ │ └── it should deregister the to-be-kicked operator +│ ├── given the operator has *not* previously registered a pubkey +│ │ └── ***it should attempt to register the provided pubkey with the BLSApkRegistry +│ ├── given that *has* previously registered a pubkey +│ │ └── ***it should fetch the operator's pubkey from the BLSApkRegistry +│ └── ***it should attempt to register the operator, via the `_registerOperator` function (see below) +│ +├── deregisterOperator() +│ ├── given that operator deregistration is paused +│ │ └── it should revert +│ └── ***it should attempt to deregister the caller, via the `_deregisterOperator` function (see below) +│ +├── ejectOperator() +│ ├── given that caller is not the ejector +│ │ └── it should revert +│ └── ***it should attempt to deregister the operator, via the `_deregisterOperator` function (see below) +│ +├── updateOperators() +│ ├── given that operator updates are paused +│ │ └── it should revert +│ └── ***for each operator, it should attempt to update the operator's stake information for all quorums +│ that the operator is currently registered for, via the `_updateOperator` function (see below) +│ +├── updateOperatorsForQuorum() +│ ├── given that operator updates are paused +│ │ └── it should revert +│ ├── given that any of the provided quorum numbers is for a non-existant quorum +│ │ └── it should revert +│ ├── given that the length of the provided array of operator lists does not match the length of the provided quorum numbers +│ │ └── it should revert +│ ├── given that the length of any provided list of operators does not match the current number of operators in that quorum +│ │ └── it should revert +│ ├── given that of any of the provided lists of operators contains an operator who is not currently registered for the quorum +│ │ └── it should revert +│ ├── given that of any of the provided lists of operators contains a duplicate +│ │ └── it should revert +│ ├── given that of any of the provided lists of operators is not in ascending address order +│ │ └── it should revert +│ └── ***for each operator, it should attempt to update the operator's stake information, via the `_updateOperator` function (see below) +│ for each quorum, it should increase the quorumUpdateBlockNumber to the current block number +│ +├── updateSocket() +│ ├── given that the caller is not a registered operator +│ │ └── it should revert +│ └── it should emit an OperatorSocketUpdate event +│ +├── createQuorum() +│ ├── given that the caller is not a registered operator +│ │ └── it should revert +│ ├── given that the current quorum count meets or exceeds the MAX_QUORUM_COUNT +│ │ └── it should revert +│ └── it should set the OperatorSetParams for the new quorum +│ *** it should initialize the new quorum on the StakeRegistry, IndexRegistry, and BLSApkRegistry +│ +├── setOperatorSetParams() +│ ├── given that the caller is not the owner +│ │ └── it should revert +│ ├── given that the quorum does not already exist +│ │ └── it should revert +│ └── it should set the OperatorSetParams for the quorum, and emit an event +│ +├── setChurnApprover() +│ ├── given that the caller is not the owner +│ │ └── it should revert +│ └── it should update the churnApprover address and emit an event +│ +├── setEjector() +│ ├── given that the caller is not the owner +│ │ └── it should revert +│ └── it should update the ejector address and emit an event +│ +│ +├── getQuorumBitmapIndicesAtBlockNumber() +│ ├── given that any of the operatorIDs was not registered at the block number +│ │ └── it should revert +│ └── it should return the proper index of the entry in each operatorID's quorum bitmap history +│ +├── getQuorumBitmapAtBlockNumberByIndex() +│ ├── given that the operatorID was not registered at the block number +│ │ └── it should revert +│ ├── given that the index specifies a bitmap that became invalid prior to the block number +│ │ └── it should revert +│ ├── given that the index specifies a bitmap that became valid after the block number +│ │ └── it should revert +│ └── it should return the quorum bitmap of the operatorID at the block number +│ +├── _registerOperator() (internal function -- see mentions above) +│ ├── given that no quorums are being registered for +│ │ └── it should revert +│ ├── given that any quorums being registered for do not (yet) exist +│ │ └── it should revert +│ ├── given that the operator is already registered for any of quorums being registered for +│ │ └── it should revert +│ ├── given that the operator is not already registered at the AVS level +│ │ └── it should mark the operator as registered +│ │ *** and call the serviceManager to register the operator on the EigenLayer level +│ └── it should update the operator's bitmap history, via the `_updateOperatorBitmap` function (see below), +│ with a new entry that includes the added quorums +│ *** and call the BLSApkRegistry, StakeRegistry, and IndexRegistry to complete operator registration +│ +├── _deregisterOperator() (internal function -- see mentions above) +│ ├── given that the operator is not registered for the AVS +│ │ └── it should revert +│ ├── given that no quorums are being removed +│ │ └── it should revert +│ ├── given that any quorums being removed do not (yet) exist +│ │ └── it should revert +│ ├── given that the operator is *not* currently registered for any of quorums being removed +│ │ └── it should revert +│ ├── given that the operator is being removed from all quorums that they were registered for +│ │ └── it should mark the operator as no longer registered +│ │ *** and call the serviceManager to deregister the operator on the EigenLayer level +│ └── it should update the operator's bitmap history, via the `_updateOperatorBitmap` function (see below), +│ with a new entry that excludes the removed quorums +│ *** and call the BLSApkRegistry, StakeRegistry, and IndexRegistry to complete operator deregistration +│ +├── _updateOperator() (internal function -- see mentions above) +│ ├── given that the operator is not actively registered +│ │ └── it should do nothing ("no-op") +│ └── *** it should call the StakeRegistry to make it perform a stake update on the operator +│ *** and remove the operator from any quorums where the StakeRegistry returns that the operator +│ no longer meets the requirements, via the `_deregisterOperator` function (see above) +│ +└── _updateOperatorBitmap() (internal function -- see mentions above) + ├── given that the operator has no previous entries in their bitmap history OR given that the latest entry in the operator's bitmap history occurred in the current block + │ └── it should push a new entry with an "unset" (i.e. zero) nextUpdateBlock, and an updateBlockNumber of the current block + └── otherwise, it should only update the bitmap in the latest entry \ No newline at end of file From fd56b76f48e689b83dcba9750b3e5340dcc4bc1e Mon Sep 17 00:00:00 2001 From: ChaoticWalrus <93558947+ChaoticWalrus@users.noreply.github.com> Date: Tue, 2 Jan 2024 15:54:52 -0800 Subject: [PATCH 32/48] chore: fix tree file name --- test/tree/RegistryManagerUnit.tree | 158 ----------------------------- 1 file changed, 158 deletions(-) delete mode 100644 test/tree/RegistryManagerUnit.tree diff --git a/test/tree/RegistryManagerUnit.tree b/test/tree/RegistryManagerUnit.tree deleted file mode 100644 index 10238e2e..00000000 --- a/test/tree/RegistryManagerUnit.tree +++ /dev/null @@ -1,158 +0,0 @@ -├── RegistryCoordinatorUnit.t.sol (*** denotes that integration tests are needed to validate path) -│ -├── initialize -│ ├── given that initialize has been called previously -│ │ └── it should revert -│ ├── given that the variable-length array input lengths don't all match -│ │ └── it should revert -│ └── it should set the storage variables correctly (owner, pauserRegistry, paused status, churnApprover, ejector, quorum params) -│ -├── registerOperator() -│ ├── given that operator registration is paused -│ │ └── it should revert -│ ├── given that the current number of operators for any of the quorums to register for already meets or exceeds the quorum's operator cap -│ │ └── it should revert -│ ├── given the operator has *not* previously registered a pubkey -│ │ └── ***it should attempt to register the provided pubkey with the BLSApkRegistry -│ ├── given that *has* previously registered a pubkey -│ │ └── ***it should fetch the operator's pubkey from the BLSApkRegistry -│ └── ***it should attempt to register the operator, via the `_registerOperator` function (see below) -│ -├── registerOperatorWithChurn() -│ ├── given that operator registration is paused -│ │ └── it should revert -│ ├── given that the provided operatorKickParams are not the same length as the provided quorumNumbers -│ │ └── it should revert -│ ├── given that the churnApprover did not sign the operatorKickParams and the caller's operatorID -│ │ └── it should revert -│ ├── given that the current number of operators for any of the quorums to register for already meets or exceeds the quorum's operator cap -│ │ └── for each quorum, it should check that the new and to-be-kicked operators' stakes meet the configured requirements -│ │ ├─ given that the configured requirements are not met -│ │ │ └── it should revert -│ │ └── it should deregister the to-be-kicked operator -│ ├── given the operator has *not* previously registered a pubkey -│ │ └── ***it should attempt to register the provided pubkey with the BLSApkRegistry -│ ├── given that *has* previously registered a pubkey -│ │ └── ***it should fetch the operator's pubkey from the BLSApkRegistry -│ └── ***it should attempt to register the operator, via the `_registerOperator` function (see below) -│ -├── deregisterOperator() -│ ├── given that operator deregistration is paused -│ │ └── it should revert -│ └── ***it should attempt to deregister the caller, via the `_deregisterOperator` function (see below) -│ -├── ejectOperator() -│ ├── given that caller is not the ejector -│ │ └── it should revert -│ └── ***it should attempt to deregister the operator, via the `_deregisterOperator` function (see below) -│ -├── updateOperators() -│ ├── given that operator updates are paused -│ │ └── it should revert -│ └── ***for each operator, it should attempt to update the operator's stake information for all quorums -│ that the operator is currently registered for, via the `_updateOperator` function (see below) -│ -├── updateOperatorsForQuorum() -│ ├── given that operator updates are paused -│ │ └── it should revert -│ ├── given that any of the provided quorum numbers is for a non-existant quorum -│ │ └── it should revert -│ ├── given that the length of the provided array of operator lists does not match the length of the provided quorum numbers -│ │ └── it should revert -│ ├── given that the length of any provided list of operators does not match the current number of operators in that quorum -│ │ └── it should revert -│ ├── given that of any of the provided lists of operators contains an operator who is not currently registered for the quorum -│ │ └── it should revert -│ ├── given that of any of the provided lists of operators contains a duplicate -│ │ └── it should revert -│ ├── given that of any of the provided lists of operators is not in ascending address order -│ │ └── it should revert -│ └── ***for each operator, it should attempt to update the operator's stake information, via the `_updateOperator` function (see below) -│ for each quorum, it should increase the quorumUpdateBlockNumber to the current block number -│ -├── updateSocket() -│ ├── given that the caller is not a registered operator -│ │ └── it should revert -│ └── it should emit an OperatorSocketUpdate event -│ -├── createQuorum() -│ ├── given that the caller is not a registered operator -│ │ └── it should revert -│ ├── given that the current quorum count meets or exceeds the MAX_QUORUM_COUNT -│ │ └── it should revert -│ └── it should set the OperatorSetParams for the new quorum -│ *** it should initialize the new quorum on the StakeRegistry, IndexRegistry, and BLSApkRegistry -│ -├── setOperatorSetParams() -│ ├── given that the caller is not the owner -│ │ └── it should revert -│ ├── given that the quorum does not already exist -│ │ └── it should revert -│ └── it should set the OperatorSetParams for the quorum, and emit an event -│ -├── setChurnApprover() -│ ├── given that the caller is not the owner -│ │ └── it should revert -│ └── it should update the churnApprover address and emit an event -│ -├── setEjector() -│ ├── given that the caller is not the owner -│ │ └── it should revert -│ └── it should update the ejector address and emit an event -│ -│ -├── getQuorumBitmapIndicesAtBlockNumber() -│ ├── given that any of the operatorIDs was not registered at the block number -│ │ └── it should revert -│ └── it should return the proper index of the entry in each operatorID's quorum bitmap history -│ -├── getQuorumBitmapAtBlockNumberByIndex() -│ ├── given that the operatorID was not registered at the block number -│ │ └── it should revert -│ ├── given that the index specifies a bitmap that became invalid prior to the block number -│ │ └── it should revert -│ ├── given that the index specifies a bitmap that became valid after the block number -│ │ └── it should revert -│ └── it should return the quorum bitmap of the operatorID at the block number -│ -├── _registerOperator() (internal function -- see mentions above) -│ ├── given that no quorums are being registered for -│ │ └── it should revert -│ ├── given that any quorums being registered for do not (yet) exist -│ │ └── it should revert -│ ├── given that the operator is already registered for any of quorums being registered for -│ │ └── it should revert -│ ├── given that the operator is not already registered at the AVS level -│ │ └── it should mark the operator as registered -│ │ *** and call the serviceManager to register the operator on the EigenLayer level -│ └── it should update the operator's bitmap history, via the `_updateOperatorBitmap` function (see below), -│ with a new entry that includes the added quorums -│ *** and call the BLSApkRegistry, StakeRegistry, and IndexRegistry to complete operator registration -│ -├── _deregisterOperator() (internal function -- see mentions above) -│ ├── given that the operator is not registered for the AVS -│ │ └── it should revert -│ ├── given that no quorums are being removed -│ │ └── it should revert -│ ├── given that any quorums being removed do not (yet) exist -│ │ └── it should revert -│ ├── given that the operator is *not* currently registered for any of quorums being removed -│ │ └── it should revert -│ ├── given that the operator is being removed from all quorums that they were registered for -│ │ └── it should mark the operator as no longer registered -│ │ *** and call the serviceManager to deregister the operator on the EigenLayer level -│ └── it should update the operator's bitmap history, via the `_updateOperatorBitmap` function (see below), -│ with a new entry that excludes the removed quorums -│ *** and call the BLSApkRegistry, StakeRegistry, and IndexRegistry to complete operator deregistration -│ -├── _updateOperator() (internal function -- see mentions above) -│ ├── given that the operator is not actively registered -│ │ └── it should do nothing ("no-op") -│ └── *** it should call the StakeRegistry to make it perform a stake update on the operator -│ *** and remove the operator from any quorums where the StakeRegistry returns that the operator -│ no longer meets the requirements, via the `_deregisterOperator` function (see above) -│ -└── _updateOperatorBitmap() (internal function -- see mentions above) - ├── given that the operator has no previous entries in their bitmap history OR given that the latest entry in the operator's bitmap history occurred in the current block - │ └── it should push a new entry with an "unset" (i.e. zero) nextUpdateBlock, and an updateBlockNumber of the current block - └── otherwise, it should only update the bitmap in the latest entry \ No newline at end of file From 6b30068ad83627441921c752bbb246af00b579ea Mon Sep 17 00:00:00 2001 From: ChaoticWalrus <93558947+ChaoticWalrus@users.noreply.github.com> Date: Wed, 3 Jan 2024 11:10:05 -0800 Subject: [PATCH 33/48] feat: add a test for partial deregistration also improve some formatting + fix some typos, and add a touch more documentation --- test/unit/RegistryCoordinatorUnit.t.sol | 166 ++++++++++++++++++++---- 1 file changed, 138 insertions(+), 28 deletions(-) diff --git a/test/unit/RegistryCoordinatorUnit.t.sol b/test/unit/RegistryCoordinatorUnit.t.sol index d42852c0..946db5f5 100644 --- a/test/unit/RegistryCoordinatorUnit.t.sol +++ b/test/unit/RegistryCoordinatorUnit.t.sol @@ -44,7 +44,15 @@ contract RegistryCoordinatorUnitTests is MockAVSDeployer { _deployMockEigenLayerAndAVS(numQuorums); } - function _testRegisterOperatorWithChurn_SetUp(uint256 pseudoRandomNumber, bytes memory quorumNumbers, uint96 operatorToKickStake) internal returns(address operatorToRegister, BN254.G1Point memory operatorToRegisterPubKey, IRegistryCoordinator.OperatorKickParam[] memory operatorKickParams) { + function _test_registerOperatorWithChurn_SetUp( + uint256 pseudoRandomNumber, + bytes memory quorumNumbers, + uint96 operatorToKickStake + ) internal returns( + address operatorToRegister, + BN254.G1Point memory operatorToRegisterPubKey, + IRegistryCoordinator.OperatorKickParam[] memory operatorKickParams + ) { uint32 kickRegistrationBlockNumber = 100; uint256 quorumBitmap = BitmapUtils.orderedBytesArrayToBitmap(quorumNumbers); @@ -233,7 +241,7 @@ contract RegistryCoordinatorUnitTests_Initialization_Setters is RegistryCoordina contract RegistryCoordinatorUnitTests_RegisterOperator is RegistryCoordinatorUnitTests { - function testRegisterOperator_revert_paused() public { + function test_registerOperator_revert_paused() public { bytes memory emptyQuorumNumbers = new bytes(0); ISignatureUtils.SignatureWithSaltAndExpiry memory emptySig; @@ -246,7 +254,7 @@ contract RegistryCoordinatorUnitTests_RegisterOperator is RegistryCoordinatorUni registryCoordinator.registerOperator(emptyQuorumNumbers, defaultSocket, pubkeyRegistrationParams, emptySig); } - function testRegisterOperator_revert_emptyQuorumNumbers() public { + function test_registerOperator_revert_emptyQuorumNumbers() public { bytes memory emptyQuorumNumbers = new bytes(0); ISignatureUtils.SignatureWithSaltAndExpiry memory emptySig; @@ -255,7 +263,7 @@ contract RegistryCoordinatorUnitTests_RegisterOperator is RegistryCoordinatorUni registryCoordinator.registerOperator(emptyQuorumNumbers, defaultSocket, pubkeyRegistrationParams, emptySig); } - function testRegisterOperator_revert_invalidQuorum() public { + function test_registerOperator_revert_invalidQuorum() public { bytes memory quorumNumbersTooLarge = new bytes(1); ISignatureUtils.SignatureWithSaltAndExpiry memory emptySig; @@ -298,7 +306,7 @@ contract RegistryCoordinatorUnitTests_RegisterOperator is RegistryCoordinatorUni cheats.prank(defaultOperator); registryCoordinator.registerOperator(quorumNumbers, defaultSocket, pubkeyRegistrationParams, emptySig); uint256 gasAfter = gasleft(); - emit log_named_uint("gasUsed", gasBefore - gasAfter); + emit log_named_uint("gasUsed, register for single quorum", gasBefore - gasAfter); uint256 quorumBitmap = BitmapUtils.orderedBytesArrayToBitmap(quorumNumbers); @@ -321,7 +329,9 @@ contract RegistryCoordinatorUnitTests_RegisterOperator is RegistryCoordinatorUni ); } + // @notice tests registering an operator for a fuzzed assortment of quorums function testFuzz_registerOperator(uint256 quorumBitmap) public { + // filter the fuzzed input down to only valid quorums quorumBitmap = quorumBitmap & MAX_QUORUM_BITMAP; ISignatureUtils.SignatureWithSaltAndExpiry memory emptySig; cheats.assume(quorumBitmap != 0); @@ -373,6 +383,7 @@ contract RegistryCoordinatorUnitTests_RegisterOperator is RegistryCoordinatorUni ); } + // @notice tests registering an operator for a single quorum and later registering them for an additional quorum function test_registerOperator_addingQuorumsAfterInitialRegistration() public { uint256 registrationBlockNumber = block.number + 100; uint256 nextRegistrationBlockNumber = registrationBlockNumber + 100; @@ -526,6 +537,7 @@ contract RegistryCoordinatorUnitTests_DeregisterOperator_EjectOperator is Regist registryCoordinator.deregisterOperator(quorumNumbers); } + // @notice verifies that an operator who was registered for a single quorum can be deregistered function test_deregisterOperator_singleQuorumAndSingleOperator() public { ISignatureUtils.SignatureWithSaltAndExpiry memory emptySig; uint32 registrationBlockNumber = 100; @@ -574,11 +586,14 @@ contract RegistryCoordinatorUnitTests_DeregisterOperator_EjectOperator is Regist ); } + // @notice verifies that an operator who was registered for a fuzzed set of quorums can be deregistered + // @dev deregisters the operator from *all* quorums for which they we registered. function testFuzz_deregisterOperator_fuzzedQuorumAndSingleOperator(uint256 quorumBitmap) public { ISignatureUtils.SignatureWithSaltAndExpiry memory emptySig; uint32 registrationBlockNumber = 100; uint32 deregistrationBlockNumber = 200; + // filter down fuzzed input to only valid quorums quorumBitmap = quorumBitmap & MAX_QUORUM_BITMAP; cheats.assume(quorumBitmap != 0); bytes memory quorumNumbers = BitmapUtils.bitmapToBytesArray(quorumBitmap); @@ -625,7 +640,95 @@ contract RegistryCoordinatorUnitTests_DeregisterOperator_EjectOperator is Regist }))) ); } + // @notice verifies that an operator who was registered for a fuzzed set of quorums can be deregistered from a subset of those quorums + // @dev deregisters the operator from a fuzzed subset of the quorums for which they we registered. + function testFuzz_deregisterOperator_singleOperator_partialDeregistration( + uint256 registrationQuorumBitmap, + uint256 deregistrationQuorumBitmap + ) public { + ISignatureUtils.SignatureWithSaltAndExpiry memory emptySig; + uint32 registrationBlockNumber = 100; + uint32 deregistrationBlockNumber = 200; + + // filter down fuzzed input to only valid quorums + registrationQuorumBitmap = registrationQuorumBitmap & MAX_QUORUM_BITMAP; + cheats.assume(registrationQuorumBitmap != 0); + // filter the other fuzzed input to a subset of the first fuzzed input + deregistrationQuorumBitmap = deregistrationQuorumBitmap & registrationQuorumBitmap; + cheats.assume(deregistrationQuorumBitmap != 0); + bytes memory registrationquorumNumbers = BitmapUtils.bitmapToBytesArray(registrationQuorumBitmap); + + for (uint i = 0; i < registrationquorumNumbers.length; i++) { + stakeRegistry.setOperatorWeight(uint8(registrationquorumNumbers[i]), defaultOperator, defaultStake); + } + + cheats.startPrank(defaultOperator); + + cheats.roll(registrationBlockNumber); + + registryCoordinator.registerOperator(registrationquorumNumbers, defaultSocket, pubkeyRegistrationParams, emptySig); + + bytes memory deregistrationquorumNumbers = BitmapUtils.bitmapToBytesArray(deregistrationQuorumBitmap); + + cheats.expectEmit(true, true, true, true, address(blsApkRegistry)); + emit OperatorRemovedFromQuorums(defaultOperator, deregistrationquorumNumbers); + for (uint i = 0; i < deregistrationquorumNumbers.length; i++) { + cheats.expectEmit(true, true, true, true, address(stakeRegistry)); + emit OperatorStakeUpdate(defaultOperatorId, uint8(deregistrationquorumNumbers[i]), 0); + } + cheats.roll(deregistrationBlockNumber); + + uint256 gasBefore = gasleft(); + registryCoordinator.deregisterOperator(deregistrationquorumNumbers); + uint256 gasAfter = gasleft(); + emit log_named_uint("gasUsed", gasBefore - gasAfter); + emit log_named_uint("numQuorums", deregistrationquorumNumbers.length); + + // check that the operator is marked as 'degregistered' only if deregistered from *all* quorums + if (deregistrationQuorumBitmap == registrationQuorumBitmap) { + assertEq( + keccak256(abi.encode(registryCoordinator.getOperator(defaultOperator))), + keccak256(abi.encode(IRegistryCoordinator.OperatorInfo({ + operatorId: defaultOperatorId, + status: IRegistryCoordinator.OperatorStatus.DEREGISTERED + }))) + ); + } else { + assertEq( + keccak256(abi.encode(registryCoordinator.getOperator(defaultOperator))), + keccak256(abi.encode(IRegistryCoordinator.OperatorInfo({ + operatorId: defaultOperatorId, + status: IRegistryCoordinator.OperatorStatus.REGISTERED + }))) + ); + } + // ensure that the operator's current quorum bitmap matches the expectation + uint256 expectedQuorumBitmap = BitmapUtils.minus(registrationQuorumBitmap, deregistrationQuorumBitmap); + assertEq(registryCoordinator.getCurrentQuorumBitmap(defaultOperatorId), expectedQuorumBitmap); + // check that the quorum bitmap history is as expected + assertEq( + keccak256(abi.encode(registryCoordinator.getQuorumBitmapUpdateByIndex(defaultOperatorId, 0))), + keccak256(abi.encode(IRegistryCoordinator.QuorumBitmapUpdate({ + quorumBitmap: uint192(registrationQuorumBitmap), + updateBlockNumber: registrationBlockNumber, + nextUpdateBlockNumber: deregistrationBlockNumber + }))) + ); + // note: there will be no second entry in the operator's bitmap history in the event that the operator has totally deregistered + if (deregistrationQuorumBitmap != registrationQuorumBitmap) { + assertEq( + keccak256(abi.encode(registryCoordinator.getQuorumBitmapUpdateByIndex(defaultOperatorId, 1))), + keccak256(abi.encode(IRegistryCoordinator.QuorumBitmapUpdate({ + quorumBitmap: uint192(expectedQuorumBitmap), + updateBlockNumber: deregistrationBlockNumber, + nextUpdateBlockNumber: 0 + }))) + ); + } + } + + // @notice registers the max number of operators with fuzzed bitmaps and then deregisters a pseudorandom operator (from all of their quorums) function testFuzz_deregisterOperator_manyOperators(uint256 pseudoRandomNumber) public { uint32 numOperators = defaultMaxOperatorCount; @@ -657,11 +760,11 @@ contract RegistryCoordinatorUnitTests_DeregisterOperator_EjectOperator is Regist } } - uint256 indexOfOperatorToDerigister = pseudoRandomNumber % numOperators; - address operatorToDerigister = _incrementAddress(defaultOperator, indexOfOperatorToDerigister); - BN254.G1Point memory operatorToDeregisterPubKey = BN254.hashToG1(keccak256(abi.encodePacked(pseudoRandomNumber, indexOfOperatorToDerigister))); - bytes32 operatorToDerigisterId = BN254.hashG1Point(operatorToDeregisterPubKey); - uint256 operatorToDeregisterQuorumBitmap = quorumBitmaps[indexOfOperatorToDerigister]; + uint256 indexOfOperatorToDeregister = pseudoRandomNumber % numOperators; + address operatorToDeregister = _incrementAddress(defaultOperator, indexOfOperatorToDeregister); + BN254.G1Point memory operatorToDeregisterPubKey = BN254.hashToG1(keccak256(abi.encodePacked(pseudoRandomNumber, indexOfOperatorToDeregister))); + bytes32 operatorToDeregisterId = BN254.hashG1Point(operatorToDeregisterPubKey); + uint256 operatorToDeregisterQuorumBitmap = quorumBitmaps[indexOfOperatorToDeregister]; bytes memory operatorToDeregisterQuorumNumbers = BitmapUtils.bitmapToBytesArray(operatorToDeregisterQuorumBitmap); bytes32[] memory operatorIdsToSwap = new bytes32[](operatorToDeregisterQuorumNumbers.length); @@ -670,28 +773,28 @@ contract RegistryCoordinatorUnitTests_DeregisterOperator_EjectOperator is Regist } cheats.expectEmit(true, true, true, true, address(blsApkRegistry)); - emit OperatorRemovedFromQuorums(operatorToDerigister, operatorToDeregisterQuorumNumbers); + emit OperatorRemovedFromQuorums(operatorToDeregister, operatorToDeregisterQuorumNumbers); for (uint i = 0; i < operatorToDeregisterQuorumNumbers.length; i++) { cheats.expectEmit(true, true, true, true, address(stakeRegistry)); - emit OperatorStakeUpdate(operatorToDerigisterId, uint8(operatorToDeregisterQuorumNumbers[i]), 0); + emit OperatorStakeUpdate(operatorToDeregisterId, uint8(operatorToDeregisterQuorumNumbers[i]), 0); } cheats.roll(deregistrationBlockNumber); - cheats.prank(operatorToDerigister); + cheats.prank(operatorToDeregister); registryCoordinator.deregisterOperator(operatorToDeregisterQuorumNumbers); assertEq( - keccak256(abi.encode(registryCoordinator.getOperator(operatorToDerigister))), + keccak256(abi.encode(registryCoordinator.getOperator(operatorToDeregister))), keccak256(abi.encode(IRegistryCoordinator.OperatorInfo({ - operatorId: operatorToDerigisterId, + operatorId: operatorToDeregisterId, status: IRegistryCoordinator.OperatorStatus.DEREGISTERED }))) ); assertEq(registryCoordinator.getCurrentQuorumBitmap(defaultOperatorId), 0); assertEq( - keccak256(abi.encode(registryCoordinator.getQuorumBitmapUpdateByIndex(operatorToDerigisterId, 0))), + keccak256(abi.encode(registryCoordinator.getQuorumBitmapUpdateByIndex(operatorToDeregisterId, 0))), keccak256(abi.encode(IRegistryCoordinator.QuorumBitmapUpdate({ quorumBitmap: uint192(operatorToDeregisterQuorumBitmap), updateBlockNumber: registrationBlockNumber, @@ -819,10 +922,11 @@ contract RegistryCoordinatorUnitTests_DeregisterOperator_EjectOperator is Regist status: IRegistryCoordinator.OperatorStatus.REGISTERED }))) ); - // make sure the operator is not in any quorums + // make sure the operator is properly removed from the quorums assertEq( - registryCoordinator.getCurrentQuorumBitmap(defaultOperatorId), - BitmapUtils.orderedBytesArrayToBitmap(quorumNumbers) & ~BitmapUtils.orderedBytesArrayToBitmap(quorumNumbersToEject) // quorumsRegisteredFor & ~quorumsEjectedFrom + registryCoordinator.getCurrentQuorumBitmap(defaultOperatorId), + // quorumsRegisteredFor & ~quorumsEjectedFrom + BitmapUtils.orderedBytesArrayToBitmap(quorumNumbers) & ~BitmapUtils.orderedBytesArrayToBitmap(quorumNumbersToEject) ); } @@ -843,6 +947,7 @@ contract RegistryCoordinatorUnitTests_DeregisterOperator_EjectOperator is Regist } contract RegistryCoordinatorUnitTests_RegisterOperatorWithChurn is RegistryCoordinatorUnitTests { + // @notice registers an operator for a single quorum, with a fuzzed pubkey, churning out another operator from the quorum function testFuzz_registerOperatorWithChurn(uint256 pseudoRandomNumber) public { uint32 numOperators = defaultMaxOperatorCount; uint32 kickRegistrationBlockNumber = 100; @@ -909,7 +1014,8 @@ contract RegistryCoordinatorUnitTests_RegisterOperatorWithChurn is RegistryCoord { ISignatureUtils.SignatureWithSaltAndExpiry memory emptyAVSRegSig; - ISignatureUtils.SignatureWithSaltAndExpiry memory signatureWithExpiry = _signOperatorChurnApproval(operatorToRegisterId, operatorKickParams, defaultSalt, block.timestamp + 10); + ISignatureUtils.SignatureWithSaltAndExpiry memory signatureWithExpiry = + _signOperatorChurnApproval(operatorToRegisterId, operatorKickParams, defaultSalt, block.timestamp + 10); cheats.prank(operatorToRegister); uint256 gasBefore = gasleft(); registryCoordinator.registerOperatorWithChurn( @@ -957,13 +1063,14 @@ contract RegistryCoordinatorUnitTests_RegisterOperatorWithChurn is RegistryCoord address operatorToRegister, BN254.G1Point memory operatorToRegisterPubKey, IRegistryCoordinator.OperatorKickParam[] memory operatorKickParams - ) = _testRegisterOperatorWithChurn_SetUp(pseudoRandomNumber, quorumNumbers, defaultStake); + ) = _test_registerOperatorWithChurn_SetUp(pseudoRandomNumber, quorumNumbers, defaultStake); bytes32 operatorToRegisterId = BN254.hashG1Point(operatorToRegisterPubKey); stakeRegistry.setOperatorWeight(defaultQuorumNumber, operatorToRegister, defaultStake); cheats.roll(registrationBlockNumber); - ISignatureUtils.SignatureWithSaltAndExpiry memory signatureWithExpiry = _signOperatorChurnApproval(operatorToRegisterId, operatorKickParams, defaultSalt, block.timestamp + 10); + ISignatureUtils.SignatureWithSaltAndExpiry memory signatureWithExpiry = + _signOperatorChurnApproval(operatorToRegisterId, operatorKickParams, defaultSalt, block.timestamp + 10); cheats.prank(operatorToRegister); cheats.expectRevert("RegistryCoordinator._validateChurn: incoming operator has insufficient stake for churn"); registryCoordinator.registerOperatorWithChurn( @@ -986,7 +1093,7 @@ contract RegistryCoordinatorUnitTests_RegisterOperatorWithChurn is RegistryCoord address operatorToRegister, BN254.G1Point memory operatorToRegisterPubKey, IRegistryCoordinator.OperatorKickParam[] memory operatorKickParams - ) = _testRegisterOperatorWithChurn_SetUp(pseudoRandomNumber, quorumNumbers, operatorToKickStake); + ) = _test_registerOperatorWithChurn_SetUp(pseudoRandomNumber, quorumNumbers, operatorToKickStake); bytes32 operatorToRegisterId = BN254.hashG1Point(operatorToRegisterPubKey); @@ -994,7 +1101,8 @@ contract RegistryCoordinatorUnitTests_RegisterOperatorWithChurn is RegistryCoord stakeRegistry.setOperatorWeight(defaultQuorumNumber, operatorToRegister, operatorToKickStake * defaultKickBIPsOfOperatorStake / 10000 + 1); cheats.roll(registrationBlockNumber); - ISignatureUtils.SignatureWithSaltAndExpiry memory signatureWithExpiry = _signOperatorChurnApproval(operatorToRegisterId, operatorKickParams, defaultSalt, block.timestamp + 10); + ISignatureUtils.SignatureWithSaltAndExpiry memory signatureWithExpiry = + _signOperatorChurnApproval(operatorToRegisterId, operatorKickParams, defaultSalt, block.timestamp + 10); cheats.prank(operatorToRegister); cheats.expectRevert("RegistryCoordinator._validateChurn: cannot kick operator with more than kickBIPsOfTotalStake"); registryCoordinator.registerOperatorWithChurn( @@ -1016,7 +1124,7 @@ contract RegistryCoordinatorUnitTests_RegisterOperatorWithChurn is RegistryCoord address operatorToRegister, , IRegistryCoordinator.OperatorKickParam[] memory operatorKickParams - ) = _testRegisterOperatorWithChurn_SetUp(pseudoRandomNumber, quorumNumbers, defaultStake); + ) = _test_registerOperatorWithChurn_SetUp(pseudoRandomNumber, quorumNumbers, defaultStake); uint96 registeringStake = defaultKickBIPsOfOperatorStake * defaultStake; stakeRegistry.setOperatorWeight(defaultQuorumNumber, operatorToRegister, registeringStake); @@ -1024,7 +1132,8 @@ contract RegistryCoordinatorUnitTests_RegisterOperatorWithChurn is RegistryCoord cheats.roll(registrationBlockNumber); ISignatureUtils.SignatureWithSaltAndExpiry memory signatureWithSaltAndExpiry; signatureWithSaltAndExpiry.expiry = block.timestamp + 10; - signatureWithSaltAndExpiry.signature = hex"000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001B"; + signatureWithSaltAndExpiry.signature = + hex"000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001B"; signatureWithSaltAndExpiry.salt = defaultSalt; cheats.prank(operatorToRegister); cheats.expectRevert("ECDSA: invalid signature"); @@ -1047,14 +1156,15 @@ contract RegistryCoordinatorUnitTests_RegisterOperatorWithChurn is RegistryCoord address operatorToRegister, BN254.G1Point memory operatorToRegisterPubKey, IRegistryCoordinator.OperatorKickParam[] memory operatorKickParams - ) = _testRegisterOperatorWithChurn_SetUp(pseudoRandomNumber, quorumNumbers, defaultStake); + ) = _test_registerOperatorWithChurn_SetUp(pseudoRandomNumber, quorumNumbers, defaultStake); bytes32 operatorToRegisterId = BN254.hashG1Point(operatorToRegisterPubKey); uint96 registeringStake = defaultKickBIPsOfOperatorStake * defaultStake; stakeRegistry.setOperatorWeight(defaultQuorumNumber, operatorToRegister, registeringStake); cheats.roll(registrationBlockNumber); - ISignatureUtils.SignatureWithSaltAndExpiry memory signatureWithSaltAndExpiry = _signOperatorChurnApproval(operatorToRegisterId, operatorKickParams, defaultSalt, block.timestamp - 1); + ISignatureUtils.SignatureWithSaltAndExpiry memory signatureWithSaltAndExpiry = + _signOperatorChurnApproval(operatorToRegisterId, operatorKickParams, defaultSalt, block.timestamp - 1); cheats.prank(operatorToRegister); cheats.expectRevert("RegistryCoordinator._verifyChurnApproverSignature: churnApprover signature expired"); registryCoordinator.registerOperatorWithChurn( From 21a4ad278100cc81c2f7643cdf40d0adf9253342 Mon Sep 17 00:00:00 2001 From: ChaoticWalrus <93558947+ChaoticWalrus@users.noreply.github.com> Date: Wed, 3 Jan 2024 15:13:45 -0800 Subject: [PATCH 34/48] feat: expose more internal functions in harnessed contract --- .../RegistryCoordinatorHarness.t.sol | 42 ++++++++++++++----- test/unit/BLSSignatureCheckerUnit.t.sol | 4 +- 2 files changed, 33 insertions(+), 13 deletions(-) diff --git a/test/harnesses/RegistryCoordinatorHarness.t.sol b/test/harnesses/RegistryCoordinatorHarness.t.sol index 70bd306b..d1af8be7 100644 --- a/test/harnesses/RegistryCoordinatorHarness.t.sol +++ b/test/harnesses/RegistryCoordinatorHarness.t.sol @@ -24,16 +24,36 @@ contract RegistryCoordinatorHarness is RegistryCoordinator, Test { _operatorInfo[operator].operatorId = operatorId; } - function recordOperatorQuorumBitmapUpdate(bytes32 operatorId, uint192 quorumBitmap) external { - uint256 operatorQuorumBitmapHistoryLength = _operatorBitmapHistory[operatorId].length; - if (operatorQuorumBitmapHistoryLength != 0) { - _operatorBitmapHistory[operatorId][operatorQuorumBitmapHistoryLength - 1].nextUpdateBlockNumber = uint32(block.number); - } - - _operatorBitmapHistory[operatorId].push(QuorumBitmapUpdate({ - updateBlockNumber: uint32(block.number), - nextUpdateBlockNumber: 0, - quorumBitmap: quorumBitmap - })); + // @notice exposes the internal `_registerOperator` function, overriding all access controls + function _registerOperatorExternal( + address operator, + bytes32 operatorId, + bytes calldata quorumNumbers, + string memory socket, + SignatureWithSaltAndExpiry memory operatorSignature + ) external returns (RegisterResults memory results) { + return _registerOperator(operator, operatorId, quorumNumbers, socket, operatorSignature); + } + + // @notice exposes the internal `_deregisterOperator` function, overriding all access controls + function _deregisterOperatorExternal( + address operator, + bytes calldata quorumNumbers + ) external { + _deregisterOperator(operator, quorumNumbers); + } + + // @notice exposes the internal `_updateOperator` function, overriding all access controls + function _updateOperatorExternal( + address operator, + OperatorInfo memory operatorInfo, + bytes memory quorumsToUpdate + ) external { + _updateOperator(operator, operatorInfo, quorumsToUpdate); + } + + // @notice exposes the internal `_updateOperatorBitmap` function, overriding all access controls + function _updateOperatorBitmapExternal(bytes32 operatorId, uint192 quorumBitmap) external { + _updateOperatorBitmap(operatorId, quorumBitmap); } } diff --git a/test/unit/BLSSignatureCheckerUnit.t.sol b/test/unit/BLSSignatureCheckerUnit.t.sol index 46974297..1ccd829a 100644 --- a/test/unit/BLSSignatureCheckerUnit.t.sol +++ b/test/unit/BLSSignatureCheckerUnit.t.sol @@ -82,8 +82,8 @@ contract BLSSignatureCheckerUnitTests is BLSMockAVSDeployer { (uint32 referenceBlockNumber, BLSSignatureChecker.NonSignerStakesAndSignature memory nonSignerStakesAndSignature) = _registerSignatoriesAndGetNonSignerStakeAndSignatureRandom(pseudoRandomNumber, numNonSigners, quorumBitmap); - // record a quorumBitmap update - registryCoordinator.recordOperatorQuorumBitmapUpdate(nonSignerStakesAndSignature.nonSignerPubkeys[0].hashG1Point(), uint192(quorumBitmap | 2)); + // record a quorumBitmap update via a harnessed function + registryCoordinator._updateOperatorBitmapExternal(nonSignerStakesAndSignature.nonSignerPubkeys[0].hashG1Point(), uint192(quorumBitmap | 2)); // set the nonSignerQuorumBitmapIndices to a different value nonSignerStakesAndSignature.nonSignerQuorumBitmapIndices[0] = 1; From 2fd5089ea70575dc081fe1cde9fec6765a1b40c0 Mon Sep 17 00:00:00 2001 From: ChaoticWalrus <93558947+ChaoticWalrus@users.noreply.github.com> Date: Wed, 3 Jan 2024 15:30:23 -0800 Subject: [PATCH 35/48] feat: add some simple coverage for the internal `_registerOperator` fnc --- test/unit/RegistryCoordinatorUnit.t.sol | 70 +++++++++++++++++++++++++ 1 file changed, 70 insertions(+) diff --git a/test/unit/RegistryCoordinatorUnit.t.sol b/test/unit/RegistryCoordinatorUnit.t.sol index 946db5f5..63df11a3 100644 --- a/test/unit/RegistryCoordinatorUnit.t.sol +++ b/test/unit/RegistryCoordinatorUnit.t.sol @@ -493,6 +493,76 @@ contract RegistryCoordinatorUnitTests_RegisterOperator is RegistryCoordinatorUni registryCoordinator.registerOperator(quorumNumbers, defaultSocket, pubkeyRegistrationParams, emptySig); } + + // tests for the internal `_registerOperator` function: + function test_registerOperatorInternal_revert_noQuorums() public { + bytes memory emptyQuorumNumbers = new bytes(0); + ISignatureUtils.SignatureWithSaltAndExpiry memory emptySig; + + cheats.expectRevert("RegistryCoordinator._registerOperator: bitmap cannot be 0"); + registryCoordinator._registerOperatorExternal(defaultOperator, defaultOperatorId, emptyQuorumNumbers, defaultSocket, emptySig); + } + + function test_registerOperatorInternal_revert_nonexistentQuorum() public { + bytes memory quorumNumbersTooLarge = new bytes(1); + ISignatureUtils.SignatureWithSaltAndExpiry memory emptySig; + + quorumNumbersTooLarge[0] = 0xC0; + + cheats.expectRevert("BitmapUtils.orderedBytesArrayToBitmap: bitmap exceeds max value"); + registryCoordinator._registerOperatorExternal(defaultOperator, defaultOperatorId, quorumNumbersTooLarge, defaultSocket, emptySig); + } + + function test_registerOperatorInternal_revert_operatorAlreadyRegisteredForQuorum() public { + ISignatureUtils.SignatureWithSaltAndExpiry memory emptySig; + bytes memory quorumNumbers = new bytes(1); + quorumNumbers[0] = bytes1(defaultQuorumNumber); + + stakeRegistry.setOperatorWeight(uint8(quorumNumbers[0]), defaultOperator, defaultStake); + registryCoordinator._registerOperatorExternal(defaultOperator, defaultOperatorId, quorumNumbers, defaultSocket, emptySig); + + cheats.expectRevert("RegistryCoordinator._registerOperator: operator already registered for some quorums being registered for"); + registryCoordinator._registerOperatorExternal(defaultOperator, defaultOperatorId, quorumNumbers, defaultSocket, emptySig); + } + + function test_registerOperatorInternal() public { + bytes memory quorumNumbers = new bytes(1); + ISignatureUtils.SignatureWithSaltAndExpiry memory emptySig; + quorumNumbers[0] = bytes1(defaultQuorumNumber); + + stakeRegistry.setOperatorWeight(uint8(quorumNumbers[0]), defaultOperator, defaultStake); + + cheats.expectEmit(true, true, true, true, address(registryCoordinator)); + emit OperatorSocketUpdate(defaultOperatorId, defaultSocket); + cheats.expectEmit(true, true, true, true, address(blsApkRegistry)); + emit OperatorAddedToQuorums(defaultOperator, quorumNumbers); + cheats.expectEmit(true, true, true, true, address(stakeRegistry)); + emit OperatorStakeUpdate(defaultOperatorId, defaultQuorumNumber, defaultStake); + cheats.expectEmit(true, true, true, true, address(indexRegistry)); + emit QuorumIndexUpdate(defaultOperatorId, defaultQuorumNumber, 0); + + registryCoordinator._registerOperatorExternal(defaultOperator, defaultOperatorId, quorumNumbers, defaultSocket, emptySig); + + uint256 quorumBitmap = BitmapUtils.orderedBytesArrayToBitmap(quorumNumbers); + + assertEq(registryCoordinator.getOperatorId(defaultOperator), defaultOperatorId); + assertEq( + keccak256(abi.encode(registryCoordinator.getOperator(defaultOperator))), + keccak256(abi.encode(IRegistryCoordinator.OperatorInfo({ + operatorId: defaultOperatorId, + status: IRegistryCoordinator.OperatorStatus.REGISTERED + }))) + ); + assertEq(registryCoordinator.getCurrentQuorumBitmap(defaultOperatorId), quorumBitmap); + assertEq( + keccak256(abi.encode(registryCoordinator.getQuorumBitmapUpdateByIndex(defaultOperatorId, 0))), + keccak256(abi.encode(IRegistryCoordinator.QuorumBitmapUpdate({ + quorumBitmap: uint192(quorumBitmap), + updateBlockNumber: uint32(block.number), + nextUpdateBlockNumber: 0 + }))) + ); + } } contract RegistryCoordinatorUnitTests_DeregisterOperator_EjectOperator is RegistryCoordinatorUnitTests { From 9b1eabf15fa098d352868e0b595f664ed1c2f723 Mon Sep 17 00:00:00 2001 From: ChaoticWalrus <93558947+ChaoticWalrus@users.noreply.github.com> Date: Wed, 3 Jan 2024 15:45:23 -0800 Subject: [PATCH 36/48] feat: add test coverage for the internal `deregisterOperator` fnc also clarify somewhat ambiguous wording in the tree file --- test/tree/RegistryCoordinatorUnit.tree | 2 +- test/unit/RegistryCoordinatorUnit.t.sol | 135 +++++++++++++++++++++++- 2 files changed, 135 insertions(+), 2 deletions(-) diff --git a/test/tree/RegistryCoordinatorUnit.tree b/test/tree/RegistryCoordinatorUnit.tree index 10238e2e..159fd6b6 100644 --- a/test/tree/RegistryCoordinatorUnit.tree +++ b/test/tree/RegistryCoordinatorUnit.tree @@ -136,7 +136,7 @@ │ │ └── it should revert │ ├── given that any quorums being removed do not (yet) exist │ │ └── it should revert -│ ├── given that the operator is *not* currently registered for any of quorums being removed +│ ├── given that the operator is *not* currently registered for any or all of quorums being removed │ │ └── it should revert │ ├── given that the operator is being removed from all quorums that they were registered for │ │ └── it should mark the operator as no longer registered diff --git a/test/unit/RegistryCoordinatorUnit.t.sol b/test/unit/RegistryCoordinatorUnit.t.sol index 63df11a3..e6e68936 100644 --- a/test/unit/RegistryCoordinatorUnit.t.sol +++ b/test/unit/RegistryCoordinatorUnit.t.sol @@ -274,7 +274,7 @@ contract RegistryCoordinatorUnitTests_RegisterOperator is RegistryCoordinatorUni registryCoordinator.registerOperator(quorumNumbersTooLarge, defaultSocket, pubkeyRegistrationParams, emptySig); } - function test_registerOperator_revert_NonexistentQuorum() public { + function test_registerOperator_revert_nonexistentQuorum() public { _deployMockEigenLayerAndAVS(10); bytes memory quorumNumbersNotCreated = new bytes(1); ISignatureUtils.SignatureWithSaltAndExpiry memory emptySig; @@ -924,6 +924,139 @@ contract RegistryCoordinatorUnitTests_DeregisterOperator_EjectOperator is Regist ); } + // tests for the internal `_deregisterOperator` function: + function test_deregisterOperatorExternal_revert_noQuorums() public { + ISignatureUtils.SignatureWithSaltAndExpiry memory emptySig; + uint32 registrationBlockNumber = 100; + uint32 deregistrationBlockNumber = 200; + + bytes memory quorumNumbers = new bytes(1); + quorumNumbers[0] = bytes1(defaultQuorumNumber); + + stakeRegistry.setOperatorWeight(uint8(quorumNumbers[0]), defaultOperator, defaultStake); + + cheats.roll(registrationBlockNumber); + cheats.startPrank(defaultOperator); + registryCoordinator.registerOperator(quorumNumbers, defaultSocket, pubkeyRegistrationParams, emptySig); + + bytes memory emptyQuorumNumbers = new bytes(0); + + cheats.roll(deregistrationBlockNumber); + cheats.expectRevert("RegistryCoordinator._deregisterOperator: bitmap cannot be 0"); + registryCoordinator._deregisterOperatorExternal(defaultOperator, emptyQuorumNumbers); + } + + function test_deregisterOperatorExternal_revert_notRegistered() public { + bytes memory emptyQuorumNumbers = new bytes(0); + cheats.expectRevert("RegistryCoordinator._deregisterOperator: operator is not registered"); + registryCoordinator._deregisterOperatorExternal(defaultOperator, emptyQuorumNumbers); + } + + function test_deregisterOperatorExternal_revert_incorrectQuorums() public { + ISignatureUtils.SignatureWithSaltAndExpiry memory emptySig; + uint32 registrationBlockNumber = 100; + uint32 deregistrationBlockNumber = 200; + + bytes memory quorumNumbers = new bytes(1); + quorumNumbers[0] = bytes1(defaultQuorumNumber); + + stakeRegistry.setOperatorWeight(uint8(quorumNumbers[0]), defaultOperator, defaultStake); + + cheats.roll(registrationBlockNumber); + cheats.startPrank(defaultOperator); + registryCoordinator.registerOperator(quorumNumbers, defaultSocket, pubkeyRegistrationParams, emptySig); + + bytes memory incorrectQuorum = new bytes(1); + incorrectQuorum[0] = bytes1(defaultQuorumNumber + 1); + + cheats.roll(deregistrationBlockNumber); + cheats.expectRevert("RegistryCoordinator._deregisterOperator: operator is not registered for specified quorums"); + registryCoordinator._deregisterOperatorExternal(defaultOperator, incorrectQuorum); + } + + // note: this is not possible to test, because there is no route to getting the operator registered for nonexistent quorums + // function test_deregisterOperatorExternal_revert_nonexistentQuorums() public { + + function testFuzz_deregisterOperatorInternal_partialDeregistration( + uint256 registrationQuorumBitmap, + uint256 deregistrationQuorumBitmap + ) public { + ISignatureUtils.SignatureWithSaltAndExpiry memory emptySig; + uint32 registrationBlockNumber = 100; + uint32 deregistrationBlockNumber = 200; + + // filter down fuzzed input to only valid quorums + registrationQuorumBitmap = registrationQuorumBitmap & MAX_QUORUM_BITMAP; + cheats.assume(registrationQuorumBitmap != 0); + // filter the other fuzzed input to a subset of the first fuzzed input + deregistrationQuorumBitmap = deregistrationQuorumBitmap & registrationQuorumBitmap; + cheats.assume(deregistrationQuorumBitmap != 0); + bytes memory registrationquorumNumbers = BitmapUtils.bitmapToBytesArray(registrationQuorumBitmap); + + for (uint i = 0; i < registrationquorumNumbers.length; i++) { + stakeRegistry.setOperatorWeight(uint8(registrationquorumNumbers[i]), defaultOperator, defaultStake); + } + + cheats.roll(registrationBlockNumber); + cheats.startPrank(defaultOperator); + registryCoordinator.registerOperator(registrationquorumNumbers, defaultSocket, pubkeyRegistrationParams, emptySig); + + bytes memory deregistrationquorumNumbers = BitmapUtils.bitmapToBytesArray(deregistrationQuorumBitmap); + + cheats.expectEmit(true, true, true, true, address(blsApkRegistry)); + emit OperatorRemovedFromQuorums(defaultOperator, deregistrationquorumNumbers); + for (uint i = 0; i < deregistrationquorumNumbers.length; i++) { + cheats.expectEmit(true, true, true, true, address(stakeRegistry)); + emit OperatorStakeUpdate(defaultOperatorId, uint8(deregistrationquorumNumbers[i]), 0); + } + + cheats.roll(deregistrationBlockNumber); + + registryCoordinator._deregisterOperatorExternal(defaultOperator, deregistrationquorumNumbers); + + // check that the operator is marked as 'degregistered' only if deregistered from *all* quorums + if (deregistrationQuorumBitmap == registrationQuorumBitmap) { + assertEq( + keccak256(abi.encode(registryCoordinator.getOperator(defaultOperator))), + keccak256(abi.encode(IRegistryCoordinator.OperatorInfo({ + operatorId: defaultOperatorId, + status: IRegistryCoordinator.OperatorStatus.DEREGISTERED + }))) + ); + } else { + assertEq( + keccak256(abi.encode(registryCoordinator.getOperator(defaultOperator))), + keccak256(abi.encode(IRegistryCoordinator.OperatorInfo({ + operatorId: defaultOperatorId, + status: IRegistryCoordinator.OperatorStatus.REGISTERED + }))) + ); + } + // ensure that the operator's current quorum bitmap matches the expectation + uint256 expectedQuorumBitmap = BitmapUtils.minus(registrationQuorumBitmap, deregistrationQuorumBitmap); + assertEq(registryCoordinator.getCurrentQuorumBitmap(defaultOperatorId), expectedQuorumBitmap); + // check that the quorum bitmap history is as expected + assertEq( + keccak256(abi.encode(registryCoordinator.getQuorumBitmapUpdateByIndex(defaultOperatorId, 0))), + keccak256(abi.encode(IRegistryCoordinator.QuorumBitmapUpdate({ + quorumBitmap: uint192(registrationQuorumBitmap), + updateBlockNumber: registrationBlockNumber, + nextUpdateBlockNumber: deregistrationBlockNumber + }))) + ); + // note: there will be no second entry in the operator's bitmap history in the event that the operator has totally deregistered + if (deregistrationQuorumBitmap != registrationQuorumBitmap) { + assertEq( + keccak256(abi.encode(registryCoordinator.getQuorumBitmapUpdateByIndex(defaultOperatorId, 1))), + keccak256(abi.encode(IRegistryCoordinator.QuorumBitmapUpdate({ + quorumBitmap: uint192(expectedQuorumBitmap), + updateBlockNumber: deregistrationBlockNumber, + nextUpdateBlockNumber: 0 + }))) + ); + } + } + function test_ejectOperator_allQuorums() public { // register operator with default stake with default quorum number bytes memory quorumNumbers = new bytes(1); From 98dd6a753846531864f7029da75e63037859defb Mon Sep 17 00:00:00 2001 From: ChaoticWalrus <93558947+ChaoticWalrus@users.noreply.github.com> Date: Wed, 3 Jan 2024 16:03:48 -0800 Subject: [PATCH 37/48] feat: add simple test coverage for the internal `_updateOperatorBitmap` fnc --- test/unit/RegistryCoordinatorUnit.t.sol | 57 +++++++++++++++++++++++++ 1 file changed, 57 insertions(+) diff --git a/test/unit/RegistryCoordinatorUnit.t.sol b/test/unit/RegistryCoordinatorUnit.t.sol index e6e68936..59c6d372 100644 --- a/test/unit/RegistryCoordinatorUnit.t.sol +++ b/test/unit/RegistryCoordinatorUnit.t.sol @@ -1379,6 +1379,63 @@ contract RegistryCoordinatorUnitTests_RegisterOperatorWithChurn is RegistryCoord emptyAVSRegSig ); } +} + +contract RegistryCoordinatorUnitTests_UpdateOperators is RegistryCoordinatorUnitTests { + // @notice tests that the internal `_updateOperatorBitmap` function works as expected, for fuzzed inputs + function testFuzz_updateOperatorBitmapInternal_noPreviousEntries(uint192 newBitmap) public { + registryCoordinator._updateOperatorBitmapExternal(defaultOperatorId, newBitmap); + assertEq( + keccak256(abi.encode(registryCoordinator.getQuorumBitmapUpdateByIndex(defaultOperatorId, 0))), + keccak256(abi.encode(IRegistryCoordinator.QuorumBitmapUpdate({ + quorumBitmap: uint192(newBitmap), + updateBlockNumber: uint32(block.number), + nextUpdateBlockNumber: 0 + }))) + ); + } + // @notice tests that the internal `_updateOperatorBitmap` function works as expected, for fuzzed inputs + function testFuzz_updateOperatorBitmapInternal_previousEntryInCurrentBlock(uint192 newBitmap) public { + uint192 pastBitmap = 1; + testFuzz_updateOperatorBitmapInternal_noPreviousEntries(pastBitmap); + + registryCoordinator._updateOperatorBitmapExternal(defaultOperatorId, newBitmap); + assertEq( + keccak256(abi.encode(registryCoordinator.getQuorumBitmapUpdateByIndex(defaultOperatorId, 0))), + keccak256(abi.encode(IRegistryCoordinator.QuorumBitmapUpdate({ + quorumBitmap: uint192(newBitmap), + updateBlockNumber: uint32(block.number), + nextUpdateBlockNumber: 0 + }))) + ); + } + + // @notice tests that the internal `_updateOperatorBitmap` function works as expected, for fuzzed inputs + function testFuzz_updateOperatorBitmapInternal_previousEntryInPastBlock(uint192 newBitmap) public { + uint192 pastBitmap = 1; + testFuzz_updateOperatorBitmapInternal_noPreviousEntries(pastBitmap); + // advance the block number + uint256 previousBlockNumber = block.number; + cheats.roll(previousBlockNumber + 1); + + registryCoordinator._updateOperatorBitmapExternal(defaultOperatorId, newBitmap); + assertEq( + keccak256(abi.encode(registryCoordinator.getQuorumBitmapUpdateByIndex(defaultOperatorId, 0))), + keccak256(abi.encode(IRegistryCoordinator.QuorumBitmapUpdate({ + quorumBitmap: uint192(pastBitmap), + updateBlockNumber: uint32(previousBlockNumber), + nextUpdateBlockNumber: uint32(block.number) + }))) + ); + assertEq( + keccak256(abi.encode(registryCoordinator.getQuorumBitmapUpdateByIndex(defaultOperatorId, 1))), + keccak256(abi.encode(IRegistryCoordinator.QuorumBitmapUpdate({ + quorumBitmap: uint192(newBitmap), + updateBlockNumber: uint32(block.number), + nextUpdateBlockNumber: 0 + }))) + ); + } } From a81fdc0cbf934f6ac00f223d7a749d340e235698 Mon Sep 17 00:00:00 2001 From: ChaoticWalrus <93558947+ChaoticWalrus@users.noreply.github.com> Date: Thu, 4 Jan 2024 09:17:39 -0800 Subject: [PATCH 38/48] chore: remove commitlint job that reviews all commits in PR from CI This was causing a *lot* of CI failures, including for merge commits With this commit, CI will still check the _latest_ commit for meeting conventions, it just won't run over all commits in a PR This may lead to a few more "unconventional" commits making it through, but the CI should still flag when someone is just not using conventional commits at all, which I think was the original goal. --- .github/workflows/commitlint.yml | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/.github/workflows/commitlint.yml b/.github/workflows/commitlint.yml index 352413f0..b2dfb166 100644 --- a/.github/workflows/commitlint.yml +++ b/.github/workflows/commitlint.yml @@ -16,8 +16,4 @@ jobs: - name: Validate current commit (last commit) with commitlint if: github.event_name == 'push' - run: npx commitlint --from HEAD~1 --to HEAD --verbose - - - name: Validate PR commits with commitlint - if: github.event_name == 'pull_request' - run: npx commitlint --from ${{ github.event.pull_request.head.sha }}~${{ github.event.pull_request.commits }} --to ${{ github.event.pull_request.head.sha }} --verbose + run: npx commitlint --from HEAD~1 --to HEAD --verbose \ No newline at end of file From c899a38e0c41bd5bce70b0a63100f9366a948b0d Mon Sep 17 00:00:00 2001 From: ChaoticWalrus <93558947+ChaoticWalrus@users.noreply.github.com> Date: Thu, 4 Jan 2024 11:08:58 -0800 Subject: [PATCH 39/48] feat: add some coverage for `updateOperators(ForQuorums)` fncs --- test/unit/RegistryCoordinatorUnit.t.sol | 69 +++++++++++++++++++++++++ 1 file changed, 69 insertions(+) diff --git a/test/unit/RegistryCoordinatorUnit.t.sol b/test/unit/RegistryCoordinatorUnit.t.sol index 59c6d372..6f71cf17 100644 --- a/test/unit/RegistryCoordinatorUnit.t.sol +++ b/test/unit/RegistryCoordinatorUnit.t.sol @@ -8,6 +8,7 @@ contract RegistryCoordinatorUnitTests is MockAVSDeployer { uint8 internal constant PAUSED_REGISTER_OPERATOR = 0; uint8 internal constant PAUSED_DEREGISTER_OPERATOR = 1; + uint8 internal constant PAUSED_UPDATE_OPERATOR = 2; uint8 internal constant MAX_QUORUM_COUNT = 192; event OperatorSocketUpdate(bytes32 indexed operatorId, string socket); @@ -1382,6 +1383,73 @@ contract RegistryCoordinatorUnitTests_RegisterOperatorWithChurn is RegistryCoord } contract RegistryCoordinatorUnitTests_UpdateOperators is RegistryCoordinatorUnitTests { + function test_updateOperators_revert_paused() public { + cheats.prank(pauser); + registryCoordinator.pause(2 ** PAUSED_UPDATE_OPERATOR); + + address[] memory operatorsToUpdate = new address[](1); + operatorsToUpdate[0] = defaultOperator; + + cheats.expectRevert(bytes("Pausable: index is paused")); + registryCoordinator.updateOperators(operatorsToUpdate); + } + + // @notice tests the `updateOperators` function with a single registered operator as input + function test_updateOperators_singleOperator() public { + // register the default operator + ISignatureUtils.SignatureWithSaltAndExpiry memory emptySig; + uint32 registrationBlockNumber = 100; + bytes memory quorumNumbers = new bytes(1); + quorumNumbers[0] = bytes1(defaultQuorumNumber); + stakeRegistry.setOperatorWeight(uint8(quorumNumbers[0]), defaultOperator, defaultStake); + cheats.startPrank(defaultOperator); + cheats.roll(registrationBlockNumber); + registryCoordinator.registerOperator(quorumNumbers, defaultSocket, pubkeyRegistrationParams, emptySig); + + address[] memory operatorsToUpdate = new address[](1); + operatorsToUpdate[0] = defaultOperator; + + // TODO: any additional checks here? mostly this just calls the StakeRegistry, so more appropriate for an integration test + registryCoordinator.updateOperators(operatorsToUpdate); + } + + // @notice tests the `updateOperators` function with a single *un*registered operator as input + function test_updateOperators_unregisteredOperator() public { + address[] memory operatorsToUpdate = new address[](1); + operatorsToUpdate[0] = defaultOperator; + + // force a staticcall to the `updateOperators` function -- this should *pass* because the call should be a strict no-op! + (bool success, ) = address(registryCoordinator).staticcall(abi.encodeWithSignature("updateOperators(address[])", operatorsToUpdate)); + require(success, "staticcall failed!"); + } + + function test_updateOperatorsForQuorum_revert_paused() public { + cheats.prank(pauser); + registryCoordinator.pause(2 ** PAUSED_UPDATE_OPERATOR); + + address[][] memory operatorsToUpdate = new address[][](1); + address[] memory operatorArray = new address[](1); + operatorArray[0] = defaultOperator; + operatorsToUpdate[0] = operatorArray; + bytes memory quorumNumbers = new bytes(1); + quorumNumbers[0] = bytes1(defaultQuorumNumber); + + cheats.expectRevert(bytes("Pausable: index is paused")); + registryCoordinator.updateOperatorsForQuorum(operatorsToUpdate, quorumNumbers); + } + + function test_updateOperatorsForQuorum_revert_nonexistentQuorum() public { + _deployMockEigenLayerAndAVS(10); + bytes memory quorumNumbersNotCreated = new bytes(1); + ISignatureUtils.SignatureWithSaltAndExpiry memory emptySig; + quorumNumbersNotCreated[0] = 0x0B; + address[][] memory operatorsToUpdate = new address[][](1); + + cheats.prank(defaultOperator); + cheats.expectRevert("BitmapUtils.orderedBytesArrayToBitmap: bitmap exceeds max value"); + registryCoordinator.updateOperatorsForQuorum(operatorsToUpdate, quorumNumbersNotCreated); + } + // @notice tests that the internal `_updateOperatorBitmap` function works as expected, for fuzzed inputs function testFuzz_updateOperatorBitmapInternal_noPreviousEntries(uint192 newBitmap) public { registryCoordinator._updateOperatorBitmapExternal(defaultOperatorId, newBitmap); @@ -1394,6 +1462,7 @@ contract RegistryCoordinatorUnitTests_UpdateOperators is RegistryCoordinatorUnit }))) ); } + // @notice tests that the internal `_updateOperatorBitmap` function works as expected, for fuzzed inputs function testFuzz_updateOperatorBitmapInternal_previousEntryInCurrentBlock(uint192 newBitmap) public { uint192 pastBitmap = 1; From ada8b4c812c7d02dbd377b622e2a964e403843ad Mon Sep 17 00:00:00 2001 From: ChaoticWalrus <93558947+ChaoticWalrus@users.noreply.github.com> Date: Thu, 4 Jan 2024 15:36:17 -0800 Subject: [PATCH 40/48] feat: add testing for `updateOperatorsForQuorum` function --- test/unit/RegistryCoordinatorUnit.t.sol | 161 +++++++++++++++++++++++- 1 file changed, 159 insertions(+), 2 deletions(-) diff --git a/test/unit/RegistryCoordinatorUnit.t.sol b/test/unit/RegistryCoordinatorUnit.t.sol index 6f71cf17..2081a3bc 100644 --- a/test/unit/RegistryCoordinatorUnit.t.sol +++ b/test/unit/RegistryCoordinatorUnit.t.sol @@ -41,6 +41,8 @@ contract RegistryCoordinatorUnitTests is MockAVSDeployer { event EjectorUpdated(address prevEjector, address newEjector); + event QuorumBlockNumberUpdated(uint8 indexed quorumNumber, uint256 blocknumber); + function setUp() virtual public { _deployMockEigenLayerAndAVS(numQuorums); } @@ -1414,7 +1416,7 @@ contract RegistryCoordinatorUnitTests_UpdateOperators is RegistryCoordinatorUnit } // @notice tests the `updateOperators` function with a single *un*registered operator as input - function test_updateOperators_unregisteredOperator() public { + function test_updateOperators_unregisteredOperator() public view { address[] memory operatorsToUpdate = new address[](1); operatorsToUpdate[0] = defaultOperator; @@ -1441,7 +1443,6 @@ contract RegistryCoordinatorUnitTests_UpdateOperators is RegistryCoordinatorUnit function test_updateOperatorsForQuorum_revert_nonexistentQuorum() public { _deployMockEigenLayerAndAVS(10); bytes memory quorumNumbersNotCreated = new bytes(1); - ISignatureUtils.SignatureWithSaltAndExpiry memory emptySig; quorumNumbersNotCreated[0] = 0x0B; address[][] memory operatorsToUpdate = new address[][](1); @@ -1450,6 +1451,162 @@ contract RegistryCoordinatorUnitTests_UpdateOperators is RegistryCoordinatorUnit registryCoordinator.updateOperatorsForQuorum(operatorsToUpdate, quorumNumbersNotCreated); } + function test_updateOperatorsForQuorum_revert_inputLengthMismatch() public { + address[][] memory operatorsToUpdate = new address[][](2); + bytes memory quorumNumbers = new bytes(1); + quorumNumbers[0] = bytes1(defaultQuorumNumber); + + cheats.expectRevert(bytes("RegistryCoordinator.updateOperatorsForQuorum: input length mismatch")); + registryCoordinator.updateOperatorsForQuorum(operatorsToUpdate, quorumNumbers); + } + + function test_updateOperatorsForQuorum_revert_incorrectNumberOfOperators() public { + address[][] memory operatorsToUpdate = new address[][](1); + address[] memory operatorArray = new address[](1); + operatorArray[0] = defaultOperator; + operatorsToUpdate[0] = operatorArray; + bytes memory quorumNumbers = new bytes(1); + quorumNumbers[0] = bytes1(defaultQuorumNumber); + + cheats.expectRevert(bytes("RegistryCoordinator.updateOperatorsForQuorum: number of updated operators does not match quorum total")); + registryCoordinator.updateOperatorsForQuorum(operatorsToUpdate, quorumNumbers); + } + + function test_updateOperatorsForQuorum_revert_unregisteredOperator() public { + // register the default operator + ISignatureUtils.SignatureWithSaltAndExpiry memory emptySig; + uint32 registrationBlockNumber = 100; + bytes memory quorumNumbers = new bytes(1); + quorumNumbers[0] = bytes1(defaultQuorumNumber); + stakeRegistry.setOperatorWeight(uint8(quorumNumbers[0]), defaultOperator, defaultStake); + cheats.startPrank(defaultOperator); + cheats.roll(registrationBlockNumber); + registryCoordinator.registerOperator(quorumNumbers, defaultSocket, pubkeyRegistrationParams, emptySig); + + address[][] memory operatorsToUpdate = new address[][](1); + address[] memory operatorArray = new address[](1); + // use an unregistered operator address as input + operatorArray[0] = _incrementAddress(defaultOperator, 1); + operatorsToUpdate[0] = operatorArray; + + cheats.expectRevert(bytes("RegistryCoordinator.updateOperatorsForQuorum: operator not in quorum")); + registryCoordinator.updateOperatorsForQuorum(operatorsToUpdate, quorumNumbers); + } + + // note: there is not an explicit check for duplicates, as checking for explicit ordering covers this + function test_updateOperatorsForQuorum_revert_duplicateOperator(uint256 pseudoRandomNumber) public { + // register 2 operators + uint32 numOperators = 2; + uint32 registrationBlockNumber = 200; + bytes memory quorumNumbers = new bytes(1); + quorumNumbers[0] = bytes1(defaultQuorumNumber); + uint256 quorumBitmap = BitmapUtils.orderedBytesArrayToBitmap(quorumNumbers); + cheats.roll(registrationBlockNumber); + for (uint i = 0; i < numOperators; i++) { + BN254.G1Point memory pubKey = BN254.hashToG1(keccak256(abi.encodePacked(pseudoRandomNumber, i))); + address operator = _incrementAddress(defaultOperator, i); + + _registerOperatorWithCoordinator(operator, quorumBitmap, pubKey); + } + + address[][] memory operatorsToUpdate = new address[][](1); + address[] memory operatorArray = new address[](2); + // use the same operator address twice as input + operatorArray[0] = defaultOperator; + operatorArray[1] = defaultOperator; + operatorsToUpdate[0] = operatorArray; + + // note: there is not an explicit check for duplicates, as checking for explicit ordering covers this + cheats.expectRevert(bytes("RegistryCoordinator.updateOperatorsForQuorum: operators array must be sorted in ascending address order")); + registryCoordinator.updateOperatorsForQuorum(operatorsToUpdate, quorumNumbers); + } + + function test_updateOperatorsForQuorum_revert_incorrectListOrder(uint256 pseudoRandomNumber) public { + // register 2 operators + uint32 numOperators = 2; + uint32 registrationBlockNumber = 200; + bytes memory quorumNumbers = new bytes(1); + quorumNumbers[0] = bytes1(defaultQuorumNumber); + uint256 quorumBitmap = BitmapUtils.orderedBytesArrayToBitmap(quorumNumbers); + cheats.roll(registrationBlockNumber); + for (uint i = 0; i < numOperators; i++) { + BN254.G1Point memory pubKey = BN254.hashToG1(keccak256(abi.encodePacked(pseudoRandomNumber, i))); + address operator = _incrementAddress(defaultOperator, i); + + _registerOperatorWithCoordinator(operator, quorumBitmap, pubKey); + } + + address[][] memory operatorsToUpdate = new address[][](1); + address[] memory operatorArray = new address[](2); + // order the operator addresses in descending order, instead of ascending order + operatorArray[0] = _incrementAddress(defaultOperator, 1); + operatorArray[1] = defaultOperator; + operatorsToUpdate[0] = operatorArray; + + cheats.expectRevert(bytes("RegistryCoordinator.updateOperatorsForQuorum: operators array must be sorted in ascending address order")); + registryCoordinator.updateOperatorsForQuorum(operatorsToUpdate, quorumNumbers); + } + + function test_updateOperatorsForQuorum_singleOperator() public { + // register the default operator + ISignatureUtils.SignatureWithSaltAndExpiry memory emptySig; + uint32 registrationBlockNumber = 100; + bytes memory quorumNumbers = new bytes(1); + quorumNumbers[0] = bytes1(defaultQuorumNumber); + stakeRegistry.setOperatorWeight(uint8(quorumNumbers[0]), defaultOperator, defaultStake); + cheats.startPrank(defaultOperator); + cheats.roll(registrationBlockNumber); + registryCoordinator.registerOperator(quorumNumbers, defaultSocket, pubkeyRegistrationParams, emptySig); + + address[][] memory operatorsToUpdate = new address[][](1); + address[] memory operatorArray = new address[](1); + operatorArray[0] = defaultOperator; + operatorsToUpdate[0] = operatorArray; + + uint256 quorumUpdateBlockNumberBefore = registryCoordinator.quorumUpdateBlockNumber(defaultQuorumNumber); + require(quorumUpdateBlockNumberBefore != block.number, "bad test setup!"); + + cheats.expectEmit(true, true, true, true, address(registryCoordinator)); + emit QuorumBlockNumberUpdated(defaultQuorumNumber, block.number); + registryCoordinator.updateOperatorsForQuorum(operatorsToUpdate, quorumNumbers); + + uint256 quorumUpdateBlockNumberAfter = registryCoordinator.quorumUpdateBlockNumber(defaultQuorumNumber); + assertEq(quorumUpdateBlockNumberAfter, block.number, "quorumUpdateBlockNumber not set correctly"); + } + + function test_updateOperatorsForQuorum_twoOperators(uint256 pseudoRandomNumber) public { + // register 2 operators + uint32 numOperators = 2; + uint32 registrationBlockNumber = 200; + bytes memory quorumNumbers = new bytes(1); + quorumNumbers[0] = bytes1(defaultQuorumNumber); + uint256 quorumBitmap = BitmapUtils.orderedBytesArrayToBitmap(quorumNumbers); + cheats.roll(registrationBlockNumber); + for (uint i = 0; i < numOperators; i++) { + BN254.G1Point memory pubKey = BN254.hashToG1(keccak256(abi.encodePacked(pseudoRandomNumber, i))); + address operator = _incrementAddress(defaultOperator, i); + + _registerOperatorWithCoordinator(operator, quorumBitmap, pubKey); + } + + address[][] memory operatorsToUpdate = new address[][](1); + address[] memory operatorArray = new address[](2); + // order the operator addresses in descending order, instead of ascending order + operatorArray[0] = defaultOperator; + operatorArray[1] = _incrementAddress(defaultOperator, 1); + operatorsToUpdate[0] = operatorArray; + + uint256 quorumUpdateBlockNumberBefore = registryCoordinator.quorumUpdateBlockNumber(defaultQuorumNumber); + require(quorumUpdateBlockNumberBefore != block.number, "bad test setup!"); + + cheats.expectEmit(true, true, true, true, address(registryCoordinator)); + emit QuorumBlockNumberUpdated(defaultQuorumNumber, block.number); + registryCoordinator.updateOperatorsForQuorum(operatorsToUpdate, quorumNumbers); + + uint256 quorumUpdateBlockNumberAfter = registryCoordinator.quorumUpdateBlockNumber(defaultQuorumNumber); + assertEq(quorumUpdateBlockNumberAfter, block.number, "quorumUpdateBlockNumber not set correctly"); + } + // @notice tests that the internal `_updateOperatorBitmap` function works as expected, for fuzzed inputs function testFuzz_updateOperatorBitmapInternal_noPreviousEntries(uint192 newBitmap) public { registryCoordinator._updateOperatorBitmapExternal(defaultOperatorId, newBitmap); From 97e942aebc91f4a97a37df90029b9333eb37e454 Mon Sep 17 00:00:00 2001 From: ChaoticWalrus <93558947+ChaoticWalrus@users.noreply.github.com> Date: Mon, 8 Jan 2024 13:11:26 -0800 Subject: [PATCH 41/48] feat: add some coverage for complex view functions note: this commit also adds a TODO around a currently-failing test. I plan to discuss the correct path forwards here and then push another commit. --- test/unit/RegistryCoordinatorUnit.t.sol | 84 ++++++++++++++++++++++++- 1 file changed, 82 insertions(+), 2 deletions(-) diff --git a/test/unit/RegistryCoordinatorUnit.t.sol b/test/unit/RegistryCoordinatorUnit.t.sol index 2081a3bc..719d17fe 100644 --- a/test/unit/RegistryCoordinatorUnit.t.sol +++ b/test/unit/RegistryCoordinatorUnit.t.sol @@ -568,6 +568,7 @@ contract RegistryCoordinatorUnitTests_RegisterOperator is RegistryCoordinatorUni } } +// @dev note that this contract also contains tests for the `getQuorumBitmapIndicesAtBlockNumber` and `getQuorumBitmapAtBlockNumberByIndex` view fncs contract RegistryCoordinatorUnitTests_DeregisterOperator_EjectOperator is RegistryCoordinatorUnitTests { function test_deregisterOperator_revert_paused() public { bytes memory quorumNumbers = new bytes(1); @@ -1150,6 +1151,86 @@ contract RegistryCoordinatorUnitTests_DeregisterOperator_EjectOperator is Regist cheats.prank(defaultOperator); registryCoordinator.ejectOperator(defaultOperator, quorumNumbers); } + + // TODO: this test currently fails. either need to document behavior + modify the test, or modify the code + function test_getQuorumBitmapIndicesAtBlockNumber_revert_notRegistered() public { + uint32 blockNumber; + bytes32[] memory operatorIds = new bytes32[](1); + cheats.expectRevert("RegistryCoordinator.getQuorumBitmapIndicesAtBlockNumber: operatorId has no quorumBitmaps at blockNumber"); + registryCoordinator.getQuorumBitmapIndicesAtBlockNumber(blockNumber, operatorIds); + } + + // @notice tests for correct reversion and return values in the event that an operator registers and later deregisters + function test_getQuorumBitmapIndicesAtBlockNumber_operatorDeregistered() public { + test_deregisterOperator_singleQuorumAndSingleOperator(); + uint32 registrationBlockNumber = 100; + uint32 deregistrationBlockNumber = 200; + uint32 blockNumber = 0; + bytes32[] memory operatorIds = new bytes32[](1); + operatorIds[0] = defaultOperatorId; + + uint32[] memory returnArray = registryCoordinator.getQuorumBitmapIndicesAtBlockNumber(blockNumber, operatorIds); + assertEq(returnArray[0], 0, "defaultOperator bitmap index at blockNumber 0 was not 0"); + + blockNumber = registrationBlockNumber; + returnArray = registryCoordinator.getQuorumBitmapIndicesAtBlockNumber(blockNumber, operatorIds); + assertEq(returnArray[0], 0, "defaultOperator bitmap index at blockNumber registrationBlockNumber was not 0"); + + blockNumber = registrationBlockNumber + 1; + returnArray = registryCoordinator.getQuorumBitmapIndicesAtBlockNumber(blockNumber, operatorIds); + assertEq(returnArray[0], 0, "defaultOperator bitmap index at blockNumber registrationBlockNumber + 1 was not 0"); + + blockNumber = deregistrationBlockNumber; + returnArray = registryCoordinator.getQuorumBitmapIndicesAtBlockNumber(blockNumber, operatorIds); + assertEq(returnArray[0], 1, "defaultOperator bitmap index at blockNumber deregistrationBlockNumber was not 1"); + + blockNumber = deregistrationBlockNumber + 1; + returnArray = registryCoordinator.getQuorumBitmapIndicesAtBlockNumber(blockNumber, operatorIds); + assertEq(returnArray[0], 1, "defaultOperator bitmap index at blockNumber deregistrationBlockNumber + 1 was not 1"); + } + + // @notice tests for correct reversion and return values in the event that an operator registers and later deregisters + function test_getQuorumBitmapAtBlockNumberByIndex_operatorDeregistered() public { + test_deregisterOperator_singleQuorumAndSingleOperator(); + uint32 registrationBlockNumber = 100; + uint32 deregistrationBlockNumber = 200; + uint32 blockNumber = 0; + bytes32 operatorId = defaultOperatorId; + uint256 index = 0; + + uint192 defaultQuorumBitmap = 1; + uint192 emptyBitmap = 0; + + // try an incorrect blockNumber input and confirm reversion + cheats.expectRevert("RegistryCoordinator.getQuorumBitmapAtBlockNumberByIndex: quorumBitmapUpdate is from after blockNumber"); + uint192 returnVal = registryCoordinator.getQuorumBitmapAtBlockNumberByIndex(operatorId, blockNumber, index); + + blockNumber = registrationBlockNumber; + returnVal = registryCoordinator.getQuorumBitmapAtBlockNumberByIndex(operatorId, blockNumber, index); + assertEq(returnVal, defaultQuorumBitmap, "defaultOperator bitmap index at blockNumber registrationBlockNumber was not defaultQuorumBitmap"); + + blockNumber = registrationBlockNumber + 1; + returnVal = registryCoordinator.getQuorumBitmapAtBlockNumberByIndex(operatorId, blockNumber, index); + assertEq(returnVal, defaultQuorumBitmap, "defaultOperator bitmap index at blockNumber registrationBlockNumber + 1 was not defaultQuorumBitmap"); + + // try an incorrect index input and confirm reversion + index = 1; + cheats.expectRevert("RegistryCoordinator.getQuorumBitmapAtBlockNumberByIndex: quorumBitmapUpdate is from after blockNumber"); + returnVal = registryCoordinator.getQuorumBitmapAtBlockNumberByIndex(operatorId, blockNumber, index); + + blockNumber = deregistrationBlockNumber; + returnVal = registryCoordinator.getQuorumBitmapAtBlockNumberByIndex(operatorId, blockNumber, index); + assertEq(returnVal, emptyBitmap, "defaultOperator bitmap index at blockNumber deregistrationBlockNumber was not emptyBitmap"); + + blockNumber = deregistrationBlockNumber + 1; + returnVal = registryCoordinator.getQuorumBitmapAtBlockNumberByIndex(operatorId, blockNumber, index); + assertEq(returnVal, emptyBitmap, "defaultOperator bitmap index at blockNumber deregistrationBlockNumber + 1 was not emptyBitmap"); + + // try an incorrect index input and confirm reversion + index = 0; + cheats.expectRevert("RegistryCoordinator.getQuorumBitmapAtBlockNumberByIndex: quorumBitmapUpdate is from before blockNumber"); + returnVal = registryCoordinator.getQuorumBitmapAtBlockNumberByIndex(operatorId, blockNumber, index); + } } contract RegistryCoordinatorUnitTests_RegisterOperatorWithChurn is RegistryCoordinatorUnitTests { @@ -1663,5 +1744,4 @@ contract RegistryCoordinatorUnitTests_UpdateOperators is RegistryCoordinatorUnit }))) ); } -} - +} \ No newline at end of file From 4b7de1909714dab85c3030c140c9a7ace4feddd7 Mon Sep 17 00:00:00 2001 From: ChaoticWalrus <93558947+ChaoticWalrus@users.noreply.github.com> Date: Tue, 9 Jan 2024 10:53:19 -0800 Subject: [PATCH 42/48] chore: clarify NatSpec comments --- src/RegistryCoordinator.sol | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/RegistryCoordinator.sol b/src/RegistryCoordinator.sol index b96a32f1..39ff0267 100644 --- a/src/RegistryCoordinator.sol +++ b/src/RegistryCoordinator.sol @@ -783,8 +783,10 @@ contract RegistryCoordinator is } /** - * @notice Returns the quorum bitmap for the given `operatorId` at the given `blockNumber` via the `index` - * @dev reverts if `index` is incorrect + * @notice Returns the quorum bitmap for the given `operatorId` at the given `blockNumber` via the `index`, + * reverting if `index` is incorrect + * @dev This function is meant to be used in concert with `getQuorumBitmapIndicesAtBlockNumber`, which + * helps off-chain processes to fetch the correct `index` input */ function getQuorumBitmapAtBlockNumberByIndex( bytes32 operatorId, From 23c789557a976845b13743cdf0bb3d70d1b0331c Mon Sep 17 00:00:00 2001 From: ChaoticWalrus <93558947+ChaoticWalrus@users.noreply.github.com> Date: Tue, 9 Jan 2024 12:34:15 -0800 Subject: [PATCH 43/48] fix: make `getQuorumBitmapIndicesAtBlockNumber` revert if operator was registered the logic is now more in-line with the logic in the StakeRegistry -- for reference, see: https://github.com/layr-labs/eigenlayer-middleware/blob/ 98f884454d9e9de1e344bb6fba9a2cd3915e5b57/src/StakeRegistry.sol#L297-L299 --- src/RegistryCoordinator.sol | 46 +++++++++++++++++-------- test/unit/RegistryCoordinatorUnit.t.sol | 7 ++-- 2 files changed, 36 insertions(+), 17 deletions(-) diff --git a/src/RegistryCoordinator.sol b/src/RegistryCoordinator.sol index 39ff0267..52eaa643 100644 --- a/src/RegistryCoordinator.sol +++ b/src/RegistryCoordinator.sol @@ -758,30 +758,48 @@ contract RegistryCoordinator is return _operatorInfo[operator].status; } - /// @notice Returns the indices of the quorumBitmaps for the provided `operatorIds` at the given `blockNumber` + /** + * @notice Returns the indices of the quorumBitmaps for the provided `operatorIds` at the given `blockNumber` + * @dev Reverts if any of the `operatorIds` was not (yet) registered at `blockNumber` + * @dev This function is designed to find proper inputs to the `getQuorumBitmapAtBlockNumberByIndex` function + */ function getQuorumBitmapIndicesAtBlockNumber( uint32 blockNumber, bytes32[] memory operatorIds ) external view returns (uint32[] memory) { uint32[] memory indices = new uint32[](operatorIds.length); for (uint256 i = 0; i < operatorIds.length; i++) { - uint256 length = _operatorBitmapHistory[operatorIds[i]].length; - for (uint256 j = 0; j < length; j++) { - if (_operatorBitmapHistory[operatorIds[i]][length - j - 1].updateBlockNumber <= blockNumber) { - uint32 nextUpdateBlockNumber = - _operatorBitmapHistory[operatorIds[i]][length - j - 1].nextUpdateBlockNumber; - require( - nextUpdateBlockNumber == 0 || nextUpdateBlockNumber > blockNumber, - "RegistryCoordinator.getQuorumBitmapIndicesAtBlockNumber: operatorId has no quorumBitmaps at blockNumber" - ); - indices[i] = uint32(length - j - 1); - break; - } - } + indices[i] = _getQuorumBitmapIndexAtBlockNumber(blockNumber, operatorIds[i]); } return indices; } + /** + * @notice Returns the index of the quorumBitmap for the provided `operatorId` at the given `blockNumber` + * @dev Reverts if the operator had not yet (ever) registered at `blockNumber` + * @dev This function is designed to find proper inputs to the `getQuorumBitmapAtBlockNumberByIndex` function + */ + function _getQuorumBitmapIndexAtBlockNumber( + uint32 blockNumber, + bytes32 operatorId + ) internal view returns (uint32 index) { + uint256 length = _operatorBitmapHistory[operatorId].length; + for (uint256 i = 0; i < length; i++) { + if (_operatorBitmapHistory[operatorId][length - i - 1].updateBlockNumber <= blockNumber) { + uint32 nextUpdateBlockNumber = + _operatorBitmapHistory[operatorId][length - i - 1].nextUpdateBlockNumber; + require( + nextUpdateBlockNumber == 0 || nextUpdateBlockNumber > blockNumber, + "RegistryCoordinator.getQuorumBitmapIndexAtBlockNumber: operatorId has no bitmap update at blockNumber" + ); + return uint32(length - i - 1); + } + } + revert( + "RegistryCoordinator.getQuorumBitmapIndexAtBlockNumber: no bitmap update found for operatorId at block number" + ); + } + /** * @notice Returns the quorum bitmap for the given `operatorId` at the given `blockNumber` via the `index`, * reverting if `index` is incorrect diff --git a/test/unit/RegistryCoordinatorUnit.t.sol b/test/unit/RegistryCoordinatorUnit.t.sol index 719d17fe..59578db9 100644 --- a/test/unit/RegistryCoordinatorUnit.t.sol +++ b/test/unit/RegistryCoordinatorUnit.t.sol @@ -1156,7 +1156,7 @@ contract RegistryCoordinatorUnitTests_DeregisterOperator_EjectOperator is Regist function test_getQuorumBitmapIndicesAtBlockNumber_revert_notRegistered() public { uint32 blockNumber; bytes32[] memory operatorIds = new bytes32[](1); - cheats.expectRevert("RegistryCoordinator.getQuorumBitmapIndicesAtBlockNumber: operatorId has no quorumBitmaps at blockNumber"); + cheats.expectRevert("RegistryCoordinator.getQuorumBitmapIndexAtBlockNumber: no bitmap update found for operatorId at block number"); registryCoordinator.getQuorumBitmapIndicesAtBlockNumber(blockNumber, operatorIds); } @@ -1169,8 +1169,9 @@ contract RegistryCoordinatorUnitTests_DeregisterOperator_EjectOperator is Regist bytes32[] memory operatorIds = new bytes32[](1); operatorIds[0] = defaultOperatorId; - uint32[] memory returnArray = registryCoordinator.getQuorumBitmapIndicesAtBlockNumber(blockNumber, operatorIds); - assertEq(returnArray[0], 0, "defaultOperator bitmap index at blockNumber 0 was not 0"); + uint32[] memory returnArray; + cheats.expectRevert("RegistryCoordinator.getQuorumBitmapIndexAtBlockNumber: no bitmap update found for operatorId at block number"); + registryCoordinator.getQuorumBitmapIndicesAtBlockNumber(blockNumber, operatorIds); blockNumber = registrationBlockNumber; returnArray = registryCoordinator.getQuorumBitmapIndicesAtBlockNumber(blockNumber, operatorIds); From 8f931ba42a9e77ba9359c6a75fc6efe4402b36a3 Mon Sep 17 00:00:00 2001 From: ChaoticWalrus <93558947+ChaoticWalrus@users.noreply.github.com> Date: Tue, 9 Jan 2024 12:38:41 -0800 Subject: [PATCH 44/48] feat: add simple unit test for `getQuorumBitmapIndicesAtBlockNumber` also improve wording in the 'tree' file --- test/tree/RegistryCoordinatorUnit.tree | 2 +- test/unit/RegistryCoordinatorUnit.t.sol | 29 +++++++++++++++++++++++++ 2 files changed, 30 insertions(+), 1 deletion(-) diff --git a/test/tree/RegistryCoordinatorUnit.tree b/test/tree/RegistryCoordinatorUnit.tree index 159fd6b6..e089625d 100644 --- a/test/tree/RegistryCoordinatorUnit.tree +++ b/test/tree/RegistryCoordinatorUnit.tree @@ -102,7 +102,7 @@ │ │ ├── getQuorumBitmapIndicesAtBlockNumber() -│ ├── given that any of the operatorIDs was not registered at the block number +│ ├── given that any of the operatorIDs had not yet registered at the block number │ │ └── it should revert │ └── it should return the proper index of the entry in each operatorID's quorum bitmap history │ diff --git a/test/unit/RegistryCoordinatorUnit.t.sol b/test/unit/RegistryCoordinatorUnit.t.sol index 59578db9..97f29b45 100644 --- a/test/unit/RegistryCoordinatorUnit.t.sol +++ b/test/unit/RegistryCoordinatorUnit.t.sol @@ -1160,6 +1160,35 @@ contract RegistryCoordinatorUnitTests_DeregisterOperator_EjectOperator is Regist registryCoordinator.getQuorumBitmapIndicesAtBlockNumber(blockNumber, operatorIds); } + // @notice tests for correct reversion and return values in the event that an operator registers + function test_getQuorumBitmapIndicesAtBlockNumber_operatorRegistered() public { + // register the operator + ISignatureUtils.SignatureWithSaltAndExpiry memory emptySig; + uint32 registrationBlockNumber = 100; + bytes memory quorumNumbers = new bytes(1); + quorumNumbers[0] = bytes1(defaultQuorumNumber); + stakeRegistry.setOperatorWeight(uint8(quorumNumbers[0]), defaultOperator, defaultStake); + cheats.roll(registrationBlockNumber); + cheats.startPrank(defaultOperator); + registryCoordinator.registerOperator(quorumNumbers, defaultSocket, pubkeyRegistrationParams, emptySig); + + uint32 blockNumber = 0; + bytes32[] memory operatorIds = new bytes32[](1); + operatorIds[0] = defaultOperatorId; + + uint32[] memory returnArray; + cheats.expectRevert("RegistryCoordinator.getQuorumBitmapIndexAtBlockNumber: no bitmap update found for operatorId at block number"); + registryCoordinator.getQuorumBitmapIndicesAtBlockNumber(blockNumber, operatorIds); + + blockNumber = registrationBlockNumber; + returnArray = registryCoordinator.getQuorumBitmapIndicesAtBlockNumber(blockNumber, operatorIds); + assertEq(returnArray[0], 0, "defaultOperator bitmap index at blockNumber registrationBlockNumber was not 0"); + + blockNumber = registrationBlockNumber + 1; + returnArray = registryCoordinator.getQuorumBitmapIndicesAtBlockNumber(blockNumber, operatorIds); + assertEq(returnArray[0], 0, "defaultOperator bitmap index at blockNumber registrationBlockNumber + 1 was not 0"); + } + // @notice tests for correct reversion and return values in the event that an operator registers and later deregisters function test_getQuorumBitmapIndicesAtBlockNumber_operatorDeregistered() public { test_deregisterOperator_singleQuorumAndSingleOperator(); From 5f185835c9b5da51afab57afba91f51374b9c466 Mon Sep 17 00:00:00 2001 From: ChaoticWalrus <93558947+ChaoticWalrus@users.noreply.github.com> Date: Tue, 9 Jan 2024 13:47:05 -0800 Subject: [PATCH 45/48] chore: move `_getQuorumBitmapIndexAtBlockNumber` into the section with other internal fncs --- src/RegistryCoordinator.sol | 52 ++++++++++++++++++------------------- 1 file changed, 26 insertions(+), 26 deletions(-) diff --git a/src/RegistryCoordinator.sol b/src/RegistryCoordinator.sol index 52eaa643..9c7b6622 100644 --- a/src/RegistryCoordinator.sol +++ b/src/RegistryCoordinator.sol @@ -714,6 +714,32 @@ contract RegistryCoordinator is } } + /** + * @notice Returns the index of the quorumBitmap for the provided `operatorId` at the given `blockNumber` + * @dev Reverts if the operator had not yet (ever) registered at `blockNumber` + * @dev This function is designed to find proper inputs to the `getQuorumBitmapAtBlockNumberByIndex` function + */ + function _getQuorumBitmapIndexAtBlockNumber( + uint32 blockNumber, + bytes32 operatorId + ) internal view returns (uint32 index) { + uint256 length = _operatorBitmapHistory[operatorId].length; + for (uint256 i = 0; i < length; i++) { + if (_operatorBitmapHistory[operatorId][length - i - 1].updateBlockNumber <= blockNumber) { + uint32 nextUpdateBlockNumber = + _operatorBitmapHistory[operatorId][length - i - 1].nextUpdateBlockNumber; + require( + nextUpdateBlockNumber == 0 || nextUpdateBlockNumber > blockNumber, + "RegistryCoordinator.getQuorumBitmapIndexAtBlockNumber: operatorId has no bitmap update at blockNumber" + ); + return uint32(length - i - 1); + } + } + revert( + "RegistryCoordinator.getQuorumBitmapIndexAtBlockNumber: no bitmap update found for operatorId at block number" + ); + } + function _setOperatorSetParams(uint8 quorumNumber, OperatorSetParam memory operatorSetParams) internal { _quorumParams[quorumNumber] = operatorSetParams; emit OperatorSetParamsUpdated(quorumNumber, operatorSetParams); @@ -774,32 +800,6 @@ contract RegistryCoordinator is return indices; } - /** - * @notice Returns the index of the quorumBitmap for the provided `operatorId` at the given `blockNumber` - * @dev Reverts if the operator had not yet (ever) registered at `blockNumber` - * @dev This function is designed to find proper inputs to the `getQuorumBitmapAtBlockNumberByIndex` function - */ - function _getQuorumBitmapIndexAtBlockNumber( - uint32 blockNumber, - bytes32 operatorId - ) internal view returns (uint32 index) { - uint256 length = _operatorBitmapHistory[operatorId].length; - for (uint256 i = 0; i < length; i++) { - if (_operatorBitmapHistory[operatorId][length - i - 1].updateBlockNumber <= blockNumber) { - uint32 nextUpdateBlockNumber = - _operatorBitmapHistory[operatorId][length - i - 1].nextUpdateBlockNumber; - require( - nextUpdateBlockNumber == 0 || nextUpdateBlockNumber > blockNumber, - "RegistryCoordinator.getQuorumBitmapIndexAtBlockNumber: operatorId has no bitmap update at blockNumber" - ); - return uint32(length - i - 1); - } - } - revert( - "RegistryCoordinator.getQuorumBitmapIndexAtBlockNumber: no bitmap update found for operatorId at block number" - ); - } - /** * @notice Returns the quorum bitmap for the given `operatorId` at the given `blockNumber` via the `index`, * reverting if `index` is incorrect From 9f965c1d44ef0f95ed684ce350e175dddaf8f9b6 Mon Sep 17 00:00:00 2001 From: ChaoticWalrus <93558947+ChaoticWalrus@users.noreply.github.com> Date: Tue, 9 Jan 2024 13:48:27 -0800 Subject: [PATCH 46/48] chore: remove unnecessary require statement + improve code clarity --- src/RegistryCoordinator.sol | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/src/RegistryCoordinator.sol b/src/RegistryCoordinator.sol index 9c7b6622..5f68b35a 100644 --- a/src/RegistryCoordinator.sol +++ b/src/RegistryCoordinator.sol @@ -726,13 +726,8 @@ contract RegistryCoordinator is uint256 length = _operatorBitmapHistory[operatorId].length; for (uint256 i = 0; i < length; i++) { if (_operatorBitmapHistory[operatorId][length - i - 1].updateBlockNumber <= blockNumber) { - uint32 nextUpdateBlockNumber = - _operatorBitmapHistory[operatorId][length - i - 1].nextUpdateBlockNumber; - require( - nextUpdateBlockNumber == 0 || nextUpdateBlockNumber > blockNumber, - "RegistryCoordinator.getQuorumBitmapIndexAtBlockNumber: operatorId has no bitmap update at blockNumber" - ); - return uint32(length - i - 1); + index = length - i - 1; + return index; } } revert( From 9a328e50df7a006dd61040a0a3b9ff206290d44c Mon Sep 17 00:00:00 2001 From: ChaoticWalrus <93558947+ChaoticWalrus@users.noreply.github.com> Date: Tue, 9 Jan 2024 14:02:58 -0800 Subject: [PATCH 47/48] fix: correct a compiler error for implicit type conversion --- src/RegistryCoordinator.sol | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/RegistryCoordinator.sol b/src/RegistryCoordinator.sol index 5f68b35a..348786c7 100644 --- a/src/RegistryCoordinator.sol +++ b/src/RegistryCoordinator.sol @@ -726,7 +726,7 @@ contract RegistryCoordinator is uint256 length = _operatorBitmapHistory[operatorId].length; for (uint256 i = 0; i < length; i++) { if (_operatorBitmapHistory[operatorId][length - i - 1].updateBlockNumber <= blockNumber) { - index = length - i - 1; + index = uint32(length - i - 1); return index; } } From f7244a26b54bb77b050e486a14c3ab604d390c60 Mon Sep 17 00:00:00 2001 From: ChaoticWalrus <93558947+ChaoticWalrus@users.noreply.github.com> Date: Tue, 9 Jan 2024 14:42:15 -0800 Subject: [PATCH 48/48] feat: address TODOs in tests --- test/mocks/StakeRegistryMock.sol | 16 +++++++--- test/unit/RegistryCoordinatorUnit.t.sol | 40 +++++++++++++++++++++++-- 2 files changed, 50 insertions(+), 6 deletions(-) diff --git a/test/mocks/StakeRegistryMock.sol b/test/mocks/StakeRegistryMock.sol index 455163a1..9ff83503 100644 --- a/test/mocks/StakeRegistryMock.sol +++ b/test/mocks/StakeRegistryMock.sol @@ -9,6 +9,12 @@ import "../../src/interfaces/IRegistryCoordinator.sol"; * @author Layr Labs, Inc. */ contract StakeRegistryMock is IStakeRegistry { + // bitmap returned by the mocked `updateOperatorStake` function + uint192 updateOperatorStakeReturnBitmap; + + function set_updateOperatorStakeReturnBitmap(uint192 newValue) external { + updateOperatorStakeReturnBitmap = newValue; + } function registryCoordinator() external view returns (address) {} @@ -195,10 +201,12 @@ contract StakeRegistryMock is IStakeRegistry { * added to the */ function updateOperatorStake( - address operator, - bytes32 operatorId, - bytes calldata quorumNumbers - ) external returns (uint192) {} + address /*operator*/, + bytes32 /*operatorId*/, + bytes calldata /*quorumNumbers*/ + ) external returns (uint192) { + return updateOperatorStakeReturnBitmap; + } function getMockOperatorId(address operator) external pure returns(bytes32) { return bytes32(uint256(keccak256(abi.encodePacked(operator, "operatorId")))); diff --git a/test/unit/RegistryCoordinatorUnit.t.sol b/test/unit/RegistryCoordinatorUnit.t.sol index 97f29b45..bb0439dc 100644 --- a/test/unit/RegistryCoordinatorUnit.t.sol +++ b/test/unit/RegistryCoordinatorUnit.t.sol @@ -1152,7 +1152,6 @@ contract RegistryCoordinatorUnitTests_DeregisterOperator_EjectOperator is Regist registryCoordinator.ejectOperator(defaultOperator, quorumNumbers); } - // TODO: this test currently fails. either need to document behavior + modify the test, or modify the code function test_getQuorumBitmapIndicesAtBlockNumber_revert_notRegistered() public { uint32 blockNumber; bytes32[] memory operatorIds = new bytes32[](1); @@ -1522,10 +1521,47 @@ contract RegistryCoordinatorUnitTests_UpdateOperators is RegistryCoordinatorUnit address[] memory operatorsToUpdate = new address[](1); operatorsToUpdate[0] = defaultOperator; - // TODO: any additional checks here? mostly this just calls the StakeRegistry, so more appropriate for an integration test registryCoordinator.updateOperators(operatorsToUpdate); } + // @notice tests the `updateOperators` function with a single registered operator as input + // @dev also sets up return data from the StakeRegistry + function testFuzz_updateOperators_singleOperator(uint192 registrationBitmap, uint192 mockReturnData) public { + // filter fuzzed inputs to only valid inputs + cheats.assume(registrationBitmap != 0); + mockReturnData = (mockReturnData & registrationBitmap); + emit log_named_uint("mockReturnData", mockReturnData); + + // register the default operator + ISignatureUtils.SignatureWithSaltAndExpiry memory emptySig; + uint32 registrationBlockNumber = 100; + bytes memory quorumNumbers = BitmapUtils.bitmapToBytesArray(registrationBitmap); + for (uint256 i = 0; i < quorumNumbers.length; ++i) { + stakeRegistry.setOperatorWeight(uint8(quorumNumbers[i]), defaultOperator, defaultStake); + } + cheats.startPrank(defaultOperator); + cheats.roll(registrationBlockNumber); + registryCoordinator.registerOperator(quorumNumbers, defaultSocket, pubkeyRegistrationParams, emptySig); + + address[] memory operatorsToUpdate = new address[](1); + operatorsToUpdate[0] = defaultOperator; + + uint192 quorumBitmapBefore = registryCoordinator.getCurrentQuorumBitmap(defaultOperatorId); + assertEq(quorumBitmapBefore, registrationBitmap, "operator bitmap somehow incorrect"); + + // make the stake registry return info that the operator should be removed from quorums + uint192 quorumBitmapToRemove = mockReturnData; + bytes memory quorumNumbersToRemove = BitmapUtils.bitmapToBytesArray(quorumBitmapToRemove); + for (uint256 i = 0; i < quorumNumbersToRemove.length; ++i) { + stakeRegistry.setOperatorWeight(uint8(quorumNumbersToRemove[i]), defaultOperator, 0); + } + uint256 expectedQuorumBitmap = BitmapUtils.minus(quorumBitmapBefore, quorumBitmapToRemove); + + registryCoordinator.updateOperators(operatorsToUpdate); + uint192 quorumBitmapAfter = registryCoordinator.getCurrentQuorumBitmap(defaultOperatorId); + assertEq(expectedQuorumBitmap, quorumBitmapAfter, "quorum bitmap did not update correctly"); + } + // @notice tests the `updateOperators` function with a single *un*registered operator as input function test_updateOperators_unregisteredOperator() public view { address[] memory operatorsToUpdate = new address[](1);