Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

refactor: quorum creation is the divine right of the registry coordinator #28

Merged
merged 25 commits into from
Nov 6, 2023

Conversation

wadealexc
Copy link
Collaborator

@wadealexc wadealexc commented Oct 26, 2023

Motivation:

  • fix word soup variable and function naming
  • take advantage of new quorum existence invariant due to RegistryCoordinator.createQuorum and its associated initializeQuorum methods in each registry
  • registries each keep "histories" of updates. The status quo is that every time anything changes, we push a new history update. this means that when updates happen multiple times in the same block, the history contains multiple updates from the same block number, making it harder to determine what the canonical state at a given block number is.

This PR:

  • Moves createQuorum to the registry coordinator, and adds quorum initialization logic for each registry contract
  • Replaces logic that handles "0-length histories" with simple checks, since we now have an invariant that initialized quorums have nonzero histories (see any registry's initializeQuorum method)
  • changes registry histories to update their "most recent entry" rather than pushing a new entry, if the entry was made in the current block
  • Renames state variables in the registry coordinator and registry contracts to be less wordy
  • adds a new mapping to BLSPublicKeyCompendium : operatorToPubkey, which maps operator addresses to G1Point
  • adds a method BLSPublicKeyCompendium.getRegisteredPubkey(address) which performs a pubkey lookup along with validation that the pubkey exists/isn't the zero pubkey.
    • Validation logic here was previously performed in the BLSPubkeyRegistry
  • Removes the "pubkey" parameter from various functions, like in the BLSPubkeyRegistry and BLSRegistryCoordinatorWithIndices. The pubkey registry is able to look up pubkeys with just an operator address, now.

For reviewers, this order may work best:

  1. Review IndexRegistry changes in the first commit, which contains the functional changes mentioned above, as well as readability/logical simplification. This is the worst commit to review, sorry! The diff looks intimidating, so I'd recommend opening the new/old IndexRegistry files side by side and comparing like that. 🙏
  2. The rest of the commits are style/refactor commits. I'd recommend skimming the renaming changes below and only dipping into the commits themselves if you feel like it.

Renaming changes:

  • IndexRegistry state vars:
    • operatorIdToIndex -> currentOperatorIndex
    • _totalOperatorsHistory -> _operatorCountHistory
    • _indexToOperatorIdHistory -> _indexHistory
  • IndexRegistry functions:
    • _getTotalOperatorsForQuorumAtBlockNumber -> _operatorCountAtBlockNumber
    • _getOperatorIdAtIndexForQuorumAtBlockNumber -> _operatorIdForIndexAtBlockNumber
    • getOperatorIndexUpdateOfIndexForQuorumAtIndex -> getOperatorUpdateAtIndex
    • getTotalOperatorsForQuorumAtBlockNumberByIndex -> getTotalOperatorsForIndexAtBlockNumber
    • getOperatorListForQuorumAtBlockNumber -> getOperatorListAtBlockNumber
  • StakeRegistry state vars:
    • operatorIdToStakeHistory -> operatorStakeHistory
    • strategiesConsideredAndMultipliers -> strategyParams
  • StakeRegistry structs:
    • StrategyAndWeightingMultiplier -> StrategyParams
  • StakeRegistry functions:
    • strategiesConsideredAndMultipliersLength -> strategyParamsLength
    • strategyAndWeightingMultiplierForQuorumByIndex -> strategyParamsByIndex
    • getOperatorIdToStakeHistory -> getOperatorStakeHistory
    • getLengthOfTotalStakeHistoryForQuorum -> getTotalStakeHistoryLength
    • getTotalStakeUpdateForQuorumFromIndex -> getTotalStakeUpdateAtIndex
    • getStakeUpdateIndexForOperatorIdForQuorumAtBlockNumber -> getStakeUpdateIndexForOperatorAtBlockNumber
    • getTotalStakeIndicesByQuorumNumbersAtBlockNumber -> getTotalStakeIndicesAtBlockNumber
    • getStakeUpdateForQuorumFromOperatorIdAndIndex -> getStakeUpdateForOperatorAtIndex
    • getStakeForQuorumAtBlockNumberFromOperatorIdAndIndex -> getOperatorStakeAtBlockNumberAndIndex
    • getStakeForOperatorIdForQuorumAtBlockNumber -> getOperatorStakeAtBlockNumber
    • getLengthOfOperatorIdStakeHistoryForQuorum -> getOperatorStakeHistoryLength
  • BLSRegistryCoordinatorWithIndices state vars:
    • _quorumOperatorSetParams -> _quorumParams
    • _operatorIdToQuorumBitmapHistory -> _operatorBitmapHistory
    • _operators -> _operatorInfo
  • BLSRegistryCoordinatorWithIndices state modifying functions:
    • registerOperatorWithCoordinator -> registerOperator
    • registerOperatorWithCoordinator -> registerOperatorWithChurn
    • deregisterOperatorWithCoordinator -> deregisterOperator
    • ejectOperatorFromCoordinator -> ejectOperator
  • BLSRegistryCoordinatorWithIndices view functions:
    • getQuorumBitmapIndicesByOperatorIdsAtBlockNumber -> getQuorumBitmapIndicesAtBlockNumber
    • getQuorumBitmapByOperatorIdAtBlockNumberByIndex -> getQuorumBitmapAtBlockNumberByIndex
    • getQuorumBitmapUpdateByOperatorIdByIndex -> getQuorumBitmapUpdateByIndex
    • getCurrentQuorumBitmapByOperatorId -> getCurrentQuorumBitmap
    • getQuorumBitmapUpdateByOperatorIdLength -> getQuorumBitmapHistoryLength
  • BLSPubkeyRegistry state variable naming changes (these better match the rest of the contracts):
    • quorumApk -> currentApk
    • quorumApkUpdates -> apkHistory
  • BLSPubkeyRegistry view function naming changes:
    • getApkIndicesForQuorumsAtBlockNumber -> getApkIndicesAtBlockNumber
    • getApkForQuorum -> getApk
    • getApkUpdateForQuorumByIndex -> getApkUpdateAtIndex
    • getApkHashForQuorumAtBlockNumberFromIndex -> getApkHashAtBlockNumberAndIndex
    • getQuorumApkHistoryLength -> getApkHistoryLength

IndexRegistry changes:

  • Refactored index registry history updates to have a more readable register/deregister flow:
    • registerOperator is now _increaseOperatorCount + _assignOperatorToIndex. Functional changes:
      • If the most recent QuorumUpdate/OperatorUpdate is from this block, we update the last entry rather than pushing a new one.
    • deregisterOperator is now _decreaseOperatorCount + _popLastOperator + _assignOperatorToIndex. Functional changes:
      • As above, histories are updated rather than pushed if the last entry was from the current block.
      • Previously, we assigned currentOperatorIndex[quorum][OPERATOR_DOES_NOT_EXIST_ID] to the removed index, and emitted an event for this update. This logic has been removed, since the currentOperatorIndex of the null id doesn't matter. See previous logic vs new logic

src/IndexRegistry.sol Outdated Show resolved Hide resolved
uint96 stake;
}

// EVENTS
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

2 event sections should be made 1

src/StakeRegistry.sol Outdated Show resolved Hide resolved
@wadealexc wadealexc force-pushed the alex/refactor-quorums branch from 596f20f to b4709cb Compare October 30, 2023 15:04
@wadealexc wadealexc marked this pull request as ready for review November 2, 2023 19:33

unchecked {
++i;
}
}
}

// TODO - should this fail if apkUpdate.apkHash == 0? This will be the case for the first entry in each quorum
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

good question. relatedly, should we actually push the hash of the zero point in the first update, rather than zero per se?
I think this probably shouldn't revert in this case, but am not strongly opinionated

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

question for @gpsanant

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

good point, let's push the zero hash

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

so to confirm, the change is to push the zero hash for the first update, and have this method fail if apkHash == 0?

Should it also fail if apkHash == ZERO_HASH?

Comment on lines 137 to 140
_indexToOperatorIdHistory[quorumNumber][index].push(OperatorUpdate({
operatorId: operatorId,
fromBlockNumber: uint32(block.number)
}));
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

it's weird that elsewhere we use 24 bytes (as in the BLSPubkeyRegistry) for the equivalent of operatorId, and here we use 32 bytes. Not sure why this line in particular made me notice this, but it's just...a bit odd?
should operatorIds perhaps be shorter, so we can do the same storage packing everywhere?

Comment on lines +63 to +64
// TODO - not a huge fan of this dependency on the reg coord, but i do prefer this
// over the stakereg also keeping its own count.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this makes sense. it's a bit ugly, but in my mind the registries are basically all "puppets" of the RegistryCoordinator, so calling into the coordinator to fetch a bit of data is fine.
I wouldn't be super opposed to just keeping count in the StakeRegistry either, as it'd be more efficient -- we could just increment a quorumCount variable in the initializeQuorum() function? Then the two can only be out-of-sync if the RegistryCoordinator either fails to call initializeQuorum appropriately or calls it inappropriately.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is marked TODO because I was intending a future PR to move updateStakes into the registry coordinator. I'm working on that locally and will have something to share next week

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ahhh OK that works!

Comment on lines +216 to +217
* @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".
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I realize these probably weren't part of this PR (it probably just moves the comments?) but:
typo: 'concious' => 'conscious'
and I think 'trades' should be 'treats'

Comment on lines 226 to 248
/**
* @notice Remove strategies and their associated weights from the quorum's considered strategies
* @dev higher indices should be *first* in the list of @param indicesToRemove, since otherwise
* the removal of lower index entries will cause a shift in the indices of the other strategies to remove
*/
function removeStrategies(
uint8 quorumNumber,
uint256[] memory indicesToRemove
) public virtual onlyServiceManagerOwner quorumExists(quorumNumber) {
uint256 toRemoveLength = indicesToRemove.length;
require(toRemoveLength > 0, "StakeRegistry.removeStrategyParams: no indices to remove provided");

StrategyAndWeightingMultiplier[] storage strategyParams = strategiesConsideredAndMultipliers[quorumNumber];

for (uint256 i = 0; i < toRemoveLength; i++) {
emit StrategyRemovedFromQuorum(quorumNumber, strategyParams[indicesToRemove[i]].strategy);
emit StrategyMultiplierUpdated(quorumNumber, strategyParams[indicesToRemove[i]].strategy, 0);

// Replace index to remove with the last item in the list, then pop the last item
strategyParams[indicesToRemove[i]] = strategyParams[strategyParams.length - 1];
strategyParams.pop();
}
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@wadealexc thoughts on the safety of this function?
it previously had a redundant parameter which it checked against, to ensure that the desired strategies were removed

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I havent given any thought to the safety of the method... I think that'll come when we start working on integration tests. Essentially we should be testing that stake updates occur as expected even if we interweave strategy modifications in between updates.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

sorry, wasn't related to this PR, probably should have asked elsewhere, but here's a link to the previous behavior I was thinking of https://github.com/Layr-Labs/eigenlayer-contracts/blob/bd211f12fe8ed84f691ca44db3f812412defcb1e/src/contracts/middleware/VoteWeigherBase.sol#L134-L138

this could be described as an "idiot-check", but I do have a feeling it'd be easy to call this function "incorrectly" (or at least, in a way that doesn't have the desired outcome) with the way it works now

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

gotcha. i think there's a footgun here if the method is called incorrectly, but since this is an admin function, i think it's ok. footguns inherently exist when performing "irregular state transitions" like this, and i'd rather keep the methods clean so it's clear what's going on - rather than adding checks that correct for mistakes we make off chain.

worst case we realize we did this incorrectly and we can pause/correct the mistake. but i think any time we call these we should do our due diligence and feel confident that we're doing it correctly.

Copy link
Contributor

@ChaoticWalrus ChaoticWalrus left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Feels like a good change overall.
I made some smaller comments but don't think I have any major points.
Am in agreement with the small open items noted by @gpsanant

@wadealexc wadealexc merged commit a4b039e into m2-mainnet Nov 6, 2023
1 check passed
@wadealexc wadealexc deleted the alex/refactor-quorums branch November 6, 2023 15:54
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants