From 4777e2b4b20cacd3b98a1d9540c879a1f19a1827 Mon Sep 17 00:00:00 2001 From: Shawn <44221603+shaspitz@users.noreply.github.com> Date: Thu, 18 Jul 2024 12:03:29 -0700 Subject: [PATCH] docs: clarify MevCommitAVS behavior w/ associated tests (#235) * docs: clarify isValidatorOptedIn behavior w/ associated test * docs: clarify requestValidatorsDeregistration + deregisterValidators behavior w/ associated test --- .../validator-registry/avs/MevCommitAVS.sol | 3 + .../validator-registry/avs/README.md | 53 ++++++------- .../avs/MevCommitAVSTest.sol | 76 +++++++++++++++++++ 3 files changed, 104 insertions(+), 28 deletions(-) diff --git a/contracts/contracts/validator-registry/avs/MevCommitAVS.sol b/contracts/contracts/validator-registry/avs/MevCommitAVS.sol index 93d5cfc33..4bd0563a0 100644 --- a/contracts/contracts/validator-registry/avs/MevCommitAVS.sol +++ b/contracts/contracts/validator-registry/avs/MevCommitAVS.sol @@ -360,6 +360,9 @@ contract MevCommitAVS is IMevCommitAVS, MevCommitAVSStorage, /// @dev Internal function to register validators by their pod owner. /// @notice Invalid pubkeys should not correspond to VALIDATOR_STATUS.ACTIVE due to validations in EigenPod.sol + /// @dev A successful call to this function gauruntees isValidatorOptedIn() returns true for each pubkey immediately after + /// this function returns. However, sucessive state-changes (ex: delegated operator deregisters) may result in changes + /// to validator opt-in state. function _registerValidatorsByPodOwner( bytes[] calldata valPubKeys, address podOwner diff --git a/contracts/contracts/validator-registry/avs/README.md b/contracts/contracts/validator-registry/avs/README.md index 28cf0a9be..2c83a0595 100644 --- a/contracts/contracts/validator-registry/avs/README.md +++ b/contracts/contracts/validator-registry/avs/README.md @@ -16,25 +16,42 @@ Operators mainly serve the purpose of (optionally) being able to register valida ## Validator Opt-in -Recall that a native-restaking enabled validator opting-in to mev-commit requires two steps: +Recall that a native-restaking enabled validator opting-in to mev-commit requires two high level steps: -1. The validator must delegate their native stake to an Operator who's registered with the mev-commit AVS. -2. The validator must separately *register* with the mev-commit AVS, confirming their attestation to follow the rules of the protocol. +1. The validator must delegate (via eigenlayer core) their native stake to an Operator who's registered with the mev-commit AVS. +2. The validator must separately be *registered* with the mev-commit AVS, confirming their attestation to follow the rules of the protocol. -Multiple validator public keys can be registered at once, alongside their associated eigenpod owner `podOwner` address. Note each eigenpod owner account can represent one or many restaked validators: +Multiple validator public keys can be registered at once, alongside their associated eigenpod owner `podOwner` address. Note each eigenpod owner account may represent one or many restaked validators: ```solidity function registerValidatorsByPodOwner(bytes[] calldata valPubKeys, address podOwner); ``` -This function stores relevant state and ensures that the provided pubkeys are indeed actively restaked with `podOwner`'s eigenPod. Note two entities are able to register validator pub keys in this way: +This function verifies and updates state such that directly after the call, `isValidatorOptedIn(valPubKey)` will return true for each `valPubKey`. + +Note two entities are able to register validator pub keys in this way: 1. The eigenpod owner account itself. -2. An Operator account, so long as the relevant eigenpod is delegated to that Operator. +2. The (delegated and fully registered) Operator account. + +If an Operator is registering pubkeys on behalf of validators, it's expected that the Operator manages those validators itself, or represents the validators to an extent that the Operator can realistically attest to the validator following the rules of mev-commit (staking-as-a-service providers for example). This trustful relationship between validators and their delegated Operator piggybacks off already agreed upon trust assumptions with eigenlayer delegation. + +Validator deregistration requires calling `requestValidatorsDeregistration`, waiting a configurable amount of blocks, then calling `deregisterValidators`. These functions are similarly callable by the eigenpod owner OR delegated operator. A delegated operator calling either `requestValidatorsDeregistration` or `deregisterValidators` does not require that operator to be registered with the MevCommitAVS (this is allowed due to aforementioned trust assumptions between validators and their delegated Operator). + +### What defines a validator staying "opted-in" + +A validator staying opted-in following registration is explicitly defined by the following criteria: -Note if an Operator is registering pubkeys on behalf of validators, it's expected that the Operator manages those validators itself, or represents the validators to an extent that the Operator can realistically attest to the validator following the rules of mev-commit (staking-as-a-service providers for example). This trustful relationship between validators and their delegated Operator piggybacks off already agreed upon trust assumptions with eigenlayer delegation. +1. The validator's registration entry must still exists with the MevCommitAVS (ie. validator has not been deregistered). +2. The validator must not be frozen. +3. The validator must not have requested deregistration with the MevCommitAVS. +4. The validator must be `VALIDATOR_STATUS.ACTIVE` with respect to its eigenpod. +5. The validator's delegated operator must be registered with the MevCommitAVS. +6. The validator's delegated operator must not have requested deregistration with the MevCommitAVS. -Deregistration requires calling `requestValidatorsDeregistration`, waiting a configurable amount of blocks, then calling `deregisterValidators`. These functions are similarly callable by the eigenpod owner OR delegated operator. +Directly following a successful call to `registerValidatorsByPodOwner`, all of these criteria will be true as enforced by the function. However, anyone of these criteria becoming false will result in the validator no longer being "opted-in" from the mev-commit protocol's perspective. + +For example if an opted-in validator's delegated operator requests deregistration with the MevCommitAVS, the eigenpod owner representing this validator needs to [redelegate to a new operator](https://docs.eigenlayer.xyz/eigenlayer/restaking-guides/restaking-user-guide/restaker-delegation/redelegation-process) who's registered with the MevCommitAVS, to reclaim opted-in status. ## LST Restaker Registration @@ -52,26 +69,6 @@ LST restakers will receive points/rewards commensurate with their chosen validat Validator opt-in state can be queried with `isValidatorOptedIn()`. This query offers concrete criteria that must be true for an LST restaker to accrue points/rewards over time from a chosen validator. -```solidity -function isValidatorOptedIn(bytes calldata valPubKey) returns (bool) { - IMevCommitAVS.ValidatorRegistrationInfo memory valRegistration = validatorRegistrations[valPubKey]; - bool isValRegistered = valRegistration.exists; - bool isFrozen = valRegistration.freezeHeight.exists; - bool isValDeregRequested = valRegistration.deregRequestHeight.exists; - - IEigenPod pod = _eigenPodManager.getPod(valRegistration.podOwner); - bool isValActive = pod.validatorPubkeyToInfo(valPubKey).status == IEigenPod.VALIDATOR_STATUS.ACTIVE; - - address delegatedOperator = _delegationManager.delegatedTo(valRegistration.podOwner); - IMevCommitAVS.OperatorRegistrationInfo memory operatorRegistration = operatorRegistrations[delegatedOperator]; - bool isOperatorRegistered = operatorRegistration.exists; - bool isOperatorDeregRequested = operatorRegistration.deregRequestHeight.exists; - - return isValRegistered && !isFrozen && !isValDeregRequested && isValActive - && isOperatorRegistered && !isOperatorDeregRequested; -} -``` - Since validators are chosen in sets, an LST restaker can only choose a new set of validators by deregistering, and registering again with the new set. This simplifies contract implementation and enforces an LST restaker is responsible for the actions of its chosen validator(s). Points/rewards for LST restakers would be computed off-chain, with heavy use of indexed events. As there is not an efficient on-chain mapping from each validator to the set of LST restakers who've chosen that validator. When a rewards/points system is introduced, it may consider the following information (and possibly more): diff --git a/contracts/test/validator-registry/avs/MevCommitAVSTest.sol b/contracts/test/validator-registry/avs/MevCommitAVSTest.sol index b39fec6f3..363df57cd 100644 --- a/contracts/test/validator-registry/avs/MevCommitAVSTest.sol +++ b/contracts/test/validator-registry/avs/MevCommitAVSTest.sol @@ -1001,4 +1001,80 @@ contract MevCommitAVSTest is Test { assertTrue(mevCommitAVS.getValidatorRegInfo(valPubkeys[1]).exists); assertFalse(mevCommitAVS.getValidatorRegInfo(valPubkeys[1]).freezeHeight.exists); } + + function testValidatorIsOptedIn() public { + testRegisterValidatorsByPodOwners(); + + bytes[] memory valPubkeys = new bytes[](2); + valPubkeys[0] = bytes("valPubkey1"); + valPubkeys[1] = bytes("valPubkey2"); + + assertTrue(mevCommitAVS.isValidatorOptedIn(valPubkeys[0])); + assertTrue(mevCommitAVS.isValidatorOptedIn(valPubkeys[1])); + + address operator = address(0x888); + vm.prank(operator); + mevCommitAVS.requestOperatorDeregistration(operator); + + assertFalse(mevCommitAVS.isValidatorOptedIn(valPubkeys[0])); + assertFalse(mevCommitAVS.isValidatorOptedIn(valPubkeys[1])); + + address newOperator = address(0x999); + delegationManagerMock.setIsOperator(newOperator, true); + + vm.prank(newOperator); + ISignatureUtils.SignatureWithSaltAndExpiry memory newOperatorSigWithSalt = ISignatureUtils.SignatureWithSaltAndExpiry({ + signature: bytes("signature"), + salt: bytes32("salt"), + expiry: block.timestamp + 1 days + }); + mevCommitAVS.registerOperator(newOperatorSigWithSalt); + assertTrue(mevCommitAVS.getOperatorRegInfo(newOperator).exists); + + address podOwner = address(0x420); + vm.prank(podOwner); + ISignatureUtils.SignatureWithExpiry memory newOperatorSig = ISignatureUtils.SignatureWithExpiry({ + signature: bytes("signature"), + expiry: block.timestamp + 1 days + }); + delegationManagerMock.delegateTo(newOperator, newOperatorSig, bytes32("salt")); + + assertTrue(mevCommitAVS.isValidatorOptedIn(valPubkeys[0])); + assertTrue(mevCommitAVS.isValidatorOptedIn(valPubkeys[1])); + } + + function testDeregisteredOperatorCanStillDeregisterValidators() public { + testRegisterValidatorsByPodOwners(); + + address operator = address(0x888); + vm.prank(operator); + mevCommitAVS.requestOperatorDeregistration(operator); + assertTrue(mevCommitAVS.getOperatorRegInfo(operator).exists); + assertTrue(mevCommitAVS.getOperatorRegInfo(operator).deregRequestHeight.exists); + + bytes[] memory valPubkeys = new bytes[](2); + valPubkeys[0] = bytes("valPubkey1"); + valPubkeys[1] = bytes("valPubkey2"); + + address podOwner = address(0x420); + vm.expectEmit(true, true, true, true); + emit ValidatorDeregistrationRequested(valPubkeys[0], podOwner); + vm.expectEmit(true, true, true, true); + emit ValidatorDeregistrationRequested(valPubkeys[1], podOwner); + vm.prank(operator); + mevCommitAVS.requestValidatorsDeregistration(valPubkeys); + + vm.roll(2000); + + vm.prank(operator); + mevCommitAVS.deregisterOperator(operator); + assertFalse(mevCommitAVS.getOperatorRegInfo(operator).exists); + + vm.expectEmit(true, true, true, true); + emit ValidatorDeregistered(valPubkeys[0], podOwner); + vm.expectEmit(true, true, true, true); + emit ValidatorDeregistered(valPubkeys[1], podOwner); + vm.prank(operator); + mevCommitAVS.deregisterValidators(valPubkeys); + } }