datahaven/contracts/test/ServiceManagerRewardsRegistry.t.sol

704 lines
25 KiB
Solidity
Raw Permalink Normal View History

feat: ✨ initial rewards registry (#17) This PR adds an initial implementation for a rewards registry, which will be the contract in charge of allowing DataHaven validators to claim the rewards they earned for being validators in the previous epoch. The logic behind it is as follows: - Whenever an epoch finishes, the corresponding BEEFY block gets relayed to Ethereum through Snowbridge. This BEEFY block contains, in its `extra` field, the merkle root of the tree that contains as leafs all the message commitments of the messages of corresponding block, one of which is the rewards distribution message. - The rewards distribution message commitment is the root of the merkle tree where each leaf is a tuple of the operator ID and the obtained era points in the finished epoch. In this case, the operator ID is the corresponding validator's Ethereum address. - When the rewards distribution message is received, Snowbridge validates it using the aforementioned BEEFY block and then dispatches it. The dispatch invokes the `callContract` function of the `RewardsAgent` agent, with the corresponding parameters so that this agent calls the `updateRewardsMerkleRoot` function of the `RewardsRegistry` contract with the new rewards distribution message commitment. - After this root is updated, any validator/operator can submit a proof that it is in a leaf of the merkle tree that produced that root, which means it has pending rewards to claim, through the `ServiceManagerBase`'s `claimOperatorRewards` function. - Each operator set of the AVS can have an assigned `RewardsRegistry` contract. Operator sets that do not have an assigned `RewardsRegistry` contract won't be able to received rewards. This PR also adds two separate unit-test suites: one for the added functionality to the `ServiceManagerBase` contract and one specific to the new `RewardsRegistry` contract. > [!CAUTION] The `RewardsAgent` agent is the only one allowed to update the rewards' merkle root, which means if a malicious user could get access to it it could set the pending rewards to be claimed to an arbitrary tree that benefits it. Extreme caution must be taken in the Substrate side so only validated messages are sent to the Ethereum side, as to not allow any users to impersonate being this agent. ### TODO: Ideally, we would use the `RewardsCoordinator` contract from the EigenLayer core to distribute the rewards, but currently that adds a huge overhead for Operators since they'd have to wait for EigenLayer's SideCar to snapshot state and update the distribution root (which happens once a day), generate a proof that they belong to the tree of that distribution root, store it while waiting for the `activationDelay` (currently a week) to pass, and just then they would be able to claim their earned rewards. --------- Co-authored-by: Facundo Farall <37149322+ffarall@users.noreply.github.com>
2025-03-31 19:54:23 +00:00
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.13;
/* solhint-disable func-name-mixedcase */
feat: ✨ initial rewards registry (#17) This PR adds an initial implementation for a rewards registry, which will be the contract in charge of allowing DataHaven validators to claim the rewards they earned for being validators in the previous epoch. The logic behind it is as follows: - Whenever an epoch finishes, the corresponding BEEFY block gets relayed to Ethereum through Snowbridge. This BEEFY block contains, in its `extra` field, the merkle root of the tree that contains as leafs all the message commitments of the messages of corresponding block, one of which is the rewards distribution message. - The rewards distribution message commitment is the root of the merkle tree where each leaf is a tuple of the operator ID and the obtained era points in the finished epoch. In this case, the operator ID is the corresponding validator's Ethereum address. - When the rewards distribution message is received, Snowbridge validates it using the aforementioned BEEFY block and then dispatches it. The dispatch invokes the `callContract` function of the `RewardsAgent` agent, with the corresponding parameters so that this agent calls the `updateRewardsMerkleRoot` function of the `RewardsRegistry` contract with the new rewards distribution message commitment. - After this root is updated, any validator/operator can submit a proof that it is in a leaf of the merkle tree that produced that root, which means it has pending rewards to claim, through the `ServiceManagerBase`'s `claimOperatorRewards` function. - Each operator set of the AVS can have an assigned `RewardsRegistry` contract. Operator sets that do not have an assigned `RewardsRegistry` contract won't be able to received rewards. This PR also adds two separate unit-test suites: one for the added functionality to the `ServiceManagerBase` contract and one specific to the new `RewardsRegistry` contract. > [!CAUTION] The `RewardsAgent` agent is the only one allowed to update the rewards' merkle root, which means if a malicious user could get access to it it could set the pending rewards to be claimed to an arbitrary tree that benefits it. Extreme caution must be taken in the Substrate side so only validated messages are sent to the Ethereum side, as to not allow any users to impersonate being this agent. ### TODO: Ideally, we would use the `RewardsCoordinator` contract from the EigenLayer core to distribute the rewards, but currently that adds a huge overhead for Operators since they'd have to wait for EigenLayer's SideCar to snapshot state and update the distribution root (which happens once a day), generate a proof that they belong to the tree of that distribution root, store it while waiting for the `activationDelay` (currently a week) to pass, and just then they would be able to claim their earned rewards. --------- Co-authored-by: Facundo Farall <37149322+ffarall@users.noreply.github.com>
2025-03-31 19:54:23 +00:00
import {Test, console, stdError} from "forge-std/Test.sol";
fix: Resolve Foundry build errors and apply code formatting (#241) ## Summary Fixes the CI build failure in the `task-ts-build` workflow caused by Foundry v1.4.2's Solar linter not being able to resolve Snowbridge's context-specific import remappings. ## Problem The Snowbridge submodule uses context-specific remappings (prefixed with `:`) for its dependencies: - `lib/snowbridge/contracts/:openzeppelin/` → OpenZeppelin contracts - `lib/snowbridge/contracts/:prb/math/` → PRB Math library Foundry v1.4.2's Solar linter doesn't understand these context-specific remappings and fails with errors like: ``` error: file openzeppelin/utils/cryptography/MerkleProof.sol not found error: file prb/math/src/UD60x18.sol not found ``` ## Solution Added global remappings that the linter can understand: ```toml "openzeppelin/=lib/snowbridge/contracts/lib/openzeppelin-contracts/contracts/", "prb/math/=lib/snowbridge/contracts/lib/prb-math/", ``` ### Why This Works - The linter can now resolve `openzeppelin/` and `prb/math/` imports globally - These global remappings take **lower precedence** than context-specific ones during compilation - The compiler still uses the context-specific remappings (with `:`) when compiling Snowbridge contracts - The linter uses the global remappings when checking all files ## Changes ### Commit 1: Add global remappings - `contracts/foundry.toml`: Added 2 global remapping entries ### Commit 2: Apply forge fmt - Applied automatic formatting via `forge fmt` to ensure code style consistency - Multi-line formatting for long import statements and function signatures - No functional changes - purely formatting updates ## Testing ✅ Local build succeeds with `forge build` ✅ No Snowbridge import resolution errors ✅ `forge fmt --check` passes with no formatting issues ✅ Only linting notes/warnings remain (not errors) 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude <noreply@anthropic.com>
2025-10-20 08:20:59 +00:00
import {
IAllocationManager
} from "eigenlayer-contracts/src/contracts/interfaces/IAllocationManager.sol";
feat: ✨ initial rewards registry (#17) This PR adds an initial implementation for a rewards registry, which will be the contract in charge of allowing DataHaven validators to claim the rewards they earned for being validators in the previous epoch. The logic behind it is as follows: - Whenever an epoch finishes, the corresponding BEEFY block gets relayed to Ethereum through Snowbridge. This BEEFY block contains, in its `extra` field, the merkle root of the tree that contains as leafs all the message commitments of the messages of corresponding block, one of which is the rewards distribution message. - The rewards distribution message commitment is the root of the merkle tree where each leaf is a tuple of the operator ID and the obtained era points in the finished epoch. In this case, the operator ID is the corresponding validator's Ethereum address. - When the rewards distribution message is received, Snowbridge validates it using the aforementioned BEEFY block and then dispatches it. The dispatch invokes the `callContract` function of the `RewardsAgent` agent, with the corresponding parameters so that this agent calls the `updateRewardsMerkleRoot` function of the `RewardsRegistry` contract with the new rewards distribution message commitment. - After this root is updated, any validator/operator can submit a proof that it is in a leaf of the merkle tree that produced that root, which means it has pending rewards to claim, through the `ServiceManagerBase`'s `claimOperatorRewards` function. - Each operator set of the AVS can have an assigned `RewardsRegistry` contract. Operator sets that do not have an assigned `RewardsRegistry` contract won't be able to received rewards. This PR also adds two separate unit-test suites: one for the added functionality to the `ServiceManagerBase` contract and one specific to the new `RewardsRegistry` contract. > [!CAUTION] The `RewardsAgent` agent is the only one allowed to update the rewards' merkle root, which means if a malicious user could get access to it it could set the pending rewards to be claimed to an arbitrary tree that benefits it. Extreme caution must be taken in the Substrate side so only validated messages are sent to the Ethereum side, as to not allow any users to impersonate being this agent. ### TODO: Ideally, we would use the `RewardsCoordinator` contract from the EigenLayer core to distribute the rewards, but currently that adds a huge overhead for Operators since they'd have to wait for EigenLayer's SideCar to snapshot state and update the distribution root (which happens once a day), generate a proof that they belong to the tree of that distribution root, store it while waiting for the `activationDelay` (currently a week) to pass, and just then they would be able to claim their earned rewards. --------- Co-authored-by: Facundo Farall <37149322+ffarall@users.noreply.github.com>
2025-03-31 19:54:23 +00:00
import {AVSDeployer} from "./utils/AVSDeployer.sol";
feat: ✨ initial rewards registry (#17) This PR adds an initial implementation for a rewards registry, which will be the contract in charge of allowing DataHaven validators to claim the rewards they earned for being validators in the previous epoch. The logic behind it is as follows: - Whenever an epoch finishes, the corresponding BEEFY block gets relayed to Ethereum through Snowbridge. This BEEFY block contains, in its `extra` field, the merkle root of the tree that contains as leafs all the message commitments of the messages of corresponding block, one of which is the rewards distribution message. - The rewards distribution message commitment is the root of the merkle tree where each leaf is a tuple of the operator ID and the obtained era points in the finished epoch. In this case, the operator ID is the corresponding validator's Ethereum address. - When the rewards distribution message is received, Snowbridge validates it using the aforementioned BEEFY block and then dispatches it. The dispatch invokes the `callContract` function of the `RewardsAgent` agent, with the corresponding parameters so that this agent calls the `updateRewardsMerkleRoot` function of the `RewardsRegistry` contract with the new rewards distribution message commitment. - After this root is updated, any validator/operator can submit a proof that it is in a leaf of the merkle tree that produced that root, which means it has pending rewards to claim, through the `ServiceManagerBase`'s `claimOperatorRewards` function. - Each operator set of the AVS can have an assigned `RewardsRegistry` contract. Operator sets that do not have an assigned `RewardsRegistry` contract won't be able to received rewards. This PR also adds two separate unit-test suites: one for the added functionality to the `ServiceManagerBase` contract and one specific to the new `RewardsRegistry` contract. > [!CAUTION] The `RewardsAgent` agent is the only one allowed to update the rewards' merkle root, which means if a malicious user could get access to it it could set the pending rewards to be claimed to an arbitrary tree that benefits it. Extreme caution must be taken in the Substrate side so only validated messages are sent to the Ethereum side, as to not allow any users to impersonate being this agent. ### TODO: Ideally, we would use the `RewardsCoordinator` contract from the EigenLayer core to distribute the rewards, but currently that adds a huge overhead for Operators since they'd have to wait for EigenLayer's SideCar to snapshot state and update the distribution root (which happens once a day), generate a proof that they belong to the tree of that distribution root, store it while waiting for the `activationDelay` (currently a week) to pass, and just then they would be able to claim their earned rewards. --------- Co-authored-by: Facundo Farall <37149322+ffarall@users.noreply.github.com>
2025-03-31 19:54:23 +00:00
import {RewardsRegistry} from "../src/middleware/RewardsRegistry.sol";
import {IRewardsRegistry, IRewardsRegistryErrors} from "../src/interfaces/IRewardsRegistry.sol";
import {ServiceManagerMock} from "./mocks/ServiceManagerMock.sol";
import {IServiceManager, IServiceManagerErrors} from "../src/interfaces/IServiceManager.sol";
import {ScaleCodec} from "snowbridge/src/utils/ScaleCodec.sol";
feat: ✨ initial rewards registry (#17) This PR adds an initial implementation for a rewards registry, which will be the contract in charge of allowing DataHaven validators to claim the rewards they earned for being validators in the previous epoch. The logic behind it is as follows: - Whenever an epoch finishes, the corresponding BEEFY block gets relayed to Ethereum through Snowbridge. This BEEFY block contains, in its `extra` field, the merkle root of the tree that contains as leafs all the message commitments of the messages of corresponding block, one of which is the rewards distribution message. - The rewards distribution message commitment is the root of the merkle tree where each leaf is a tuple of the operator ID and the obtained era points in the finished epoch. In this case, the operator ID is the corresponding validator's Ethereum address. - When the rewards distribution message is received, Snowbridge validates it using the aforementioned BEEFY block and then dispatches it. The dispatch invokes the `callContract` function of the `RewardsAgent` agent, with the corresponding parameters so that this agent calls the `updateRewardsMerkleRoot` function of the `RewardsRegistry` contract with the new rewards distribution message commitment. - After this root is updated, any validator/operator can submit a proof that it is in a leaf of the merkle tree that produced that root, which means it has pending rewards to claim, through the `ServiceManagerBase`'s `claimOperatorRewards` function. - Each operator set of the AVS can have an assigned `RewardsRegistry` contract. Operator sets that do not have an assigned `RewardsRegistry` contract won't be able to received rewards. This PR also adds two separate unit-test suites: one for the added functionality to the `ServiceManagerBase` contract and one specific to the new `RewardsRegistry` contract. > [!CAUTION] The `RewardsAgent` agent is the only one allowed to update the rewards' merkle root, which means if a malicious user could get access to it it could set the pending rewards to be claimed to an arbitrary tree that benefits it. Extreme caution must be taken in the Substrate side so only validated messages are sent to the Ethereum side, as to not allow any users to impersonate being this agent. ### TODO: Ideally, we would use the `RewardsCoordinator` contract from the EigenLayer core to distribute the rewards, but currently that adds a huge overhead for Operators since they'd have to wait for EigenLayer's SideCar to snapshot state and update the distribution root (which happens once a day), generate a proof that they belong to the tree of that distribution root, store it while waiting for the `activationDelay` (currently a week) to pass, and just then they would be able to claim their earned rewards. --------- Co-authored-by: Facundo Farall <37149322+ffarall@users.noreply.github.com>
2025-03-31 19:54:23 +00:00
contract ServiceManagerRewardsRegistryTest is AVSDeployer {
feat: ✨ initial rewards registry (#17) This PR adds an initial implementation for a rewards registry, which will be the contract in charge of allowing DataHaven validators to claim the rewards they earned for being validators in the previous epoch. The logic behind it is as follows: - Whenever an epoch finishes, the corresponding BEEFY block gets relayed to Ethereum through Snowbridge. This BEEFY block contains, in its `extra` field, the merkle root of the tree that contains as leafs all the message commitments of the messages of corresponding block, one of which is the rewards distribution message. - The rewards distribution message commitment is the root of the merkle tree where each leaf is a tuple of the operator ID and the obtained era points in the finished epoch. In this case, the operator ID is the corresponding validator's Ethereum address. - When the rewards distribution message is received, Snowbridge validates it using the aforementioned BEEFY block and then dispatches it. The dispatch invokes the `callContract` function of the `RewardsAgent` agent, with the corresponding parameters so that this agent calls the `updateRewardsMerkleRoot` function of the `RewardsRegistry` contract with the new rewards distribution message commitment. - After this root is updated, any validator/operator can submit a proof that it is in a leaf of the merkle tree that produced that root, which means it has pending rewards to claim, through the `ServiceManagerBase`'s `claimOperatorRewards` function. - Each operator set of the AVS can have an assigned `RewardsRegistry` contract. Operator sets that do not have an assigned `RewardsRegistry` contract won't be able to received rewards. This PR also adds two separate unit-test suites: one for the added functionality to the `ServiceManagerBase` contract and one specific to the new `RewardsRegistry` contract. > [!CAUTION] The `RewardsAgent` agent is the only one allowed to update the rewards' merkle root, which means if a malicious user could get access to it it could set the pending rewards to be claimed to an arbitrary tree that benefits it. Extreme caution must be taken in the Substrate side so only validated messages are sent to the Ethereum side, as to not allow any users to impersonate being this agent. ### TODO: Ideally, we would use the `RewardsCoordinator` contract from the EigenLayer core to distribute the rewards, but currently that adds a huge overhead for Operators since they'd have to wait for EigenLayer's SideCar to snapshot state and update the distribution root (which happens once a day), generate a proof that they belong to the tree of that distribution root, store it while waiting for the `activationDelay` (currently a week) to pass, and just then they would be able to claim their earned rewards. --------- Co-authored-by: Facundo Farall <37149322+ffarall@users.noreply.github.com>
2025-03-31 19:54:23 +00:00
// Test addresses
address public operatorAddress;
address public nonOperatorAddress;
// Test data
uint32 public operatorSetId;
bytes32 public merkleRoot;
feat: :sparkles: make `RewardsRegistry` keep a history of roots and claim status (#106) # Description This PR implements a comprehensive overhaul of the `RewardsRegistry` contract to maintain complete history of reward merkle roots while providing index-based claim tracking for operators. The new architecture enables operators to claim rewards from any historical merkle root instead of only the latest one. To do so, it: - Adds the `merkleRootHistory` storage array to the contract, in which we keep all rewards roots that ever came from the DataHaven side. - Adds the `operatorClaimedByIndex` storage map to the contract, in which we keep track, for each validator and root index, if it has claimed it or not. - This works even for new validators, since theoretically with this system you could argue they could claim older roots that they were not a part of which would be catastrophic, but they could never draft a correct proof for those to claim them. - Keeps some of the interface from before the overhaul, to have quick access to the latest rewards merkle root through `getLatestMerkleRoot()` and to claim rewards for it with `claimRewards()`. This is because the expected behaviour is for validators to claim their rewards every era. - Adds a way to batch claim rewards with `claimRewardsBatch()`. This function allows a validator to claim rewards for multiple root indices in one call by providing multiple proofs, useful if the validator has fallen behind claims and has to catch up, although special care will have to be taken by it to avoid reaching the gas limit of a transaction. ## Storage Efficiency Analysis One might think this solution is not as storage-efficient as other solutions that we can think of (I even had two other alternatives in mind as well), but a simple back-of-the-envelope calculation gives us peace of mind that the impact of this solution on the overal state size of the chain is negligible: ### Assumptions (Worst Case Scenario): - 1,000 validators (actual estimate for DataHaven: ~50/100 validators) - 6-hour eras (most-likely scenario, following what Polkadot does: ~24-hour eras) - Which means 4 merkle root updates per day ### Annual Storage Requirements: - Merkle Root History: **46,720 bytes/year** - 4 roots/day × 32 bytes/root × 365 days/year = 46,720 bytes/year - Operator Claim Tracking: **~1.46 MB/year** - 1,000 operators × 1 boolean/(operator * root index) × 1 byte/boolean × 4 root indices/day × 365 days/year = 1,460,000 bytes/year - **Total: ~1.5 MB/year** This represents negligible storage overhead compared to the significant operational benefits gained. ## TODO Since we want to allow the operators/validators to only have to interact with the AVS contract (that's why the `claimRewards` functions have the `onlyAVS` modifier), we still have to: - [x] Add the required functions to the AVS to allow operators to claim their rewards. - [x] Adds comprehensive unit tests for them. --------- Co-authored-by: Steve Degosserie <723552+stiiifff@users.noreply.github.com> Co-authored-by: Ahmad Kaouk <56095276+ahmadkaouk@users.noreply.github.com> Co-authored-by: Ahmad Kaouk <ahmadkaouk.93@gmail.com>
2025-07-10 06:47:39 +00:00
bytes32 public secondMerkleRoot;
bytes32 public thirdMerkleRoot;
feat: ✨ initial rewards registry (#17) This PR adds an initial implementation for a rewards registry, which will be the contract in charge of allowing DataHaven validators to claim the rewards they earned for being validators in the previous epoch. The logic behind it is as follows: - Whenever an epoch finishes, the corresponding BEEFY block gets relayed to Ethereum through Snowbridge. This BEEFY block contains, in its `extra` field, the merkle root of the tree that contains as leafs all the message commitments of the messages of corresponding block, one of which is the rewards distribution message. - The rewards distribution message commitment is the root of the merkle tree where each leaf is a tuple of the operator ID and the obtained era points in the finished epoch. In this case, the operator ID is the corresponding validator's Ethereum address. - When the rewards distribution message is received, Snowbridge validates it using the aforementioned BEEFY block and then dispatches it. The dispatch invokes the `callContract` function of the `RewardsAgent` agent, with the corresponding parameters so that this agent calls the `updateRewardsMerkleRoot` function of the `RewardsRegistry` contract with the new rewards distribution message commitment. - After this root is updated, any validator/operator can submit a proof that it is in a leaf of the merkle tree that produced that root, which means it has pending rewards to claim, through the `ServiceManagerBase`'s `claimOperatorRewards` function. - Each operator set of the AVS can have an assigned `RewardsRegistry` contract. Operator sets that do not have an assigned `RewardsRegistry` contract won't be able to received rewards. This PR also adds two separate unit-test suites: one for the added functionality to the `ServiceManagerBase` contract and one specific to the new `RewardsRegistry` contract. > [!CAUTION] The `RewardsAgent` agent is the only one allowed to update the rewards' merkle root, which means if a malicious user could get access to it it could set the pending rewards to be claimed to an arbitrary tree that benefits it. Extreme caution must be taken in the Substrate side so only validated messages are sent to the Ethereum side, as to not allow any users to impersonate being this agent. ### TODO: Ideally, we would use the `RewardsCoordinator` contract from the EigenLayer core to distribute the rewards, but currently that adds a huge overhead for Operators since they'd have to wait for EigenLayer's SideCar to snapshot state and update the distribution root (which happens once a day), generate a proof that they belong to the tree of that distribution root, store it while waiting for the `activationDelay` (currently a week) to pass, and just then they would be able to claim their earned rewards. --------- Co-authored-by: Facundo Farall <37149322+ffarall@users.noreply.github.com>
2025-03-31 19:54:23 +00:00
uint256 public operatorPoints;
feat: :sparkles: make `RewardsRegistry` keep a history of roots and claim status (#106) # Description This PR implements a comprehensive overhaul of the `RewardsRegistry` contract to maintain complete history of reward merkle roots while providing index-based claim tracking for operators. The new architecture enables operators to claim rewards from any historical merkle root instead of only the latest one. To do so, it: - Adds the `merkleRootHistory` storage array to the contract, in which we keep all rewards roots that ever came from the DataHaven side. - Adds the `operatorClaimedByIndex` storage map to the contract, in which we keep track, for each validator and root index, if it has claimed it or not. - This works even for new validators, since theoretically with this system you could argue they could claim older roots that they were not a part of which would be catastrophic, but they could never draft a correct proof for those to claim them. - Keeps some of the interface from before the overhaul, to have quick access to the latest rewards merkle root through `getLatestMerkleRoot()` and to claim rewards for it with `claimRewards()`. This is because the expected behaviour is for validators to claim their rewards every era. - Adds a way to batch claim rewards with `claimRewardsBatch()`. This function allows a validator to claim rewards for multiple root indices in one call by providing multiple proofs, useful if the validator has fallen behind claims and has to catch up, although special care will have to be taken by it to avoid reaching the gas limit of a transaction. ## Storage Efficiency Analysis One might think this solution is not as storage-efficient as other solutions that we can think of (I even had two other alternatives in mind as well), but a simple back-of-the-envelope calculation gives us peace of mind that the impact of this solution on the overal state size of the chain is negligible: ### Assumptions (Worst Case Scenario): - 1,000 validators (actual estimate for DataHaven: ~50/100 validators) - 6-hour eras (most-likely scenario, following what Polkadot does: ~24-hour eras) - Which means 4 merkle root updates per day ### Annual Storage Requirements: - Merkle Root History: **46,720 bytes/year** - 4 roots/day × 32 bytes/root × 365 days/year = 46,720 bytes/year - Operator Claim Tracking: **~1.46 MB/year** - 1,000 operators × 1 boolean/(operator * root index) × 1 byte/boolean × 4 root indices/day × 365 days/year = 1,460,000 bytes/year - **Total: ~1.5 MB/year** This represents negligible storage overhead compared to the significant operational benefits gained. ## TODO Since we want to allow the operators/validators to only have to interact with the AVS contract (that's why the `claimRewards` functions have the `onlyAVS` modifier), we still have to: - [x] Add the required functions to the AVS to allow operators to claim their rewards. - [x] Adds comprehensive unit tests for them. --------- Co-authored-by: Steve Degosserie <723552+stiiifff@users.noreply.github.com> Co-authored-by: Ahmad Kaouk <56095276+ahmadkaouk@users.noreply.github.com> Co-authored-by: Ahmad Kaouk <ahmadkaouk.93@gmail.com>
2025-07-10 06:47:39 +00:00
uint256 public secondOperatorPoints;
uint256 public thirdOperatorPoints;
uint256 public leafIndex;
uint256 public numberOfLeaves;
feat: ✨ initial rewards registry (#17) This PR adds an initial implementation for a rewards registry, which will be the contract in charge of allowing DataHaven validators to claim the rewards they earned for being validators in the previous epoch. The logic behind it is as follows: - Whenever an epoch finishes, the corresponding BEEFY block gets relayed to Ethereum through Snowbridge. This BEEFY block contains, in its `extra` field, the merkle root of the tree that contains as leafs all the message commitments of the messages of corresponding block, one of which is the rewards distribution message. - The rewards distribution message commitment is the root of the merkle tree where each leaf is a tuple of the operator ID and the obtained era points in the finished epoch. In this case, the operator ID is the corresponding validator's Ethereum address. - When the rewards distribution message is received, Snowbridge validates it using the aforementioned BEEFY block and then dispatches it. The dispatch invokes the `callContract` function of the `RewardsAgent` agent, with the corresponding parameters so that this agent calls the `updateRewardsMerkleRoot` function of the `RewardsRegistry` contract with the new rewards distribution message commitment. - After this root is updated, any validator/operator can submit a proof that it is in a leaf of the merkle tree that produced that root, which means it has pending rewards to claim, through the `ServiceManagerBase`'s `claimOperatorRewards` function. - Each operator set of the AVS can have an assigned `RewardsRegistry` contract. Operator sets that do not have an assigned `RewardsRegistry` contract won't be able to received rewards. This PR also adds two separate unit-test suites: one for the added functionality to the `ServiceManagerBase` contract and one specific to the new `RewardsRegistry` contract. > [!CAUTION] The `RewardsAgent` agent is the only one allowed to update the rewards' merkle root, which means if a malicious user could get access to it it could set the pending rewards to be claimed to an arbitrary tree that benefits it. Extreme caution must be taken in the Substrate side so only validated messages are sent to the Ethereum side, as to not allow any users to impersonate being this agent. ### TODO: Ideally, we would use the `RewardsCoordinator` contract from the EigenLayer core to distribute the rewards, but currently that adds a huge overhead for Operators since they'd have to wait for EigenLayer's SideCar to snapshot state and update the distribution root (which happens once a day), generate a proof that they belong to the tree of that distribution root, store it while waiting for the `activationDelay` (currently a week) to pass, and just then they would be able to claim their earned rewards. --------- Co-authored-by: Facundo Farall <37149322+ffarall@users.noreply.github.com>
2025-03-31 19:54:23 +00:00
bytes32[] public validProof;
feat: :sparkles: make `RewardsRegistry` keep a history of roots and claim status (#106) # Description This PR implements a comprehensive overhaul of the `RewardsRegistry` contract to maintain complete history of reward merkle roots while providing index-based claim tracking for operators. The new architecture enables operators to claim rewards from any historical merkle root instead of only the latest one. To do so, it: - Adds the `merkleRootHistory` storage array to the contract, in which we keep all rewards roots that ever came from the DataHaven side. - Adds the `operatorClaimedByIndex` storage map to the contract, in which we keep track, for each validator and root index, if it has claimed it or not. - This works even for new validators, since theoretically with this system you could argue they could claim older roots that they were not a part of which would be catastrophic, but they could never draft a correct proof for those to claim them. - Keeps some of the interface from before the overhaul, to have quick access to the latest rewards merkle root through `getLatestMerkleRoot()` and to claim rewards for it with `claimRewards()`. This is because the expected behaviour is for validators to claim their rewards every era. - Adds a way to batch claim rewards with `claimRewardsBatch()`. This function allows a validator to claim rewards for multiple root indices in one call by providing multiple proofs, useful if the validator has fallen behind claims and has to catch up, although special care will have to be taken by it to avoid reaching the gas limit of a transaction. ## Storage Efficiency Analysis One might think this solution is not as storage-efficient as other solutions that we can think of (I even had two other alternatives in mind as well), but a simple back-of-the-envelope calculation gives us peace of mind that the impact of this solution on the overal state size of the chain is negligible: ### Assumptions (Worst Case Scenario): - 1,000 validators (actual estimate for DataHaven: ~50/100 validators) - 6-hour eras (most-likely scenario, following what Polkadot does: ~24-hour eras) - Which means 4 merkle root updates per day ### Annual Storage Requirements: - Merkle Root History: **46,720 bytes/year** - 4 roots/day × 32 bytes/root × 365 days/year = 46,720 bytes/year - Operator Claim Tracking: **~1.46 MB/year** - 1,000 operators × 1 boolean/(operator * root index) × 1 byte/boolean × 4 root indices/day × 365 days/year = 1,460,000 bytes/year - **Total: ~1.5 MB/year** This represents negligible storage overhead compared to the significant operational benefits gained. ## TODO Since we want to allow the operators/validators to only have to interact with the AVS contract (that's why the `claimRewards` functions have the `onlyAVS` modifier), we still have to: - [x] Add the required functions to the AVS to allow operators to claim their rewards. - [x] Adds comprehensive unit tests for them. --------- Co-authored-by: Steve Degosserie <723552+stiiifff@users.noreply.github.com> Co-authored-by: Ahmad Kaouk <56095276+ahmadkaouk@users.noreply.github.com> Co-authored-by: Ahmad Kaouk <ahmadkaouk.93@gmail.com>
2025-07-10 06:47:39 +00:00
bytes32[] public secondValidProof;
bytes32[] public thirdValidProof;
feat: ✨ initial rewards registry (#17) This PR adds an initial implementation for a rewards registry, which will be the contract in charge of allowing DataHaven validators to claim the rewards they earned for being validators in the previous epoch. The logic behind it is as follows: - Whenever an epoch finishes, the corresponding BEEFY block gets relayed to Ethereum through Snowbridge. This BEEFY block contains, in its `extra` field, the merkle root of the tree that contains as leafs all the message commitments of the messages of corresponding block, one of which is the rewards distribution message. - The rewards distribution message commitment is the root of the merkle tree where each leaf is a tuple of the operator ID and the obtained era points in the finished epoch. In this case, the operator ID is the corresponding validator's Ethereum address. - When the rewards distribution message is received, Snowbridge validates it using the aforementioned BEEFY block and then dispatches it. The dispatch invokes the `callContract` function of the `RewardsAgent` agent, with the corresponding parameters so that this agent calls the `updateRewardsMerkleRoot` function of the `RewardsRegistry` contract with the new rewards distribution message commitment. - After this root is updated, any validator/operator can submit a proof that it is in a leaf of the merkle tree that produced that root, which means it has pending rewards to claim, through the `ServiceManagerBase`'s `claimOperatorRewards` function. - Each operator set of the AVS can have an assigned `RewardsRegistry` contract. Operator sets that do not have an assigned `RewardsRegistry` contract won't be able to received rewards. This PR also adds two separate unit-test suites: one for the added functionality to the `ServiceManagerBase` contract and one specific to the new `RewardsRegistry` contract. > [!CAUTION] The `RewardsAgent` agent is the only one allowed to update the rewards' merkle root, which means if a malicious user could get access to it it could set the pending rewards to be claimed to an arbitrary tree that benefits it. Extreme caution must be taken in the Substrate side so only validated messages are sent to the Ethereum side, as to not allow any users to impersonate being this agent. ### TODO: Ideally, we would use the `RewardsCoordinator` contract from the EigenLayer core to distribute the rewards, but currently that adds a huge overhead for Operators since they'd have to wait for EigenLayer's SideCar to snapshot state and update the distribution root (which happens once a day), generate a proof that they belong to the tree of that distribution root, store it while waiting for the `activationDelay` (currently a week) to pass, and just then they would be able to claim their earned rewards. --------- Co-authored-by: Facundo Farall <37149322+ffarall@users.noreply.github.com>
2025-03-31 19:54:23 +00:00
// Events
event RewardsRegistrySet(uint32 indexed operatorSetId, address indexed rewardsRegistry);
feat: :sparkles: make `RewardsRegistry` keep a history of roots and claim status (#106) # Description This PR implements a comprehensive overhaul of the `RewardsRegistry` contract to maintain complete history of reward merkle roots while providing index-based claim tracking for operators. The new architecture enables operators to claim rewards from any historical merkle root instead of only the latest one. To do so, it: - Adds the `merkleRootHistory` storage array to the contract, in which we keep all rewards roots that ever came from the DataHaven side. - Adds the `operatorClaimedByIndex` storage map to the contract, in which we keep track, for each validator and root index, if it has claimed it or not. - This works even for new validators, since theoretically with this system you could argue they could claim older roots that they were not a part of which would be catastrophic, but they could never draft a correct proof for those to claim them. - Keeps some of the interface from before the overhaul, to have quick access to the latest rewards merkle root through `getLatestMerkleRoot()` and to claim rewards for it with `claimRewards()`. This is because the expected behaviour is for validators to claim their rewards every era. - Adds a way to batch claim rewards with `claimRewardsBatch()`. This function allows a validator to claim rewards for multiple root indices in one call by providing multiple proofs, useful if the validator has fallen behind claims and has to catch up, although special care will have to be taken by it to avoid reaching the gas limit of a transaction. ## Storage Efficiency Analysis One might think this solution is not as storage-efficient as other solutions that we can think of (I even had two other alternatives in mind as well), but a simple back-of-the-envelope calculation gives us peace of mind that the impact of this solution on the overal state size of the chain is negligible: ### Assumptions (Worst Case Scenario): - 1,000 validators (actual estimate for DataHaven: ~50/100 validators) - 6-hour eras (most-likely scenario, following what Polkadot does: ~24-hour eras) - Which means 4 merkle root updates per day ### Annual Storage Requirements: - Merkle Root History: **46,720 bytes/year** - 4 roots/day × 32 bytes/root × 365 days/year = 46,720 bytes/year - Operator Claim Tracking: **~1.46 MB/year** - 1,000 operators × 1 boolean/(operator * root index) × 1 byte/boolean × 4 root indices/day × 365 days/year = 1,460,000 bytes/year - **Total: ~1.5 MB/year** This represents negligible storage overhead compared to the significant operational benefits gained. ## TODO Since we want to allow the operators/validators to only have to interact with the AVS contract (that's why the `claimRewards` functions have the `onlyAVS` modifier), we still have to: - [x] Add the required functions to the AVS to allow operators to claim their rewards. - [x] Adds comprehensive unit tests for them. --------- Co-authored-by: Steve Degosserie <723552+stiiifff@users.noreply.github.com> Co-authored-by: Ahmad Kaouk <56095276+ahmadkaouk@users.noreply.github.com> Co-authored-by: Ahmad Kaouk <ahmadkaouk.93@gmail.com>
2025-07-10 06:47:39 +00:00
event RewardsClaimedForIndex(
address indexed operatorAddress,
uint256 indexed rootIndex,
uint256 points,
uint256 rewardsAmount
);
event RewardsBatchClaimedForIndices(
address indexed operatorAddress,
uint256[] rootIndices,
uint256[] points,
uint256 totalRewardsAmount
);
feat: ✨ initial rewards registry (#17) This PR adds an initial implementation for a rewards registry, which will be the contract in charge of allowing DataHaven validators to claim the rewards they earned for being validators in the previous epoch. The logic behind it is as follows: - Whenever an epoch finishes, the corresponding BEEFY block gets relayed to Ethereum through Snowbridge. This BEEFY block contains, in its `extra` field, the merkle root of the tree that contains as leafs all the message commitments of the messages of corresponding block, one of which is the rewards distribution message. - The rewards distribution message commitment is the root of the merkle tree where each leaf is a tuple of the operator ID and the obtained era points in the finished epoch. In this case, the operator ID is the corresponding validator's Ethereum address. - When the rewards distribution message is received, Snowbridge validates it using the aforementioned BEEFY block and then dispatches it. The dispatch invokes the `callContract` function of the `RewardsAgent` agent, with the corresponding parameters so that this agent calls the `updateRewardsMerkleRoot` function of the `RewardsRegistry` contract with the new rewards distribution message commitment. - After this root is updated, any validator/operator can submit a proof that it is in a leaf of the merkle tree that produced that root, which means it has pending rewards to claim, through the `ServiceManagerBase`'s `claimOperatorRewards` function. - Each operator set of the AVS can have an assigned `RewardsRegistry` contract. Operator sets that do not have an assigned `RewardsRegistry` contract won't be able to received rewards. This PR also adds two separate unit-test suites: one for the added functionality to the `ServiceManagerBase` contract and one specific to the new `RewardsRegistry` contract. > [!CAUTION] The `RewardsAgent` agent is the only one allowed to update the rewards' merkle root, which means if a malicious user could get access to it it could set the pending rewards to be claimed to an arbitrary tree that benefits it. Extreme caution must be taken in the Substrate side so only validated messages are sent to the Ethereum side, as to not allow any users to impersonate being this agent. ### TODO: Ideally, we would use the `RewardsCoordinator` contract from the EigenLayer core to distribute the rewards, but currently that adds a huge overhead for Operators since they'd have to wait for EigenLayer's SideCar to snapshot state and update the distribution root (which happens once a day), generate a proof that they belong to the tree of that distribution root, store it while waiting for the `activationDelay` (currently a week) to pass, and just then they would be able to claim their earned rewards. --------- Co-authored-by: Facundo Farall <37149322+ffarall@users.noreply.github.com>
2025-03-31 19:54:23 +00:00
function setUp() public {
_deployMockEigenLayerAndAVS();
// Set up test addresses
operatorAddress = address(0xABCD);
nonOperatorAddress = address(0x5678);
// Configure test data
operatorSetId = 1;
operatorPoints = 100;
feat: :sparkles: make `RewardsRegistry` keep a history of roots and claim status (#106) # Description This PR implements a comprehensive overhaul of the `RewardsRegistry` contract to maintain complete history of reward merkle roots while providing index-based claim tracking for operators. The new architecture enables operators to claim rewards from any historical merkle root instead of only the latest one. To do so, it: - Adds the `merkleRootHistory` storage array to the contract, in which we keep all rewards roots that ever came from the DataHaven side. - Adds the `operatorClaimedByIndex` storage map to the contract, in which we keep track, for each validator and root index, if it has claimed it or not. - This works even for new validators, since theoretically with this system you could argue they could claim older roots that they were not a part of which would be catastrophic, but they could never draft a correct proof for those to claim them. - Keeps some of the interface from before the overhaul, to have quick access to the latest rewards merkle root through `getLatestMerkleRoot()` and to claim rewards for it with `claimRewards()`. This is because the expected behaviour is for validators to claim their rewards every era. - Adds a way to batch claim rewards with `claimRewardsBatch()`. This function allows a validator to claim rewards for multiple root indices in one call by providing multiple proofs, useful if the validator has fallen behind claims and has to catch up, although special care will have to be taken by it to avoid reaching the gas limit of a transaction. ## Storage Efficiency Analysis One might think this solution is not as storage-efficient as other solutions that we can think of (I even had two other alternatives in mind as well), but a simple back-of-the-envelope calculation gives us peace of mind that the impact of this solution on the overal state size of the chain is negligible: ### Assumptions (Worst Case Scenario): - 1,000 validators (actual estimate for DataHaven: ~50/100 validators) - 6-hour eras (most-likely scenario, following what Polkadot does: ~24-hour eras) - Which means 4 merkle root updates per day ### Annual Storage Requirements: - Merkle Root History: **46,720 bytes/year** - 4 roots/day × 32 bytes/root × 365 days/year = 46,720 bytes/year - Operator Claim Tracking: **~1.46 MB/year** - 1,000 operators × 1 boolean/(operator * root index) × 1 byte/boolean × 4 root indices/day × 365 days/year = 1,460,000 bytes/year - **Total: ~1.5 MB/year** This represents negligible storage overhead compared to the significant operational benefits gained. ## TODO Since we want to allow the operators/validators to only have to interact with the AVS contract (that's why the `claimRewards` functions have the `onlyAVS` modifier), we still have to: - [x] Add the required functions to the AVS to allow operators to claim their rewards. - [x] Adds comprehensive unit tests for them. --------- Co-authored-by: Steve Degosserie <723552+stiiifff@users.noreply.github.com> Co-authored-by: Ahmad Kaouk <56095276+ahmadkaouk@users.noreply.github.com> Co-authored-by: Ahmad Kaouk <ahmadkaouk.93@gmail.com>
2025-07-10 06:47:39 +00:00
secondOperatorPoints = 200;
thirdOperatorPoints = 150;
leafIndex = 0; // Position of our leaf in the tree
numberOfLeaves = 2; // Simple tree with 2 leaves
feat: ✨ initial rewards registry (#17) This PR adds an initial implementation for a rewards registry, which will be the contract in charge of allowing DataHaven validators to claim the rewards they earned for being validators in the previous epoch. The logic behind it is as follows: - Whenever an epoch finishes, the corresponding BEEFY block gets relayed to Ethereum through Snowbridge. This BEEFY block contains, in its `extra` field, the merkle root of the tree that contains as leafs all the message commitments of the messages of corresponding block, one of which is the rewards distribution message. - The rewards distribution message commitment is the root of the merkle tree where each leaf is a tuple of the operator ID and the obtained era points in the finished epoch. In this case, the operator ID is the corresponding validator's Ethereum address. - When the rewards distribution message is received, Snowbridge validates it using the aforementioned BEEFY block and then dispatches it. The dispatch invokes the `callContract` function of the `RewardsAgent` agent, with the corresponding parameters so that this agent calls the `updateRewardsMerkleRoot` function of the `RewardsRegistry` contract with the new rewards distribution message commitment. - After this root is updated, any validator/operator can submit a proof that it is in a leaf of the merkle tree that produced that root, which means it has pending rewards to claim, through the `ServiceManagerBase`'s `claimOperatorRewards` function. - Each operator set of the AVS can have an assigned `RewardsRegistry` contract. Operator sets that do not have an assigned `RewardsRegistry` contract won't be able to received rewards. This PR also adds two separate unit-test suites: one for the added functionality to the `ServiceManagerBase` contract and one specific to the new `RewardsRegistry` contract. > [!CAUTION] The `RewardsAgent` agent is the only one allowed to update the rewards' merkle root, which means if a malicious user could get access to it it could set the pending rewards to be claimed to an arbitrary tree that benefits it. Extreme caution must be taken in the Substrate side so only validated messages are sent to the Ethereum side, as to not allow any users to impersonate being this agent. ### TODO: Ideally, we would use the `RewardsCoordinator` contract from the EigenLayer core to distribute the rewards, but currently that adds a huge overhead for Operators since they'd have to wait for EigenLayer's SideCar to snapshot state and update the distribution root (which happens once a day), generate a proof that they belong to the tree of that distribution root, store it while waiting for the `activationDelay` (currently a week) to pass, and just then they would be able to claim their earned rewards. --------- Co-authored-by: Facundo Farall <37149322+ffarall@users.noreply.github.com>
2025-03-31 19:54:23 +00:00
feat: :sparkles: make `RewardsRegistry` keep a history of roots and claim status (#106) # Description This PR implements a comprehensive overhaul of the `RewardsRegistry` contract to maintain complete history of reward merkle roots while providing index-based claim tracking for operators. The new architecture enables operators to claim rewards from any historical merkle root instead of only the latest one. To do so, it: - Adds the `merkleRootHistory` storage array to the contract, in which we keep all rewards roots that ever came from the DataHaven side. - Adds the `operatorClaimedByIndex` storage map to the contract, in which we keep track, for each validator and root index, if it has claimed it or not. - This works even for new validators, since theoretically with this system you could argue they could claim older roots that they were not a part of which would be catastrophic, but they could never draft a correct proof for those to claim them. - Keeps some of the interface from before the overhaul, to have quick access to the latest rewards merkle root through `getLatestMerkleRoot()` and to claim rewards for it with `claimRewards()`. This is because the expected behaviour is for validators to claim their rewards every era. - Adds a way to batch claim rewards with `claimRewardsBatch()`. This function allows a validator to claim rewards for multiple root indices in one call by providing multiple proofs, useful if the validator has fallen behind claims and has to catch up, although special care will have to be taken by it to avoid reaching the gas limit of a transaction. ## Storage Efficiency Analysis One might think this solution is not as storage-efficient as other solutions that we can think of (I even had two other alternatives in mind as well), but a simple back-of-the-envelope calculation gives us peace of mind that the impact of this solution on the overal state size of the chain is negligible: ### Assumptions (Worst Case Scenario): - 1,000 validators (actual estimate for DataHaven: ~50/100 validators) - 6-hour eras (most-likely scenario, following what Polkadot does: ~24-hour eras) - Which means 4 merkle root updates per day ### Annual Storage Requirements: - Merkle Root History: **46,720 bytes/year** - 4 roots/day × 32 bytes/root × 365 days/year = 46,720 bytes/year - Operator Claim Tracking: **~1.46 MB/year** - 1,000 operators × 1 boolean/(operator * root index) × 1 byte/boolean × 4 root indices/day × 365 days/year = 1,460,000 bytes/year - **Total: ~1.5 MB/year** This represents negligible storage overhead compared to the significant operational benefits gained. ## TODO Since we want to allow the operators/validators to only have to interact with the AVS contract (that's why the `claimRewards` functions have the `onlyAVS` modifier), we still have to: - [x] Add the required functions to the AVS to allow operators to claim their rewards. - [x] Adds comprehensive unit tests for them. --------- Co-authored-by: Steve Degosserie <723552+stiiifff@users.noreply.github.com> Co-authored-by: Ahmad Kaouk <56095276+ahmadkaouk@users.noreply.github.com> Co-authored-by: Ahmad Kaouk <ahmadkaouk.93@gmail.com>
2025-07-10 06:47:39 +00:00
// Create multiple merkle trees for comprehensive batch testing
_createFirstMerkleTree();
_createSecondMerkleTree();
_createThirdMerkleTree();
feat: ✨ initial rewards registry (#17) This PR adds an initial implementation for a rewards registry, which will be the contract in charge of allowing DataHaven validators to claim the rewards they earned for being validators in the previous epoch. The logic behind it is as follows: - Whenever an epoch finishes, the corresponding BEEFY block gets relayed to Ethereum through Snowbridge. This BEEFY block contains, in its `extra` field, the merkle root of the tree that contains as leafs all the message commitments of the messages of corresponding block, one of which is the rewards distribution message. - The rewards distribution message commitment is the root of the merkle tree where each leaf is a tuple of the operator ID and the obtained era points in the finished epoch. In this case, the operator ID is the corresponding validator's Ethereum address. - When the rewards distribution message is received, Snowbridge validates it using the aforementioned BEEFY block and then dispatches it. The dispatch invokes the `callContract` function of the `RewardsAgent` agent, with the corresponding parameters so that this agent calls the `updateRewardsMerkleRoot` function of the `RewardsRegistry` contract with the new rewards distribution message commitment. - After this root is updated, any validator/operator can submit a proof that it is in a leaf of the merkle tree that produced that root, which means it has pending rewards to claim, through the `ServiceManagerBase`'s `claimOperatorRewards` function. - Each operator set of the AVS can have an assigned `RewardsRegistry` contract. Operator sets that do not have an assigned `RewardsRegistry` contract won't be able to received rewards. This PR also adds two separate unit-test suites: one for the added functionality to the `ServiceManagerBase` contract and one specific to the new `RewardsRegistry` contract. > [!CAUTION] The `RewardsAgent` agent is the only one allowed to update the rewards' merkle root, which means if a malicious user could get access to it it could set the pending rewards to be claimed to an arbitrary tree that benefits it. Extreme caution must be taken in the Substrate side so only validated messages are sent to the Ethereum side, as to not allow any users to impersonate being this agent. ### TODO: Ideally, we would use the `RewardsCoordinator` contract from the EigenLayer core to distribute the rewards, but currently that adds a huge overhead for Operators since they'd have to wait for EigenLayer's SideCar to snapshot state and update the distribution root (which happens once a day), generate a proof that they belong to the tree of that distribution root, store it while waiting for the `activationDelay` (currently a week) to pass, and just then they would be able to claim their earned rewards. --------- Co-authored-by: Facundo Farall <37149322+ffarall@users.noreply.github.com>
2025-03-31 19:54:23 +00:00
// Set up the rewards registry for the operator set
vm.prank(avsOwner);
serviceManager.setRewardsRegistry(operatorSetId, IRewardsRegistry(address(rewardsRegistry)));
feat: :sparkles: make `RewardsRegistry` keep a history of roots and claim status (#106) # Description This PR implements a comprehensive overhaul of the `RewardsRegistry` contract to maintain complete history of reward merkle roots while providing index-based claim tracking for operators. The new architecture enables operators to claim rewards from any historical merkle root instead of only the latest one. To do so, it: - Adds the `merkleRootHistory` storage array to the contract, in which we keep all rewards roots that ever came from the DataHaven side. - Adds the `operatorClaimedByIndex` storage map to the contract, in which we keep track, for each validator and root index, if it has claimed it or not. - This works even for new validators, since theoretically with this system you could argue they could claim older roots that they were not a part of which would be catastrophic, but they could never draft a correct proof for those to claim them. - Keeps some of the interface from before the overhaul, to have quick access to the latest rewards merkle root through `getLatestMerkleRoot()` and to claim rewards for it with `claimRewards()`. This is because the expected behaviour is for validators to claim their rewards every era. - Adds a way to batch claim rewards with `claimRewardsBatch()`. This function allows a validator to claim rewards for multiple root indices in one call by providing multiple proofs, useful if the validator has fallen behind claims and has to catch up, although special care will have to be taken by it to avoid reaching the gas limit of a transaction. ## Storage Efficiency Analysis One might think this solution is not as storage-efficient as other solutions that we can think of (I even had two other alternatives in mind as well), but a simple back-of-the-envelope calculation gives us peace of mind that the impact of this solution on the overal state size of the chain is negligible: ### Assumptions (Worst Case Scenario): - 1,000 validators (actual estimate for DataHaven: ~50/100 validators) - 6-hour eras (most-likely scenario, following what Polkadot does: ~24-hour eras) - Which means 4 merkle root updates per day ### Annual Storage Requirements: - Merkle Root History: **46,720 bytes/year** - 4 roots/day × 32 bytes/root × 365 days/year = 46,720 bytes/year - Operator Claim Tracking: **~1.46 MB/year** - 1,000 operators × 1 boolean/(operator * root index) × 1 byte/boolean × 4 root indices/day × 365 days/year = 1,460,000 bytes/year - **Total: ~1.5 MB/year** This represents negligible storage overhead compared to the significant operational benefits gained. ## TODO Since we want to allow the operators/validators to only have to interact with the AVS contract (that's why the `claimRewards` functions have the `onlyAVS` modifier), we still have to: - [x] Add the required functions to the AVS to allow operators to claim their rewards. - [x] Adds comprehensive unit tests for them. --------- Co-authored-by: Steve Degosserie <723552+stiiifff@users.noreply.github.com> Co-authored-by: Ahmad Kaouk <56095276+ahmadkaouk@users.noreply.github.com> Co-authored-by: Ahmad Kaouk <ahmadkaouk.93@gmail.com>
2025-07-10 06:47:39 +00:00
// Set all three merkle roots to create a history
vm.prank(mockRewardsAgent);
feat: ✨ initial rewards registry (#17) This PR adds an initial implementation for a rewards registry, which will be the contract in charge of allowing DataHaven validators to claim the rewards they earned for being validators in the previous epoch. The logic behind it is as follows: - Whenever an epoch finishes, the corresponding BEEFY block gets relayed to Ethereum through Snowbridge. This BEEFY block contains, in its `extra` field, the merkle root of the tree that contains as leafs all the message commitments of the messages of corresponding block, one of which is the rewards distribution message. - The rewards distribution message commitment is the root of the merkle tree where each leaf is a tuple of the operator ID and the obtained era points in the finished epoch. In this case, the operator ID is the corresponding validator's Ethereum address. - When the rewards distribution message is received, Snowbridge validates it using the aforementioned BEEFY block and then dispatches it. The dispatch invokes the `callContract` function of the `RewardsAgent` agent, with the corresponding parameters so that this agent calls the `updateRewardsMerkleRoot` function of the `RewardsRegistry` contract with the new rewards distribution message commitment. - After this root is updated, any validator/operator can submit a proof that it is in a leaf of the merkle tree that produced that root, which means it has pending rewards to claim, through the `ServiceManagerBase`'s `claimOperatorRewards` function. - Each operator set of the AVS can have an assigned `RewardsRegistry` contract. Operator sets that do not have an assigned `RewardsRegistry` contract won't be able to received rewards. This PR also adds two separate unit-test suites: one for the added functionality to the `ServiceManagerBase` contract and one specific to the new `RewardsRegistry` contract. > [!CAUTION] The `RewardsAgent` agent is the only one allowed to update the rewards' merkle root, which means if a malicious user could get access to it it could set the pending rewards to be claimed to an arbitrary tree that benefits it. Extreme caution must be taken in the Substrate side so only validated messages are sent to the Ethereum side, as to not allow any users to impersonate being this agent. ### TODO: Ideally, we would use the `RewardsCoordinator` contract from the EigenLayer core to distribute the rewards, but currently that adds a huge overhead for Operators since they'd have to wait for EigenLayer's SideCar to snapshot state and update the distribution root (which happens once a day), generate a proof that they belong to the tree of that distribution root, store it while waiting for the `activationDelay` (currently a week) to pass, and just then they would be able to claim their earned rewards. --------- Co-authored-by: Facundo Farall <37149322+ffarall@users.noreply.github.com>
2025-03-31 19:54:23 +00:00
rewardsRegistry.updateRewardsMerkleRoot(merkleRoot);
feat: :sparkles: make `RewardsRegistry` keep a history of roots and claim status (#106) # Description This PR implements a comprehensive overhaul of the `RewardsRegistry` contract to maintain complete history of reward merkle roots while providing index-based claim tracking for operators. The new architecture enables operators to claim rewards from any historical merkle root instead of only the latest one. To do so, it: - Adds the `merkleRootHistory` storage array to the contract, in which we keep all rewards roots that ever came from the DataHaven side. - Adds the `operatorClaimedByIndex` storage map to the contract, in which we keep track, for each validator and root index, if it has claimed it or not. - This works even for new validators, since theoretically with this system you could argue they could claim older roots that they were not a part of which would be catastrophic, but they could never draft a correct proof for those to claim them. - Keeps some of the interface from before the overhaul, to have quick access to the latest rewards merkle root through `getLatestMerkleRoot()` and to claim rewards for it with `claimRewards()`. This is because the expected behaviour is for validators to claim their rewards every era. - Adds a way to batch claim rewards with `claimRewardsBatch()`. This function allows a validator to claim rewards for multiple root indices in one call by providing multiple proofs, useful if the validator has fallen behind claims and has to catch up, although special care will have to be taken by it to avoid reaching the gas limit of a transaction. ## Storage Efficiency Analysis One might think this solution is not as storage-efficient as other solutions that we can think of (I even had two other alternatives in mind as well), but a simple back-of-the-envelope calculation gives us peace of mind that the impact of this solution on the overal state size of the chain is negligible: ### Assumptions (Worst Case Scenario): - 1,000 validators (actual estimate for DataHaven: ~50/100 validators) - 6-hour eras (most-likely scenario, following what Polkadot does: ~24-hour eras) - Which means 4 merkle root updates per day ### Annual Storage Requirements: - Merkle Root History: **46,720 bytes/year** - 4 roots/day × 32 bytes/root × 365 days/year = 46,720 bytes/year - Operator Claim Tracking: **~1.46 MB/year** - 1,000 operators × 1 boolean/(operator * root index) × 1 byte/boolean × 4 root indices/day × 365 days/year = 1,460,000 bytes/year - **Total: ~1.5 MB/year** This represents negligible storage overhead compared to the significant operational benefits gained. ## TODO Since we want to allow the operators/validators to only have to interact with the AVS contract (that's why the `claimRewards` functions have the `onlyAVS` modifier), we still have to: - [x] Add the required functions to the AVS to allow operators to claim their rewards. - [x] Adds comprehensive unit tests for them. --------- Co-authored-by: Steve Degosserie <723552+stiiifff@users.noreply.github.com> Co-authored-by: Ahmad Kaouk <56095276+ahmadkaouk@users.noreply.github.com> Co-authored-by: Ahmad Kaouk <ahmadkaouk.93@gmail.com>
2025-07-10 06:47:39 +00:00
vm.prank(mockRewardsAgent);
rewardsRegistry.updateRewardsMerkleRoot(secondMerkleRoot);
vm.prank(mockRewardsAgent);
rewardsRegistry.updateRewardsMerkleRoot(thirdMerkleRoot);
feat: ✨ initial rewards registry (#17) This PR adds an initial implementation for a rewards registry, which will be the contract in charge of allowing DataHaven validators to claim the rewards they earned for being validators in the previous epoch. The logic behind it is as follows: - Whenever an epoch finishes, the corresponding BEEFY block gets relayed to Ethereum through Snowbridge. This BEEFY block contains, in its `extra` field, the merkle root of the tree that contains as leafs all the message commitments of the messages of corresponding block, one of which is the rewards distribution message. - The rewards distribution message commitment is the root of the merkle tree where each leaf is a tuple of the operator ID and the obtained era points in the finished epoch. In this case, the operator ID is the corresponding validator's Ethereum address. - When the rewards distribution message is received, Snowbridge validates it using the aforementioned BEEFY block and then dispatches it. The dispatch invokes the `callContract` function of the `RewardsAgent` agent, with the corresponding parameters so that this agent calls the `updateRewardsMerkleRoot` function of the `RewardsRegistry` contract with the new rewards distribution message commitment. - After this root is updated, any validator/operator can submit a proof that it is in a leaf of the merkle tree that produced that root, which means it has pending rewards to claim, through the `ServiceManagerBase`'s `claimOperatorRewards` function. - Each operator set of the AVS can have an assigned `RewardsRegistry` contract. Operator sets that do not have an assigned `RewardsRegistry` contract won't be able to received rewards. This PR also adds two separate unit-test suites: one for the added functionality to the `ServiceManagerBase` contract and one specific to the new `RewardsRegistry` contract. > [!CAUTION] The `RewardsAgent` agent is the only one allowed to update the rewards' merkle root, which means if a malicious user could get access to it it could set the pending rewards to be claimed to an arbitrary tree that benefits it. Extreme caution must be taken in the Substrate side so only validated messages are sent to the Ethereum side, as to not allow any users to impersonate being this agent. ### TODO: Ideally, we would use the `RewardsCoordinator` contract from the EigenLayer core to distribute the rewards, but currently that adds a huge overhead for Operators since they'd have to wait for EigenLayer's SideCar to snapshot state and update the distribution root (which happens once a day), generate a proof that they belong to the tree of that distribution root, store it while waiting for the `activationDelay` (currently a week) to pass, and just then they would be able to claim their earned rewards. --------- Co-authored-by: Facundo Farall <37149322+ffarall@users.noreply.github.com>
2025-03-31 19:54:23 +00:00
// Add funds to the registry for rewards
vm.deal(address(rewardsRegistry), 1000 ether);
}
feat: :sparkles: make `RewardsRegistry` keep a history of roots and claim status (#106) # Description This PR implements a comprehensive overhaul of the `RewardsRegistry` contract to maintain complete history of reward merkle roots while providing index-based claim tracking for operators. The new architecture enables operators to claim rewards from any historical merkle root instead of only the latest one. To do so, it: - Adds the `merkleRootHistory` storage array to the contract, in which we keep all rewards roots that ever came from the DataHaven side. - Adds the `operatorClaimedByIndex` storage map to the contract, in which we keep track, for each validator and root index, if it has claimed it or not. - This works even for new validators, since theoretically with this system you could argue they could claim older roots that they were not a part of which would be catastrophic, but they could never draft a correct proof for those to claim them. - Keeps some of the interface from before the overhaul, to have quick access to the latest rewards merkle root through `getLatestMerkleRoot()` and to claim rewards for it with `claimRewards()`. This is because the expected behaviour is for validators to claim their rewards every era. - Adds a way to batch claim rewards with `claimRewardsBatch()`. This function allows a validator to claim rewards for multiple root indices in one call by providing multiple proofs, useful if the validator has fallen behind claims and has to catch up, although special care will have to be taken by it to avoid reaching the gas limit of a transaction. ## Storage Efficiency Analysis One might think this solution is not as storage-efficient as other solutions that we can think of (I even had two other alternatives in mind as well), but a simple back-of-the-envelope calculation gives us peace of mind that the impact of this solution on the overal state size of the chain is negligible: ### Assumptions (Worst Case Scenario): - 1,000 validators (actual estimate for DataHaven: ~50/100 validators) - 6-hour eras (most-likely scenario, following what Polkadot does: ~24-hour eras) - Which means 4 merkle root updates per day ### Annual Storage Requirements: - Merkle Root History: **46,720 bytes/year** - 4 roots/day × 32 bytes/root × 365 days/year = 46,720 bytes/year - Operator Claim Tracking: **~1.46 MB/year** - 1,000 operators × 1 boolean/(operator * root index) × 1 byte/boolean × 4 root indices/day × 365 days/year = 1,460,000 bytes/year - **Total: ~1.5 MB/year** This represents negligible storage overhead compared to the significant operational benefits gained. ## TODO Since we want to allow the operators/validators to only have to interact with the AVS contract (that's why the `claimRewards` functions have the `onlyAVS` modifier), we still have to: - [x] Add the required functions to the AVS to allow operators to claim their rewards. - [x] Adds comprehensive unit tests for them. --------- Co-authored-by: Steve Degosserie <723552+stiiifff@users.noreply.github.com> Co-authored-by: Ahmad Kaouk <56095276+ahmadkaouk@users.noreply.github.com> Co-authored-by: Ahmad Kaouk <ahmadkaouk.93@gmail.com>
2025-07-10 06:47:39 +00:00
function _createFirstMerkleTree() internal {
// Create first merkle tree with Substrate-compatible SCALE encoding
bytes memory preimage =
abi.encodePacked(operatorAddress, ScaleCodec.encodeU32(uint32(operatorPoints)));
bytes32 leaf = keccak256(preimage);
bytes memory siblingPreimage =
abi.encodePacked(address(0x1111), ScaleCodec.encodeU32(uint32(50)));
bytes32 siblingLeaf = keccak256(siblingPreimage);
// For Substrate positional merkle proof, we construct the root based on position
// Since leafIndex = 0, our leaf is on the left
merkleRoot = keccak256(abi.encodePacked(leaf, siblingLeaf));
feat: :sparkles: make `RewardsRegistry` keep a history of roots and claim status (#106) # Description This PR implements a comprehensive overhaul of the `RewardsRegistry` contract to maintain complete history of reward merkle roots while providing index-based claim tracking for operators. The new architecture enables operators to claim rewards from any historical merkle root instead of only the latest one. To do so, it: - Adds the `merkleRootHistory` storage array to the contract, in which we keep all rewards roots that ever came from the DataHaven side. - Adds the `operatorClaimedByIndex` storage map to the contract, in which we keep track, for each validator and root index, if it has claimed it or not. - This works even for new validators, since theoretically with this system you could argue they could claim older roots that they were not a part of which would be catastrophic, but they could never draft a correct proof for those to claim them. - Keeps some of the interface from before the overhaul, to have quick access to the latest rewards merkle root through `getLatestMerkleRoot()` and to claim rewards for it with `claimRewards()`. This is because the expected behaviour is for validators to claim their rewards every era. - Adds a way to batch claim rewards with `claimRewardsBatch()`. This function allows a validator to claim rewards for multiple root indices in one call by providing multiple proofs, useful if the validator has fallen behind claims and has to catch up, although special care will have to be taken by it to avoid reaching the gas limit of a transaction. ## Storage Efficiency Analysis One might think this solution is not as storage-efficient as other solutions that we can think of (I even had two other alternatives in mind as well), but a simple back-of-the-envelope calculation gives us peace of mind that the impact of this solution on the overal state size of the chain is negligible: ### Assumptions (Worst Case Scenario): - 1,000 validators (actual estimate for DataHaven: ~50/100 validators) - 6-hour eras (most-likely scenario, following what Polkadot does: ~24-hour eras) - Which means 4 merkle root updates per day ### Annual Storage Requirements: - Merkle Root History: **46,720 bytes/year** - 4 roots/day × 32 bytes/root × 365 days/year = 46,720 bytes/year - Operator Claim Tracking: **~1.46 MB/year** - 1,000 operators × 1 boolean/(operator * root index) × 1 byte/boolean × 4 root indices/day × 365 days/year = 1,460,000 bytes/year - **Total: ~1.5 MB/year** This represents negligible storage overhead compared to the significant operational benefits gained. ## TODO Since we want to allow the operators/validators to only have to interact with the AVS contract (that's why the `claimRewards` functions have the `onlyAVS` modifier), we still have to: - [x] Add the required functions to the AVS to allow operators to claim their rewards. - [x] Adds comprehensive unit tests for them. --------- Co-authored-by: Steve Degosserie <723552+stiiifff@users.noreply.github.com> Co-authored-by: Ahmad Kaouk <56095276+ahmadkaouk@users.noreply.github.com> Co-authored-by: Ahmad Kaouk <ahmadkaouk.93@gmail.com>
2025-07-10 06:47:39 +00:00
validProof = new bytes32[](1);
validProof[0] = siblingLeaf;
}
function _createSecondMerkleTree() internal {
// Create second merkle tree with different points using SCALE encoding
bytes memory preimage =
abi.encodePacked(operatorAddress, ScaleCodec.encodeU32(uint32(secondOperatorPoints)));
bytes32 leaf = keccak256(preimage);
bytes memory siblingPreimage =
abi.encodePacked(address(0x2222), ScaleCodec.encodeU32(uint32(75)));
bytes32 siblingLeaf = keccak256(siblingPreimage);
// Since leafIndex = 0, our leaf is on the left
secondMerkleRoot = keccak256(abi.encodePacked(leaf, siblingLeaf));
feat: :sparkles: make `RewardsRegistry` keep a history of roots and claim status (#106) # Description This PR implements a comprehensive overhaul of the `RewardsRegistry` contract to maintain complete history of reward merkle roots while providing index-based claim tracking for operators. The new architecture enables operators to claim rewards from any historical merkle root instead of only the latest one. To do so, it: - Adds the `merkleRootHistory` storage array to the contract, in which we keep all rewards roots that ever came from the DataHaven side. - Adds the `operatorClaimedByIndex` storage map to the contract, in which we keep track, for each validator and root index, if it has claimed it or not. - This works even for new validators, since theoretically with this system you could argue they could claim older roots that they were not a part of which would be catastrophic, but they could never draft a correct proof for those to claim them. - Keeps some of the interface from before the overhaul, to have quick access to the latest rewards merkle root through `getLatestMerkleRoot()` and to claim rewards for it with `claimRewards()`. This is because the expected behaviour is for validators to claim their rewards every era. - Adds a way to batch claim rewards with `claimRewardsBatch()`. This function allows a validator to claim rewards for multiple root indices in one call by providing multiple proofs, useful if the validator has fallen behind claims and has to catch up, although special care will have to be taken by it to avoid reaching the gas limit of a transaction. ## Storage Efficiency Analysis One might think this solution is not as storage-efficient as other solutions that we can think of (I even had two other alternatives in mind as well), but a simple back-of-the-envelope calculation gives us peace of mind that the impact of this solution on the overal state size of the chain is negligible: ### Assumptions (Worst Case Scenario): - 1,000 validators (actual estimate for DataHaven: ~50/100 validators) - 6-hour eras (most-likely scenario, following what Polkadot does: ~24-hour eras) - Which means 4 merkle root updates per day ### Annual Storage Requirements: - Merkle Root History: **46,720 bytes/year** - 4 roots/day × 32 bytes/root × 365 days/year = 46,720 bytes/year - Operator Claim Tracking: **~1.46 MB/year** - 1,000 operators × 1 boolean/(operator * root index) × 1 byte/boolean × 4 root indices/day × 365 days/year = 1,460,000 bytes/year - **Total: ~1.5 MB/year** This represents negligible storage overhead compared to the significant operational benefits gained. ## TODO Since we want to allow the operators/validators to only have to interact with the AVS contract (that's why the `claimRewards` functions have the `onlyAVS` modifier), we still have to: - [x] Add the required functions to the AVS to allow operators to claim their rewards. - [x] Adds comprehensive unit tests for them. --------- Co-authored-by: Steve Degosserie <723552+stiiifff@users.noreply.github.com> Co-authored-by: Ahmad Kaouk <56095276+ahmadkaouk@users.noreply.github.com> Co-authored-by: Ahmad Kaouk <ahmadkaouk.93@gmail.com>
2025-07-10 06:47:39 +00:00
secondValidProof = new bytes32[](1);
secondValidProof[0] = siblingLeaf;
}
function _createThirdMerkleTree() internal {
// Create third merkle tree with different points using SCALE encoding
bytes memory preimage =
abi.encodePacked(operatorAddress, ScaleCodec.encodeU32(uint32(thirdOperatorPoints)));
bytes32 leaf = keccak256(preimage);
bytes memory siblingPreimage =
abi.encodePacked(address(0x3333), ScaleCodec.encodeU32(uint32(60)));
bytes32 siblingLeaf = keccak256(siblingPreimage);
// Since leafIndex = 0, our leaf is on the left
thirdMerkleRoot = keccak256(abi.encodePacked(leaf, siblingLeaf));
feat: :sparkles: make `RewardsRegistry` keep a history of roots and claim status (#106) # Description This PR implements a comprehensive overhaul of the `RewardsRegistry` contract to maintain complete history of reward merkle roots while providing index-based claim tracking for operators. The new architecture enables operators to claim rewards from any historical merkle root instead of only the latest one. To do so, it: - Adds the `merkleRootHistory` storage array to the contract, in which we keep all rewards roots that ever came from the DataHaven side. - Adds the `operatorClaimedByIndex` storage map to the contract, in which we keep track, for each validator and root index, if it has claimed it or not. - This works even for new validators, since theoretically with this system you could argue they could claim older roots that they were not a part of which would be catastrophic, but they could never draft a correct proof for those to claim them. - Keeps some of the interface from before the overhaul, to have quick access to the latest rewards merkle root through `getLatestMerkleRoot()` and to claim rewards for it with `claimRewards()`. This is because the expected behaviour is for validators to claim their rewards every era. - Adds a way to batch claim rewards with `claimRewardsBatch()`. This function allows a validator to claim rewards for multiple root indices in one call by providing multiple proofs, useful if the validator has fallen behind claims and has to catch up, although special care will have to be taken by it to avoid reaching the gas limit of a transaction. ## Storage Efficiency Analysis One might think this solution is not as storage-efficient as other solutions that we can think of (I even had two other alternatives in mind as well), but a simple back-of-the-envelope calculation gives us peace of mind that the impact of this solution on the overal state size of the chain is negligible: ### Assumptions (Worst Case Scenario): - 1,000 validators (actual estimate for DataHaven: ~50/100 validators) - 6-hour eras (most-likely scenario, following what Polkadot does: ~24-hour eras) - Which means 4 merkle root updates per day ### Annual Storage Requirements: - Merkle Root History: **46,720 bytes/year** - 4 roots/day × 32 bytes/root × 365 days/year = 46,720 bytes/year - Operator Claim Tracking: **~1.46 MB/year** - 1,000 operators × 1 boolean/(operator * root index) × 1 byte/boolean × 4 root indices/day × 365 days/year = 1,460,000 bytes/year - **Total: ~1.5 MB/year** This represents negligible storage overhead compared to the significant operational benefits gained. ## TODO Since we want to allow the operators/validators to only have to interact with the AVS contract (that's why the `claimRewards` functions have the `onlyAVS` modifier), we still have to: - [x] Add the required functions to the AVS to allow operators to claim their rewards. - [x] Adds comprehensive unit tests for them. --------- Co-authored-by: Steve Degosserie <723552+stiiifff@users.noreply.github.com> Co-authored-by: Ahmad Kaouk <56095276+ahmadkaouk@users.noreply.github.com> Co-authored-by: Ahmad Kaouk <ahmadkaouk.93@gmail.com>
2025-07-10 06:47:39 +00:00
thirdValidProof = new bytes32[](1);
thirdValidProof[0] = siblingLeaf;
}
feat: ✨ initial rewards registry (#17) This PR adds an initial implementation for a rewards registry, which will be the contract in charge of allowing DataHaven validators to claim the rewards they earned for being validators in the previous epoch. The logic behind it is as follows: - Whenever an epoch finishes, the corresponding BEEFY block gets relayed to Ethereum through Snowbridge. This BEEFY block contains, in its `extra` field, the merkle root of the tree that contains as leafs all the message commitments of the messages of corresponding block, one of which is the rewards distribution message. - The rewards distribution message commitment is the root of the merkle tree where each leaf is a tuple of the operator ID and the obtained era points in the finished epoch. In this case, the operator ID is the corresponding validator's Ethereum address. - When the rewards distribution message is received, Snowbridge validates it using the aforementioned BEEFY block and then dispatches it. The dispatch invokes the `callContract` function of the `RewardsAgent` agent, with the corresponding parameters so that this agent calls the `updateRewardsMerkleRoot` function of the `RewardsRegistry` contract with the new rewards distribution message commitment. - After this root is updated, any validator/operator can submit a proof that it is in a leaf of the merkle tree that produced that root, which means it has pending rewards to claim, through the `ServiceManagerBase`'s `claimOperatorRewards` function. - Each operator set of the AVS can have an assigned `RewardsRegistry` contract. Operator sets that do not have an assigned `RewardsRegistry` contract won't be able to received rewards. This PR also adds two separate unit-test suites: one for the added functionality to the `ServiceManagerBase` contract and one specific to the new `RewardsRegistry` contract. > [!CAUTION] The `RewardsAgent` agent is the only one allowed to update the rewards' merkle root, which means if a malicious user could get access to it it could set the pending rewards to be claimed to an arbitrary tree that benefits it. Extreme caution must be taken in the Substrate side so only validated messages are sent to the Ethereum side, as to not allow any users to impersonate being this agent. ### TODO: Ideally, we would use the `RewardsCoordinator` contract from the EigenLayer core to distribute the rewards, but currently that adds a huge overhead for Operators since they'd have to wait for EigenLayer's SideCar to snapshot state and update the distribution root (which happens once a day), generate a proof that they belong to the tree of that distribution root, store it while waiting for the `activationDelay` (currently a week) to pass, and just then they would be able to claim their earned rewards. --------- Co-authored-by: Facundo Farall <37149322+ffarall@users.noreply.github.com>
2025-03-31 19:54:23 +00:00
function test_setRewardsRegistry() public {
uint32 newOperatorSetId = 2;
RewardsRegistry newRewardsRegistry =
new RewardsRegistry(address(serviceManager), mockRewardsAgent);
feat: ✨ initial rewards registry (#17) This PR adds an initial implementation for a rewards registry, which will be the contract in charge of allowing DataHaven validators to claim the rewards they earned for being validators in the previous epoch. The logic behind it is as follows: - Whenever an epoch finishes, the corresponding BEEFY block gets relayed to Ethereum through Snowbridge. This BEEFY block contains, in its `extra` field, the merkle root of the tree that contains as leafs all the message commitments of the messages of corresponding block, one of which is the rewards distribution message. - The rewards distribution message commitment is the root of the merkle tree where each leaf is a tuple of the operator ID and the obtained era points in the finished epoch. In this case, the operator ID is the corresponding validator's Ethereum address. - When the rewards distribution message is received, Snowbridge validates it using the aforementioned BEEFY block and then dispatches it. The dispatch invokes the `callContract` function of the `RewardsAgent` agent, with the corresponding parameters so that this agent calls the `updateRewardsMerkleRoot` function of the `RewardsRegistry` contract with the new rewards distribution message commitment. - After this root is updated, any validator/operator can submit a proof that it is in a leaf of the merkle tree that produced that root, which means it has pending rewards to claim, through the `ServiceManagerBase`'s `claimOperatorRewards` function. - Each operator set of the AVS can have an assigned `RewardsRegistry` contract. Operator sets that do not have an assigned `RewardsRegistry` contract won't be able to received rewards. This PR also adds two separate unit-test suites: one for the added functionality to the `ServiceManagerBase` contract and one specific to the new `RewardsRegistry` contract. > [!CAUTION] The `RewardsAgent` agent is the only one allowed to update the rewards' merkle root, which means if a malicious user could get access to it it could set the pending rewards to be claimed to an arbitrary tree that benefits it. Extreme caution must be taken in the Substrate side so only validated messages are sent to the Ethereum side, as to not allow any users to impersonate being this agent. ### TODO: Ideally, we would use the `RewardsCoordinator` contract from the EigenLayer core to distribute the rewards, but currently that adds a huge overhead for Operators since they'd have to wait for EigenLayer's SideCar to snapshot state and update the distribution root (which happens once a day), generate a proof that they belong to the tree of that distribution root, store it while waiting for the `activationDelay` (currently a week) to pass, and just then they would be able to claim their earned rewards. --------- Co-authored-by: Facundo Farall <37149322+ffarall@users.noreply.github.com>
2025-03-31 19:54:23 +00:00
vm.prank(avsOwner);
vm.expectEmit(true, true, true, true);
emit RewardsRegistrySet(newOperatorSetId, address(newRewardsRegistry));
serviceManager.setRewardsRegistry(
newOperatorSetId, IRewardsRegistry(address(newRewardsRegistry))
);
assertEq(
address(serviceManager.operatorSetToRewardsRegistry(newOperatorSetId)),
feat: ✨ initial rewards registry (#17) This PR adds an initial implementation for a rewards registry, which will be the contract in charge of allowing DataHaven validators to claim the rewards they earned for being validators in the previous epoch. The logic behind it is as follows: - Whenever an epoch finishes, the corresponding BEEFY block gets relayed to Ethereum through Snowbridge. This BEEFY block contains, in its `extra` field, the merkle root of the tree that contains as leafs all the message commitments of the messages of corresponding block, one of which is the rewards distribution message. - The rewards distribution message commitment is the root of the merkle tree where each leaf is a tuple of the operator ID and the obtained era points in the finished epoch. In this case, the operator ID is the corresponding validator's Ethereum address. - When the rewards distribution message is received, Snowbridge validates it using the aforementioned BEEFY block and then dispatches it. The dispatch invokes the `callContract` function of the `RewardsAgent` agent, with the corresponding parameters so that this agent calls the `updateRewardsMerkleRoot` function of the `RewardsRegistry` contract with the new rewards distribution message commitment. - After this root is updated, any validator/operator can submit a proof that it is in a leaf of the merkle tree that produced that root, which means it has pending rewards to claim, through the `ServiceManagerBase`'s `claimOperatorRewards` function. - Each operator set of the AVS can have an assigned `RewardsRegistry` contract. Operator sets that do not have an assigned `RewardsRegistry` contract won't be able to received rewards. This PR also adds two separate unit-test suites: one for the added functionality to the `ServiceManagerBase` contract and one specific to the new `RewardsRegistry` contract. > [!CAUTION] The `RewardsAgent` agent is the only one allowed to update the rewards' merkle root, which means if a malicious user could get access to it it could set the pending rewards to be claimed to an arbitrary tree that benefits it. Extreme caution must be taken in the Substrate side so only validated messages are sent to the Ethereum side, as to not allow any users to impersonate being this agent. ### TODO: Ideally, we would use the `RewardsCoordinator` contract from the EigenLayer core to distribute the rewards, but currently that adds a huge overhead for Operators since they'd have to wait for EigenLayer's SideCar to snapshot state and update the distribution root (which happens once a day), generate a proof that they belong to the tree of that distribution root, store it while waiting for the `activationDelay` (currently a week) to pass, and just then they would be able to claim their earned rewards. --------- Co-authored-by: Facundo Farall <37149322+ffarall@users.noreply.github.com>
2025-03-31 19:54:23 +00:00
address(newRewardsRegistry),
"Rewards registry should be set correctly"
);
}
function test_setRewardsRegistry_NotOwner() public {
uint32 newOperatorSetId = 2;
RewardsRegistry newRewardsRegistry =
new RewardsRegistry(address(serviceManager), mockRewardsAgent);
feat: ✨ initial rewards registry (#17) This PR adds an initial implementation for a rewards registry, which will be the contract in charge of allowing DataHaven validators to claim the rewards they earned for being validators in the previous epoch. The logic behind it is as follows: - Whenever an epoch finishes, the corresponding BEEFY block gets relayed to Ethereum through Snowbridge. This BEEFY block contains, in its `extra` field, the merkle root of the tree that contains as leafs all the message commitments of the messages of corresponding block, one of which is the rewards distribution message. - The rewards distribution message commitment is the root of the merkle tree where each leaf is a tuple of the operator ID and the obtained era points in the finished epoch. In this case, the operator ID is the corresponding validator's Ethereum address. - When the rewards distribution message is received, Snowbridge validates it using the aforementioned BEEFY block and then dispatches it. The dispatch invokes the `callContract` function of the `RewardsAgent` agent, with the corresponding parameters so that this agent calls the `updateRewardsMerkleRoot` function of the `RewardsRegistry` contract with the new rewards distribution message commitment. - After this root is updated, any validator/operator can submit a proof that it is in a leaf of the merkle tree that produced that root, which means it has pending rewards to claim, through the `ServiceManagerBase`'s `claimOperatorRewards` function. - Each operator set of the AVS can have an assigned `RewardsRegistry` contract. Operator sets that do not have an assigned `RewardsRegistry` contract won't be able to received rewards. This PR also adds two separate unit-test suites: one for the added functionality to the `ServiceManagerBase` contract and one specific to the new `RewardsRegistry` contract. > [!CAUTION] The `RewardsAgent` agent is the only one allowed to update the rewards' merkle root, which means if a malicious user could get access to it it could set the pending rewards to be claimed to an arbitrary tree that benefits it. Extreme caution must be taken in the Substrate side so only validated messages are sent to the Ethereum side, as to not allow any users to impersonate being this agent. ### TODO: Ideally, we would use the `RewardsCoordinator` contract from the EigenLayer core to distribute the rewards, but currently that adds a huge overhead for Operators since they'd have to wait for EigenLayer's SideCar to snapshot state and update the distribution root (which happens once a day), generate a proof that they belong to the tree of that distribution root, store it while waiting for the `activationDelay` (currently a week) to pass, and just then they would be able to claim their earned rewards. --------- Co-authored-by: Facundo Farall <37149322+ffarall@users.noreply.github.com>
2025-03-31 19:54:23 +00:00
vm.prank(nonOperatorAddress);
vm.expectRevert(bytes("Ownable: caller is not the owner"));
serviceManager.setRewardsRegistry(
newOperatorSetId, IRewardsRegistry(address(newRewardsRegistry))
);
}
feat: :sparkles: make `RewardsRegistry` keep a history of roots and claim status (#106) # Description This PR implements a comprehensive overhaul of the `RewardsRegistry` contract to maintain complete history of reward merkle roots while providing index-based claim tracking for operators. The new architecture enables operators to claim rewards from any historical merkle root instead of only the latest one. To do so, it: - Adds the `merkleRootHistory` storage array to the contract, in which we keep all rewards roots that ever came from the DataHaven side. - Adds the `operatorClaimedByIndex` storage map to the contract, in which we keep track, for each validator and root index, if it has claimed it or not. - This works even for new validators, since theoretically with this system you could argue they could claim older roots that they were not a part of which would be catastrophic, but they could never draft a correct proof for those to claim them. - Keeps some of the interface from before the overhaul, to have quick access to the latest rewards merkle root through `getLatestMerkleRoot()` and to claim rewards for it with `claimRewards()`. This is because the expected behaviour is for validators to claim their rewards every era. - Adds a way to batch claim rewards with `claimRewardsBatch()`. This function allows a validator to claim rewards for multiple root indices in one call by providing multiple proofs, useful if the validator has fallen behind claims and has to catch up, although special care will have to be taken by it to avoid reaching the gas limit of a transaction. ## Storage Efficiency Analysis One might think this solution is not as storage-efficient as other solutions that we can think of (I even had two other alternatives in mind as well), but a simple back-of-the-envelope calculation gives us peace of mind that the impact of this solution on the overal state size of the chain is negligible: ### Assumptions (Worst Case Scenario): - 1,000 validators (actual estimate for DataHaven: ~50/100 validators) - 6-hour eras (most-likely scenario, following what Polkadot does: ~24-hour eras) - Which means 4 merkle root updates per day ### Annual Storage Requirements: - Merkle Root History: **46,720 bytes/year** - 4 roots/day × 32 bytes/root × 365 days/year = 46,720 bytes/year - Operator Claim Tracking: **~1.46 MB/year** - 1,000 operators × 1 boolean/(operator * root index) × 1 byte/boolean × 4 root indices/day × 365 days/year = 1,460,000 bytes/year - **Total: ~1.5 MB/year** This represents negligible storage overhead compared to the significant operational benefits gained. ## TODO Since we want to allow the operators/validators to only have to interact with the AVS contract (that's why the `claimRewards` functions have the `onlyAVS` modifier), we still have to: - [x] Add the required functions to the AVS to allow operators to claim their rewards. - [x] Adds comprehensive unit tests for them. --------- Co-authored-by: Steve Degosserie <723552+stiiifff@users.noreply.github.com> Co-authored-by: Ahmad Kaouk <56095276+ahmadkaouk@users.noreply.github.com> Co-authored-by: Ahmad Kaouk <ahmadkaouk.93@gmail.com>
2025-07-10 06:47:39 +00:00
function test_claimLatestOperatorRewards() public {
feat: ✨ initial rewards registry (#17) This PR adds an initial implementation for a rewards registry, which will be the contract in charge of allowing DataHaven validators to claim the rewards they earned for being validators in the previous epoch. The logic behind it is as follows: - Whenever an epoch finishes, the corresponding BEEFY block gets relayed to Ethereum through Snowbridge. This BEEFY block contains, in its `extra` field, the merkle root of the tree that contains as leafs all the message commitments of the messages of corresponding block, one of which is the rewards distribution message. - The rewards distribution message commitment is the root of the merkle tree where each leaf is a tuple of the operator ID and the obtained era points in the finished epoch. In this case, the operator ID is the corresponding validator's Ethereum address. - When the rewards distribution message is received, Snowbridge validates it using the aforementioned BEEFY block and then dispatches it. The dispatch invokes the `callContract` function of the `RewardsAgent` agent, with the corresponding parameters so that this agent calls the `updateRewardsMerkleRoot` function of the `RewardsRegistry` contract with the new rewards distribution message commitment. - After this root is updated, any validator/operator can submit a proof that it is in a leaf of the merkle tree that produced that root, which means it has pending rewards to claim, through the `ServiceManagerBase`'s `claimOperatorRewards` function. - Each operator set of the AVS can have an assigned `RewardsRegistry` contract. Operator sets that do not have an assigned `RewardsRegistry` contract won't be able to received rewards. This PR also adds two separate unit-test suites: one for the added functionality to the `ServiceManagerBase` contract and one specific to the new `RewardsRegistry` contract. > [!CAUTION] The `RewardsAgent` agent is the only one allowed to update the rewards' merkle root, which means if a malicious user could get access to it it could set the pending rewards to be claimed to an arbitrary tree that benefits it. Extreme caution must be taken in the Substrate side so only validated messages are sent to the Ethereum side, as to not allow any users to impersonate being this agent. ### TODO: Ideally, we would use the `RewardsCoordinator` contract from the EigenLayer core to distribute the rewards, but currently that adds a huge overhead for Operators since they'd have to wait for EigenLayer's SideCar to snapshot state and update the distribution root (which happens once a day), generate a proof that they belong to the tree of that distribution root, store it while waiting for the `activationDelay` (currently a week) to pass, and just then they would be able to claim their earned rewards. --------- Co-authored-by: Facundo Farall <37149322+ffarall@users.noreply.github.com>
2025-03-31 19:54:23 +00:00
uint256 initialBalance = operatorAddress.balance;
vm.mockCall(
address(allocationManager),
abi.encodeWithSelector(IAllocationManager.isMemberOfOperatorSet.selector),
abi.encode(true)
);
feat: ✨ initial rewards registry (#17) This PR adds an initial implementation for a rewards registry, which will be the contract in charge of allowing DataHaven validators to claim the rewards they earned for being validators in the previous epoch. The logic behind it is as follows: - Whenever an epoch finishes, the corresponding BEEFY block gets relayed to Ethereum through Snowbridge. This BEEFY block contains, in its `extra` field, the merkle root of the tree that contains as leafs all the message commitments of the messages of corresponding block, one of which is the rewards distribution message. - The rewards distribution message commitment is the root of the merkle tree where each leaf is a tuple of the operator ID and the obtained era points in the finished epoch. In this case, the operator ID is the corresponding validator's Ethereum address. - When the rewards distribution message is received, Snowbridge validates it using the aforementioned BEEFY block and then dispatches it. The dispatch invokes the `callContract` function of the `RewardsAgent` agent, with the corresponding parameters so that this agent calls the `updateRewardsMerkleRoot` function of the `RewardsRegistry` contract with the new rewards distribution message commitment. - After this root is updated, any validator/operator can submit a proof that it is in a leaf of the merkle tree that produced that root, which means it has pending rewards to claim, through the `ServiceManagerBase`'s `claimOperatorRewards` function. - Each operator set of the AVS can have an assigned `RewardsRegistry` contract. Operator sets that do not have an assigned `RewardsRegistry` contract won't be able to received rewards. This PR also adds two separate unit-test suites: one for the added functionality to the `ServiceManagerBase` contract and one specific to the new `RewardsRegistry` contract. > [!CAUTION] The `RewardsAgent` agent is the only one allowed to update the rewards' merkle root, which means if a malicious user could get access to it it could set the pending rewards to be claimed to an arbitrary tree that benefits it. Extreme caution must be taken in the Substrate side so only validated messages are sent to the Ethereum side, as to not allow any users to impersonate being this agent. ### TODO: Ideally, we would use the `RewardsCoordinator` contract from the EigenLayer core to distribute the rewards, but currently that adds a huge overhead for Operators since they'd have to wait for EigenLayer's SideCar to snapshot state and update the distribution root (which happens once a day), generate a proof that they belong to the tree of that distribution root, store it while waiting for the `activationDelay` (currently a week) to pass, and just then they would be able to claim their earned rewards. --------- Co-authored-by: Facundo Farall <37149322+ffarall@users.noreply.github.com>
2025-03-31 19:54:23 +00:00
vm.prank(operatorAddress);
vm.expectEmit(true, true, true, true);
feat: :sparkles: make `RewardsRegistry` keep a history of roots and claim status (#106) # Description This PR implements a comprehensive overhaul of the `RewardsRegistry` contract to maintain complete history of reward merkle roots while providing index-based claim tracking for operators. The new architecture enables operators to claim rewards from any historical merkle root instead of only the latest one. To do so, it: - Adds the `merkleRootHistory` storage array to the contract, in which we keep all rewards roots that ever came from the DataHaven side. - Adds the `operatorClaimedByIndex` storage map to the contract, in which we keep track, for each validator and root index, if it has claimed it or not. - This works even for new validators, since theoretically with this system you could argue they could claim older roots that they were not a part of which would be catastrophic, but they could never draft a correct proof for those to claim them. - Keeps some of the interface from before the overhaul, to have quick access to the latest rewards merkle root through `getLatestMerkleRoot()` and to claim rewards for it with `claimRewards()`. This is because the expected behaviour is for validators to claim their rewards every era. - Adds a way to batch claim rewards with `claimRewardsBatch()`. This function allows a validator to claim rewards for multiple root indices in one call by providing multiple proofs, useful if the validator has fallen behind claims and has to catch up, although special care will have to be taken by it to avoid reaching the gas limit of a transaction. ## Storage Efficiency Analysis One might think this solution is not as storage-efficient as other solutions that we can think of (I even had two other alternatives in mind as well), but a simple back-of-the-envelope calculation gives us peace of mind that the impact of this solution on the overal state size of the chain is negligible: ### Assumptions (Worst Case Scenario): - 1,000 validators (actual estimate for DataHaven: ~50/100 validators) - 6-hour eras (most-likely scenario, following what Polkadot does: ~24-hour eras) - Which means 4 merkle root updates per day ### Annual Storage Requirements: - Merkle Root History: **46,720 bytes/year** - 4 roots/day × 32 bytes/root × 365 days/year = 46,720 bytes/year - Operator Claim Tracking: **~1.46 MB/year** - 1,000 operators × 1 boolean/(operator * root index) × 1 byte/boolean × 4 root indices/day × 365 days/year = 1,460,000 bytes/year - **Total: ~1.5 MB/year** This represents negligible storage overhead compared to the significant operational benefits gained. ## TODO Since we want to allow the operators/validators to only have to interact with the AVS contract (that's why the `claimRewards` functions have the `onlyAVS` modifier), we still have to: - [x] Add the required functions to the AVS to allow operators to claim their rewards. - [x] Adds comprehensive unit tests for them. --------- Co-authored-by: Steve Degosserie <723552+stiiifff@users.noreply.github.com> Co-authored-by: Ahmad Kaouk <56095276+ahmadkaouk@users.noreply.github.com> Co-authored-by: Ahmad Kaouk <ahmadkaouk.93@gmail.com>
2025-07-10 06:47:39 +00:00
emit RewardsClaimedForIndex(operatorAddress, 2, thirdOperatorPoints, thirdOperatorPoints);
feat: ✨ initial rewards registry (#17) This PR adds an initial implementation for a rewards registry, which will be the contract in charge of allowing DataHaven validators to claim the rewards they earned for being validators in the previous epoch. The logic behind it is as follows: - Whenever an epoch finishes, the corresponding BEEFY block gets relayed to Ethereum through Snowbridge. This BEEFY block contains, in its `extra` field, the merkle root of the tree that contains as leafs all the message commitments of the messages of corresponding block, one of which is the rewards distribution message. - The rewards distribution message commitment is the root of the merkle tree where each leaf is a tuple of the operator ID and the obtained era points in the finished epoch. In this case, the operator ID is the corresponding validator's Ethereum address. - When the rewards distribution message is received, Snowbridge validates it using the aforementioned BEEFY block and then dispatches it. The dispatch invokes the `callContract` function of the `RewardsAgent` agent, with the corresponding parameters so that this agent calls the `updateRewardsMerkleRoot` function of the `RewardsRegistry` contract with the new rewards distribution message commitment. - After this root is updated, any validator/operator can submit a proof that it is in a leaf of the merkle tree that produced that root, which means it has pending rewards to claim, through the `ServiceManagerBase`'s `claimOperatorRewards` function. - Each operator set of the AVS can have an assigned `RewardsRegistry` contract. Operator sets that do not have an assigned `RewardsRegistry` contract won't be able to received rewards. This PR also adds two separate unit-test suites: one for the added functionality to the `ServiceManagerBase` contract and one specific to the new `RewardsRegistry` contract. > [!CAUTION] The `RewardsAgent` agent is the only one allowed to update the rewards' merkle root, which means if a malicious user could get access to it it could set the pending rewards to be claimed to an arbitrary tree that benefits it. Extreme caution must be taken in the Substrate side so only validated messages are sent to the Ethereum side, as to not allow any users to impersonate being this agent. ### TODO: Ideally, we would use the `RewardsCoordinator` contract from the EigenLayer core to distribute the rewards, but currently that adds a huge overhead for Operators since they'd have to wait for EigenLayer's SideCar to snapshot state and update the distribution root (which happens once a day), generate a proof that they belong to the tree of that distribution root, store it while waiting for the `activationDelay` (currently a week) to pass, and just then they would be able to claim their earned rewards. --------- Co-authored-by: Facundo Farall <37149322+ffarall@users.noreply.github.com>
2025-03-31 19:54:23 +00:00
feat: :sparkles: make `RewardsRegistry` keep a history of roots and claim status (#106) # Description This PR implements a comprehensive overhaul of the `RewardsRegistry` contract to maintain complete history of reward merkle roots while providing index-based claim tracking for operators. The new architecture enables operators to claim rewards from any historical merkle root instead of only the latest one. To do so, it: - Adds the `merkleRootHistory` storage array to the contract, in which we keep all rewards roots that ever came from the DataHaven side. - Adds the `operatorClaimedByIndex` storage map to the contract, in which we keep track, for each validator and root index, if it has claimed it or not. - This works even for new validators, since theoretically with this system you could argue they could claim older roots that they were not a part of which would be catastrophic, but they could never draft a correct proof for those to claim them. - Keeps some of the interface from before the overhaul, to have quick access to the latest rewards merkle root through `getLatestMerkleRoot()` and to claim rewards for it with `claimRewards()`. This is because the expected behaviour is for validators to claim their rewards every era. - Adds a way to batch claim rewards with `claimRewardsBatch()`. This function allows a validator to claim rewards for multiple root indices in one call by providing multiple proofs, useful if the validator has fallen behind claims and has to catch up, although special care will have to be taken by it to avoid reaching the gas limit of a transaction. ## Storage Efficiency Analysis One might think this solution is not as storage-efficient as other solutions that we can think of (I even had two other alternatives in mind as well), but a simple back-of-the-envelope calculation gives us peace of mind that the impact of this solution on the overal state size of the chain is negligible: ### Assumptions (Worst Case Scenario): - 1,000 validators (actual estimate for DataHaven: ~50/100 validators) - 6-hour eras (most-likely scenario, following what Polkadot does: ~24-hour eras) - Which means 4 merkle root updates per day ### Annual Storage Requirements: - Merkle Root History: **46,720 bytes/year** - 4 roots/day × 32 bytes/root × 365 days/year = 46,720 bytes/year - Operator Claim Tracking: **~1.46 MB/year** - 1,000 operators × 1 boolean/(operator * root index) × 1 byte/boolean × 4 root indices/day × 365 days/year = 1,460,000 bytes/year - **Total: ~1.5 MB/year** This represents negligible storage overhead compared to the significant operational benefits gained. ## TODO Since we want to allow the operators/validators to only have to interact with the AVS contract (that's why the `claimRewards` functions have the `onlyAVS` modifier), we still have to: - [x] Add the required functions to the AVS to allow operators to claim their rewards. - [x] Adds comprehensive unit tests for them. --------- Co-authored-by: Steve Degosserie <723552+stiiifff@users.noreply.github.com> Co-authored-by: Ahmad Kaouk <56095276+ahmadkaouk@users.noreply.github.com> Co-authored-by: Ahmad Kaouk <ahmadkaouk.93@gmail.com>
2025-07-10 06:47:39 +00:00
serviceManager.claimLatestOperatorRewards(
operatorSetId, thirdOperatorPoints, numberOfLeaves, leafIndex, thirdValidProof
feat: :sparkles: make `RewardsRegistry` keep a history of roots and claim status (#106) # Description This PR implements a comprehensive overhaul of the `RewardsRegistry` contract to maintain complete history of reward merkle roots while providing index-based claim tracking for operators. The new architecture enables operators to claim rewards from any historical merkle root instead of only the latest one. To do so, it: - Adds the `merkleRootHistory` storage array to the contract, in which we keep all rewards roots that ever came from the DataHaven side. - Adds the `operatorClaimedByIndex` storage map to the contract, in which we keep track, for each validator and root index, if it has claimed it or not. - This works even for new validators, since theoretically with this system you could argue they could claim older roots that they were not a part of which would be catastrophic, but they could never draft a correct proof for those to claim them. - Keeps some of the interface from before the overhaul, to have quick access to the latest rewards merkle root through `getLatestMerkleRoot()` and to claim rewards for it with `claimRewards()`. This is because the expected behaviour is for validators to claim their rewards every era. - Adds a way to batch claim rewards with `claimRewardsBatch()`. This function allows a validator to claim rewards for multiple root indices in one call by providing multiple proofs, useful if the validator has fallen behind claims and has to catch up, although special care will have to be taken by it to avoid reaching the gas limit of a transaction. ## Storage Efficiency Analysis One might think this solution is not as storage-efficient as other solutions that we can think of (I even had two other alternatives in mind as well), but a simple back-of-the-envelope calculation gives us peace of mind that the impact of this solution on the overal state size of the chain is negligible: ### Assumptions (Worst Case Scenario): - 1,000 validators (actual estimate for DataHaven: ~50/100 validators) - 6-hour eras (most-likely scenario, following what Polkadot does: ~24-hour eras) - Which means 4 merkle root updates per day ### Annual Storage Requirements: - Merkle Root History: **46,720 bytes/year** - 4 roots/day × 32 bytes/root × 365 days/year = 46,720 bytes/year - Operator Claim Tracking: **~1.46 MB/year** - 1,000 operators × 1 boolean/(operator * root index) × 1 byte/boolean × 4 root indices/day × 365 days/year = 1,460,000 bytes/year - **Total: ~1.5 MB/year** This represents negligible storage overhead compared to the significant operational benefits gained. ## TODO Since we want to allow the operators/validators to only have to interact with the AVS contract (that's why the `claimRewards` functions have the `onlyAVS` modifier), we still have to: - [x] Add the required functions to the AVS to allow operators to claim their rewards. - [x] Adds comprehensive unit tests for them. --------- Co-authored-by: Steve Degosserie <723552+stiiifff@users.noreply.github.com> Co-authored-by: Ahmad Kaouk <56095276+ahmadkaouk@users.noreply.github.com> Co-authored-by: Ahmad Kaouk <ahmadkaouk.93@gmail.com>
2025-07-10 06:47:39 +00:00
);
feat: ✨ initial rewards registry (#17) This PR adds an initial implementation for a rewards registry, which will be the contract in charge of allowing DataHaven validators to claim the rewards they earned for being validators in the previous epoch. The logic behind it is as follows: - Whenever an epoch finishes, the corresponding BEEFY block gets relayed to Ethereum through Snowbridge. This BEEFY block contains, in its `extra` field, the merkle root of the tree that contains as leafs all the message commitments of the messages of corresponding block, one of which is the rewards distribution message. - The rewards distribution message commitment is the root of the merkle tree where each leaf is a tuple of the operator ID and the obtained era points in the finished epoch. In this case, the operator ID is the corresponding validator's Ethereum address. - When the rewards distribution message is received, Snowbridge validates it using the aforementioned BEEFY block and then dispatches it. The dispatch invokes the `callContract` function of the `RewardsAgent` agent, with the corresponding parameters so that this agent calls the `updateRewardsMerkleRoot` function of the `RewardsRegistry` contract with the new rewards distribution message commitment. - After this root is updated, any validator/operator can submit a proof that it is in a leaf of the merkle tree that produced that root, which means it has pending rewards to claim, through the `ServiceManagerBase`'s `claimOperatorRewards` function. - Each operator set of the AVS can have an assigned `RewardsRegistry` contract. Operator sets that do not have an assigned `RewardsRegistry` contract won't be able to received rewards. This PR also adds two separate unit-test suites: one for the added functionality to the `ServiceManagerBase` contract and one specific to the new `RewardsRegistry` contract. > [!CAUTION] The `RewardsAgent` agent is the only one allowed to update the rewards' merkle root, which means if a malicious user could get access to it it could set the pending rewards to be claimed to an arbitrary tree that benefits it. Extreme caution must be taken in the Substrate side so only validated messages are sent to the Ethereum side, as to not allow any users to impersonate being this agent. ### TODO: Ideally, we would use the `RewardsCoordinator` contract from the EigenLayer core to distribute the rewards, but currently that adds a huge overhead for Operators since they'd have to wait for EigenLayer's SideCar to snapshot state and update the distribution root (which happens once a day), generate a proof that they belong to the tree of that distribution root, store it while waiting for the `activationDelay` (currently a week) to pass, and just then they would be able to claim their earned rewards. --------- Co-authored-by: Facundo Farall <37149322+ffarall@users.noreply.github.com>
2025-03-31 19:54:23 +00:00
assertEq(
operatorAddress.balance,
feat: :sparkles: make `RewardsRegistry` keep a history of roots and claim status (#106) # Description This PR implements a comprehensive overhaul of the `RewardsRegistry` contract to maintain complete history of reward merkle roots while providing index-based claim tracking for operators. The new architecture enables operators to claim rewards from any historical merkle root instead of only the latest one. To do so, it: - Adds the `merkleRootHistory` storage array to the contract, in which we keep all rewards roots that ever came from the DataHaven side. - Adds the `operatorClaimedByIndex` storage map to the contract, in which we keep track, for each validator and root index, if it has claimed it or not. - This works even for new validators, since theoretically with this system you could argue they could claim older roots that they were not a part of which would be catastrophic, but they could never draft a correct proof for those to claim them. - Keeps some of the interface from before the overhaul, to have quick access to the latest rewards merkle root through `getLatestMerkleRoot()` and to claim rewards for it with `claimRewards()`. This is because the expected behaviour is for validators to claim their rewards every era. - Adds a way to batch claim rewards with `claimRewardsBatch()`. This function allows a validator to claim rewards for multiple root indices in one call by providing multiple proofs, useful if the validator has fallen behind claims and has to catch up, although special care will have to be taken by it to avoid reaching the gas limit of a transaction. ## Storage Efficiency Analysis One might think this solution is not as storage-efficient as other solutions that we can think of (I even had two other alternatives in mind as well), but a simple back-of-the-envelope calculation gives us peace of mind that the impact of this solution on the overal state size of the chain is negligible: ### Assumptions (Worst Case Scenario): - 1,000 validators (actual estimate for DataHaven: ~50/100 validators) - 6-hour eras (most-likely scenario, following what Polkadot does: ~24-hour eras) - Which means 4 merkle root updates per day ### Annual Storage Requirements: - Merkle Root History: **46,720 bytes/year** - 4 roots/day × 32 bytes/root × 365 days/year = 46,720 bytes/year - Operator Claim Tracking: **~1.46 MB/year** - 1,000 operators × 1 boolean/(operator * root index) × 1 byte/boolean × 4 root indices/day × 365 days/year = 1,460,000 bytes/year - **Total: ~1.5 MB/year** This represents negligible storage overhead compared to the significant operational benefits gained. ## TODO Since we want to allow the operators/validators to only have to interact with the AVS contract (that's why the `claimRewards` functions have the `onlyAVS` modifier), we still have to: - [x] Add the required functions to the AVS to allow operators to claim their rewards. - [x] Adds comprehensive unit tests for them. --------- Co-authored-by: Steve Degosserie <723552+stiiifff@users.noreply.github.com> Co-authored-by: Ahmad Kaouk <56095276+ahmadkaouk@users.noreply.github.com> Co-authored-by: Ahmad Kaouk <ahmadkaouk.93@gmail.com>
2025-07-10 06:47:39 +00:00
initialBalance + thirdOperatorPoints,
feat: ✨ initial rewards registry (#17) This PR adds an initial implementation for a rewards registry, which will be the contract in charge of allowing DataHaven validators to claim the rewards they earned for being validators in the previous epoch. The logic behind it is as follows: - Whenever an epoch finishes, the corresponding BEEFY block gets relayed to Ethereum through Snowbridge. This BEEFY block contains, in its `extra` field, the merkle root of the tree that contains as leafs all the message commitments of the messages of corresponding block, one of which is the rewards distribution message. - The rewards distribution message commitment is the root of the merkle tree where each leaf is a tuple of the operator ID and the obtained era points in the finished epoch. In this case, the operator ID is the corresponding validator's Ethereum address. - When the rewards distribution message is received, Snowbridge validates it using the aforementioned BEEFY block and then dispatches it. The dispatch invokes the `callContract` function of the `RewardsAgent` agent, with the corresponding parameters so that this agent calls the `updateRewardsMerkleRoot` function of the `RewardsRegistry` contract with the new rewards distribution message commitment. - After this root is updated, any validator/operator can submit a proof that it is in a leaf of the merkle tree that produced that root, which means it has pending rewards to claim, through the `ServiceManagerBase`'s `claimOperatorRewards` function. - Each operator set of the AVS can have an assigned `RewardsRegistry` contract. Operator sets that do not have an assigned `RewardsRegistry` contract won't be able to received rewards. This PR also adds two separate unit-test suites: one for the added functionality to the `ServiceManagerBase` contract and one specific to the new `RewardsRegistry` contract. > [!CAUTION] The `RewardsAgent` agent is the only one allowed to update the rewards' merkle root, which means if a malicious user could get access to it it could set the pending rewards to be claimed to an arbitrary tree that benefits it. Extreme caution must be taken in the Substrate side so only validated messages are sent to the Ethereum side, as to not allow any users to impersonate being this agent. ### TODO: Ideally, we would use the `RewardsCoordinator` contract from the EigenLayer core to distribute the rewards, but currently that adds a huge overhead for Operators since they'd have to wait for EigenLayer's SideCar to snapshot state and update the distribution root (which happens once a day), generate a proof that they belong to the tree of that distribution root, store it while waiting for the `activationDelay` (currently a week) to pass, and just then they would be able to claim their earned rewards. --------- Co-authored-by: Facundo Farall <37149322+ffarall@users.noreply.github.com>
2025-03-31 19:54:23 +00:00
"Operator should receive correct rewards"
);
}
feat: :sparkles: make `RewardsRegistry` keep a history of roots and claim status (#106) # Description This PR implements a comprehensive overhaul of the `RewardsRegistry` contract to maintain complete history of reward merkle roots while providing index-based claim tracking for operators. The new architecture enables operators to claim rewards from any historical merkle root instead of only the latest one. To do so, it: - Adds the `merkleRootHistory` storage array to the contract, in which we keep all rewards roots that ever came from the DataHaven side. - Adds the `operatorClaimedByIndex` storage map to the contract, in which we keep track, for each validator and root index, if it has claimed it or not. - This works even for new validators, since theoretically with this system you could argue they could claim older roots that they were not a part of which would be catastrophic, but they could never draft a correct proof for those to claim them. - Keeps some of the interface from before the overhaul, to have quick access to the latest rewards merkle root through `getLatestMerkleRoot()` and to claim rewards for it with `claimRewards()`. This is because the expected behaviour is for validators to claim their rewards every era. - Adds a way to batch claim rewards with `claimRewardsBatch()`. This function allows a validator to claim rewards for multiple root indices in one call by providing multiple proofs, useful if the validator has fallen behind claims and has to catch up, although special care will have to be taken by it to avoid reaching the gas limit of a transaction. ## Storage Efficiency Analysis One might think this solution is not as storage-efficient as other solutions that we can think of (I even had two other alternatives in mind as well), but a simple back-of-the-envelope calculation gives us peace of mind that the impact of this solution on the overal state size of the chain is negligible: ### Assumptions (Worst Case Scenario): - 1,000 validators (actual estimate for DataHaven: ~50/100 validators) - 6-hour eras (most-likely scenario, following what Polkadot does: ~24-hour eras) - Which means 4 merkle root updates per day ### Annual Storage Requirements: - Merkle Root History: **46,720 bytes/year** - 4 roots/day × 32 bytes/root × 365 days/year = 46,720 bytes/year - Operator Claim Tracking: **~1.46 MB/year** - 1,000 operators × 1 boolean/(operator * root index) × 1 byte/boolean × 4 root indices/day × 365 days/year = 1,460,000 bytes/year - **Total: ~1.5 MB/year** This represents negligible storage overhead compared to the significant operational benefits gained. ## TODO Since we want to allow the operators/validators to only have to interact with the AVS contract (that's why the `claimRewards` functions have the `onlyAVS` modifier), we still have to: - [x] Add the required functions to the AVS to allow operators to claim their rewards. - [x] Adds comprehensive unit tests for them. --------- Co-authored-by: Steve Degosserie <723552+stiiifff@users.noreply.github.com> Co-authored-by: Ahmad Kaouk <56095276+ahmadkaouk@users.noreply.github.com> Co-authored-by: Ahmad Kaouk <ahmadkaouk.93@gmail.com>
2025-07-10 06:47:39 +00:00
function test_claimLatestOperatorRewards_NoRewardsRegistry() public {
feat: ✨ initial rewards registry (#17) This PR adds an initial implementation for a rewards registry, which will be the contract in charge of allowing DataHaven validators to claim the rewards they earned for being validators in the previous epoch. The logic behind it is as follows: - Whenever an epoch finishes, the corresponding BEEFY block gets relayed to Ethereum through Snowbridge. This BEEFY block contains, in its `extra` field, the merkle root of the tree that contains as leafs all the message commitments of the messages of corresponding block, one of which is the rewards distribution message. - The rewards distribution message commitment is the root of the merkle tree where each leaf is a tuple of the operator ID and the obtained era points in the finished epoch. In this case, the operator ID is the corresponding validator's Ethereum address. - When the rewards distribution message is received, Snowbridge validates it using the aforementioned BEEFY block and then dispatches it. The dispatch invokes the `callContract` function of the `RewardsAgent` agent, with the corresponding parameters so that this agent calls the `updateRewardsMerkleRoot` function of the `RewardsRegistry` contract with the new rewards distribution message commitment. - After this root is updated, any validator/operator can submit a proof that it is in a leaf of the merkle tree that produced that root, which means it has pending rewards to claim, through the `ServiceManagerBase`'s `claimOperatorRewards` function. - Each operator set of the AVS can have an assigned `RewardsRegistry` contract. Operator sets that do not have an assigned `RewardsRegistry` contract won't be able to received rewards. This PR also adds two separate unit-test suites: one for the added functionality to the `ServiceManagerBase` contract and one specific to the new `RewardsRegistry` contract. > [!CAUTION] The `RewardsAgent` agent is the only one allowed to update the rewards' merkle root, which means if a malicious user could get access to it it could set the pending rewards to be claimed to an arbitrary tree that benefits it. Extreme caution must be taken in the Substrate side so only validated messages are sent to the Ethereum side, as to not allow any users to impersonate being this agent. ### TODO: Ideally, we would use the `RewardsCoordinator` contract from the EigenLayer core to distribute the rewards, but currently that adds a huge overhead for Operators since they'd have to wait for EigenLayer's SideCar to snapshot state and update the distribution root (which happens once a day), generate a proof that they belong to the tree of that distribution root, store it while waiting for the `activationDelay` (currently a week) to pass, and just then they would be able to claim their earned rewards. --------- Co-authored-by: Facundo Farall <37149322+ffarall@users.noreply.github.com>
2025-03-31 19:54:23 +00:00
uint32 invalidSetId = 999;
vm.prank(operatorAddress);
vm.expectRevert(
abi.encodeWithSelector(IServiceManagerErrors.NoRewardsRegistryForOperatorSet.selector)
);
serviceManager.claimLatestOperatorRewards(
invalidSetId, operatorPoints, numberOfLeaves, leafIndex, validProof
);
feat: :sparkles: make `RewardsRegistry` keep a history of roots and claim status (#106) # Description This PR implements a comprehensive overhaul of the `RewardsRegistry` contract to maintain complete history of reward merkle roots while providing index-based claim tracking for operators. The new architecture enables operators to claim rewards from any historical merkle root instead of only the latest one. To do so, it: - Adds the `merkleRootHistory` storage array to the contract, in which we keep all rewards roots that ever came from the DataHaven side. - Adds the `operatorClaimedByIndex` storage map to the contract, in which we keep track, for each validator and root index, if it has claimed it or not. - This works even for new validators, since theoretically with this system you could argue they could claim older roots that they were not a part of which would be catastrophic, but they could never draft a correct proof for those to claim them. - Keeps some of the interface from before the overhaul, to have quick access to the latest rewards merkle root through `getLatestMerkleRoot()` and to claim rewards for it with `claimRewards()`. This is because the expected behaviour is for validators to claim their rewards every era. - Adds a way to batch claim rewards with `claimRewardsBatch()`. This function allows a validator to claim rewards for multiple root indices in one call by providing multiple proofs, useful if the validator has fallen behind claims and has to catch up, although special care will have to be taken by it to avoid reaching the gas limit of a transaction. ## Storage Efficiency Analysis One might think this solution is not as storage-efficient as other solutions that we can think of (I even had two other alternatives in mind as well), but a simple back-of-the-envelope calculation gives us peace of mind that the impact of this solution on the overal state size of the chain is negligible: ### Assumptions (Worst Case Scenario): - 1,000 validators (actual estimate for DataHaven: ~50/100 validators) - 6-hour eras (most-likely scenario, following what Polkadot does: ~24-hour eras) - Which means 4 merkle root updates per day ### Annual Storage Requirements: - Merkle Root History: **46,720 bytes/year** - 4 roots/day × 32 bytes/root × 365 days/year = 46,720 bytes/year - Operator Claim Tracking: **~1.46 MB/year** - 1,000 operators × 1 boolean/(operator * root index) × 1 byte/boolean × 4 root indices/day × 365 days/year = 1,460,000 bytes/year - **Total: ~1.5 MB/year** This represents negligible storage overhead compared to the significant operational benefits gained. ## TODO Since we want to allow the operators/validators to only have to interact with the AVS contract (that's why the `claimRewards` functions have the `onlyAVS` modifier), we still have to: - [x] Add the required functions to the AVS to allow operators to claim their rewards. - [x] Adds comprehensive unit tests for them. --------- Co-authored-by: Steve Degosserie <723552+stiiifff@users.noreply.github.com> Co-authored-by: Ahmad Kaouk <56095276+ahmadkaouk@users.noreply.github.com> Co-authored-by: Ahmad Kaouk <ahmadkaouk.93@gmail.com>
2025-07-10 06:47:39 +00:00
}
function test_claimLatestOperatorRewards_AlreadyClaimed() public {
vm.mockCall(
address(allocationManager),
abi.encodeWithSelector(IAllocationManager.isMemberOfOperatorSet.selector),
abi.encode(true)
);
// First claim (uses latest merkle root - index 2)
vm.prank(operatorAddress);
serviceManager.claimLatestOperatorRewards(
operatorSetId, thirdOperatorPoints, numberOfLeaves, leafIndex, thirdValidProof
feat: :sparkles: make `RewardsRegistry` keep a history of roots and claim status (#106) # Description This PR implements a comprehensive overhaul of the `RewardsRegistry` contract to maintain complete history of reward merkle roots while providing index-based claim tracking for operators. The new architecture enables operators to claim rewards from any historical merkle root instead of only the latest one. To do so, it: - Adds the `merkleRootHistory` storage array to the contract, in which we keep all rewards roots that ever came from the DataHaven side. - Adds the `operatorClaimedByIndex` storage map to the contract, in which we keep track, for each validator and root index, if it has claimed it or not. - This works even for new validators, since theoretically with this system you could argue they could claim older roots that they were not a part of which would be catastrophic, but they could never draft a correct proof for those to claim them. - Keeps some of the interface from before the overhaul, to have quick access to the latest rewards merkle root through `getLatestMerkleRoot()` and to claim rewards for it with `claimRewards()`. This is because the expected behaviour is for validators to claim their rewards every era. - Adds a way to batch claim rewards with `claimRewardsBatch()`. This function allows a validator to claim rewards for multiple root indices in one call by providing multiple proofs, useful if the validator has fallen behind claims and has to catch up, although special care will have to be taken by it to avoid reaching the gas limit of a transaction. ## Storage Efficiency Analysis One might think this solution is not as storage-efficient as other solutions that we can think of (I even had two other alternatives in mind as well), but a simple back-of-the-envelope calculation gives us peace of mind that the impact of this solution on the overal state size of the chain is negligible: ### Assumptions (Worst Case Scenario): - 1,000 validators (actual estimate for DataHaven: ~50/100 validators) - 6-hour eras (most-likely scenario, following what Polkadot does: ~24-hour eras) - Which means 4 merkle root updates per day ### Annual Storage Requirements: - Merkle Root History: **46,720 bytes/year** - 4 roots/day × 32 bytes/root × 365 days/year = 46,720 bytes/year - Operator Claim Tracking: **~1.46 MB/year** - 1,000 operators × 1 boolean/(operator * root index) × 1 byte/boolean × 4 root indices/day × 365 days/year = 1,460,000 bytes/year - **Total: ~1.5 MB/year** This represents negligible storage overhead compared to the significant operational benefits gained. ## TODO Since we want to allow the operators/validators to only have to interact with the AVS contract (that's why the `claimRewards` functions have the `onlyAVS` modifier), we still have to: - [x] Add the required functions to the AVS to allow operators to claim their rewards. - [x] Adds comprehensive unit tests for them. --------- Co-authored-by: Steve Degosserie <723552+stiiifff@users.noreply.github.com> Co-authored-by: Ahmad Kaouk <56095276+ahmadkaouk@users.noreply.github.com> Co-authored-by: Ahmad Kaouk <ahmadkaouk.93@gmail.com>
2025-07-10 06:47:39 +00:00
);
// Second claim should fail
vm.prank(operatorAddress);
vm.expectRevert(
abi.encodeWithSelector(IRewardsRegistryErrors.RewardsAlreadyClaimedForIndex.selector)
);
serviceManager.claimLatestOperatorRewards(
operatorSetId, thirdOperatorPoints, numberOfLeaves, leafIndex, thirdValidProof
feat: :sparkles: make `RewardsRegistry` keep a history of roots and claim status (#106) # Description This PR implements a comprehensive overhaul of the `RewardsRegistry` contract to maintain complete history of reward merkle roots while providing index-based claim tracking for operators. The new architecture enables operators to claim rewards from any historical merkle root instead of only the latest one. To do so, it: - Adds the `merkleRootHistory` storage array to the contract, in which we keep all rewards roots that ever came from the DataHaven side. - Adds the `operatorClaimedByIndex` storage map to the contract, in which we keep track, for each validator and root index, if it has claimed it or not. - This works even for new validators, since theoretically with this system you could argue they could claim older roots that they were not a part of which would be catastrophic, but they could never draft a correct proof for those to claim them. - Keeps some of the interface from before the overhaul, to have quick access to the latest rewards merkle root through `getLatestMerkleRoot()` and to claim rewards for it with `claimRewards()`. This is because the expected behaviour is for validators to claim their rewards every era. - Adds a way to batch claim rewards with `claimRewardsBatch()`. This function allows a validator to claim rewards for multiple root indices in one call by providing multiple proofs, useful if the validator has fallen behind claims and has to catch up, although special care will have to be taken by it to avoid reaching the gas limit of a transaction. ## Storage Efficiency Analysis One might think this solution is not as storage-efficient as other solutions that we can think of (I even had two other alternatives in mind as well), but a simple back-of-the-envelope calculation gives us peace of mind that the impact of this solution on the overal state size of the chain is negligible: ### Assumptions (Worst Case Scenario): - 1,000 validators (actual estimate for DataHaven: ~50/100 validators) - 6-hour eras (most-likely scenario, following what Polkadot does: ~24-hour eras) - Which means 4 merkle root updates per day ### Annual Storage Requirements: - Merkle Root History: **46,720 bytes/year** - 4 roots/day × 32 bytes/root × 365 days/year = 46,720 bytes/year - Operator Claim Tracking: **~1.46 MB/year** - 1,000 operators × 1 boolean/(operator * root index) × 1 byte/boolean × 4 root indices/day × 365 days/year = 1,460,000 bytes/year - **Total: ~1.5 MB/year** This represents negligible storage overhead compared to the significant operational benefits gained. ## TODO Since we want to allow the operators/validators to only have to interact with the AVS contract (that's why the `claimRewards` functions have the `onlyAVS` modifier), we still have to: - [x] Add the required functions to the AVS to allow operators to claim their rewards. - [x] Adds comprehensive unit tests for them. --------- Co-authored-by: Steve Degosserie <723552+stiiifff@users.noreply.github.com> Co-authored-by: Ahmad Kaouk <56095276+ahmadkaouk@users.noreply.github.com> Co-authored-by: Ahmad Kaouk <ahmadkaouk.93@gmail.com>
2025-07-10 06:47:39 +00:00
);
}
function test_claimOperatorRewards() public {
uint256 initialBalance = operatorAddress.balance;
vm.mockCall(
address(allocationManager),
abi.encodeWithSelector(IAllocationManager.isMemberOfOperatorSet.selector),
abi.encode(true)
);
vm.prank(operatorAddress);
vm.expectEmit(true, true, true, true);
emit RewardsClaimedForIndex(operatorAddress, 0, operatorPoints, operatorPoints);
serviceManager.claimOperatorRewards(
operatorSetId, 0, operatorPoints, numberOfLeaves, leafIndex, validProof
);
feat: :sparkles: make `RewardsRegistry` keep a history of roots and claim status (#106) # Description This PR implements a comprehensive overhaul of the `RewardsRegistry` contract to maintain complete history of reward merkle roots while providing index-based claim tracking for operators. The new architecture enables operators to claim rewards from any historical merkle root instead of only the latest one. To do so, it: - Adds the `merkleRootHistory` storage array to the contract, in which we keep all rewards roots that ever came from the DataHaven side. - Adds the `operatorClaimedByIndex` storage map to the contract, in which we keep track, for each validator and root index, if it has claimed it or not. - This works even for new validators, since theoretically with this system you could argue they could claim older roots that they were not a part of which would be catastrophic, but they could never draft a correct proof for those to claim them. - Keeps some of the interface from before the overhaul, to have quick access to the latest rewards merkle root through `getLatestMerkleRoot()` and to claim rewards for it with `claimRewards()`. This is because the expected behaviour is for validators to claim their rewards every era. - Adds a way to batch claim rewards with `claimRewardsBatch()`. This function allows a validator to claim rewards for multiple root indices in one call by providing multiple proofs, useful if the validator has fallen behind claims and has to catch up, although special care will have to be taken by it to avoid reaching the gas limit of a transaction. ## Storage Efficiency Analysis One might think this solution is not as storage-efficient as other solutions that we can think of (I even had two other alternatives in mind as well), but a simple back-of-the-envelope calculation gives us peace of mind that the impact of this solution on the overal state size of the chain is negligible: ### Assumptions (Worst Case Scenario): - 1,000 validators (actual estimate for DataHaven: ~50/100 validators) - 6-hour eras (most-likely scenario, following what Polkadot does: ~24-hour eras) - Which means 4 merkle root updates per day ### Annual Storage Requirements: - Merkle Root History: **46,720 bytes/year** - 4 roots/day × 32 bytes/root × 365 days/year = 46,720 bytes/year - Operator Claim Tracking: **~1.46 MB/year** - 1,000 operators × 1 boolean/(operator * root index) × 1 byte/boolean × 4 root indices/day × 365 days/year = 1,460,000 bytes/year - **Total: ~1.5 MB/year** This represents negligible storage overhead compared to the significant operational benefits gained. ## TODO Since we want to allow the operators/validators to only have to interact with the AVS contract (that's why the `claimRewards` functions have the `onlyAVS` modifier), we still have to: - [x] Add the required functions to the AVS to allow operators to claim their rewards. - [x] Adds comprehensive unit tests for them. --------- Co-authored-by: Steve Degosserie <723552+stiiifff@users.noreply.github.com> Co-authored-by: Ahmad Kaouk <56095276+ahmadkaouk@users.noreply.github.com> Co-authored-by: Ahmad Kaouk <ahmadkaouk.93@gmail.com>
2025-07-10 06:47:39 +00:00
assertEq(
operatorAddress.balance,
initialBalance + operatorPoints,
"Operator should receive correct rewards"
);
}
function test_claimOperatorRewards_DifferentIndices() public {
uint256 initialBalance = operatorAddress.balance;
vm.mockCall(
address(allocationManager),
abi.encodeWithSelector(IAllocationManager.isMemberOfOperatorSet.selector),
abi.encode(true)
);
// Claim from index 1 (second merkle root)
vm.prank(operatorAddress);
serviceManager.claimOperatorRewards(
operatorSetId, 1, secondOperatorPoints, numberOfLeaves, leafIndex, secondValidProof
feat: :sparkles: make `RewardsRegistry` keep a history of roots and claim status (#106) # Description This PR implements a comprehensive overhaul of the `RewardsRegistry` contract to maintain complete history of reward merkle roots while providing index-based claim tracking for operators. The new architecture enables operators to claim rewards from any historical merkle root instead of only the latest one. To do so, it: - Adds the `merkleRootHistory` storage array to the contract, in which we keep all rewards roots that ever came from the DataHaven side. - Adds the `operatorClaimedByIndex` storage map to the contract, in which we keep track, for each validator and root index, if it has claimed it or not. - This works even for new validators, since theoretically with this system you could argue they could claim older roots that they were not a part of which would be catastrophic, but they could never draft a correct proof for those to claim them. - Keeps some of the interface from before the overhaul, to have quick access to the latest rewards merkle root through `getLatestMerkleRoot()` and to claim rewards for it with `claimRewards()`. This is because the expected behaviour is for validators to claim their rewards every era. - Adds a way to batch claim rewards with `claimRewardsBatch()`. This function allows a validator to claim rewards for multiple root indices in one call by providing multiple proofs, useful if the validator has fallen behind claims and has to catch up, although special care will have to be taken by it to avoid reaching the gas limit of a transaction. ## Storage Efficiency Analysis One might think this solution is not as storage-efficient as other solutions that we can think of (I even had two other alternatives in mind as well), but a simple back-of-the-envelope calculation gives us peace of mind that the impact of this solution on the overal state size of the chain is negligible: ### Assumptions (Worst Case Scenario): - 1,000 validators (actual estimate for DataHaven: ~50/100 validators) - 6-hour eras (most-likely scenario, following what Polkadot does: ~24-hour eras) - Which means 4 merkle root updates per day ### Annual Storage Requirements: - Merkle Root History: **46,720 bytes/year** - 4 roots/day × 32 bytes/root × 365 days/year = 46,720 bytes/year - Operator Claim Tracking: **~1.46 MB/year** - 1,000 operators × 1 boolean/(operator * root index) × 1 byte/boolean × 4 root indices/day × 365 days/year = 1,460,000 bytes/year - **Total: ~1.5 MB/year** This represents negligible storage overhead compared to the significant operational benefits gained. ## TODO Since we want to allow the operators/validators to only have to interact with the AVS contract (that's why the `claimRewards` functions have the `onlyAVS` modifier), we still have to: - [x] Add the required functions to the AVS to allow operators to claim their rewards. - [x] Adds comprehensive unit tests for them. --------- Co-authored-by: Steve Degosserie <723552+stiiifff@users.noreply.github.com> Co-authored-by: Ahmad Kaouk <56095276+ahmadkaouk@users.noreply.github.com> Co-authored-by: Ahmad Kaouk <ahmadkaouk.93@gmail.com>
2025-07-10 06:47:39 +00:00
);
assertEq(
operatorAddress.balance,
initialBalance + secondOperatorPoints,
"Operator should receive rewards from second root"
);
// Claim from index 2 (third merkle root)
vm.prank(operatorAddress);
serviceManager.claimOperatorRewards(
operatorSetId, 2, thirdOperatorPoints, numberOfLeaves, leafIndex, thirdValidProof
);
feat: :sparkles: make `RewardsRegistry` keep a history of roots and claim status (#106) # Description This PR implements a comprehensive overhaul of the `RewardsRegistry` contract to maintain complete history of reward merkle roots while providing index-based claim tracking for operators. The new architecture enables operators to claim rewards from any historical merkle root instead of only the latest one. To do so, it: - Adds the `merkleRootHistory` storage array to the contract, in which we keep all rewards roots that ever came from the DataHaven side. - Adds the `operatorClaimedByIndex` storage map to the contract, in which we keep track, for each validator and root index, if it has claimed it or not. - This works even for new validators, since theoretically with this system you could argue they could claim older roots that they were not a part of which would be catastrophic, but they could never draft a correct proof for those to claim them. - Keeps some of the interface from before the overhaul, to have quick access to the latest rewards merkle root through `getLatestMerkleRoot()` and to claim rewards for it with `claimRewards()`. This is because the expected behaviour is for validators to claim their rewards every era. - Adds a way to batch claim rewards with `claimRewardsBatch()`. This function allows a validator to claim rewards for multiple root indices in one call by providing multiple proofs, useful if the validator has fallen behind claims and has to catch up, although special care will have to be taken by it to avoid reaching the gas limit of a transaction. ## Storage Efficiency Analysis One might think this solution is not as storage-efficient as other solutions that we can think of (I even had two other alternatives in mind as well), but a simple back-of-the-envelope calculation gives us peace of mind that the impact of this solution on the overal state size of the chain is negligible: ### Assumptions (Worst Case Scenario): - 1,000 validators (actual estimate for DataHaven: ~50/100 validators) - 6-hour eras (most-likely scenario, following what Polkadot does: ~24-hour eras) - Which means 4 merkle root updates per day ### Annual Storage Requirements: - Merkle Root History: **46,720 bytes/year** - 4 roots/day × 32 bytes/root × 365 days/year = 46,720 bytes/year - Operator Claim Tracking: **~1.46 MB/year** - 1,000 operators × 1 boolean/(operator * root index) × 1 byte/boolean × 4 root indices/day × 365 days/year = 1,460,000 bytes/year - **Total: ~1.5 MB/year** This represents negligible storage overhead compared to the significant operational benefits gained. ## TODO Since we want to allow the operators/validators to only have to interact with the AVS contract (that's why the `claimRewards` functions have the `onlyAVS` modifier), we still have to: - [x] Add the required functions to the AVS to allow operators to claim their rewards. - [x] Adds comprehensive unit tests for them. --------- Co-authored-by: Steve Degosserie <723552+stiiifff@users.noreply.github.com> Co-authored-by: Ahmad Kaouk <56095276+ahmadkaouk@users.noreply.github.com> Co-authored-by: Ahmad Kaouk <ahmadkaouk.93@gmail.com>
2025-07-10 06:47:39 +00:00
assertEq(
operatorAddress.balance,
initialBalance + secondOperatorPoints + thirdOperatorPoints,
"Operator should receive rewards from both roots"
);
// Verify claim status
assertFalse(
rewardsRegistry.hasClaimedByIndex(operatorAddress, 0), "Index 0 should not be claimed"
);
assertTrue(
rewardsRegistry.hasClaimedByIndex(operatorAddress, 1), "Index 1 should be claimed"
);
assertTrue(
rewardsRegistry.hasClaimedByIndex(operatorAddress, 2), "Index 2 should be claimed"
);
}
function test_claimOperatorRewards_InvalidIndex() public {
vm.mockCall(
address(allocationManager),
abi.encodeWithSelector(IAllocationManager.isMemberOfOperatorSet.selector),
abi.encode(true)
);
vm.prank(operatorAddress);
vm.expectRevert(
abi.encodeWithSelector(IRewardsRegistryErrors.InvalidMerkleRootIndex.selector)
);
serviceManager.claimOperatorRewards(
operatorSetId, 999, operatorPoints, numberOfLeaves, leafIndex, validProof
);
feat: ✨ initial rewards registry (#17) This PR adds an initial implementation for a rewards registry, which will be the contract in charge of allowing DataHaven validators to claim the rewards they earned for being validators in the previous epoch. The logic behind it is as follows: - Whenever an epoch finishes, the corresponding BEEFY block gets relayed to Ethereum through Snowbridge. This BEEFY block contains, in its `extra` field, the merkle root of the tree that contains as leafs all the message commitments of the messages of corresponding block, one of which is the rewards distribution message. - The rewards distribution message commitment is the root of the merkle tree where each leaf is a tuple of the operator ID and the obtained era points in the finished epoch. In this case, the operator ID is the corresponding validator's Ethereum address. - When the rewards distribution message is received, Snowbridge validates it using the aforementioned BEEFY block and then dispatches it. The dispatch invokes the `callContract` function of the `RewardsAgent` agent, with the corresponding parameters so that this agent calls the `updateRewardsMerkleRoot` function of the `RewardsRegistry` contract with the new rewards distribution message commitment. - After this root is updated, any validator/operator can submit a proof that it is in a leaf of the merkle tree that produced that root, which means it has pending rewards to claim, through the `ServiceManagerBase`'s `claimOperatorRewards` function. - Each operator set of the AVS can have an assigned `RewardsRegistry` contract. Operator sets that do not have an assigned `RewardsRegistry` contract won't be able to received rewards. This PR also adds two separate unit-test suites: one for the added functionality to the `ServiceManagerBase` contract and one specific to the new `RewardsRegistry` contract. > [!CAUTION] The `RewardsAgent` agent is the only one allowed to update the rewards' merkle root, which means if a malicious user could get access to it it could set the pending rewards to be claimed to an arbitrary tree that benefits it. Extreme caution must be taken in the Substrate side so only validated messages are sent to the Ethereum side, as to not allow any users to impersonate being this agent. ### TODO: Ideally, we would use the `RewardsCoordinator` contract from the EigenLayer core to distribute the rewards, but currently that adds a huge overhead for Operators since they'd have to wait for EigenLayer's SideCar to snapshot state and update the distribution root (which happens once a day), generate a proof that they belong to the tree of that distribution root, store it while waiting for the `activationDelay` (currently a week) to pass, and just then they would be able to claim their earned rewards. --------- Co-authored-by: Facundo Farall <37149322+ffarall@users.noreply.github.com>
2025-03-31 19:54:23 +00:00
}
function test_claimOperatorRewards_AlreadyClaimed() public {
vm.mockCall(
address(allocationManager),
abi.encodeWithSelector(IAllocationManager.isMemberOfOperatorSet.selector),
abi.encode(true)
);
feat: ✨ initial rewards registry (#17) This PR adds an initial implementation for a rewards registry, which will be the contract in charge of allowing DataHaven validators to claim the rewards they earned for being validators in the previous epoch. The logic behind it is as follows: - Whenever an epoch finishes, the corresponding BEEFY block gets relayed to Ethereum through Snowbridge. This BEEFY block contains, in its `extra` field, the merkle root of the tree that contains as leafs all the message commitments of the messages of corresponding block, one of which is the rewards distribution message. - The rewards distribution message commitment is the root of the merkle tree where each leaf is a tuple of the operator ID and the obtained era points in the finished epoch. In this case, the operator ID is the corresponding validator's Ethereum address. - When the rewards distribution message is received, Snowbridge validates it using the aforementioned BEEFY block and then dispatches it. The dispatch invokes the `callContract` function of the `RewardsAgent` agent, with the corresponding parameters so that this agent calls the `updateRewardsMerkleRoot` function of the `RewardsRegistry` contract with the new rewards distribution message commitment. - After this root is updated, any validator/operator can submit a proof that it is in a leaf of the merkle tree that produced that root, which means it has pending rewards to claim, through the `ServiceManagerBase`'s `claimOperatorRewards` function. - Each operator set of the AVS can have an assigned `RewardsRegistry` contract. Operator sets that do not have an assigned `RewardsRegistry` contract won't be able to received rewards. This PR also adds two separate unit-test suites: one for the added functionality to the `ServiceManagerBase` contract and one specific to the new `RewardsRegistry` contract. > [!CAUTION] The `RewardsAgent` agent is the only one allowed to update the rewards' merkle root, which means if a malicious user could get access to it it could set the pending rewards to be claimed to an arbitrary tree that benefits it. Extreme caution must be taken in the Substrate side so only validated messages are sent to the Ethereum side, as to not allow any users to impersonate being this agent. ### TODO: Ideally, we would use the `RewardsCoordinator` contract from the EigenLayer core to distribute the rewards, but currently that adds a huge overhead for Operators since they'd have to wait for EigenLayer's SideCar to snapshot state and update the distribution root (which happens once a day), generate a proof that they belong to the tree of that distribution root, store it while waiting for the `activationDelay` (currently a week) to pass, and just then they would be able to claim their earned rewards. --------- Co-authored-by: Facundo Farall <37149322+ffarall@users.noreply.github.com>
2025-03-31 19:54:23 +00:00
// First claim
vm.prank(operatorAddress);
serviceManager.claimOperatorRewards(
operatorSetId, 0, operatorPoints, numberOfLeaves, leafIndex, validProof
);
feat: ✨ initial rewards registry (#17) This PR adds an initial implementation for a rewards registry, which will be the contract in charge of allowing DataHaven validators to claim the rewards they earned for being validators in the previous epoch. The logic behind it is as follows: - Whenever an epoch finishes, the corresponding BEEFY block gets relayed to Ethereum through Snowbridge. This BEEFY block contains, in its `extra` field, the merkle root of the tree that contains as leafs all the message commitments of the messages of corresponding block, one of which is the rewards distribution message. - The rewards distribution message commitment is the root of the merkle tree where each leaf is a tuple of the operator ID and the obtained era points in the finished epoch. In this case, the operator ID is the corresponding validator's Ethereum address. - When the rewards distribution message is received, Snowbridge validates it using the aforementioned BEEFY block and then dispatches it. The dispatch invokes the `callContract` function of the `RewardsAgent` agent, with the corresponding parameters so that this agent calls the `updateRewardsMerkleRoot` function of the `RewardsRegistry` contract with the new rewards distribution message commitment. - After this root is updated, any validator/operator can submit a proof that it is in a leaf of the merkle tree that produced that root, which means it has pending rewards to claim, through the `ServiceManagerBase`'s `claimOperatorRewards` function. - Each operator set of the AVS can have an assigned `RewardsRegistry` contract. Operator sets that do not have an assigned `RewardsRegistry` contract won't be able to received rewards. This PR also adds two separate unit-test suites: one for the added functionality to the `ServiceManagerBase` contract and one specific to the new `RewardsRegistry` contract. > [!CAUTION] The `RewardsAgent` agent is the only one allowed to update the rewards' merkle root, which means if a malicious user could get access to it it could set the pending rewards to be claimed to an arbitrary tree that benefits it. Extreme caution must be taken in the Substrate side so only validated messages are sent to the Ethereum side, as to not allow any users to impersonate being this agent. ### TODO: Ideally, we would use the `RewardsCoordinator` contract from the EigenLayer core to distribute the rewards, but currently that adds a huge overhead for Operators since they'd have to wait for EigenLayer's SideCar to snapshot state and update the distribution root (which happens once a day), generate a proof that they belong to the tree of that distribution root, store it while waiting for the `activationDelay` (currently a week) to pass, and just then they would be able to claim their earned rewards. --------- Co-authored-by: Facundo Farall <37149322+ffarall@users.noreply.github.com>
2025-03-31 19:54:23 +00:00
// Second claim should fail
vm.prank(operatorAddress);
vm.expectRevert(
feat: :sparkles: make `RewardsRegistry` keep a history of roots and claim status (#106) # Description This PR implements a comprehensive overhaul of the `RewardsRegistry` contract to maintain complete history of reward merkle roots while providing index-based claim tracking for operators. The new architecture enables operators to claim rewards from any historical merkle root instead of only the latest one. To do so, it: - Adds the `merkleRootHistory` storage array to the contract, in which we keep all rewards roots that ever came from the DataHaven side. - Adds the `operatorClaimedByIndex` storage map to the contract, in which we keep track, for each validator and root index, if it has claimed it or not. - This works even for new validators, since theoretically with this system you could argue they could claim older roots that they were not a part of which would be catastrophic, but they could never draft a correct proof for those to claim them. - Keeps some of the interface from before the overhaul, to have quick access to the latest rewards merkle root through `getLatestMerkleRoot()` and to claim rewards for it with `claimRewards()`. This is because the expected behaviour is for validators to claim their rewards every era. - Adds a way to batch claim rewards with `claimRewardsBatch()`. This function allows a validator to claim rewards for multiple root indices in one call by providing multiple proofs, useful if the validator has fallen behind claims and has to catch up, although special care will have to be taken by it to avoid reaching the gas limit of a transaction. ## Storage Efficiency Analysis One might think this solution is not as storage-efficient as other solutions that we can think of (I even had two other alternatives in mind as well), but a simple back-of-the-envelope calculation gives us peace of mind that the impact of this solution on the overal state size of the chain is negligible: ### Assumptions (Worst Case Scenario): - 1,000 validators (actual estimate for DataHaven: ~50/100 validators) - 6-hour eras (most-likely scenario, following what Polkadot does: ~24-hour eras) - Which means 4 merkle root updates per day ### Annual Storage Requirements: - Merkle Root History: **46,720 bytes/year** - 4 roots/day × 32 bytes/root × 365 days/year = 46,720 bytes/year - Operator Claim Tracking: **~1.46 MB/year** - 1,000 operators × 1 boolean/(operator * root index) × 1 byte/boolean × 4 root indices/day × 365 days/year = 1,460,000 bytes/year - **Total: ~1.5 MB/year** This represents negligible storage overhead compared to the significant operational benefits gained. ## TODO Since we want to allow the operators/validators to only have to interact with the AVS contract (that's why the `claimRewards` functions have the `onlyAVS` modifier), we still have to: - [x] Add the required functions to the AVS to allow operators to claim their rewards. - [x] Adds comprehensive unit tests for them. --------- Co-authored-by: Steve Degosserie <723552+stiiifff@users.noreply.github.com> Co-authored-by: Ahmad Kaouk <56095276+ahmadkaouk@users.noreply.github.com> Co-authored-by: Ahmad Kaouk <ahmadkaouk.93@gmail.com>
2025-07-10 06:47:39 +00:00
abi.encodeWithSelector(IRewardsRegistryErrors.RewardsAlreadyClaimedForIndex.selector)
);
serviceManager.claimOperatorRewards(
operatorSetId,
0,
operatorPoints,
2, // numberOfLeaves (operator + sibling)
0, // leafIndex (assuming operator leaf comes first)
validProof
);
feat: :sparkles: make `RewardsRegistry` keep a history of roots and claim status (#106) # Description This PR implements a comprehensive overhaul of the `RewardsRegistry` contract to maintain complete history of reward merkle roots while providing index-based claim tracking for operators. The new architecture enables operators to claim rewards from any historical merkle root instead of only the latest one. To do so, it: - Adds the `merkleRootHistory` storage array to the contract, in which we keep all rewards roots that ever came from the DataHaven side. - Adds the `operatorClaimedByIndex` storage map to the contract, in which we keep track, for each validator and root index, if it has claimed it or not. - This works even for new validators, since theoretically with this system you could argue they could claim older roots that they were not a part of which would be catastrophic, but they could never draft a correct proof for those to claim them. - Keeps some of the interface from before the overhaul, to have quick access to the latest rewards merkle root through `getLatestMerkleRoot()` and to claim rewards for it with `claimRewards()`. This is because the expected behaviour is for validators to claim their rewards every era. - Adds a way to batch claim rewards with `claimRewardsBatch()`. This function allows a validator to claim rewards for multiple root indices in one call by providing multiple proofs, useful if the validator has fallen behind claims and has to catch up, although special care will have to be taken by it to avoid reaching the gas limit of a transaction. ## Storage Efficiency Analysis One might think this solution is not as storage-efficient as other solutions that we can think of (I even had two other alternatives in mind as well), but a simple back-of-the-envelope calculation gives us peace of mind that the impact of this solution on the overal state size of the chain is negligible: ### Assumptions (Worst Case Scenario): - 1,000 validators (actual estimate for DataHaven: ~50/100 validators) - 6-hour eras (most-likely scenario, following what Polkadot does: ~24-hour eras) - Which means 4 merkle root updates per day ### Annual Storage Requirements: - Merkle Root History: **46,720 bytes/year** - 4 roots/day × 32 bytes/root × 365 days/year = 46,720 bytes/year - Operator Claim Tracking: **~1.46 MB/year** - 1,000 operators × 1 boolean/(operator * root index) × 1 byte/boolean × 4 root indices/day × 365 days/year = 1,460,000 bytes/year - **Total: ~1.5 MB/year** This represents negligible storage overhead compared to the significant operational benefits gained. ## TODO Since we want to allow the operators/validators to only have to interact with the AVS contract (that's why the `claimRewards` functions have the `onlyAVS` modifier), we still have to: - [x] Add the required functions to the AVS to allow operators to claim their rewards. - [x] Adds comprehensive unit tests for them. --------- Co-authored-by: Steve Degosserie <723552+stiiifff@users.noreply.github.com> Co-authored-by: Ahmad Kaouk <56095276+ahmadkaouk@users.noreply.github.com> Co-authored-by: Ahmad Kaouk <ahmadkaouk.93@gmail.com>
2025-07-10 06:47:39 +00:00
}
function test_claimOperatorRewardsBatch() public {
// Test claiming from multiple different merkle root indices
uint256[] memory rootIndices = new uint256[](3);
rootIndices[0] = 0; // First merkle root
rootIndices[1] = 1; // Second merkle root
rootIndices[2] = 2; // Third merkle root
uint256[] memory points = new uint256[](3);
points[0] = operatorPoints;
points[1] = secondOperatorPoints;
points[2] = thirdOperatorPoints;
bytes32[][] memory proofs = new bytes32[][](3);
proofs[0] = validProof;
proofs[1] = secondValidProof;
proofs[2] = thirdValidProof;
uint256 expectedTotalRewards = operatorPoints + secondOperatorPoints + thirdOperatorPoints;
uint256 initialBalance = operatorAddress.balance;
vm.mockCall(
address(allocationManager),
abi.encodeWithSelector(IAllocationManager.isMemberOfOperatorSet.selector),
abi.encode(true)
);
vm.prank(operatorAddress);
vm.expectEmit(true, true, true, true);
emit RewardsBatchClaimedForIndices(
operatorAddress, rootIndices, points, expectedTotalRewards
);
uint256[] memory widths = new uint256[](3);
widths[0] = 2;
widths[1] = 2;
widths[2] = 2;
uint256[] memory leafIdxs = new uint256[](3);
leafIdxs[0] = 0;
leafIdxs[1] = 0;
leafIdxs[2] = 0;
serviceManager.claimOperatorRewardsBatch(
operatorSetId, rootIndices, points, widths, leafIdxs, proofs
);
feat: :sparkles: make `RewardsRegistry` keep a history of roots and claim status (#106) # Description This PR implements a comprehensive overhaul of the `RewardsRegistry` contract to maintain complete history of reward merkle roots while providing index-based claim tracking for operators. The new architecture enables operators to claim rewards from any historical merkle root instead of only the latest one. To do so, it: - Adds the `merkleRootHistory` storage array to the contract, in which we keep all rewards roots that ever came from the DataHaven side. - Adds the `operatorClaimedByIndex` storage map to the contract, in which we keep track, for each validator and root index, if it has claimed it or not. - This works even for new validators, since theoretically with this system you could argue they could claim older roots that they were not a part of which would be catastrophic, but they could never draft a correct proof for those to claim them. - Keeps some of the interface from before the overhaul, to have quick access to the latest rewards merkle root through `getLatestMerkleRoot()` and to claim rewards for it with `claimRewards()`. This is because the expected behaviour is for validators to claim their rewards every era. - Adds a way to batch claim rewards with `claimRewardsBatch()`. This function allows a validator to claim rewards for multiple root indices in one call by providing multiple proofs, useful if the validator has fallen behind claims and has to catch up, although special care will have to be taken by it to avoid reaching the gas limit of a transaction. ## Storage Efficiency Analysis One might think this solution is not as storage-efficient as other solutions that we can think of (I even had two other alternatives in mind as well), but a simple back-of-the-envelope calculation gives us peace of mind that the impact of this solution on the overal state size of the chain is negligible: ### Assumptions (Worst Case Scenario): - 1,000 validators (actual estimate for DataHaven: ~50/100 validators) - 6-hour eras (most-likely scenario, following what Polkadot does: ~24-hour eras) - Which means 4 merkle root updates per day ### Annual Storage Requirements: - Merkle Root History: **46,720 bytes/year** - 4 roots/day × 32 bytes/root × 365 days/year = 46,720 bytes/year - Operator Claim Tracking: **~1.46 MB/year** - 1,000 operators × 1 boolean/(operator * root index) × 1 byte/boolean × 4 root indices/day × 365 days/year = 1,460,000 bytes/year - **Total: ~1.5 MB/year** This represents negligible storage overhead compared to the significant operational benefits gained. ## TODO Since we want to allow the operators/validators to only have to interact with the AVS contract (that's why the `claimRewards` functions have the `onlyAVS` modifier), we still have to: - [x] Add the required functions to the AVS to allow operators to claim their rewards. - [x] Adds comprehensive unit tests for them. --------- Co-authored-by: Steve Degosserie <723552+stiiifff@users.noreply.github.com> Co-authored-by: Ahmad Kaouk <56095276+ahmadkaouk@users.noreply.github.com> Co-authored-by: Ahmad Kaouk <ahmadkaouk.93@gmail.com>
2025-07-10 06:47:39 +00:00
// Verify final balance includes all rewards
assertEq(
operatorAddress.balance,
initialBalance + expectedTotalRewards,
"Operator should receive rewards from all three claims"
);
// Verify all indices are now claimed
assertTrue(
rewardsRegistry.hasClaimedByIndex(operatorAddress, 0),
"Operator should have claimed from index 0"
);
assertTrue(
rewardsRegistry.hasClaimedByIndex(operatorAddress, 1),
"Operator should have claimed from index 1"
);
assertTrue(
rewardsRegistry.hasClaimedByIndex(operatorAddress, 2),
"Operator should have claimed from index 2"
);
}
function test_claimOperatorRewardsBatch_PartialBatch() public {
// Test claiming from only some of the available merkle roots
uint256[] memory rootIndices = new uint256[](2);
rootIndices[0] = 0; // First merkle root
rootIndices[1] = 2; // Third merkle root (skipping second)
uint256[] memory points = new uint256[](2);
points[0] = operatorPoints;
points[1] = thirdOperatorPoints;
bytes32[][] memory proofs = new bytes32[][](2);
proofs[0] = validProof;
proofs[1] = thirdValidProof;
uint256 expectedTotalRewards = operatorPoints + thirdOperatorPoints;
uint256 initialBalance = operatorAddress.balance;
vm.mockCall(
address(allocationManager),
abi.encodeWithSelector(IAllocationManager.isMemberOfOperatorSet.selector),
abi.encode(true)
);
vm.prank(operatorAddress);
uint256[] memory widths2 = new uint256[](2);
widths2[0] = 2;
widths2[1] = 2;
uint256[] memory leafIdxs2 = new uint256[](2);
leafIdxs2[0] = 0;
leafIdxs2[1] = 0;
serviceManager.claimOperatorRewardsBatch(
operatorSetId, rootIndices, points, widths2, leafIdxs2, proofs
);
feat: :sparkles: make `RewardsRegistry` keep a history of roots and claim status (#106) # Description This PR implements a comprehensive overhaul of the `RewardsRegistry` contract to maintain complete history of reward merkle roots while providing index-based claim tracking for operators. The new architecture enables operators to claim rewards from any historical merkle root instead of only the latest one. To do so, it: - Adds the `merkleRootHistory` storage array to the contract, in which we keep all rewards roots that ever came from the DataHaven side. - Adds the `operatorClaimedByIndex` storage map to the contract, in which we keep track, for each validator and root index, if it has claimed it or not. - This works even for new validators, since theoretically with this system you could argue they could claim older roots that they were not a part of which would be catastrophic, but they could never draft a correct proof for those to claim them. - Keeps some of the interface from before the overhaul, to have quick access to the latest rewards merkle root through `getLatestMerkleRoot()` and to claim rewards for it with `claimRewards()`. This is because the expected behaviour is for validators to claim their rewards every era. - Adds a way to batch claim rewards with `claimRewardsBatch()`. This function allows a validator to claim rewards for multiple root indices in one call by providing multiple proofs, useful if the validator has fallen behind claims and has to catch up, although special care will have to be taken by it to avoid reaching the gas limit of a transaction. ## Storage Efficiency Analysis One might think this solution is not as storage-efficient as other solutions that we can think of (I even had two other alternatives in mind as well), but a simple back-of-the-envelope calculation gives us peace of mind that the impact of this solution on the overal state size of the chain is negligible: ### Assumptions (Worst Case Scenario): - 1,000 validators (actual estimate for DataHaven: ~50/100 validators) - 6-hour eras (most-likely scenario, following what Polkadot does: ~24-hour eras) - Which means 4 merkle root updates per day ### Annual Storage Requirements: - Merkle Root History: **46,720 bytes/year** - 4 roots/day × 32 bytes/root × 365 days/year = 46,720 bytes/year - Operator Claim Tracking: **~1.46 MB/year** - 1,000 operators × 1 boolean/(operator * root index) × 1 byte/boolean × 4 root indices/day × 365 days/year = 1,460,000 bytes/year - **Total: ~1.5 MB/year** This represents negligible storage overhead compared to the significant operational benefits gained. ## TODO Since we want to allow the operators/validators to only have to interact with the AVS contract (that's why the `claimRewards` functions have the `onlyAVS` modifier), we still have to: - [x] Add the required functions to the AVS to allow operators to claim their rewards. - [x] Adds comprehensive unit tests for them. --------- Co-authored-by: Steve Degosserie <723552+stiiifff@users.noreply.github.com> Co-authored-by: Ahmad Kaouk <56095276+ahmadkaouk@users.noreply.github.com> Co-authored-by: Ahmad Kaouk <ahmadkaouk.93@gmail.com>
2025-07-10 06:47:39 +00:00
// Verify balance and claim status
assertEq(
operatorAddress.balance,
initialBalance + expectedTotalRewards,
"Operator should receive rewards from claimed indices"
);
assertTrue(
rewardsRegistry.hasClaimedByIndex(operatorAddress, 0),
"Operator should have claimed from index 0"
);
assertFalse(
rewardsRegistry.hasClaimedByIndex(operatorAddress, 1),
"Operator should NOT have claimed from index 1"
);
assertTrue(
rewardsRegistry.hasClaimedByIndex(operatorAddress, 2),
"Operator should have claimed from index 2"
);
}
function test_claimOperatorRewardsBatch_ArrayLengthMismatch() public {
uint256[] memory rootIndices = new uint256[](2);
uint256[] memory points = new uint256[](1); // Wrong length
bytes32[][] memory proofs = new bytes32[][](2);
vm.mockCall(
address(allocationManager),
abi.encodeWithSelector(IAllocationManager.isMemberOfOperatorSet.selector),
abi.encode(true)
);
vm.prank(operatorAddress);
vm.expectRevert(abi.encodeWithSelector(IRewardsRegistryErrors.ArrayLengthMismatch.selector));
uint256[] memory numLeaves = new uint256[](3);
numLeaves[0] = 2;
numLeaves[1] = 2;
numLeaves[2] = 2;
uint256[] memory leafIndices = new uint256[](3);
leafIndices[0] = 0;
leafIndices[1] = 0;
leafIndices[2] = 0;
serviceManager.claimOperatorRewardsBatch(
operatorSetId, rootIndices, points, numLeaves, leafIndices, proofs
);
feat: :sparkles: make `RewardsRegistry` keep a history of roots and claim status (#106) # Description This PR implements a comprehensive overhaul of the `RewardsRegistry` contract to maintain complete history of reward merkle roots while providing index-based claim tracking for operators. The new architecture enables operators to claim rewards from any historical merkle root instead of only the latest one. To do so, it: - Adds the `merkleRootHistory` storage array to the contract, in which we keep all rewards roots that ever came from the DataHaven side. - Adds the `operatorClaimedByIndex` storage map to the contract, in which we keep track, for each validator and root index, if it has claimed it or not. - This works even for new validators, since theoretically with this system you could argue they could claim older roots that they were not a part of which would be catastrophic, but they could never draft a correct proof for those to claim them. - Keeps some of the interface from before the overhaul, to have quick access to the latest rewards merkle root through `getLatestMerkleRoot()` and to claim rewards for it with `claimRewards()`. This is because the expected behaviour is for validators to claim their rewards every era. - Adds a way to batch claim rewards with `claimRewardsBatch()`. This function allows a validator to claim rewards for multiple root indices in one call by providing multiple proofs, useful if the validator has fallen behind claims and has to catch up, although special care will have to be taken by it to avoid reaching the gas limit of a transaction. ## Storage Efficiency Analysis One might think this solution is not as storage-efficient as other solutions that we can think of (I even had two other alternatives in mind as well), but a simple back-of-the-envelope calculation gives us peace of mind that the impact of this solution on the overal state size of the chain is negligible: ### Assumptions (Worst Case Scenario): - 1,000 validators (actual estimate for DataHaven: ~50/100 validators) - 6-hour eras (most-likely scenario, following what Polkadot does: ~24-hour eras) - Which means 4 merkle root updates per day ### Annual Storage Requirements: - Merkle Root History: **46,720 bytes/year** - 4 roots/day × 32 bytes/root × 365 days/year = 46,720 bytes/year - Operator Claim Tracking: **~1.46 MB/year** - 1,000 operators × 1 boolean/(operator * root index) × 1 byte/boolean × 4 root indices/day × 365 days/year = 1,460,000 bytes/year - **Total: ~1.5 MB/year** This represents negligible storage overhead compared to the significant operational benefits gained. ## TODO Since we want to allow the operators/validators to only have to interact with the AVS contract (that's why the `claimRewards` functions have the `onlyAVS` modifier), we still have to: - [x] Add the required functions to the AVS to allow operators to claim their rewards. - [x] Adds comprehensive unit tests for them. --------- Co-authored-by: Steve Degosserie <723552+stiiifff@users.noreply.github.com> Co-authored-by: Ahmad Kaouk <56095276+ahmadkaouk@users.noreply.github.com> Co-authored-by: Ahmad Kaouk <ahmadkaouk.93@gmail.com>
2025-07-10 06:47:39 +00:00
}
function test_claimOperatorRewardsBatch_AlreadyClaimedIndex() public {
// First claim from index 1
vm.mockCall(
address(allocationManager),
abi.encodeWithSelector(IAllocationManager.isMemberOfOperatorSet.selector),
abi.encode(true)
);
vm.prank(operatorAddress);
serviceManager.claimOperatorRewards(
operatorSetId,
1,
secondOperatorPoints,
2, // numberOfLeaves (operator + sibling)
0, // leafIndex (assuming operator leaf comes first)
secondValidProof
feat: :sparkles: make `RewardsRegistry` keep a history of roots and claim status (#106) # Description This PR implements a comprehensive overhaul of the `RewardsRegistry` contract to maintain complete history of reward merkle roots while providing index-based claim tracking for operators. The new architecture enables operators to claim rewards from any historical merkle root instead of only the latest one. To do so, it: - Adds the `merkleRootHistory` storage array to the contract, in which we keep all rewards roots that ever came from the DataHaven side. - Adds the `operatorClaimedByIndex` storage map to the contract, in which we keep track, for each validator and root index, if it has claimed it or not. - This works even for new validators, since theoretically with this system you could argue they could claim older roots that they were not a part of which would be catastrophic, but they could never draft a correct proof for those to claim them. - Keeps some of the interface from before the overhaul, to have quick access to the latest rewards merkle root through `getLatestMerkleRoot()` and to claim rewards for it with `claimRewards()`. This is because the expected behaviour is for validators to claim their rewards every era. - Adds a way to batch claim rewards with `claimRewardsBatch()`. This function allows a validator to claim rewards for multiple root indices in one call by providing multiple proofs, useful if the validator has fallen behind claims and has to catch up, although special care will have to be taken by it to avoid reaching the gas limit of a transaction. ## Storage Efficiency Analysis One might think this solution is not as storage-efficient as other solutions that we can think of (I even had two other alternatives in mind as well), but a simple back-of-the-envelope calculation gives us peace of mind that the impact of this solution on the overal state size of the chain is negligible: ### Assumptions (Worst Case Scenario): - 1,000 validators (actual estimate for DataHaven: ~50/100 validators) - 6-hour eras (most-likely scenario, following what Polkadot does: ~24-hour eras) - Which means 4 merkle root updates per day ### Annual Storage Requirements: - Merkle Root History: **46,720 bytes/year** - 4 roots/day × 32 bytes/root × 365 days/year = 46,720 bytes/year - Operator Claim Tracking: **~1.46 MB/year** - 1,000 operators × 1 boolean/(operator * root index) × 1 byte/boolean × 4 root indices/day × 365 days/year = 1,460,000 bytes/year - **Total: ~1.5 MB/year** This represents negligible storage overhead compared to the significant operational benefits gained. ## TODO Since we want to allow the operators/validators to only have to interact with the AVS contract (that's why the `claimRewards` functions have the `onlyAVS` modifier), we still have to: - [x] Add the required functions to the AVS to allow operators to claim their rewards. - [x] Adds comprehensive unit tests for them. --------- Co-authored-by: Steve Degosserie <723552+stiiifff@users.noreply.github.com> Co-authored-by: Ahmad Kaouk <56095276+ahmadkaouk@users.noreply.github.com> Co-authored-by: Ahmad Kaouk <ahmadkaouk.93@gmail.com>
2025-07-10 06:47:39 +00:00
);
// Now try to batch claim including the already claimed index 1
uint256[] memory rootIndices = new uint256[](2);
rootIndices[0] = 0;
rootIndices[1] = 1; // Already claimed
uint256[] memory points = new uint256[](2);
points[0] = operatorPoints;
points[1] = secondOperatorPoints;
bytes32[][] memory proofs = new bytes32[][](2);
proofs[0] = validProof;
proofs[1] = secondValidProof;
vm.prank(operatorAddress);
vm.expectRevert(
abi.encodeWithSelector(IRewardsRegistryErrors.RewardsAlreadyClaimedForIndex.selector)
);
uint256[] memory numLeaves = new uint256[](2);
numLeaves[0] = 2;
numLeaves[1] = 2;
uint256[] memory leafIndices = new uint256[](2);
leafIndices[0] = 0;
leafIndices[1] = 0;
serviceManager.claimOperatorRewardsBatch(
operatorSetId, rootIndices, points, numLeaves, leafIndices, proofs
);
feat: :sparkles: make `RewardsRegistry` keep a history of roots and claim status (#106) # Description This PR implements a comprehensive overhaul of the `RewardsRegistry` contract to maintain complete history of reward merkle roots while providing index-based claim tracking for operators. The new architecture enables operators to claim rewards from any historical merkle root instead of only the latest one. To do so, it: - Adds the `merkleRootHistory` storage array to the contract, in which we keep all rewards roots that ever came from the DataHaven side. - Adds the `operatorClaimedByIndex` storage map to the contract, in which we keep track, for each validator and root index, if it has claimed it or not. - This works even for new validators, since theoretically with this system you could argue they could claim older roots that they were not a part of which would be catastrophic, but they could never draft a correct proof for those to claim them. - Keeps some of the interface from before the overhaul, to have quick access to the latest rewards merkle root through `getLatestMerkleRoot()` and to claim rewards for it with `claimRewards()`. This is because the expected behaviour is for validators to claim their rewards every era. - Adds a way to batch claim rewards with `claimRewardsBatch()`. This function allows a validator to claim rewards for multiple root indices in one call by providing multiple proofs, useful if the validator has fallen behind claims and has to catch up, although special care will have to be taken by it to avoid reaching the gas limit of a transaction. ## Storage Efficiency Analysis One might think this solution is not as storage-efficient as other solutions that we can think of (I even had two other alternatives in mind as well), but a simple back-of-the-envelope calculation gives us peace of mind that the impact of this solution on the overal state size of the chain is negligible: ### Assumptions (Worst Case Scenario): - 1,000 validators (actual estimate for DataHaven: ~50/100 validators) - 6-hour eras (most-likely scenario, following what Polkadot does: ~24-hour eras) - Which means 4 merkle root updates per day ### Annual Storage Requirements: - Merkle Root History: **46,720 bytes/year** - 4 roots/day × 32 bytes/root × 365 days/year = 46,720 bytes/year - Operator Claim Tracking: **~1.46 MB/year** - 1,000 operators × 1 boolean/(operator * root index) × 1 byte/boolean × 4 root indices/day × 365 days/year = 1,460,000 bytes/year - **Total: ~1.5 MB/year** This represents negligible storage overhead compared to the significant operational benefits gained. ## TODO Since we want to allow the operators/validators to only have to interact with the AVS contract (that's why the `claimRewards` functions have the `onlyAVS` modifier), we still have to: - [x] Add the required functions to the AVS to allow operators to claim their rewards. - [x] Adds comprehensive unit tests for them. --------- Co-authored-by: Steve Degosserie <723552+stiiifff@users.noreply.github.com> Co-authored-by: Ahmad Kaouk <56095276+ahmadkaouk@users.noreply.github.com> Co-authored-by: Ahmad Kaouk <ahmadkaouk.93@gmail.com>
2025-07-10 06:47:39 +00:00
}
function test_claimOperatorRewardsBatch_EmptyBatch() public {
uint256[] memory rootIndices = new uint256[](0);
uint256[] memory points = new uint256[](0);
bytes32[][] memory proofs = new bytes32[][](0);
vm.mockCall(
address(allocationManager),
abi.encodeWithSelector(IAllocationManager.isMemberOfOperatorSet.selector),
abi.encode(true)
feat: ✨ initial rewards registry (#17) This PR adds an initial implementation for a rewards registry, which will be the contract in charge of allowing DataHaven validators to claim the rewards they earned for being validators in the previous epoch. The logic behind it is as follows: - Whenever an epoch finishes, the corresponding BEEFY block gets relayed to Ethereum through Snowbridge. This BEEFY block contains, in its `extra` field, the merkle root of the tree that contains as leafs all the message commitments of the messages of corresponding block, one of which is the rewards distribution message. - The rewards distribution message commitment is the root of the merkle tree where each leaf is a tuple of the operator ID and the obtained era points in the finished epoch. In this case, the operator ID is the corresponding validator's Ethereum address. - When the rewards distribution message is received, Snowbridge validates it using the aforementioned BEEFY block and then dispatches it. The dispatch invokes the `callContract` function of the `RewardsAgent` agent, with the corresponding parameters so that this agent calls the `updateRewardsMerkleRoot` function of the `RewardsRegistry` contract with the new rewards distribution message commitment. - After this root is updated, any validator/operator can submit a proof that it is in a leaf of the merkle tree that produced that root, which means it has pending rewards to claim, through the `ServiceManagerBase`'s `claimOperatorRewards` function. - Each operator set of the AVS can have an assigned `RewardsRegistry` contract. Operator sets that do not have an assigned `RewardsRegistry` contract won't be able to received rewards. This PR also adds two separate unit-test suites: one for the added functionality to the `ServiceManagerBase` contract and one specific to the new `RewardsRegistry` contract. > [!CAUTION] The `RewardsAgent` agent is the only one allowed to update the rewards' merkle root, which means if a malicious user could get access to it it could set the pending rewards to be claimed to an arbitrary tree that benefits it. Extreme caution must be taken in the Substrate side so only validated messages are sent to the Ethereum side, as to not allow any users to impersonate being this agent. ### TODO: Ideally, we would use the `RewardsCoordinator` contract from the EigenLayer core to distribute the rewards, but currently that adds a huge overhead for Operators since they'd have to wait for EigenLayer's SideCar to snapshot state and update the distribution root (which happens once a day), generate a proof that they belong to the tree of that distribution root, store it while waiting for the `activationDelay` (currently a week) to pass, and just then they would be able to claim their earned rewards. --------- Co-authored-by: Facundo Farall <37149322+ffarall@users.noreply.github.com>
2025-03-31 19:54:23 +00:00
);
feat: :sparkles: make `RewardsRegistry` keep a history of roots and claim status (#106) # Description This PR implements a comprehensive overhaul of the `RewardsRegistry` contract to maintain complete history of reward merkle roots while providing index-based claim tracking for operators. The new architecture enables operators to claim rewards from any historical merkle root instead of only the latest one. To do so, it: - Adds the `merkleRootHistory` storage array to the contract, in which we keep all rewards roots that ever came from the DataHaven side. - Adds the `operatorClaimedByIndex` storage map to the contract, in which we keep track, for each validator and root index, if it has claimed it or not. - This works even for new validators, since theoretically with this system you could argue they could claim older roots that they were not a part of which would be catastrophic, but they could never draft a correct proof for those to claim them. - Keeps some of the interface from before the overhaul, to have quick access to the latest rewards merkle root through `getLatestMerkleRoot()` and to claim rewards for it with `claimRewards()`. This is because the expected behaviour is for validators to claim their rewards every era. - Adds a way to batch claim rewards with `claimRewardsBatch()`. This function allows a validator to claim rewards for multiple root indices in one call by providing multiple proofs, useful if the validator has fallen behind claims and has to catch up, although special care will have to be taken by it to avoid reaching the gas limit of a transaction. ## Storage Efficiency Analysis One might think this solution is not as storage-efficient as other solutions that we can think of (I even had two other alternatives in mind as well), but a simple back-of-the-envelope calculation gives us peace of mind that the impact of this solution on the overal state size of the chain is negligible: ### Assumptions (Worst Case Scenario): - 1,000 validators (actual estimate for DataHaven: ~50/100 validators) - 6-hour eras (most-likely scenario, following what Polkadot does: ~24-hour eras) - Which means 4 merkle root updates per day ### Annual Storage Requirements: - Merkle Root History: **46,720 bytes/year** - 4 roots/day × 32 bytes/root × 365 days/year = 46,720 bytes/year - Operator Claim Tracking: **~1.46 MB/year** - 1,000 operators × 1 boolean/(operator * root index) × 1 byte/boolean × 4 root indices/day × 365 days/year = 1,460,000 bytes/year - **Total: ~1.5 MB/year** This represents negligible storage overhead compared to the significant operational benefits gained. ## TODO Since we want to allow the operators/validators to only have to interact with the AVS contract (that's why the `claimRewards` functions have the `onlyAVS` modifier), we still have to: - [x] Add the required functions to the AVS to allow operators to claim their rewards. - [x] Adds comprehensive unit tests for them. --------- Co-authored-by: Steve Degosserie <723552+stiiifff@users.noreply.github.com> Co-authored-by: Ahmad Kaouk <56095276+ahmadkaouk@users.noreply.github.com> Co-authored-by: Ahmad Kaouk <ahmadkaouk.93@gmail.com>
2025-07-10 06:47:39 +00:00
uint256 initialBalance = operatorAddress.balance;
uint256[] memory numLeaves = new uint256[](0);
uint256[] memory leafIndices = new uint256[](0);
feat: :sparkles: make `RewardsRegistry` keep a history of roots and claim status (#106) # Description This PR implements a comprehensive overhaul of the `RewardsRegistry` contract to maintain complete history of reward merkle roots while providing index-based claim tracking for operators. The new architecture enables operators to claim rewards from any historical merkle root instead of only the latest one. To do so, it: - Adds the `merkleRootHistory` storage array to the contract, in which we keep all rewards roots that ever came from the DataHaven side. - Adds the `operatorClaimedByIndex` storage map to the contract, in which we keep track, for each validator and root index, if it has claimed it or not. - This works even for new validators, since theoretically with this system you could argue they could claim older roots that they were not a part of which would be catastrophic, but they could never draft a correct proof for those to claim them. - Keeps some of the interface from before the overhaul, to have quick access to the latest rewards merkle root through `getLatestMerkleRoot()` and to claim rewards for it with `claimRewards()`. This is because the expected behaviour is for validators to claim their rewards every era. - Adds a way to batch claim rewards with `claimRewardsBatch()`. This function allows a validator to claim rewards for multiple root indices in one call by providing multiple proofs, useful if the validator has fallen behind claims and has to catch up, although special care will have to be taken by it to avoid reaching the gas limit of a transaction. ## Storage Efficiency Analysis One might think this solution is not as storage-efficient as other solutions that we can think of (I even had two other alternatives in mind as well), but a simple back-of-the-envelope calculation gives us peace of mind that the impact of this solution on the overal state size of the chain is negligible: ### Assumptions (Worst Case Scenario): - 1,000 validators (actual estimate for DataHaven: ~50/100 validators) - 6-hour eras (most-likely scenario, following what Polkadot does: ~24-hour eras) - Which means 4 merkle root updates per day ### Annual Storage Requirements: - Merkle Root History: **46,720 bytes/year** - 4 roots/day × 32 bytes/root × 365 days/year = 46,720 bytes/year - Operator Claim Tracking: **~1.46 MB/year** - 1,000 operators × 1 boolean/(operator * root index) × 1 byte/boolean × 4 root indices/day × 365 days/year = 1,460,000 bytes/year - **Total: ~1.5 MB/year** This represents negligible storage overhead compared to the significant operational benefits gained. ## TODO Since we want to allow the operators/validators to only have to interact with the AVS contract (that's why the `claimRewards` functions have the `onlyAVS` modifier), we still have to: - [x] Add the required functions to the AVS to allow operators to claim their rewards. - [x] Adds comprehensive unit tests for them. --------- Co-authored-by: Steve Degosserie <723552+stiiifff@users.noreply.github.com> Co-authored-by: Ahmad Kaouk <56095276+ahmadkaouk@users.noreply.github.com> Co-authored-by: Ahmad Kaouk <ahmadkaouk.93@gmail.com>
2025-07-10 06:47:39 +00:00
vm.prank(operatorAddress);
serviceManager.claimOperatorRewardsBatch(
operatorSetId, rootIndices, points, numLeaves, leafIndices, proofs
);
feat: :sparkles: make `RewardsRegistry` keep a history of roots and claim status (#106) # Description This PR implements a comprehensive overhaul of the `RewardsRegistry` contract to maintain complete history of reward merkle roots while providing index-based claim tracking for operators. The new architecture enables operators to claim rewards from any historical merkle root instead of only the latest one. To do so, it: - Adds the `merkleRootHistory` storage array to the contract, in which we keep all rewards roots that ever came from the DataHaven side. - Adds the `operatorClaimedByIndex` storage map to the contract, in which we keep track, for each validator and root index, if it has claimed it or not. - This works even for new validators, since theoretically with this system you could argue they could claim older roots that they were not a part of which would be catastrophic, but they could never draft a correct proof for those to claim them. - Keeps some of the interface from before the overhaul, to have quick access to the latest rewards merkle root through `getLatestMerkleRoot()` and to claim rewards for it with `claimRewards()`. This is because the expected behaviour is for validators to claim their rewards every era. - Adds a way to batch claim rewards with `claimRewardsBatch()`. This function allows a validator to claim rewards for multiple root indices in one call by providing multiple proofs, useful if the validator has fallen behind claims and has to catch up, although special care will have to be taken by it to avoid reaching the gas limit of a transaction. ## Storage Efficiency Analysis One might think this solution is not as storage-efficient as other solutions that we can think of (I even had two other alternatives in mind as well), but a simple back-of-the-envelope calculation gives us peace of mind that the impact of this solution on the overal state size of the chain is negligible: ### Assumptions (Worst Case Scenario): - 1,000 validators (actual estimate for DataHaven: ~50/100 validators) - 6-hour eras (most-likely scenario, following what Polkadot does: ~24-hour eras) - Which means 4 merkle root updates per day ### Annual Storage Requirements: - Merkle Root History: **46,720 bytes/year** - 4 roots/day × 32 bytes/root × 365 days/year = 46,720 bytes/year - Operator Claim Tracking: **~1.46 MB/year** - 1,000 operators × 1 boolean/(operator * root index) × 1 byte/boolean × 4 root indices/day × 365 days/year = 1,460,000 bytes/year - **Total: ~1.5 MB/year** This represents negligible storage overhead compared to the significant operational benefits gained. ## TODO Since we want to allow the operators/validators to only have to interact with the AVS contract (that's why the `claimRewards` functions have the `onlyAVS` modifier), we still have to: - [x] Add the required functions to the AVS to allow operators to claim their rewards. - [x] Adds comprehensive unit tests for them. --------- Co-authored-by: Steve Degosserie <723552+stiiifff@users.noreply.github.com> Co-authored-by: Ahmad Kaouk <56095276+ahmadkaouk@users.noreply.github.com> Co-authored-by: Ahmad Kaouk <ahmadkaouk.93@gmail.com>
2025-07-10 06:47:39 +00:00
// Balance should remain unchanged
assertEq(
operatorAddress.balance,
initialBalance,
"Balance should remain unchanged for empty batch"
);
feat: ✨ initial rewards registry (#17) This PR adds an initial implementation for a rewards registry, which will be the contract in charge of allowing DataHaven validators to claim the rewards they earned for being validators in the previous epoch. The logic behind it is as follows: - Whenever an epoch finishes, the corresponding BEEFY block gets relayed to Ethereum through Snowbridge. This BEEFY block contains, in its `extra` field, the merkle root of the tree that contains as leafs all the message commitments of the messages of corresponding block, one of which is the rewards distribution message. - The rewards distribution message commitment is the root of the merkle tree where each leaf is a tuple of the operator ID and the obtained era points in the finished epoch. In this case, the operator ID is the corresponding validator's Ethereum address. - When the rewards distribution message is received, Snowbridge validates it using the aforementioned BEEFY block and then dispatches it. The dispatch invokes the `callContract` function of the `RewardsAgent` agent, with the corresponding parameters so that this agent calls the `updateRewardsMerkleRoot` function of the `RewardsRegistry` contract with the new rewards distribution message commitment. - After this root is updated, any validator/operator can submit a proof that it is in a leaf of the merkle tree that produced that root, which means it has pending rewards to claim, through the `ServiceManagerBase`'s `claimOperatorRewards` function. - Each operator set of the AVS can have an assigned `RewardsRegistry` contract. Operator sets that do not have an assigned `RewardsRegistry` contract won't be able to received rewards. This PR also adds two separate unit-test suites: one for the added functionality to the `ServiceManagerBase` contract and one specific to the new `RewardsRegistry` contract. > [!CAUTION] The `RewardsAgent` agent is the only one allowed to update the rewards' merkle root, which means if a malicious user could get access to it it could set the pending rewards to be claimed to an arbitrary tree that benefits it. Extreme caution must be taken in the Substrate side so only validated messages are sent to the Ethereum side, as to not allow any users to impersonate being this agent. ### TODO: Ideally, we would use the `RewardsCoordinator` contract from the EigenLayer core to distribute the rewards, but currently that adds a huge overhead for Operators since they'd have to wait for EigenLayer's SideCar to snapshot state and update the distribution root (which happens once a day), generate a proof that they belong to the tree of that distribution root, store it while waiting for the `activationDelay` (currently a week) to pass, and just then they would be able to claim their earned rewards. --------- Co-authored-by: Facundo Farall <37149322+ffarall@users.noreply.github.com>
2025-03-31 19:54:23 +00:00
}
function test_integration_multipleOperatorSets() public {
// Set up a second operator set with a different registry
uint32 secondOperatorSetId = 2;
RewardsRegistry secondRegistry =
new RewardsRegistry(address(serviceManager), mockRewardsAgent);
feat: ✨ initial rewards registry (#17) This PR adds an initial implementation for a rewards registry, which will be the contract in charge of allowing DataHaven validators to claim the rewards they earned for being validators in the previous epoch. The logic behind it is as follows: - Whenever an epoch finishes, the corresponding BEEFY block gets relayed to Ethereum through Snowbridge. This BEEFY block contains, in its `extra` field, the merkle root of the tree that contains as leafs all the message commitments of the messages of corresponding block, one of which is the rewards distribution message. - The rewards distribution message commitment is the root of the merkle tree where each leaf is a tuple of the operator ID and the obtained era points in the finished epoch. In this case, the operator ID is the corresponding validator's Ethereum address. - When the rewards distribution message is received, Snowbridge validates it using the aforementioned BEEFY block and then dispatches it. The dispatch invokes the `callContract` function of the `RewardsAgent` agent, with the corresponding parameters so that this agent calls the `updateRewardsMerkleRoot` function of the `RewardsRegistry` contract with the new rewards distribution message commitment. - After this root is updated, any validator/operator can submit a proof that it is in a leaf of the merkle tree that produced that root, which means it has pending rewards to claim, through the `ServiceManagerBase`'s `claimOperatorRewards` function. - Each operator set of the AVS can have an assigned `RewardsRegistry` contract. Operator sets that do not have an assigned `RewardsRegistry` contract won't be able to received rewards. This PR also adds two separate unit-test suites: one for the added functionality to the `ServiceManagerBase` contract and one specific to the new `RewardsRegistry` contract. > [!CAUTION] The `RewardsAgent` agent is the only one allowed to update the rewards' merkle root, which means if a malicious user could get access to it it could set the pending rewards to be claimed to an arbitrary tree that benefits it. Extreme caution must be taken in the Substrate side so only validated messages are sent to the Ethereum side, as to not allow any users to impersonate being this agent. ### TODO: Ideally, we would use the `RewardsCoordinator` contract from the EigenLayer core to distribute the rewards, but currently that adds a huge overhead for Operators since they'd have to wait for EigenLayer's SideCar to snapshot state and update the distribution root (which happens once a day), generate a proof that they belong to the tree of that distribution root, store it while waiting for the `activationDelay` (currently a week) to pass, and just then they would be able to claim their earned rewards. --------- Co-authored-by: Facundo Farall <37149322+ffarall@users.noreply.github.com>
2025-03-31 19:54:23 +00:00
// Set up the second registry
vm.prank(avsOwner);
serviceManager.setRewardsRegistry(
secondOperatorSetId, IRewardsRegistry(address(secondRegistry))
);
// Create a different merkle root for the second registry using SCALE encoding
bytes memory secondLeafPreimage =
abi.encodePacked(operatorAddress, ScaleCodec.encodeU32(uint32(operatorPoints)));
bytes32 secondLeaf = keccak256(secondLeafPreimage);
bytes memory secondSiblingPreimage =
abi.encodePacked(address(0x4444), ScaleCodec.encodeU32(uint32(80)));
bytes32 secondSiblingLeaf = keccak256(secondSiblingPreimage);
// Since leafIndex = 0, our leaf is on the left
bytes32 secondRegistryMerkleRoot =
keccak256(abi.encodePacked(secondLeaf, secondSiblingLeaf));
feat: ✨ initial rewards registry (#17) This PR adds an initial implementation for a rewards registry, which will be the contract in charge of allowing DataHaven validators to claim the rewards they earned for being validators in the previous epoch. The logic behind it is as follows: - Whenever an epoch finishes, the corresponding BEEFY block gets relayed to Ethereum through Snowbridge. This BEEFY block contains, in its `extra` field, the merkle root of the tree that contains as leafs all the message commitments of the messages of corresponding block, one of which is the rewards distribution message. - The rewards distribution message commitment is the root of the merkle tree where each leaf is a tuple of the operator ID and the obtained era points in the finished epoch. In this case, the operator ID is the corresponding validator's Ethereum address. - When the rewards distribution message is received, Snowbridge validates it using the aforementioned BEEFY block and then dispatches it. The dispatch invokes the `callContract` function of the `RewardsAgent` agent, with the corresponding parameters so that this agent calls the `updateRewardsMerkleRoot` function of the `RewardsRegistry` contract with the new rewards distribution message commitment. - After this root is updated, any validator/operator can submit a proof that it is in a leaf of the merkle tree that produced that root, which means it has pending rewards to claim, through the `ServiceManagerBase`'s `claimOperatorRewards` function. - Each operator set of the AVS can have an assigned `RewardsRegistry` contract. Operator sets that do not have an assigned `RewardsRegistry` contract won't be able to received rewards. This PR also adds two separate unit-test suites: one for the added functionality to the `ServiceManagerBase` contract and one specific to the new `RewardsRegistry` contract. > [!CAUTION] The `RewardsAgent` agent is the only one allowed to update the rewards' merkle root, which means if a malicious user could get access to it it could set the pending rewards to be claimed to an arbitrary tree that benefits it. Extreme caution must be taken in the Substrate side so only validated messages are sent to the Ethereum side, as to not allow any users to impersonate being this agent. ### TODO: Ideally, we would use the `RewardsCoordinator` contract from the EigenLayer core to distribute the rewards, but currently that adds a huge overhead for Operators since they'd have to wait for EigenLayer's SideCar to snapshot state and update the distribution root (which happens once a day), generate a proof that they belong to the tree of that distribution root, store it while waiting for the `activationDelay` (currently a week) to pass, and just then they would be able to claim their earned rewards. --------- Co-authored-by: Facundo Farall <37149322+ffarall@users.noreply.github.com>
2025-03-31 19:54:23 +00:00
// Set the merkle root in the second registry
vm.prank(mockRewardsAgent);
feat: :sparkles: make `RewardsRegistry` keep a history of roots and claim status (#106) # Description This PR implements a comprehensive overhaul of the `RewardsRegistry` contract to maintain complete history of reward merkle roots while providing index-based claim tracking for operators. The new architecture enables operators to claim rewards from any historical merkle root instead of only the latest one. To do so, it: - Adds the `merkleRootHistory` storage array to the contract, in which we keep all rewards roots that ever came from the DataHaven side. - Adds the `operatorClaimedByIndex` storage map to the contract, in which we keep track, for each validator and root index, if it has claimed it or not. - This works even for new validators, since theoretically with this system you could argue they could claim older roots that they were not a part of which would be catastrophic, but they could never draft a correct proof for those to claim them. - Keeps some of the interface from before the overhaul, to have quick access to the latest rewards merkle root through `getLatestMerkleRoot()` and to claim rewards for it with `claimRewards()`. This is because the expected behaviour is for validators to claim their rewards every era. - Adds a way to batch claim rewards with `claimRewardsBatch()`. This function allows a validator to claim rewards for multiple root indices in one call by providing multiple proofs, useful if the validator has fallen behind claims and has to catch up, although special care will have to be taken by it to avoid reaching the gas limit of a transaction. ## Storage Efficiency Analysis One might think this solution is not as storage-efficient as other solutions that we can think of (I even had two other alternatives in mind as well), but a simple back-of-the-envelope calculation gives us peace of mind that the impact of this solution on the overal state size of the chain is negligible: ### Assumptions (Worst Case Scenario): - 1,000 validators (actual estimate for DataHaven: ~50/100 validators) - 6-hour eras (most-likely scenario, following what Polkadot does: ~24-hour eras) - Which means 4 merkle root updates per day ### Annual Storage Requirements: - Merkle Root History: **46,720 bytes/year** - 4 roots/day × 32 bytes/root × 365 days/year = 46,720 bytes/year - Operator Claim Tracking: **~1.46 MB/year** - 1,000 operators × 1 boolean/(operator * root index) × 1 byte/boolean × 4 root indices/day × 365 days/year = 1,460,000 bytes/year - **Total: ~1.5 MB/year** This represents negligible storage overhead compared to the significant operational benefits gained. ## TODO Since we want to allow the operators/validators to only have to interact with the AVS contract (that's why the `claimRewards` functions have the `onlyAVS` modifier), we still have to: - [x] Add the required functions to the AVS to allow operators to claim their rewards. - [x] Adds comprehensive unit tests for them. --------- Co-authored-by: Steve Degosserie <723552+stiiifff@users.noreply.github.com> Co-authored-by: Ahmad Kaouk <56095276+ahmadkaouk@users.noreply.github.com> Co-authored-by: Ahmad Kaouk <ahmadkaouk.93@gmail.com>
2025-07-10 06:47:39 +00:00
secondRegistry.updateRewardsMerkleRoot(secondRegistryMerkleRoot);
feat: ✨ initial rewards registry (#17) This PR adds an initial implementation for a rewards registry, which will be the contract in charge of allowing DataHaven validators to claim the rewards they earned for being validators in the previous epoch. The logic behind it is as follows: - Whenever an epoch finishes, the corresponding BEEFY block gets relayed to Ethereum through Snowbridge. This BEEFY block contains, in its `extra` field, the merkle root of the tree that contains as leafs all the message commitments of the messages of corresponding block, one of which is the rewards distribution message. - The rewards distribution message commitment is the root of the merkle tree where each leaf is a tuple of the operator ID and the obtained era points in the finished epoch. In this case, the operator ID is the corresponding validator's Ethereum address. - When the rewards distribution message is received, Snowbridge validates it using the aforementioned BEEFY block and then dispatches it. The dispatch invokes the `callContract` function of the `RewardsAgent` agent, with the corresponding parameters so that this agent calls the `updateRewardsMerkleRoot` function of the `RewardsRegistry` contract with the new rewards distribution message commitment. - After this root is updated, any validator/operator can submit a proof that it is in a leaf of the merkle tree that produced that root, which means it has pending rewards to claim, through the `ServiceManagerBase`'s `claimOperatorRewards` function. - Each operator set of the AVS can have an assigned `RewardsRegistry` contract. Operator sets that do not have an assigned `RewardsRegistry` contract won't be able to received rewards. This PR also adds two separate unit-test suites: one for the added functionality to the `ServiceManagerBase` contract and one specific to the new `RewardsRegistry` contract. > [!CAUTION] The `RewardsAgent` agent is the only one allowed to update the rewards' merkle root, which means if a malicious user could get access to it it could set the pending rewards to be claimed to an arbitrary tree that benefits it. Extreme caution must be taken in the Substrate side so only validated messages are sent to the Ethereum side, as to not allow any users to impersonate being this agent. ### TODO: Ideally, we would use the `RewardsCoordinator` contract from the EigenLayer core to distribute the rewards, but currently that adds a huge overhead for Operators since they'd have to wait for EigenLayer's SideCar to snapshot state and update the distribution root (which happens once a day), generate a proof that they belong to the tree of that distribution root, store it while waiting for the `activationDelay` (currently a week) to pass, and just then they would be able to claim their earned rewards. --------- Co-authored-by: Facundo Farall <37149322+ffarall@users.noreply.github.com>
2025-03-31 19:54:23 +00:00
// Fund the second registry
vm.deal(address(secondRegistry), 1000 ether);
// Create proof for second registry
bytes32[] memory secondProof = new bytes32[](1);
secondProof[0] = secondSiblingLeaf;
feat: :sparkles: make `RewardsRegistry` keep a history of roots and claim status (#106) # Description This PR implements a comprehensive overhaul of the `RewardsRegistry` contract to maintain complete history of reward merkle roots while providing index-based claim tracking for operators. The new architecture enables operators to claim rewards from any historical merkle root instead of only the latest one. To do so, it: - Adds the `merkleRootHistory` storage array to the contract, in which we keep all rewards roots that ever came from the DataHaven side. - Adds the `operatorClaimedByIndex` storage map to the contract, in which we keep track, for each validator and root index, if it has claimed it or not. - This works even for new validators, since theoretically with this system you could argue they could claim older roots that they were not a part of which would be catastrophic, but they could never draft a correct proof for those to claim them. - Keeps some of the interface from before the overhaul, to have quick access to the latest rewards merkle root through `getLatestMerkleRoot()` and to claim rewards for it with `claimRewards()`. This is because the expected behaviour is for validators to claim their rewards every era. - Adds a way to batch claim rewards with `claimRewardsBatch()`. This function allows a validator to claim rewards for multiple root indices in one call by providing multiple proofs, useful if the validator has fallen behind claims and has to catch up, although special care will have to be taken by it to avoid reaching the gas limit of a transaction. ## Storage Efficiency Analysis One might think this solution is not as storage-efficient as other solutions that we can think of (I even had two other alternatives in mind as well), but a simple back-of-the-envelope calculation gives us peace of mind that the impact of this solution on the overal state size of the chain is negligible: ### Assumptions (Worst Case Scenario): - 1,000 validators (actual estimate for DataHaven: ~50/100 validators) - 6-hour eras (most-likely scenario, following what Polkadot does: ~24-hour eras) - Which means 4 merkle root updates per day ### Annual Storage Requirements: - Merkle Root History: **46,720 bytes/year** - 4 roots/day × 32 bytes/root × 365 days/year = 46,720 bytes/year - Operator Claim Tracking: **~1.46 MB/year** - 1,000 operators × 1 boolean/(operator * root index) × 1 byte/boolean × 4 root indices/day × 365 days/year = 1,460,000 bytes/year - **Total: ~1.5 MB/year** This represents negligible storage overhead compared to the significant operational benefits gained. ## TODO Since we want to allow the operators/validators to only have to interact with the AVS contract (that's why the `claimRewards` functions have the `onlyAVS` modifier), we still have to: - [x] Add the required functions to the AVS to allow operators to claim their rewards. - [x] Adds comprehensive unit tests for them. --------- Co-authored-by: Steve Degosserie <723552+stiiifff@users.noreply.github.com> Co-authored-by: Ahmad Kaouk <56095276+ahmadkaouk@users.noreply.github.com> Co-authored-by: Ahmad Kaouk <ahmadkaouk.93@gmail.com>
2025-07-10 06:47:39 +00:00
// Claim from first registry (uses latest merkle root - index 2)
feat: ✨ initial rewards registry (#17) This PR adds an initial implementation for a rewards registry, which will be the contract in charge of allowing DataHaven validators to claim the rewards they earned for being validators in the previous epoch. The logic behind it is as follows: - Whenever an epoch finishes, the corresponding BEEFY block gets relayed to Ethereum through Snowbridge. This BEEFY block contains, in its `extra` field, the merkle root of the tree that contains as leafs all the message commitments of the messages of corresponding block, one of which is the rewards distribution message. - The rewards distribution message commitment is the root of the merkle tree where each leaf is a tuple of the operator ID and the obtained era points in the finished epoch. In this case, the operator ID is the corresponding validator's Ethereum address. - When the rewards distribution message is received, Snowbridge validates it using the aforementioned BEEFY block and then dispatches it. The dispatch invokes the `callContract` function of the `RewardsAgent` agent, with the corresponding parameters so that this agent calls the `updateRewardsMerkleRoot` function of the `RewardsRegistry` contract with the new rewards distribution message commitment. - After this root is updated, any validator/operator can submit a proof that it is in a leaf of the merkle tree that produced that root, which means it has pending rewards to claim, through the `ServiceManagerBase`'s `claimOperatorRewards` function. - Each operator set of the AVS can have an assigned `RewardsRegistry` contract. Operator sets that do not have an assigned `RewardsRegistry` contract won't be able to received rewards. This PR also adds two separate unit-test suites: one for the added functionality to the `ServiceManagerBase` contract and one specific to the new `RewardsRegistry` contract. > [!CAUTION] The `RewardsAgent` agent is the only one allowed to update the rewards' merkle root, which means if a malicious user could get access to it it could set the pending rewards to be claimed to an arbitrary tree that benefits it. Extreme caution must be taken in the Substrate side so only validated messages are sent to the Ethereum side, as to not allow any users to impersonate being this agent. ### TODO: Ideally, we would use the `RewardsCoordinator` contract from the EigenLayer core to distribute the rewards, but currently that adds a huge overhead for Operators since they'd have to wait for EigenLayer's SideCar to snapshot state and update the distribution root (which happens once a day), generate a proof that they belong to the tree of that distribution root, store it while waiting for the `activationDelay` (currently a week) to pass, and just then they would be able to claim their earned rewards. --------- Co-authored-by: Facundo Farall <37149322+ffarall@users.noreply.github.com>
2025-03-31 19:54:23 +00:00
uint256 initialBalance = operatorAddress.balance;
vm.mockCall(
address(allocationManager),
abi.encodeWithSelector(IAllocationManager.isMemberOfOperatorSet.selector),
abi.encode(true)
);
feat: ✨ initial rewards registry (#17) This PR adds an initial implementation for a rewards registry, which will be the contract in charge of allowing DataHaven validators to claim the rewards they earned for being validators in the previous epoch. The logic behind it is as follows: - Whenever an epoch finishes, the corresponding BEEFY block gets relayed to Ethereum through Snowbridge. This BEEFY block contains, in its `extra` field, the merkle root of the tree that contains as leafs all the message commitments of the messages of corresponding block, one of which is the rewards distribution message. - The rewards distribution message commitment is the root of the merkle tree where each leaf is a tuple of the operator ID and the obtained era points in the finished epoch. In this case, the operator ID is the corresponding validator's Ethereum address. - When the rewards distribution message is received, Snowbridge validates it using the aforementioned BEEFY block and then dispatches it. The dispatch invokes the `callContract` function of the `RewardsAgent` agent, with the corresponding parameters so that this agent calls the `updateRewardsMerkleRoot` function of the `RewardsRegistry` contract with the new rewards distribution message commitment. - After this root is updated, any validator/operator can submit a proof that it is in a leaf of the merkle tree that produced that root, which means it has pending rewards to claim, through the `ServiceManagerBase`'s `claimOperatorRewards` function. - Each operator set of the AVS can have an assigned `RewardsRegistry` contract. Operator sets that do not have an assigned `RewardsRegistry` contract won't be able to received rewards. This PR also adds two separate unit-test suites: one for the added functionality to the `ServiceManagerBase` contract and one specific to the new `RewardsRegistry` contract. > [!CAUTION] The `RewardsAgent` agent is the only one allowed to update the rewards' merkle root, which means if a malicious user could get access to it it could set the pending rewards to be claimed to an arbitrary tree that benefits it. Extreme caution must be taken in the Substrate side so only validated messages are sent to the Ethereum side, as to not allow any users to impersonate being this agent. ### TODO: Ideally, we would use the `RewardsCoordinator` contract from the EigenLayer core to distribute the rewards, but currently that adds a huge overhead for Operators since they'd have to wait for EigenLayer's SideCar to snapshot state and update the distribution root (which happens once a day), generate a proof that they belong to the tree of that distribution root, store it while waiting for the `activationDelay` (currently a week) to pass, and just then they would be able to claim their earned rewards. --------- Co-authored-by: Facundo Farall <37149322+ffarall@users.noreply.github.com>
2025-03-31 19:54:23 +00:00
vm.prank(operatorAddress);
feat: :sparkles: make `RewardsRegistry` keep a history of roots and claim status (#106) # Description This PR implements a comprehensive overhaul of the `RewardsRegistry` contract to maintain complete history of reward merkle roots while providing index-based claim tracking for operators. The new architecture enables operators to claim rewards from any historical merkle root instead of only the latest one. To do so, it: - Adds the `merkleRootHistory` storage array to the contract, in which we keep all rewards roots that ever came from the DataHaven side. - Adds the `operatorClaimedByIndex` storage map to the contract, in which we keep track, for each validator and root index, if it has claimed it or not. - This works even for new validators, since theoretically with this system you could argue they could claim older roots that they were not a part of which would be catastrophic, but they could never draft a correct proof for those to claim them. - Keeps some of the interface from before the overhaul, to have quick access to the latest rewards merkle root through `getLatestMerkleRoot()` and to claim rewards for it with `claimRewards()`. This is because the expected behaviour is for validators to claim their rewards every era. - Adds a way to batch claim rewards with `claimRewardsBatch()`. This function allows a validator to claim rewards for multiple root indices in one call by providing multiple proofs, useful if the validator has fallen behind claims and has to catch up, although special care will have to be taken by it to avoid reaching the gas limit of a transaction. ## Storage Efficiency Analysis One might think this solution is not as storage-efficient as other solutions that we can think of (I even had two other alternatives in mind as well), but a simple back-of-the-envelope calculation gives us peace of mind that the impact of this solution on the overal state size of the chain is negligible: ### Assumptions (Worst Case Scenario): - 1,000 validators (actual estimate for DataHaven: ~50/100 validators) - 6-hour eras (most-likely scenario, following what Polkadot does: ~24-hour eras) - Which means 4 merkle root updates per day ### Annual Storage Requirements: - Merkle Root History: **46,720 bytes/year** - 4 roots/day × 32 bytes/root × 365 days/year = 46,720 bytes/year - Operator Claim Tracking: **~1.46 MB/year** - 1,000 operators × 1 boolean/(operator * root index) × 1 byte/boolean × 4 root indices/day × 365 days/year = 1,460,000 bytes/year - **Total: ~1.5 MB/year** This represents negligible storage overhead compared to the significant operational benefits gained. ## TODO Since we want to allow the operators/validators to only have to interact with the AVS contract (that's why the `claimRewards` functions have the `onlyAVS` modifier), we still have to: - [x] Add the required functions to the AVS to allow operators to claim their rewards. - [x] Adds comprehensive unit tests for them. --------- Co-authored-by: Steve Degosserie <723552+stiiifff@users.noreply.github.com> Co-authored-by: Ahmad Kaouk <56095276+ahmadkaouk@users.noreply.github.com> Co-authored-by: Ahmad Kaouk <ahmadkaouk.93@gmail.com>
2025-07-10 06:47:39 +00:00
serviceManager.claimLatestOperatorRewards(
operatorSetId, thirdOperatorPoints, numberOfLeaves, leafIndex, thirdValidProof
feat: :sparkles: make `RewardsRegistry` keep a history of roots and claim status (#106) # Description This PR implements a comprehensive overhaul of the `RewardsRegistry` contract to maintain complete history of reward merkle roots while providing index-based claim tracking for operators. The new architecture enables operators to claim rewards from any historical merkle root instead of only the latest one. To do so, it: - Adds the `merkleRootHistory` storage array to the contract, in which we keep all rewards roots that ever came from the DataHaven side. - Adds the `operatorClaimedByIndex` storage map to the contract, in which we keep track, for each validator and root index, if it has claimed it or not. - This works even for new validators, since theoretically with this system you could argue they could claim older roots that they were not a part of which would be catastrophic, but they could never draft a correct proof for those to claim them. - Keeps some of the interface from before the overhaul, to have quick access to the latest rewards merkle root through `getLatestMerkleRoot()` and to claim rewards for it with `claimRewards()`. This is because the expected behaviour is for validators to claim their rewards every era. - Adds a way to batch claim rewards with `claimRewardsBatch()`. This function allows a validator to claim rewards for multiple root indices in one call by providing multiple proofs, useful if the validator has fallen behind claims and has to catch up, although special care will have to be taken by it to avoid reaching the gas limit of a transaction. ## Storage Efficiency Analysis One might think this solution is not as storage-efficient as other solutions that we can think of (I even had two other alternatives in mind as well), but a simple back-of-the-envelope calculation gives us peace of mind that the impact of this solution on the overal state size of the chain is negligible: ### Assumptions (Worst Case Scenario): - 1,000 validators (actual estimate for DataHaven: ~50/100 validators) - 6-hour eras (most-likely scenario, following what Polkadot does: ~24-hour eras) - Which means 4 merkle root updates per day ### Annual Storage Requirements: - Merkle Root History: **46,720 bytes/year** - 4 roots/day × 32 bytes/root × 365 days/year = 46,720 bytes/year - Operator Claim Tracking: **~1.46 MB/year** - 1,000 operators × 1 boolean/(operator * root index) × 1 byte/boolean × 4 root indices/day × 365 days/year = 1,460,000 bytes/year - **Total: ~1.5 MB/year** This represents negligible storage overhead compared to the significant operational benefits gained. ## TODO Since we want to allow the operators/validators to only have to interact with the AVS contract (that's why the `claimRewards` functions have the `onlyAVS` modifier), we still have to: - [x] Add the required functions to the AVS to allow operators to claim their rewards. - [x] Adds comprehensive unit tests for them. --------- Co-authored-by: Steve Degosserie <723552+stiiifff@users.noreply.github.com> Co-authored-by: Ahmad Kaouk <56095276+ahmadkaouk@users.noreply.github.com> Co-authored-by: Ahmad Kaouk <ahmadkaouk.93@gmail.com>
2025-07-10 06:47:39 +00:00
); // Use latest root
feat: ✨ initial rewards registry (#17) This PR adds an initial implementation for a rewards registry, which will be the contract in charge of allowing DataHaven validators to claim the rewards they earned for being validators in the previous epoch. The logic behind it is as follows: - Whenever an epoch finishes, the corresponding BEEFY block gets relayed to Ethereum through Snowbridge. This BEEFY block contains, in its `extra` field, the merkle root of the tree that contains as leafs all the message commitments of the messages of corresponding block, one of which is the rewards distribution message. - The rewards distribution message commitment is the root of the merkle tree where each leaf is a tuple of the operator ID and the obtained era points in the finished epoch. In this case, the operator ID is the corresponding validator's Ethereum address. - When the rewards distribution message is received, Snowbridge validates it using the aforementioned BEEFY block and then dispatches it. The dispatch invokes the `callContract` function of the `RewardsAgent` agent, with the corresponding parameters so that this agent calls the `updateRewardsMerkleRoot` function of the `RewardsRegistry` contract with the new rewards distribution message commitment. - After this root is updated, any validator/operator can submit a proof that it is in a leaf of the merkle tree that produced that root, which means it has pending rewards to claim, through the `ServiceManagerBase`'s `claimOperatorRewards` function. - Each operator set of the AVS can have an assigned `RewardsRegistry` contract. Operator sets that do not have an assigned `RewardsRegistry` contract won't be able to received rewards. This PR also adds two separate unit-test suites: one for the added functionality to the `ServiceManagerBase` contract and one specific to the new `RewardsRegistry` contract. > [!CAUTION] The `RewardsAgent` agent is the only one allowed to update the rewards' merkle root, which means if a malicious user could get access to it it could set the pending rewards to be claimed to an arbitrary tree that benefits it. Extreme caution must be taken in the Substrate side so only validated messages are sent to the Ethereum side, as to not allow any users to impersonate being this agent. ### TODO: Ideally, we would use the `RewardsCoordinator` contract from the EigenLayer core to distribute the rewards, but currently that adds a huge overhead for Operators since they'd have to wait for EigenLayer's SideCar to snapshot state and update the distribution root (which happens once a day), generate a proof that they belong to the tree of that distribution root, store it while waiting for the `activationDelay` (currently a week) to pass, and just then they would be able to claim their earned rewards. --------- Co-authored-by: Facundo Farall <37149322+ffarall@users.noreply.github.com>
2025-03-31 19:54:23 +00:00
// Verify balance after first claim
assertEq(
operatorAddress.balance,
feat: :sparkles: make `RewardsRegistry` keep a history of roots and claim status (#106) # Description This PR implements a comprehensive overhaul of the `RewardsRegistry` contract to maintain complete history of reward merkle roots while providing index-based claim tracking for operators. The new architecture enables operators to claim rewards from any historical merkle root instead of only the latest one. To do so, it: - Adds the `merkleRootHistory` storage array to the contract, in which we keep all rewards roots that ever came from the DataHaven side. - Adds the `operatorClaimedByIndex` storage map to the contract, in which we keep track, for each validator and root index, if it has claimed it or not. - This works even for new validators, since theoretically with this system you could argue they could claim older roots that they were not a part of which would be catastrophic, but they could never draft a correct proof for those to claim them. - Keeps some of the interface from before the overhaul, to have quick access to the latest rewards merkle root through `getLatestMerkleRoot()` and to claim rewards for it with `claimRewards()`. This is because the expected behaviour is for validators to claim their rewards every era. - Adds a way to batch claim rewards with `claimRewardsBatch()`. This function allows a validator to claim rewards for multiple root indices in one call by providing multiple proofs, useful if the validator has fallen behind claims and has to catch up, although special care will have to be taken by it to avoid reaching the gas limit of a transaction. ## Storage Efficiency Analysis One might think this solution is not as storage-efficient as other solutions that we can think of (I even had two other alternatives in mind as well), but a simple back-of-the-envelope calculation gives us peace of mind that the impact of this solution on the overal state size of the chain is negligible: ### Assumptions (Worst Case Scenario): - 1,000 validators (actual estimate for DataHaven: ~50/100 validators) - 6-hour eras (most-likely scenario, following what Polkadot does: ~24-hour eras) - Which means 4 merkle root updates per day ### Annual Storage Requirements: - Merkle Root History: **46,720 bytes/year** - 4 roots/day × 32 bytes/root × 365 days/year = 46,720 bytes/year - Operator Claim Tracking: **~1.46 MB/year** - 1,000 operators × 1 boolean/(operator * root index) × 1 byte/boolean × 4 root indices/day × 365 days/year = 1,460,000 bytes/year - **Total: ~1.5 MB/year** This represents negligible storage overhead compared to the significant operational benefits gained. ## TODO Since we want to allow the operators/validators to only have to interact with the AVS contract (that's why the `claimRewards` functions have the `onlyAVS` modifier), we still have to: - [x] Add the required functions to the AVS to allow operators to claim their rewards. - [x] Adds comprehensive unit tests for them. --------- Co-authored-by: Steve Degosserie <723552+stiiifff@users.noreply.github.com> Co-authored-by: Ahmad Kaouk <56095276+ahmadkaouk@users.noreply.github.com> Co-authored-by: Ahmad Kaouk <ahmadkaouk.93@gmail.com>
2025-07-10 06:47:39 +00:00
initialBalance + thirdOperatorPoints,
feat: ✨ initial rewards registry (#17) This PR adds an initial implementation for a rewards registry, which will be the contract in charge of allowing DataHaven validators to claim the rewards they earned for being validators in the previous epoch. The logic behind it is as follows: - Whenever an epoch finishes, the corresponding BEEFY block gets relayed to Ethereum through Snowbridge. This BEEFY block contains, in its `extra` field, the merkle root of the tree that contains as leafs all the message commitments of the messages of corresponding block, one of which is the rewards distribution message. - The rewards distribution message commitment is the root of the merkle tree where each leaf is a tuple of the operator ID and the obtained era points in the finished epoch. In this case, the operator ID is the corresponding validator's Ethereum address. - When the rewards distribution message is received, Snowbridge validates it using the aforementioned BEEFY block and then dispatches it. The dispatch invokes the `callContract` function of the `RewardsAgent` agent, with the corresponding parameters so that this agent calls the `updateRewardsMerkleRoot` function of the `RewardsRegistry` contract with the new rewards distribution message commitment. - After this root is updated, any validator/operator can submit a proof that it is in a leaf of the merkle tree that produced that root, which means it has pending rewards to claim, through the `ServiceManagerBase`'s `claimOperatorRewards` function. - Each operator set of the AVS can have an assigned `RewardsRegistry` contract. Operator sets that do not have an assigned `RewardsRegistry` contract won't be able to received rewards. This PR also adds two separate unit-test suites: one for the added functionality to the `ServiceManagerBase` contract and one specific to the new `RewardsRegistry` contract. > [!CAUTION] The `RewardsAgent` agent is the only one allowed to update the rewards' merkle root, which means if a malicious user could get access to it it could set the pending rewards to be claimed to an arbitrary tree that benefits it. Extreme caution must be taken in the Substrate side so only validated messages are sent to the Ethereum side, as to not allow any users to impersonate being this agent. ### TODO: Ideally, we would use the `RewardsCoordinator` contract from the EigenLayer core to distribute the rewards, but currently that adds a huge overhead for Operators since they'd have to wait for EigenLayer's SideCar to snapshot state and update the distribution root (which happens once a day), generate a proof that they belong to the tree of that distribution root, store it while waiting for the `activationDelay` (currently a week) to pass, and just then they would be able to claim their earned rewards. --------- Co-authored-by: Facundo Farall <37149322+ffarall@users.noreply.github.com>
2025-03-31 19:54:23 +00:00
"Operator should receive correct rewards from first registry"
);
// Claim from second registry
uint256 balanceAfterFirstClaim = operatorAddress.balance;
vm.prank(operatorAddress);
serviceManager.claimLatestOperatorRewards(
secondOperatorSetId, operatorPoints, numberOfLeaves, leafIndex, secondProof
);
feat: ✨ initial rewards registry (#17) This PR adds an initial implementation for a rewards registry, which will be the contract in charge of allowing DataHaven validators to claim the rewards they earned for being validators in the previous epoch. The logic behind it is as follows: - Whenever an epoch finishes, the corresponding BEEFY block gets relayed to Ethereum through Snowbridge. This BEEFY block contains, in its `extra` field, the merkle root of the tree that contains as leafs all the message commitments of the messages of corresponding block, one of which is the rewards distribution message. - The rewards distribution message commitment is the root of the merkle tree where each leaf is a tuple of the operator ID and the obtained era points in the finished epoch. In this case, the operator ID is the corresponding validator's Ethereum address. - When the rewards distribution message is received, Snowbridge validates it using the aforementioned BEEFY block and then dispatches it. The dispatch invokes the `callContract` function of the `RewardsAgent` agent, with the corresponding parameters so that this agent calls the `updateRewardsMerkleRoot` function of the `RewardsRegistry` contract with the new rewards distribution message commitment. - After this root is updated, any validator/operator can submit a proof that it is in a leaf of the merkle tree that produced that root, which means it has pending rewards to claim, through the `ServiceManagerBase`'s `claimOperatorRewards` function. - Each operator set of the AVS can have an assigned `RewardsRegistry` contract. Operator sets that do not have an assigned `RewardsRegistry` contract won't be able to received rewards. This PR also adds two separate unit-test suites: one for the added functionality to the `ServiceManagerBase` contract and one specific to the new `RewardsRegistry` contract. > [!CAUTION] The `RewardsAgent` agent is the only one allowed to update the rewards' merkle root, which means if a malicious user could get access to it it could set the pending rewards to be claimed to an arbitrary tree that benefits it. Extreme caution must be taken in the Substrate side so only validated messages are sent to the Ethereum side, as to not allow any users to impersonate being this agent. ### TODO: Ideally, we would use the `RewardsCoordinator` contract from the EigenLayer core to distribute the rewards, but currently that adds a huge overhead for Operators since they'd have to wait for EigenLayer's SideCar to snapshot state and update the distribution root (which happens once a day), generate a proof that they belong to the tree of that distribution root, store it while waiting for the `activationDelay` (currently a week) to pass, and just then they would be able to claim their earned rewards. --------- Co-authored-by: Facundo Farall <37149322+ffarall@users.noreply.github.com>
2025-03-31 19:54:23 +00:00
// Verify balance after second claim
assertEq(
operatorAddress.balance,
balanceAfterFirstClaim + operatorPoints,
"Operator should receive correct rewards from second registry"
);
}
feat: :sparkles: make `RewardsRegistry` keep a history of roots and claim status (#106) # Description This PR implements a comprehensive overhaul of the `RewardsRegistry` contract to maintain complete history of reward merkle roots while providing index-based claim tracking for operators. The new architecture enables operators to claim rewards from any historical merkle root instead of only the latest one. To do so, it: - Adds the `merkleRootHistory` storage array to the contract, in which we keep all rewards roots that ever came from the DataHaven side. - Adds the `operatorClaimedByIndex` storage map to the contract, in which we keep track, for each validator and root index, if it has claimed it or not. - This works even for new validators, since theoretically with this system you could argue they could claim older roots that they were not a part of which would be catastrophic, but they could never draft a correct proof for those to claim them. - Keeps some of the interface from before the overhaul, to have quick access to the latest rewards merkle root through `getLatestMerkleRoot()` and to claim rewards for it with `claimRewards()`. This is because the expected behaviour is for validators to claim their rewards every era. - Adds a way to batch claim rewards with `claimRewardsBatch()`. This function allows a validator to claim rewards for multiple root indices in one call by providing multiple proofs, useful if the validator has fallen behind claims and has to catch up, although special care will have to be taken by it to avoid reaching the gas limit of a transaction. ## Storage Efficiency Analysis One might think this solution is not as storage-efficient as other solutions that we can think of (I even had two other alternatives in mind as well), but a simple back-of-the-envelope calculation gives us peace of mind that the impact of this solution on the overal state size of the chain is negligible: ### Assumptions (Worst Case Scenario): - 1,000 validators (actual estimate for DataHaven: ~50/100 validators) - 6-hour eras (most-likely scenario, following what Polkadot does: ~24-hour eras) - Which means 4 merkle root updates per day ### Annual Storage Requirements: - Merkle Root History: **46,720 bytes/year** - 4 roots/day × 32 bytes/root × 365 days/year = 46,720 bytes/year - Operator Claim Tracking: **~1.46 MB/year** - 1,000 operators × 1 boolean/(operator * root index) × 1 byte/boolean × 4 root indices/day × 365 days/year = 1,460,000 bytes/year - **Total: ~1.5 MB/year** This represents negligible storage overhead compared to the significant operational benefits gained. ## TODO Since we want to allow the operators/validators to only have to interact with the AVS contract (that's why the `claimRewards` functions have the `onlyAVS` modifier), we still have to: - [x] Add the required functions to the AVS to allow operators to claim their rewards. - [x] Adds comprehensive unit tests for them. --------- Co-authored-by: Steve Degosserie <723552+stiiifff@users.noreply.github.com> Co-authored-by: Ahmad Kaouk <56095276+ahmadkaouk@users.noreply.github.com> Co-authored-by: Ahmad Kaouk <ahmadkaouk.93@gmail.com>
2025-07-10 06:47:39 +00:00
function test_claimLatestOperatorRewards_NotInOperatorSet() public {
vm.mockCall(
address(allocationManager),
abi.encodeWithSelector(IAllocationManager.isMemberOfOperatorSet.selector),
abi.encode(false) // Operator is NOT in the set
);
vm.prank(operatorAddress);
vm.expectRevert(
abi.encodeWithSelector(IServiceManagerErrors.OperatorNotInOperatorSet.selector)
);
serviceManager.claimLatestOperatorRewards(
operatorSetId, operatorPoints, numberOfLeaves, leafIndex, validProof
);
feat: :sparkles: make `RewardsRegistry` keep a history of roots and claim status (#106) # Description This PR implements a comprehensive overhaul of the `RewardsRegistry` contract to maintain complete history of reward merkle roots while providing index-based claim tracking for operators. The new architecture enables operators to claim rewards from any historical merkle root instead of only the latest one. To do so, it: - Adds the `merkleRootHistory` storage array to the contract, in which we keep all rewards roots that ever came from the DataHaven side. - Adds the `operatorClaimedByIndex` storage map to the contract, in which we keep track, for each validator and root index, if it has claimed it or not. - This works even for new validators, since theoretically with this system you could argue they could claim older roots that they were not a part of which would be catastrophic, but they could never draft a correct proof for those to claim them. - Keeps some of the interface from before the overhaul, to have quick access to the latest rewards merkle root through `getLatestMerkleRoot()` and to claim rewards for it with `claimRewards()`. This is because the expected behaviour is for validators to claim their rewards every era. - Adds a way to batch claim rewards with `claimRewardsBatch()`. This function allows a validator to claim rewards for multiple root indices in one call by providing multiple proofs, useful if the validator has fallen behind claims and has to catch up, although special care will have to be taken by it to avoid reaching the gas limit of a transaction. ## Storage Efficiency Analysis One might think this solution is not as storage-efficient as other solutions that we can think of (I even had two other alternatives in mind as well), but a simple back-of-the-envelope calculation gives us peace of mind that the impact of this solution on the overal state size of the chain is negligible: ### Assumptions (Worst Case Scenario): - 1,000 validators (actual estimate for DataHaven: ~50/100 validators) - 6-hour eras (most-likely scenario, following what Polkadot does: ~24-hour eras) - Which means 4 merkle root updates per day ### Annual Storage Requirements: - Merkle Root History: **46,720 bytes/year** - 4 roots/day × 32 bytes/root × 365 days/year = 46,720 bytes/year - Operator Claim Tracking: **~1.46 MB/year** - 1,000 operators × 1 boolean/(operator * root index) × 1 byte/boolean × 4 root indices/day × 365 days/year = 1,460,000 bytes/year - **Total: ~1.5 MB/year** This represents negligible storage overhead compared to the significant operational benefits gained. ## TODO Since we want to allow the operators/validators to only have to interact with the AVS contract (that's why the `claimRewards` functions have the `onlyAVS` modifier), we still have to: - [x] Add the required functions to the AVS to allow operators to claim their rewards. - [x] Adds comprehensive unit tests for them. --------- Co-authored-by: Steve Degosserie <723552+stiiifff@users.noreply.github.com> Co-authored-by: Ahmad Kaouk <56095276+ahmadkaouk@users.noreply.github.com> Co-authored-by: Ahmad Kaouk <ahmadkaouk.93@gmail.com>
2025-07-10 06:47:39 +00:00
}
function test_claimOperatorRewards_NotInOperatorSet() public {
vm.mockCall(
address(allocationManager),
abi.encodeWithSelector(IAllocationManager.isMemberOfOperatorSet.selector),
abi.encode(false) // Operator is NOT in the set
);
vm.prank(operatorAddress);
vm.expectRevert(
abi.encodeWithSelector(IServiceManagerErrors.OperatorNotInOperatorSet.selector)
);
serviceManager.claimOperatorRewards(
operatorSetId, 0, operatorPoints, numberOfLeaves, leafIndex, validProof
);
feat: :sparkles: make `RewardsRegistry` keep a history of roots and claim status (#106) # Description This PR implements a comprehensive overhaul of the `RewardsRegistry` contract to maintain complete history of reward merkle roots while providing index-based claim tracking for operators. The new architecture enables operators to claim rewards from any historical merkle root instead of only the latest one. To do so, it: - Adds the `merkleRootHistory` storage array to the contract, in which we keep all rewards roots that ever came from the DataHaven side. - Adds the `operatorClaimedByIndex` storage map to the contract, in which we keep track, for each validator and root index, if it has claimed it or not. - This works even for new validators, since theoretically with this system you could argue they could claim older roots that they were not a part of which would be catastrophic, but they could never draft a correct proof for those to claim them. - Keeps some of the interface from before the overhaul, to have quick access to the latest rewards merkle root through `getLatestMerkleRoot()` and to claim rewards for it with `claimRewards()`. This is because the expected behaviour is for validators to claim their rewards every era. - Adds a way to batch claim rewards with `claimRewardsBatch()`. This function allows a validator to claim rewards for multiple root indices in one call by providing multiple proofs, useful if the validator has fallen behind claims and has to catch up, although special care will have to be taken by it to avoid reaching the gas limit of a transaction. ## Storage Efficiency Analysis One might think this solution is not as storage-efficient as other solutions that we can think of (I even had two other alternatives in mind as well), but a simple back-of-the-envelope calculation gives us peace of mind that the impact of this solution on the overal state size of the chain is negligible: ### Assumptions (Worst Case Scenario): - 1,000 validators (actual estimate for DataHaven: ~50/100 validators) - 6-hour eras (most-likely scenario, following what Polkadot does: ~24-hour eras) - Which means 4 merkle root updates per day ### Annual Storage Requirements: - Merkle Root History: **46,720 bytes/year** - 4 roots/day × 32 bytes/root × 365 days/year = 46,720 bytes/year - Operator Claim Tracking: **~1.46 MB/year** - 1,000 operators × 1 boolean/(operator * root index) × 1 byte/boolean × 4 root indices/day × 365 days/year = 1,460,000 bytes/year - **Total: ~1.5 MB/year** This represents negligible storage overhead compared to the significant operational benefits gained. ## TODO Since we want to allow the operators/validators to only have to interact with the AVS contract (that's why the `claimRewards` functions have the `onlyAVS` modifier), we still have to: - [x] Add the required functions to the AVS to allow operators to claim their rewards. - [x] Adds comprehensive unit tests for them. --------- Co-authored-by: Steve Degosserie <723552+stiiifff@users.noreply.github.com> Co-authored-by: Ahmad Kaouk <56095276+ahmadkaouk@users.noreply.github.com> Co-authored-by: Ahmad Kaouk <ahmadkaouk.93@gmail.com>
2025-07-10 06:47:39 +00:00
}
function test_claimOperatorRewardsBatch_NotInOperatorSet() public {
uint256[] memory rootIndices = new uint256[](1);
rootIndices[0] = 0;
uint256[] memory points = new uint256[](1);
points[0] = operatorPoints;
bytes32[][] memory proofs = new bytes32[][](1);
proofs[0] = validProof;
vm.mockCall(
address(allocationManager),
abi.encodeWithSelector(IAllocationManager.isMemberOfOperatorSet.selector),
abi.encode(false) // Operator is NOT in the set
);
vm.prank(operatorAddress);
vm.expectRevert(
abi.encodeWithSelector(IServiceManagerErrors.OperatorNotInOperatorSet.selector)
);
uint256[] memory widths3 = new uint256[](1);
widths3[0] = 2;
uint256[] memory leafIdxs3 = new uint256[](1);
leafIdxs3[0] = 0;
serviceManager.claimOperatorRewardsBatch(
operatorSetId, rootIndices, points, widths3, leafIdxs3, proofs
);
feat: :sparkles: make `RewardsRegistry` keep a history of roots and claim status (#106) # Description This PR implements a comprehensive overhaul of the `RewardsRegistry` contract to maintain complete history of reward merkle roots while providing index-based claim tracking for operators. The new architecture enables operators to claim rewards from any historical merkle root instead of only the latest one. To do so, it: - Adds the `merkleRootHistory` storage array to the contract, in which we keep all rewards roots that ever came from the DataHaven side. - Adds the `operatorClaimedByIndex` storage map to the contract, in which we keep track, for each validator and root index, if it has claimed it or not. - This works even for new validators, since theoretically with this system you could argue they could claim older roots that they were not a part of which would be catastrophic, but they could never draft a correct proof for those to claim them. - Keeps some of the interface from before the overhaul, to have quick access to the latest rewards merkle root through `getLatestMerkleRoot()` and to claim rewards for it with `claimRewards()`. This is because the expected behaviour is for validators to claim their rewards every era. - Adds a way to batch claim rewards with `claimRewardsBatch()`. This function allows a validator to claim rewards for multiple root indices in one call by providing multiple proofs, useful if the validator has fallen behind claims and has to catch up, although special care will have to be taken by it to avoid reaching the gas limit of a transaction. ## Storage Efficiency Analysis One might think this solution is not as storage-efficient as other solutions that we can think of (I even had two other alternatives in mind as well), but a simple back-of-the-envelope calculation gives us peace of mind that the impact of this solution on the overal state size of the chain is negligible: ### Assumptions (Worst Case Scenario): - 1,000 validators (actual estimate for DataHaven: ~50/100 validators) - 6-hour eras (most-likely scenario, following what Polkadot does: ~24-hour eras) - Which means 4 merkle root updates per day ### Annual Storage Requirements: - Merkle Root History: **46,720 bytes/year** - 4 roots/day × 32 bytes/root × 365 days/year = 46,720 bytes/year - Operator Claim Tracking: **~1.46 MB/year** - 1,000 operators × 1 boolean/(operator * root index) × 1 byte/boolean × 4 root indices/day × 365 days/year = 1,460,000 bytes/year - **Total: ~1.5 MB/year** This represents negligible storage overhead compared to the significant operational benefits gained. ## TODO Since we want to allow the operators/validators to only have to interact with the AVS contract (that's why the `claimRewards` functions have the `onlyAVS` modifier), we still have to: - [x] Add the required functions to the AVS to allow operators to claim their rewards. - [x] Adds comprehensive unit tests for them. --------- Co-authored-by: Steve Degosserie <723552+stiiifff@users.noreply.github.com> Co-authored-by: Ahmad Kaouk <56095276+ahmadkaouk@users.noreply.github.com> Co-authored-by: Ahmad Kaouk <ahmadkaouk.93@gmail.com>
2025-07-10 06:47:39 +00:00
}
feat: ✨ initial rewards registry (#17) This PR adds an initial implementation for a rewards registry, which will be the contract in charge of allowing DataHaven validators to claim the rewards they earned for being validators in the previous epoch. The logic behind it is as follows: - Whenever an epoch finishes, the corresponding BEEFY block gets relayed to Ethereum through Snowbridge. This BEEFY block contains, in its `extra` field, the merkle root of the tree that contains as leafs all the message commitments of the messages of corresponding block, one of which is the rewards distribution message. - The rewards distribution message commitment is the root of the merkle tree where each leaf is a tuple of the operator ID and the obtained era points in the finished epoch. In this case, the operator ID is the corresponding validator's Ethereum address. - When the rewards distribution message is received, Snowbridge validates it using the aforementioned BEEFY block and then dispatches it. The dispatch invokes the `callContract` function of the `RewardsAgent` agent, with the corresponding parameters so that this agent calls the `updateRewardsMerkleRoot` function of the `RewardsRegistry` contract with the new rewards distribution message commitment. - After this root is updated, any validator/operator can submit a proof that it is in a leaf of the merkle tree that produced that root, which means it has pending rewards to claim, through the `ServiceManagerBase`'s `claimOperatorRewards` function. - Each operator set of the AVS can have an assigned `RewardsRegistry` contract. Operator sets that do not have an assigned `RewardsRegistry` contract won't be able to received rewards. This PR also adds two separate unit-test suites: one for the added functionality to the `ServiceManagerBase` contract and one specific to the new `RewardsRegistry` contract. > [!CAUTION] The `RewardsAgent` agent is the only one allowed to update the rewards' merkle root, which means if a malicious user could get access to it it could set the pending rewards to be claimed to an arbitrary tree that benefits it. Extreme caution must be taken in the Substrate side so only validated messages are sent to the Ethereum side, as to not allow any users to impersonate being this agent. ### TODO: Ideally, we would use the `RewardsCoordinator` contract from the EigenLayer core to distribute the rewards, but currently that adds a huge overhead for Operators since they'd have to wait for EigenLayer's SideCar to snapshot state and update the distribution root (which happens once a day), generate a proof that they belong to the tree of that distribution root, store it while waiting for the `activationDelay` (currently a week) to pass, and just then they would be able to claim their earned rewards. --------- Co-authored-by: Facundo Farall <37149322+ffarall@users.noreply.github.com>
2025-03-31 19:54:23 +00:00
}