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] 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) {