Merge branch 'main' into fix/verify-allowlist-tx-receipt-status

This commit is contained in:
Steve Degosserie 2026-02-24 11:38:39 +02:00 committed by GitHub
commit 4c941ba63c
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
21 changed files with 1575 additions and 659 deletions

View file

@ -21,16 +21,14 @@
"rewardsCoordinatorInitPausedStatus": 0,
"allocationManagerInitPausedStatus": 0,
"deallocationDelay": 50,
"allocationConfigurationDelay": 75,
"allocationConfigurationDelay": 0,
"beaconChainGenesisTimestamp": 1695902400
},
"avs": {
"avsOwner": "0x976EA74026E726554dB657fA54763abd0C3a0aa9",
"rewardsInitiator": "0x14dC79964da2C08b23698B3D3cc7Ca32193d9955",
"validatorSetSubmitter": "0x976EA74026E726554dB657fA54763abd0C3a0aa9",
"validatorsStrategies": [
"0xbeaC0eeEeeeeEEeEeEEEEeeEEeEeeeEeeEEBEaC0"
]
"validatorsStrategies": []
},
"snowbridge": {
"randaoCommitDelay": 4,
@ -49,4 +47,4 @@
"0xf68aec7304bf37f340dae2ea20fb5271ee28a3128812b84a615da4789e458bde"
]
}
}
}

View file

@ -1 +1 @@
aa4e5f7e459c7a4f337016e845cd05aa56cccb41
9c861e3e1d290888127bc6d772fb1a3422bdf8b3

File diff suppressed because one or more lines are too long

View file

@ -32,6 +32,10 @@ import {
} from "eigenlayer-contracts/src/contracts/permissions/PermissionController.sol";
import {EigenPodManager} from "eigenlayer-contracts/src/contracts/pods/EigenPodManager.sol";
import {IETHPOSDeposit} from "eigenlayer-contracts/src/contracts/interfaces/IETHPOSDeposit.sol";
import {IStrategy} from "eigenlayer-contracts/src/contracts/interfaces/IStrategy.sol";
import {
IRewardsCoordinatorTypes
} from "eigenlayer-contracts/src/contracts/interfaces/IRewardsCoordinator.sol";
// DataHaven imports
import {DataHavenServiceManager} from "../../src/DataHavenServiceManager.sol";
@ -41,7 +45,7 @@ import {ValidatorsUtils} from "../../script/utils/ValidatorsUtils.sol";
struct ServiceManagerInitParams {
address avsOwner;
address rewardsInitiator;
address[] validatorsStrategies;
IRewardsCoordinatorTypes.StrategyAndMultiplier[] validatorsStrategiesAndMultipliers;
address gateway;
address validatorSetSubmitter;
}
@ -246,11 +250,21 @@ abstract contract DeployBase is Script, DeployParams, Accounts {
"ServiceManager Implementation", address(serviceManagerImplementation)
);
// Build StrategyAndMultiplier[] from config addresses with default multiplier of 1.
// Multipliers can be updated post-deployment via setStrategiesAndMultipliers if needed.
IRewardsCoordinatorTypes.StrategyAndMultiplier[] memory strategiesAndMultipliers = new IRewardsCoordinatorTypes
.StrategyAndMultiplier[](avsConfig.validatorsStrategies.length);
for (uint256 i = 0; i < avsConfig.validatorsStrategies.length; i++) {
strategiesAndMultipliers[i] = IRewardsCoordinatorTypes.StrategyAndMultiplier({
strategy: IStrategy(avsConfig.validatorsStrategies[i]), multiplier: 1
});
}
// Create service manager initialisation parameters struct
ServiceManagerInitParams memory initParams = ServiceManagerInitParams({
avsOwner: avsConfig.avsOwner,
rewardsInitiator: avsConfig.rewardsInitiator,
validatorsStrategies: avsConfig.validatorsStrategies,
validatorsStrategiesAndMultipliers: strategiesAndMultipliers,
gateway: address(gateway),
validatorSetSubmitter: avsConfig.validatorSetSubmitter
});

View file

@ -120,7 +120,7 @@ contract DeployLive is DeployBase {
DataHavenServiceManager.initialize.selector,
params.avsOwner,
params.rewardsInitiator,
params.validatorsStrategies,
params.validatorsStrategiesAndMultipliers,
params.gateway,
params.validatorSetSubmitter
);

View file

@ -206,7 +206,7 @@ contract DeployLocal is DeployBase {
DataHavenServiceManager.initialize.selector,
params.avsOwner,
params.rewardsInitiator,
params.validatorsStrategies,
params.validatorsStrategiesAndMultipliers,
params.gateway,
params.validatorSetSubmitter
);
@ -356,11 +356,15 @@ contract DeployLocal is DeployBase {
function _prepareStrategiesForServiceManager(
ServiceManagerInitParams memory params
) internal view {
if (params.validatorsStrategies.length == 0) {
params.validatorsStrategies = new address[](deployedStrategies.length);
if (params.validatorsStrategiesAndMultipliers.length == 0) {
IRewardsCoordinatorTypes.StrategyAndMultiplier[] memory sm =
new IRewardsCoordinatorTypes.StrategyAndMultiplier[](deployedStrategies.length);
for (uint256 i = 0; i < deployedStrategies.length; i++) {
params.validatorsStrategies[i] = deployedStrategies[i].address_;
sm[i] = IRewardsCoordinatorTypes.StrategyAndMultiplier({
strategy: IStrategy(deployedStrategies[i].address_), multiplier: 1
});
}
params.validatorsStrategiesAndMultipliers = sm;
}
}

View file

@ -0,0 +1,61 @@
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.27;
// EigenLayer imports
import {
IAllocationManagerTypes
} from "eigenlayer-contracts/src/contracts/interfaces/IAllocationManager.sol";
import {OperatorSet} from "eigenlayer-contracts/src/contracts/libraries/OperatorSetLib.sol";
import {IStrategy} from "eigenlayer-contracts/src/contracts/interfaces/IStrategy.sol";
// Testing imports
import {Script} from "forge-std/Script.sol";
import {console} from "forge-std/console.sol";
import {Logging} from "../utils/Logging.sol";
import {ELScriptStorage} from "../utils/ELScriptStorage.s.sol";
import {DHScriptStorage} from "../utils/DHScriptStorage.s.sol";
import {Accounts} from "../utils/Accounts.sol";
/**
* @title AllocateOperatorStake
* @notice Allocates full magnitude to the validator operator set.
* Must be run AFTER SignUpValidator (needs at least 1 block gap
* for the allocation delay to initialize).
*/
contract AllocateOperatorStake is Script, ELScriptStorage, DHScriptStorage, Accounts {
function run() public {
string memory network = vm.envOr("NETWORK", string("anvil"));
Logging.logHeader("ALLOCATE OPERATOR STAKE");
console.log("| Network: %s", network);
Logging.logFooter();
_loadELContracts(network);
_loadDHContracts(network);
IStrategy[] memory strategies = new IStrategy[](deployedStrategies.length);
for (uint256 i = 0; i < deployedStrategies.length; i++) {
strategies[i] = IStrategy(address(deployedStrategies[i].strategy));
}
uint64[] memory newMagnitudes = new uint64[](strategies.length);
for (uint256 i = 0; i < strategies.length; i++) {
newMagnitudes[i] = 1e18;
}
IAllocationManagerTypes.AllocateParams[] memory allocParams =
new IAllocationManagerTypes.AllocateParams[](1);
allocParams[0] = IAllocationManagerTypes.AllocateParams({
operatorSet: OperatorSet({
avs: address(serviceManager), id: serviceManager.VALIDATORS_SET_ID()
}),
strategies: strategies,
newMagnitudes: newMagnitudes
});
vm.broadcast(_operatorPrivateKey);
allocationManager.modifyAllocations(_operator, allocParams);
Logging.logStep(
string.concat("Allocated full magnitude for operator: ", vm.toString(_operator))
);
}
}

View file

@ -33,8 +33,8 @@ normalize_json() {
| .storage
| map(
del(.astId, .contract)
# Remove unstable AST ID suffixes from type strings (e.g., t_contract(IGatewayV2)12345)
| .type |= sub("\\)[0-9]+$"; ")")
# Remove unstable AST IDs from type strings (e.g., t_contract(IGatewayV2)12345, nested mappings)
| .type |= gsub("\\)[0-9]+"; ")")
)
| sort_by(.slot | tonumber)' "$1"
}

View file

@ -42,6 +42,9 @@ contract DataHavenServiceManager is OwnableUpgradeable, IAVSRegistrar, IDataHave
/// @notice The EigenLayer operator set ID for the Validators securing the DataHaven network.
uint32 public constant VALIDATORS_SET_ID = 0;
/// @notice Maximum number of active validators in the set
uint32 public constant MAX_ACTIVE_VALIDATORS = 32;
// ============ Immutables ============
/// @notice The EigenLayer AllocationManager contract
@ -64,15 +67,17 @@ contract DataHavenServiceManager is OwnableUpgradeable, IAVSRegistrar, IDataHave
/// @inheritdoc IDataHavenServiceManager
mapping(address => address) public validatorEthAddressToSolochainAddress;
/// @inheritdoc IDataHavenServiceManager
mapping(address => address) public validatorSolochainAddressToEthAddress;
/// @inheritdoc IDataHavenServiceManager
address public validatorSetSubmitter;
/// @inheritdoc IDataHavenServiceManager
mapping(IStrategy => uint96) public strategiesAndMultipliers;
/// @notice Storage gap for upgradeability (must be at end of state variables)
// solhint-disable-next-line var-name-mixedcase
uint256[44] private __GAP;
uint256[43] private __GAP;
// ============ Modifiers ============
@ -138,7 +143,7 @@ contract DataHavenServiceManager is OwnableUpgradeable, IAVSRegistrar, IDataHave
function initialize(
address initialOwner,
address _rewardsInitiator,
IStrategy[] memory validatorsStrategies,
IRewardsCoordinatorTypes.StrategyAndMultiplier[] memory validatorsStrategiesAndMultipliers,
address _snowbridgeGatewayAddress,
address _validatorSetSubmitter
) public virtual initializer {
@ -154,11 +159,20 @@ contract DataHavenServiceManager is OwnableUpgradeable, IAVSRegistrar, IDataHave
// Register the DataHaven service in the AllocationManager.
_ALLOCATION_MANAGER.updateAVSMetadataURI(address(this), DATAHAVEN_AVS_METADATA);
// Build the strategies array and populate multipliers atomically so that
// getStrategiesInOperatorSet and strategiesAndMultipliers are always consistent.
IStrategy[] memory strategies = new IStrategy[](validatorsStrategiesAndMultipliers.length);
for (uint256 i = 0; i < validatorsStrategiesAndMultipliers.length; i++) {
strategies[i] = validatorsStrategiesAndMultipliers[i].strategy;
strategiesAndMultipliers[validatorsStrategiesAndMultipliers[i].strategy] =
validatorsStrategiesAndMultipliers[i].multiplier;
}
// Create the operator set for the DataHaven service.
IAllocationManagerTypes.CreateSetParams[] memory operatorSets =
new IAllocationManagerTypes.CreateSetParams[](1);
operatorSets[0] = IAllocationManagerTypes.CreateSetParams({
operatorSetId: VALIDATORS_SET_ID, strategies: validatorsStrategies
operatorSetId: VALIDATORS_SET_ID, strategies: strategies
});
_ALLOCATION_MANAGER.createOperatorSets(address(this), operatorSets);
@ -200,22 +214,72 @@ contract DataHavenServiceManager is OwnableUpgradeable, IAVSRegistrar, IDataHave
uint64 targetEra
) public view returns (bytes memory) {
OperatorSet memory operatorSet = OperatorSet({avs: address(this), id: VALIDATORS_SET_ID});
address[] memory currentValidatorSet = _ALLOCATION_MANAGER.getMembers(operatorSet);
address[] memory operators = _ALLOCATION_MANAGER.getMembers(operatorSet);
IStrategy[] memory strategies = _ALLOCATION_MANAGER.getStrategiesInOperatorSet(operatorSet);
// Allocate max size, then resize after filtering
address[] memory newValidatorSet = new address[](currentValidatorSet.length);
uint256 validCount = 0;
for (uint256 i = 0; i < currentValidatorSet.length; i++) {
address solochainAddr = validatorEthAddressToSolochainAddress[currentValidatorSet[i]];
if (solochainAddr != address(0)) {
newValidatorSet[validCount] = solochainAddr;
++validCount;
// Get allocated stake for all operators across all strategies
uint256[][] memory allocatedStake =
_ALLOCATION_MANAGER.getAllocatedStake(operatorSet, operators, strategies);
// Collect candidates: operators with solochain mapping and non-zero weighted stake
address[] memory candidateSolochain = new address[](operators.length);
uint256[] memory candidateStake = new uint256[](operators.length);
address[] memory candidateOperator = new address[](operators.length);
uint256 candidateCount = 0;
for (uint256 i = 0; i < operators.length; i++) {
address solochainAddr = validatorEthAddressToSolochainAddress[operators[i]];
if (solochainAddr == address(0)) continue;
// Compute weighted stake across all strategies:
// weightedStake = sum(allocatedStake[i][j] * multiplier[j])
uint256 weightedStake = 0;
for (uint256 j = 0; j < strategies.length; j++) {
weightedStake += allocatedStake[i][j]
* uint256(strategiesAndMultipliers[strategies[j]]);
}
if (weightedStake == 0) continue;
candidateSolochain[candidateCount] = solochainAddr;
candidateStake[candidateCount] = weightedStake;
candidateOperator[candidateCount] = operators[i];
candidateCount++;
}
require(candidateCount != 0, EmptyValidatorSet());
// Partial selection sort: pick top min(MAX_ACTIVE_VALIDATORS, candidateCount)
uint256 selectCount =
candidateCount < MAX_ACTIVE_VALIDATORS ? candidateCount : MAX_ACTIVE_VALIDATORS;
for (uint256 i = 0; i < selectCount; i++) {
uint256 bestIdx = i;
for (uint256 j = i + 1; j < candidateCount; j++) {
if (_isBetterCandidate(
candidateStake[j],
candidateOperator[j],
candidateStake[bestIdx],
candidateOperator[bestIdx]
)) {
bestIdx = j;
}
}
if (bestIdx != i) {
// Swap all parallel arrays
(candidateSolochain[i], candidateSolochain[bestIdx]) =
(candidateSolochain[bestIdx], candidateSolochain[i]);
(candidateStake[i], candidateStake[bestIdx]) =
(candidateStake[bestIdx], candidateStake[i]);
(candidateOperator[i], candidateOperator[bestIdx]) =
(candidateOperator[bestIdx], candidateOperator[i]);
}
}
require(validCount != 0, EmptyValidatorSet());
// Resize array to actual count
assembly {
mstore(newValidatorSet, validCount)
// Build the final validator set from sorted solochain addresses
address[] memory newValidatorSet = new address[](selectCount);
for (uint256 i = 0; i < selectCount; i++) {
newValidatorSet[i] = candidateSolochain[i];
}
return DataHavenSnowbridgeMessages.scaleEncodeNewValidatorSetMessagePayload(
@ -351,15 +415,71 @@ contract DataHavenServiceManager is OwnableUpgradeable, IAVSRegistrar, IDataHave
_ALLOCATION_MANAGER.removeStrategiesFromOperatorSet(
address(this), VALIDATORS_SET_ID, _strategies
);
for (uint256 i = 0; i < _strategies.length; i++) {
delete strategiesAndMultipliers[_strategies[i]];
}
}
/// @inheritdoc IDataHavenServiceManager
function addStrategiesToValidatorsSupportedStrategies(
IStrategy[] calldata _strategies
IRewardsCoordinatorTypes.StrategyAndMultiplier[] calldata _strategyMultipliers
) external onlyOwner {
_ALLOCATION_MANAGER.addStrategiesToOperatorSet(
address(this), VALIDATORS_SET_ID, _strategies
);
IStrategy[] memory strategies = new IStrategy[](_strategyMultipliers.length);
for (uint256 i = 0; i < _strategyMultipliers.length; i++) {
strategies[i] = _strategyMultipliers[i].strategy;
strategiesAndMultipliers[_strategyMultipliers[i].strategy] =
_strategyMultipliers[i].multiplier;
}
_ALLOCATION_MANAGER.addStrategiesToOperatorSet(address(this), VALIDATORS_SET_ID, strategies);
emit StrategiesAndMultipliersSet(_strategyMultipliers);
}
/// @inheritdoc IDataHavenServiceManager
function setStrategiesAndMultipliers(
IRewardsCoordinatorTypes.StrategyAndMultiplier[] calldata _strategyMultipliers
) external onlyOwner {
OperatorSet memory operatorSet = OperatorSet({avs: address(this), id: VALIDATORS_SET_ID});
IStrategy[] memory registered = _ALLOCATION_MANAGER.getStrategiesInOperatorSet(operatorSet);
for (uint256 i = 0; i < _strategyMultipliers.length; i++) {
bool found = false;
for (uint256 j = 0; j < registered.length; j++) {
if (registered[j] == _strategyMultipliers[i].strategy) {
found = true;
break;
}
}
require(found, StrategyNotInOperatorSet());
strategiesAndMultipliers[_strategyMultipliers[i].strategy] =
_strategyMultipliers[i].multiplier;
}
emit StrategiesAndMultipliersSet(_strategyMultipliers);
}
/// @inheritdoc IDataHavenServiceManager
function getStrategiesAndMultipliers()
external
view
returns (IRewardsCoordinatorTypes.StrategyAndMultiplier[] memory)
{
OperatorSet memory operatorSet = OperatorSet({avs: address(this), id: VALIDATORS_SET_ID});
IStrategy[] memory strategies = _ALLOCATION_MANAGER.getStrategiesInOperatorSet(operatorSet);
IRewardsCoordinatorTypes.StrategyAndMultiplier[] memory result =
new IRewardsCoordinatorTypes.StrategyAndMultiplier[](strategies.length);
for (uint256 i = 0; i < strategies.length; i++) {
result[i] = IRewardsCoordinatorTypes.StrategyAndMultiplier({
strategy: strategies[i], multiplier: strategiesAndMultipliers[strategies[i]]
});
}
return result;
}
// ============ Rewards Functions ============
@ -487,6 +607,27 @@ contract DataHavenServiceManager is OwnableUpgradeable, IAVSRegistrar, IDataHave
require(result != address(0), ZeroAddress());
}
/**
* @notice Determines if candidate A ranks higher than candidate B
* @dev Higher stake wins; on tie, lower operator address wins
* @param stakeA Weighted stake of candidate A
* @param opA Operator address of candidate A
* @param stakeB Weighted stake of candidate B
* @param opB Operator address of candidate B
* @return True if candidate A ranks higher than candidate B
*/
function _isBetterCandidate(
uint256 stakeA,
address opA,
uint256 stakeB,
address opB
) private pure returns (bool) {
if (stakeA != stakeB) {
return stakeA > stakeB;
}
return opA < opB;
}
/**
* @notice Returns the EigenLayer operator address for a Solochain validator address
* @dev Reverts if the Solochain address has not been mapped to an operator

View file

@ -42,6 +42,9 @@ interface IDataHavenServiceManagerErrors {
/// @notice Thrown when a Solochain address is already assigned to a different operator
error SolochainAddressAlreadyAssigned();
/// @notice Thrown when a strategy is not registered in the operator set
error StrategyNotInOperatorSet();
}
/**
@ -89,6 +92,10 @@ interface IDataHavenServiceManagerEvents {
/// @notice Emitted when a batch of slashing request is being successfully slashed
event SlashingComplete();
/// @notice Emitted when strategy multipliers are set or updated
/// @param strategyMultipliers Array of strategy-multiplier pairs that were set
event StrategiesAndMultipliersSet(IRewardsCoordinatorTypes
.StrategyAndMultiplier[] strategyMultipliers);
/// @notice Emitted when the validator set submitter address is updated
/// @param oldSubmitter The previous validator set submitter address
/// @param newSubmitter The new validator set submitter address
@ -166,12 +173,15 @@ interface IDataHavenServiceManager is
* @notice Initializes the DataHaven Service Manager
* @param initialOwner Address of the initial owner
* @param rewardsInitiator Address authorized to initiate rewards
* @param validatorsStrategies Array of strategies supported by validators
* @param validatorsStrategiesAndMultipliers Array of strategy-multiplier pairs for the validators
* operator set. Each multiplier must be non-zero.
* @param _snowbridgeGatewayAddress Address of the Snowbridge Gateway
* @param _validatorSetSubmitter Address authorized to submit validator set messages
*/
function initialize(
address initialOwner,
address rewardsInitiator,
IStrategy[] memory validatorsStrategies,
IRewardsCoordinatorTypes.StrategyAndMultiplier[] memory validatorsStrategiesAndMultipliers,
address _snowbridgeGatewayAddress,
address _validatorSetSubmitter
) external;
@ -193,9 +203,13 @@ interface IDataHavenServiceManager is
) external payable;
/**
* @notice Builds a new validator set message for a target era
* @notice Builds a SCALE-encoded message containing the top validators by weighted stake
* @dev Selects up to MAX_ACTIVE_VALIDATORS from registered operators. Each operator's
* weighted stake is computed as: sum(allocatedStake[j] * multiplier[j])
* across all strategies. Operators without a solochain address mapping or with zero
* weighted stake are excluded. Ties are broken by lower operator address.
* @param targetEra The target era to encode in the message
* @return The encoded message bytes to be sent to the Snowbridge Gateway
* @return The SCALE-encoded message bytes to be sent to the Snowbridge Gateway
*/
function buildNewValidatorSetMessageForEra(
uint64 targetEra
@ -251,12 +265,50 @@ interface IDataHavenServiceManager is
/**
* @notice Adds strategies to the list of supported strategies for DataHaven Validators
* @param _strategies Array of strategy contracts to add to validators operator set
* @dev Each strategy's multiplier determines its weight in the validator selection
* formula: weightedStake = sum(allocatedStake[j] * multiplier[j])
* @param _strategyMultipliers Array of strategy-multiplier pairs to add
*/
function addStrategiesToValidatorsSupportedStrategies(
IStrategy[] calldata _strategies
IRewardsCoordinatorTypes.StrategyAndMultiplier[] calldata _strategyMultipliers
) external;
/**
* @notice Returns the maximum number of active validators in the set
* @return The maximum active validators constant
*/
function MAX_ACTIVE_VALIDATORS() external pure returns (uint32);
/**
* @notice Returns the multiplier for a given strategy
* @dev The multiplier determines how much an operator's allocated stake in this strategy
* contributes to their weighted stake during validator set selection.
* @param strategy The strategy to look up
* @return The multiplier weight
*/
function strategiesAndMultipliers(
IStrategy strategy
) external view returns (uint96);
/**
* @notice Updates multipliers for strategies already in the operator set
* @dev Does not add or remove strategies from EigenLayer; only updates multiplier weights
* used in the validator selection weighted stake formula
* @param _strategyMultipliers Array of strategy-multiplier pairs to update
*/
function setStrategiesAndMultipliers(
IRewardsCoordinatorTypes.StrategyAndMultiplier[] calldata _strategyMultipliers
) external;
/**
* @notice Returns all strategies with their multipliers
* @return Array of StrategyAndMultiplier structs with strategy addresses and multiplier weights
*/
function getStrategiesAndMultipliers()
external
view
returns (IRewardsCoordinatorTypes.StrategyAndMultiplier[] memory);
// ============ Rewards Submitter Functions ============
/**

View file

@ -41,7 +41,7 @@
"type": "t_array(t_uint256)49_storage"
},
{
"astId": 23771,
"astId": 23775,
"contract": "src/DataHavenServiceManager.sol:DataHavenServiceManager",
"label": "rewardsInitiator",
"offset": 0,
@ -49,7 +49,7 @@
"type": "t_address"
},
{
"astId": 23776,
"astId": 23780,
"contract": "src/DataHavenServiceManager.sol:DataHavenServiceManager",
"label": "validatorsAllowlist",
"offset": 0,
@ -57,7 +57,7 @@
"type": "t_mapping(t_address,t_bool)"
},
{
"astId": 23780,
"astId": 23784,
"contract": "src/DataHavenServiceManager.sol:DataHavenServiceManager",
"label": "_snowbridgeGateway",
"offset": 0,
@ -65,7 +65,7 @@
"type": "t_contract(IGatewayV2)23481"
},
{
"astId": 23785,
"astId": 23789,
"contract": "src/DataHavenServiceManager.sol:DataHavenServiceManager",
"label": "validatorEthAddressToSolochainAddress",
"offset": 0,
@ -73,7 +73,7 @@
"type": "t_mapping(t_address,t_address)"
},
{
"astId": 23790,
"astId": 23793,
"contract": "src/DataHavenServiceManager.sol:DataHavenServiceManager",
"label": "validatorSolochainAddressToEthAddress",
"offset": 0,
@ -81,7 +81,7 @@
"type": "t_mapping(t_address,t_address)"
},
{
"astId": 23793,
"astId": 23796,
"contract": "src/DataHavenServiceManager.sol:DataHavenServiceManager",
"label": "validatorSetSubmitter",
"offset": 0,
@ -89,12 +89,20 @@
"type": "t_address"
},
{
"astId": 23798,
"astId": 23802,
"contract": "src/DataHavenServiceManager.sol:DataHavenServiceManager",
"label": "strategiesAndMultipliers",
"offset": 0,
"slot": "107",
"type": "t_mapping(t_contract(IStrategy)7361,t_uint96)"
},
{
"astId": 23807,
"contract": "src/DataHavenServiceManager.sol:DataHavenServiceManager",
"label": "__GAP",
"offset": 0,
"slot": "107",
"type": "t_array(t_uint256)44_storage"
"slot": "108",
"type": "t_array(t_uint256)43_storage"
}
],
"types": {
@ -103,10 +111,10 @@
"label": "address",
"numberOfBytes": "20"
},
"t_array(t_uint256)44_storage": {
"t_array(t_uint256)43_storage": {
"encoding": "inplace",
"label": "uint256[44]",
"numberOfBytes": "1408",
"label": "uint256[43]",
"numberOfBytes": "1376",
"base": "t_uint256"
},
"t_array(t_uint256)49_storage": {
@ -131,6 +139,11 @@
"label": "contract IGatewayV2",
"numberOfBytes": "20"
},
"t_contract(IStrategy)7361": {
"encoding": "inplace",
"label": "contract IStrategy",
"numberOfBytes": "20"
},
"t_mapping(t_address,t_address)": {
"encoding": "mapping",
"key": "t_address",
@ -145,6 +158,13 @@
"numberOfBytes": "32",
"value": "t_bool"
},
"t_mapping(t_contract(IStrategy)7361,t_uint96)": {
"encoding": "mapping",
"key": "t_contract(IStrategy)7361",
"label": "mapping(contract IStrategy => uint96)",
"numberOfBytes": "32",
"value": "t_uint96"
},
"t_uint256": {
"encoding": "inplace",
"label": "uint256",
@ -154,6 +174,11 @@
"encoding": "inplace",
"label": "uint8",
"numberOfBytes": "1"
},
"t_uint96": {
"encoding": "inplace",
"label": "uint96",
"numberOfBytes": "12"
}
}
}

View file

@ -24,7 +24,8 @@ contract SnowbridgeIntegrationTest is SnowbridgeAndAVSDeployer {
) public pure returns (bytes[] memory beforeTestCalldata) {
if (testSelector == this.test_sendNewValidatorsSetMessage.selector) {
beforeTestCalldata = new bytes[](1);
beforeTestCalldata[0] = abi.encodeWithSelector(this.setupValidatorsAsOperators.selector);
beforeTestCalldata[0] =
abi.encodeWithSelector(this.setupValidatorsAsOperatorsWithAllocations.selector);
}
}

View file

@ -0,0 +1,568 @@
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.27;
/* solhint-disable func-name-mixedcase */
import {SnowbridgeAndAVSDeployer} from "./utils/SnowbridgeAndAVSDeployer.sol";
import {DataHavenSnowbridgeMessages} from "../src/libraries/DataHavenSnowbridgeMessages.sol";
import {IDataHavenServiceManagerErrors} from "../src/interfaces/IDataHavenServiceManager.sol";
import {IStrategy} from "eigenlayer-contracts/src/contracts/interfaces/IStrategy.sol";
import {
IRewardsCoordinatorTypes
} from "eigenlayer-contracts/src/contracts/interfaces/IRewardsCoordinator.sol";
import {
IAllocationManagerTypes
} from "eigenlayer-contracts/src/contracts/interfaces/IAllocationManager.sol";
import {OperatorSet} from "eigenlayer-contracts/src/contracts/libraries/OperatorSetLib.sol";
import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
contract ValidatorSetSelectionTest is SnowbridgeAndAVSDeployer {
function setUp() public {
_deployMockAllContracts();
}
// ============ Helpers ============
function _getStrategies() internal view returns (IStrategy[] memory) {
IStrategy[] memory strategies = new IStrategy[](deployedStrategies.length);
for (uint256 i = 0; i < deployedStrategies.length; i++) {
strategies[i] = deployedStrategies[i];
}
return strategies;
}
function _setupMultipliers(
uint96[] memory multipliers
) internal {
IStrategy[] memory strategies = _getStrategies();
IRewardsCoordinatorTypes.StrategyAndMultiplier[] memory sm =
new IRewardsCoordinatorTypes.StrategyAndMultiplier[](strategies.length);
for (uint256 i = 0; i < strategies.length; i++) {
sm[i] = IRewardsCoordinatorTypes.StrategyAndMultiplier({
strategy: strategies[i], multiplier: multipliers[i]
});
}
cheats.startPrank(avsOwner);
serviceManager.removeStrategiesFromValidatorsSupportedStrategies(strategies);
serviceManager.addStrategiesToValidatorsSupportedStrategies(sm);
cheats.stopPrank();
}
function _uniformMultipliers() internal pure returns (uint96[] memory) {
uint96[] memory m = new uint96[](3);
m[0] = 1;
m[1] = 1;
m[2] = 1;
return m;
}
function _registerOperator(
address op,
address solochainAddr,
uint256[] memory stakeAmounts
) internal {
cheats.prank(avsOwner);
serviceManager.addValidatorToAllowlist(op);
cheats.startPrank(op);
for (uint256 j = 0; j < deployedStrategies.length; j++) {
IERC20 linkedToken = deployedStrategies[j].underlyingToken();
_setERC20Balance(address(linkedToken), op, stakeAmounts[j]);
linkedToken.approve(address(strategyManager), stakeAmounts[j]);
strategyManager.depositIntoStrategy(deployedStrategies[j], linkedToken, stakeAmounts[j]);
}
delegationManager.registerAsOperator(address(0), 0, "");
uint32[] memory operatorSetIds = new uint32[](1);
operatorSetIds[0] = serviceManager.VALIDATORS_SET_ID();
IAllocationManagerTypes.RegisterParams memory registerParams =
IAllocationManagerTypes.RegisterParams({
avs: address(serviceManager),
operatorSetIds: operatorSetIds,
data: abi.encodePacked(solochainAddr)
});
allocationManager.registerForOperatorSets(op, registerParams);
cheats.stopPrank();
}
function _uniformStakes(
uint256 amount
) internal view returns (uint256[] memory) {
uint256[] memory stakes = new uint256[](deployedStrategies.length);
for (uint256 j = 0; j < stakes.length; j++) {
stakes[j] = amount;
}
return stakes;
}
function _allocateForOperator(
address op
) internal {
IStrategy[] memory strategies = _getStrategies();
uint64[] memory newMagnitudes = new uint64[](strategies.length);
for (uint256 j = 0; j < strategies.length; j++) {
newMagnitudes[j] = 1e18;
}
IAllocationManagerTypes.AllocateParams[] memory allocParams =
new IAllocationManagerTypes.AllocateParams[](1);
allocParams[0] = IAllocationManagerTypes.AllocateParams({
operatorSet: OperatorSet({
avs: address(serviceManager), id: serviceManager.VALIDATORS_SET_ID()
}),
strategies: strategies,
newMagnitudes: newMagnitudes
});
cheats.prank(op);
allocationManager.modifyAllocations(op, allocParams);
}
function _advancePastAllocationConfigDelay() internal {
uint32 delay = allocationManager.ALLOCATION_CONFIGURATION_DELAY();
cheats.roll(block.number + delay + 1);
}
function _advancePastAllocationEffect() internal {
cheats.roll(block.number + 1);
}
function _buildExpectedMessage(
address[] memory validators,
uint64 externalIndex
) internal pure returns (bytes memory) {
return DataHavenSnowbridgeMessages.scaleEncodeNewValidatorSetMessagePayload(
DataHavenSnowbridgeMessages.NewValidatorSetPayload({
validators: validators, externalIndex: externalIndex
})
);
}
// ============ Admin Function Tests ============
// Test #7: Add strategy + multiplier in one call; verify both stored
function test_addStrategies_setsMultiplierAtomically() public {
IStrategy[] memory strategies = _getStrategies();
IRewardsCoordinatorTypes.StrategyAndMultiplier[] memory sm =
new IRewardsCoordinatorTypes.StrategyAndMultiplier[](3);
sm[0] = IRewardsCoordinatorTypes.StrategyAndMultiplier({
strategy: strategies[0], multiplier: 5000
});
sm[1] = IRewardsCoordinatorTypes.StrategyAndMultiplier({
strategy: strategies[1], multiplier: 10000
});
sm[2] = IRewardsCoordinatorTypes.StrategyAndMultiplier({
strategy: strategies[2], multiplier: 2000
});
cheats.startPrank(avsOwner);
serviceManager.removeStrategiesFromValidatorsSupportedStrategies(strategies);
serviceManager.addStrategiesToValidatorsSupportedStrategies(sm);
cheats.stopPrank();
assertEq(serviceManager.strategiesAndMultipliers(strategies[0]), 5000);
assertEq(serviceManager.strategiesAndMultipliers(strategies[1]), 10000);
assertEq(serviceManager.strategiesAndMultipliers(strategies[2]), 2000);
}
// Test #9: Remove strategy multiplier and tracking bool deleted
function test_removeStrategies_cleansUpMultiplier() public {
IStrategy[] memory strategies = _getStrategies();
IRewardsCoordinatorTypes.StrategyAndMultiplier[] memory sm =
new IRewardsCoordinatorTypes.StrategyAndMultiplier[](3);
sm[0] = IRewardsCoordinatorTypes.StrategyAndMultiplier({
strategy: strategies[0], multiplier: 5000
});
sm[1] = IRewardsCoordinatorTypes.StrategyAndMultiplier({
strategy: strategies[1], multiplier: 10000
});
sm[2] = IRewardsCoordinatorTypes.StrategyAndMultiplier({
strategy: strategies[2], multiplier: 2000
});
cheats.startPrank(avsOwner);
serviceManager.removeStrategiesFromValidatorsSupportedStrategies(strategies);
serviceManager.addStrategiesToValidatorsSupportedStrategies(sm);
assertEq(serviceManager.strategiesAndMultipliers(strategies[1]), 10000);
serviceManager.removeStrategiesFromValidatorsSupportedStrategies(strategies);
cheats.stopPrank();
assertEq(serviceManager.strategiesAndMultipliers(strategies[0]), 0);
assertEq(serviceManager.strategiesAndMultipliers(strategies[1]), 0);
assertEq(serviceManager.strategiesAndMultipliers(strategies[2]), 0);
}
// Test #11: Returns correct StrategyAndMultiplier structs
function test_getStrategiesAndMultipliers_returnsCorrect() public {
IStrategy[] memory strategies = _getStrategies();
IRewardsCoordinatorTypes.StrategyAndMultiplier[] memory sm =
new IRewardsCoordinatorTypes.StrategyAndMultiplier[](3);
sm[0] = IRewardsCoordinatorTypes.StrategyAndMultiplier({
strategy: strategies[0], multiplier: 5000
});
sm[1] = IRewardsCoordinatorTypes.StrategyAndMultiplier({
strategy: strategies[1], multiplier: 10000
});
sm[2] = IRewardsCoordinatorTypes.StrategyAndMultiplier({
strategy: strategies[2], multiplier: 2000
});
cheats.startPrank(avsOwner);
serviceManager.removeStrategiesFromValidatorsSupportedStrategies(strategies);
serviceManager.addStrategiesToValidatorsSupportedStrategies(sm);
cheats.stopPrank();
IRewardsCoordinatorTypes.StrategyAndMultiplier[] memory result =
serviceManager.getStrategiesAndMultipliers();
assertEq(result.length, 3);
for (uint256 i = 0; i < result.length; i++) {
uint96 expectedMultiplier = serviceManager.strategiesAndMultipliers(result[i].strategy);
assertEq(result[i].multiplier, expectedMultiplier);
}
}
// Test: setStrategiesAndMultipliers updates existing multipliers
function test_setStrategiesAndMultipliers_updatesMultipliers() public {
IStrategy[] memory strategies = _getStrategies();
// Set initial multipliers via _setupMultipliers
uint96[] memory initial = new uint96[](3);
initial[0] = 5000;
initial[1] = 10000;
initial[2] = 2000;
_setupMultipliers(initial);
// Update multipliers
IRewardsCoordinatorTypes.StrategyAndMultiplier[] memory updated =
new IRewardsCoordinatorTypes.StrategyAndMultiplier[](3);
updated[0] = IRewardsCoordinatorTypes.StrategyAndMultiplier({
strategy: strategies[0], multiplier: 1
});
updated[1] = IRewardsCoordinatorTypes.StrategyAndMultiplier({
strategy: strategies[1], multiplier: 1
});
updated[2] = IRewardsCoordinatorTypes.StrategyAndMultiplier({
strategy: strategies[2], multiplier: 9999
});
cheats.prank(avsOwner);
serviceManager.setStrategiesAndMultipliers(updated);
assertEq(serviceManager.strategiesAndMultipliers(strategies[0]), 1);
assertEq(serviceManager.strategiesAndMultipliers(strategies[1]), 1);
assertEq(serviceManager.strategiesAndMultipliers(strategies[2]), 9999);
}
// Test: setStrategiesAndMultipliers changes validator ranking
function test_setStrategiesAndMultipliers_affectsRanking() public {
uint96[] memory mults = new uint96[](3);
mults[0] = 10000;
mults[1] = 1;
mults[2] = 1;
_setupMultipliers(mults);
// Op A: heavy in strategy 0 (high multiplier) initially ranked first
address opA = vm.addr(801);
address solochainA = address(uint160(0x6001));
uint256[] memory stakesA = new uint256[](3);
stakesA[0] = 1000 ether;
stakesA[1] = 10 ether;
stakesA[2] = 10 ether;
_registerOperator(opA, solochainA, stakesA);
// Op B: heavy in strategy 1 (low multiplier) initially ranked second
address opB = vm.addr(802);
address solochainB = address(uint160(0x6002));
uint256[] memory stakesB = new uint256[](3);
stakesB[0] = 10 ether;
stakesB[1] = 1000 ether;
stakesB[2] = 10 ether;
_registerOperator(opB, solochainB, stakesB);
_advancePastAllocationConfigDelay();
_allocateForOperator(opA);
_allocateForOperator(opB);
_advancePastAllocationEffect();
// Before update: A ranks first (strategy 0 has multiplier 10_000)
address[] memory expectedBefore = new address[](2);
expectedBefore[0] = solochainA;
expectedBefore[1] = solochainB;
assertEq(
serviceManager.buildNewValidatorSetMessageForEra(0),
_buildExpectedMessage(expectedBefore, 0)
);
// Flip multipliers: strategy 1 now has high multiplier
IStrategy[] memory strategies = _getStrategies();
IRewardsCoordinatorTypes.StrategyAndMultiplier[] memory flipped =
new IRewardsCoordinatorTypes.StrategyAndMultiplier[](3);
flipped[0] = IRewardsCoordinatorTypes.StrategyAndMultiplier({
strategy: strategies[0], multiplier: 1
});
flipped[1] = IRewardsCoordinatorTypes.StrategyAndMultiplier({
strategy: strategies[1], multiplier: 10000
});
flipped[2] = IRewardsCoordinatorTypes.StrategyAndMultiplier({
strategy: strategies[2], multiplier: 1
});
cheats.prank(avsOwner);
serviceManager.setStrategiesAndMultipliers(flipped);
// After update: B ranks first (strategy 1 now has multiplier 10_000)
address[] memory expectedAfter = new address[](2);
expectedAfter[0] = solochainB;
expectedAfter[1] = solochainA;
assertEq(
serviceManager.buildNewValidatorSetMessageForEra(0),
_buildExpectedMessage(expectedAfter, 0)
);
}
// ============ Selection Tests ============
// Test #1: 3 strategies with different multipliers; verify correct ordering
function test_weightedStake_multipleStrategies() public {
uint96[] memory mults = new uint96[](3);
mults[0] = 5000;
mults[1] = 10000;
mults[2] = 2000;
_setupMultipliers(mults);
// Op A: heavy in strategy 0 (multiplier 5000)
address opA = vm.addr(101);
address solochainA = address(uint160(0xA01));
uint256[] memory stakesA = new uint256[](3);
stakesA[0] = 1000 ether;
stakesA[1] = 100 ether;
stakesA[2] = 100 ether;
_registerOperator(opA, solochainA, stakesA);
// Op B: heavy in strategy 1 (multiplier 10000) highest weighted stake
address opB = vm.addr(102);
address solochainB = address(uint160(0xB01));
uint256[] memory stakesB = new uint256[](3);
stakesB[0] = 100 ether;
stakesB[1] = 1000 ether;
stakesB[2] = 100 ether;
_registerOperator(opB, solochainB, stakesB);
// Op C: heavy in strategy 2 (multiplier 2000) lowest weighted stake
address opC = vm.addr(103);
address solochainC = address(uint160(0xC01));
uint256[] memory stakesC = new uint256[](3);
stakesC[0] = 100 ether;
stakesC[1] = 100 ether;
stakesC[2] = 1000 ether;
_registerOperator(opC, solochainC, stakesC);
_advancePastAllocationConfigDelay();
_allocateForOperator(opA);
_allocateForOperator(opB);
_allocateForOperator(opC);
_advancePastAllocationEffect();
// Expected order: B (highest multiplied strategy), A, C
address[] memory expected = new address[](3);
expected[0] = solochainB;
expected[1] = solochainA;
expected[2] = solochainC;
assertEq(
serviceManager.buildNewValidatorSetMessageForEra(0), _buildExpectedMessage(expected, 0)
);
}
// Test #2: 2 operators with identical weighted stake; lower Eth address ranks first
function test_tieBreak_lowerAddressWins() public {
_setupMultipliers(_uniformMultipliers());
address addrA = vm.addr(201);
address addrB = vm.addr(202);
// Ensure addrLow < addrHigh
address addrLow = addrA < addrB ? addrA : addrB;
address addrHigh = addrA < addrB ? addrB : addrA;
address solochainLow = address(uint160(0xBB));
address solochainHigh = address(uint160(0xAA));
_registerOperator(addrLow, solochainLow, _uniformStakes(500 ether));
_registerOperator(addrHigh, solochainHigh, _uniformStakes(500 ether));
_advancePastAllocationConfigDelay();
_allocateForOperator(addrLow);
_allocateForOperator(addrHigh);
_advancePastAllocationEffect();
// Lower Eth address wins tie-break
address[] memory expected = new address[](2);
expected[0] = solochainLow;
expected[1] = solochainHigh;
assertEq(
serviceManager.buildNewValidatorSetMessageForEra(0), _buildExpectedMessage(expected, 0)
);
}
// Test #3: Register 35 operators; verify only top 32 selected
function test_topN_moreThan32() public {
_setupMultipliers(_uniformMultipliers());
uint256 totalOps = 35;
address[] memory operators = new address[](totalOps);
address[] memory solochainAddrs = new address[](totalOps);
for (uint256 i = 0; i < totalOps; i++) {
operators[i] = vm.addr(300 + i);
solochainAddrs[i] = address(uint160(0x1000 + i));
_registerOperator(operators[i], solochainAddrs[i], _uniformStakes((i + 1) * 10 ether));
}
_advancePastAllocationConfigDelay();
for (uint256 i = 0; i < totalOps; i++) {
_allocateForOperator(operators[i]);
}
_advancePastAllocationEffect();
bytes memory message = serviceManager.buildNewValidatorSetMessageForEra(0);
// Top 32 by descending stake: operators at indices 34, 33, ..., 3
address[] memory expected = new address[](32);
for (uint256 i = 0; i < 32; i++) {
expected[i] = solochainAddrs[totalOps - 1 - i];
}
assertEq(message, _buildExpectedMessage(expected, 0));
}
// Test #4: 5 operators; all included in output
function test_lessThan32_includesAll() public {
_setupMultipliers(_uniformMultipliers());
uint256 totalOps = 5;
address[] memory operators = new address[](totalOps);
address[] memory solochainAddrs = new address[](totalOps);
for (uint256 i = 0; i < totalOps; i++) {
operators[i] = vm.addr(400 + i);
solochainAddrs[i] = address(uint160(0x2000 + i));
_registerOperator(operators[i], solochainAddrs[i], _uniformStakes((i + 1) * 100 ether));
}
_advancePastAllocationConfigDelay();
for (uint256 i = 0; i < totalOps; i++) {
_allocateForOperator(operators[i]);
}
_advancePastAllocationEffect();
// All 5 included, sorted by descending stake
address[] memory expected = new address[](5);
for (uint256 i = 0; i < 5; i++) {
expected[i] = solochainAddrs[totalOps - 1 - i];
}
assertEq(
serviceManager.buildNewValidatorSetMessageForEra(0), _buildExpectedMessage(expected, 0)
);
}
// Test #5: Operator with zero allocation excluded
function test_zeroWeightedStake_filtered() public {
_setupMultipliers(_uniformMultipliers());
address op1 = vm.addr(501);
address solochain1 = address(uint160(0x3001));
_registerOperator(op1, solochain1, _uniformStakes(100 ether));
address op2 = vm.addr(502);
address solochain2 = address(uint160(0x3002));
_registerOperator(op2, solochain2, _uniformStakes(200 ether));
// op3 registered but NOT allocated zero weighted stake
address op3 = vm.addr(503);
address solochain3 = address(uint160(0x3003));
_registerOperator(op3, solochain3, _uniformStakes(300 ether));
_advancePastAllocationConfigDelay();
// Only allocate for op1 and op2
_allocateForOperator(op1);
_allocateForOperator(op2);
_advancePastAllocationEffect();
// op3 should be filtered out
address[] memory expected = new address[](2);
expected[0] = solochain2;
expected[1] = solochain1;
assertEq(
serviceManager.buildNewValidatorSetMessageForEra(0), _buildExpectedMessage(expected, 0)
);
}
// Test #6: A zero multiplier is accepted and causes that strategy's stake to contribute
// no weight. The operator is still included if other strategies have non-zero multipliers.
function test_zeroMultiplier_accepted_contributesNoWeight() public {
IStrategy[] memory strategies = _getStrategies();
// Zero-out the first strategy's multiplier via setStrategiesAndMultipliers
IRewardsCoordinatorTypes.StrategyAndMultiplier[] memory sm =
new IRewardsCoordinatorTypes.StrategyAndMultiplier[](1);
sm[0] = IRewardsCoordinatorTypes.StrategyAndMultiplier({
strategy: strategies[0], multiplier: 0
});
cheats.prank(avsOwner);
serviceManager.setStrategiesAndMultipliers(sm);
assertEq(serviceManager.strategiesAndMultipliers(strategies[0]), 0);
}
// Test #12: Full integration weighted selection + correct message encoding
function test_buildMessage_encodesCorrectly() public {
_setupMultipliers(_uniformMultipliers());
address op1 = vm.addr(701);
address solochain1 = address(uint160(0x5001));
_registerOperator(op1, solochain1, _uniformStakes(500 ether));
address op2 = vm.addr(702);
address solochain2 = address(uint160(0x5002));
_registerOperator(op2, solochain2, _uniformStakes(1000 ether));
_advancePastAllocationConfigDelay();
_allocateForOperator(op1);
_allocateForOperator(op2);
_advancePastAllocationEffect();
bytes memory message = serviceManager.buildNewValidatorSetMessageForEra(0);
// op2 has higher stake first
address[] memory expected = new address[](2);
expected[0] = solochain2;
expected[1] = solochain1;
assertEq(message, _buildExpectedMessage(expected, 0));
}
}

View file

@ -12,7 +12,9 @@ import {DataHavenServiceManager} from "../src/DataHavenServiceManager.sol";
import {
TransparentUpgradeableProxy
} from "@openzeppelin/contracts/proxy/transparent/TransparentUpgradeableProxy.sol";
import {IStrategy} from "eigenlayer-contracts/src/contracts/interfaces/IStrategy.sol";
import {
IRewardsCoordinatorTypes
} from "eigenlayer-contracts/src/contracts/interfaces/IRewardsCoordinator.sol";
contract ValidatorSetSubmitterTest is SnowbridgeAndAVSDeployer {
address public submitterA = address(uint160(uint256(keccak256("submitterA"))));
@ -35,7 +37,8 @@ contract ValidatorSetSubmitterTest is SnowbridgeAndAVSDeployer {
== this.test_buildNewValidatorSetMessageForEra_exactEncoding.selector
) {
beforeTestCalldata = new bytes[](1);
beforeTestCalldata[0] = abi.encodeWithSelector(this.setupValidatorsAsOperators.selector);
beforeTestCalldata[0] =
abi.encodeWithSelector(this.setupValidatorsAsOperatorsWithAllocations.selector);
}
}
@ -179,7 +182,8 @@ contract ValidatorSetSubmitterTest is SnowbridgeAndAVSDeployer {
function test_sendNewValidatorSetForEra_revertsWhenSubmitterIsZeroAddress() public {
// Deploy a fresh proxy with address(0) as the submitter
IStrategy[] memory emptyStrategies = new IStrategy[](0);
IRewardsCoordinatorTypes.StrategyAndMultiplier[] memory emptyStrategies =
new IRewardsCoordinatorTypes.StrategyAndMultiplier[](0);
cheats.startPrank(regularDeployer);
DataHavenServiceManager zeroSubmitterSM = DataHavenServiceManager(

View file

@ -238,14 +238,6 @@ contract AVSDeployer is Test {
serviceManagerImplementation =
new DataHavenServiceManager(rewardsCoordinator, allocationManager);
// Create array for validators strategies required by DataHavenServiceManager
IStrategy[] memory validatorsStrategies = new IStrategy[](deployedStrategies.length);
// For testing purposes, we'll use the deployed strategies for validators
for (uint256 i = 0; i < deployedStrategies.length; i++) {
validatorsStrategies[i] = deployedStrategies[i];
}
serviceManager = DataHavenServiceManager(
address(
new TransparentUpgradeableProxy(
@ -255,7 +247,7 @@ contract AVSDeployer is Test {
DataHavenServiceManager.initialize.selector,
avsOwner,
rewardsInitiator,
validatorsStrategies,
defaultStrategyAndMultipliers,
address(snowbridgeGatewayMock),
avsOwner
)

View file

@ -16,6 +16,11 @@ import {TestUtils} from "./TestUtils.sol";
import {
IAllocationManagerTypes
} from "eigenlayer-contracts/src/contracts/interfaces/IAllocationManager.sol";
import {IStrategy} from "eigenlayer-contracts/src/contracts/interfaces/IStrategy.sol";
import {
IRewardsCoordinatorTypes
} from "eigenlayer-contracts/src/contracts/interfaces/IRewardsCoordinator.sol";
import {OperatorSet} from "eigenlayer-contracts/src/contracts/libraries/OperatorSetLib.sol";
import {ValidatorsUtils} from "../../script/utils/ValidatorsUtils.sol";
import {console} from "forge-std/Test.sol";
@ -145,6 +150,61 @@ contract SnowbridgeAndAVSDeployer is AVSDeployer {
serviceManager.setSnowbridgeGateway(address(gateway));
}
function setupValidatorsAsOperatorsWithAllocations() public {
setupValidatorsAsOperators();
// Remove strategies added during initialize (without multipliers)
// and re-add them with explicit multipliers
IStrategy[] memory strategies = new IStrategy[](deployedStrategies.length);
for (uint256 i = 0; i < deployedStrategies.length; i++) {
strategies[i] = deployedStrategies[i];
}
IRewardsCoordinatorTypes.StrategyAndMultiplier[] memory sm =
new IRewardsCoordinatorTypes.StrategyAndMultiplier[](deployedStrategies.length);
for (uint256 i = 0; i < deployedStrategies.length; i++) {
sm[i] = IRewardsCoordinatorTypes.StrategyAndMultiplier({
strategy: strategies[i],
multiplier: 1 // 1x multiplier for all strategies
});
}
cheats.startPrank(avsOwner);
serviceManager.removeStrategiesFromValidatorsSupportedStrategies(strategies);
serviceManager.addStrategiesToValidatorsSupportedStrategies(sm);
cheats.stopPrank();
// Advance past ALLOCATION_CONFIGURATION_DELAY (1 day = 86400 blocks in test setup)
// so operator allocation delays take effect
uint32 allocationConfigDelay = allocationManager.ALLOCATION_CONFIGURATION_DELAY();
cheats.roll(block.number + allocationConfigDelay + 1);
// For each operator, allocate full magnitude to the DataHaven operator set
for (uint256 i = 0; i < validatorsAllowlist.length; i++) {
IAllocationManagerTypes.AllocateParams[] memory allocParams =
new IAllocationManagerTypes.AllocateParams[](1);
uint64[] memory newMagnitudes = new uint64[](deployedStrategies.length);
for (uint256 j = 0; j < deployedStrategies.length; j++) {
newMagnitudes[j] = 1e18; // 100% magnitude
}
allocParams[0] = IAllocationManagerTypes.AllocateParams({
operatorSet: OperatorSet({
avs: address(serviceManager), id: serviceManager.VALIDATORS_SET_ID()
}),
strategies: strategies,
newMagnitudes: newMagnitudes
});
cheats.prank(validatorsAllowlist[i]);
allocationManager.modifyAllocations(validatorsAllowlist[i], allocParams);
}
// Advance past allocation effect delay (operator delay is 0, so just +1 block)
cheats.roll(block.number + 1);
}
function setupValidatorsAsOperators() public {
for (uint256 i = 0; i < validatorsAllowlist.length; i++) {
console.log("Setting up validator %s as operator", validatorsAllowlist[i]);

View file

@ -0,0 +1,247 @@
# Validator Set Selection Specification
## Top-32 by Weighted Stake (Continuation of PR #433)
- Status: Draft
- Owners: DataHaven Team
- Last Updated: February 12, 2026
- Depends on: PR #433 (`feat: automated validator set submission with era targeting`)
## 1. Summary
PR #433 introduced era-targeted validator-set submission with a dedicated submitter role and runtime era validation. This spec is a continuation of that work.
This document adds deterministic weighted-stake selection so the outbound validator set is ranked before it is bridged:
1. Ethereum computes weighted stake per operator.
2. Ethereum deterministically sorts operators and selects top candidates.
3. DataHaven enforces a final total active authority cap of 32 after combining whitelisted and external validators.
The era-targeting model from PR #433 remains unchanged.
## 2. Baseline From PR #433
This spec assumes the following behavior already exists:
1. `DataHavenServiceManager.sendNewValidatorSetForEra(uint64 targetEra, ...)` is used for submission.
2. Submission is restricted to `validatorSetSubmitter` (`onlyValidatorSetSubmitter`).
3. `external_index` in the Snowbridge payload is the `targetEra`.
4. DataHaven runtime enforces era validity (`targetEra` old/too-new/duplicate checks).
## 3. Goals
1. Select external validators by weighted stake instead of raw member ordering.
2. Keep selection deterministic (`same chain state -> same selected set`).
3. Preserve PR #433 era-targeting invariants and submitter authorization flow.
4. Enforce total active authority cap = 32 (`whitelisted + external`).
5. Keep payload shape stable unless there is a hard requirement to version it.
## 4. Non-Goals
1. Replacing PR #433 submitter-role model.
2. Changing PR #433 era-target validation semantics.
3. Redesigning Snowbridge transport internals.
4. Changing reward formulas in this spec.
## 5. Current Behavior (Post-PR #433)
### 5.1 Ethereum
`buildNewValidatorSetMessageForEra(targetEra)` gathers all operator-set members with a mapped solochain address and forwards them in that order. There is no stake-based ranking.
### 5.2 Payload
Current payload carries:
1. `validators`
2. `external_index` (interpreted as `targetEra`)
### 5.3 DataHaven Runtime
`set_external_validators_inner()` stores incoming validators and `ExternalIndex`, then era application and validator composition logic consume them.
### 5.4 Limitation
Without stake-aware ordering, high-stake operators may be displaced by lower-stake operators when list size pressure or downstream caps apply.
## 6. Design Decisions
### D1. Do ranking on Ethereum
EigenLayer membership/allocation context is available on Ethereum, so weighted ranking is computed there.
### D2. Keep PR #433 era semantics unchanged
`external_index` must continue to encode `targetEra`. This spec does not repurpose it (no nonce/block-number substitution).
### D3. Deterministic tie-break
For equal weighted stake, lower Ethereum operator address wins.
### D4. Cap applies to total active authorities
Final active validator set must satisfy:
`final_active = take_32(dedupe(whitelisted ++ external_sorted_limited))`
### D5. Strategy multipliers are explicit and default to zero if unset
Multipliers are owner-managed in `strategiesAndMultipliers`. If an entry is unset for a strategy, its effective multiplier is `0` (no weighted contribution).
### D6. Keep strategy list and multipliers in sync
Multiplier lifecycle is tied to strategy lifecycle:
1. Add strategy -> add multiplier in the same call via `IRewardsCoordinatorTypes.StrategyAndMultiplier` struct.
2. Remove strategy -> delete multiplier in the same call.
## 7. Weighted Stake Model
For each operator `o`:
`weightedStake(o) = sum_i( allocatedStake(o, strategy_i) * multiplier(strategy_i) )`
Where:
1. `allocatedStake` comes from EigenLayer allocation data.
2. `multiplier` is a per-strategy weight (no normalization divisor is applied during ranking).
### 7.1 Strategy Weight Semantics
1. Every supported strategy should have an explicit multiplier entry for operational clarity.
2. Missing multiplier entry is treated as `0` multiplier.
3. Multiplier values are managed explicitly by owner/governance.
### 7.2 Unit Assumption
Stake inputs must be unit-consistent across strategies. If they are not, normalize before summing.
## 8. Ethereum Contract Changes (On Top of PR #433)
File: `contracts/src/DataHavenServiceManager.sol`
### 8.1 New State
```solidity
uint32 public constant MAX_ACTIVE_VALIDATORS = 32;
mapping(IStrategy => uint96) public strategiesAndMultipliers;
```
### 8.2 New/Updated Admin APIs
```solidity
function setStrategiesAndMultipliers(IRewardsCoordinatorTypes.StrategyAndMultiplier[] calldata strategyMultipliers) external onlyOwner;
function addStrategiesToValidatorsSupportedStrategies(IRewardsCoordinatorTypes.StrategyAndMultiplier[] calldata strategyMultipliers) external onlyOwner;
function removeStrategiesFromValidatorsSupportedStrategies(IStrategy[] calldata strategies) external onlyOwner;
function getStrategiesAndMultipliers() external view returns (IRewardsCoordinatorTypes.StrategyAndMultiplier[] memory);
```
Using EigenLayer's `StrategyAndMultiplier` struct pairs each strategy with its multiplier, eliminating the possibility of length mismatches between parallel arrays. Duplicate strategies in `addStrategies` are rejected by EigenLayer's `StrategyAlreadyInOperatorSet` check; duplicates in `setStrategiesAndMultipliers` are harmless (last-write-wins on the mapping).
### 8.3 Updated Selection Flow
`buildNewValidatorSetMessageForEra(uint64 targetEra)` should:
1. Read validator operator set members.
2. Compute weighted stake per operator.
3. Filter out operators with no solochain mapping.
4. Resolve multiplier from `strategiesAndMultipliers` for each strategy used.
5. If any strategy is missing a multiplier entry, treat it as `0` multiplier.
6. Filter out operators with zero weighted stake.
7. Select at most `MAX_ACTIVE_VALIDATORS` (32) candidates by weighted stake desc + address asc tie-break (if fewer than 32 eligible candidates exist, include all).
8. Encode using existing payload shape with `externalIndex = targetEra`.
For any EigenLayer call that consumes `StrategyAndMultiplier[]`, materialize the list in ascending strategy-address order.
`sendNewValidatorSetForEra(...)` and `onlyValidatorSetSubmitter` remain unchanged from PR #433.
## 9. Bridge Message Format
No payload version bump in this spec.
Continue using existing `ReceiveValidators` message shape:
```text
[EL_MESSAGE_ID]
[MessageVersion]
[ReceiveValidators]
[validator_count]
[validators (N * 20B)]
[external_index (u64 targetEra)]
```
If stake vectors are required in the future, that should be a separate versioned command proposal.
## 10. DataHaven Runtime Changes
File: `operator/pallets/external-validators/src/lib.rs`
### 10.1 Keep PR #433 era validation
Retain existing target-era gates and error semantics (`TargetEraTooOld`, `TargetEraTooNew`, `DuplicateOrStaleTargetEra`).
### 10.2 Enforce final total cap = 32
At validator composition time:
1. `w = whitelisted.len()`
2. `external_budget = 32.saturating_sub(w)`
3. Use at most `external_budget` external validators from the ranked list.
4. Build final set as `take_32(dedupe(whitelisted ++ external_limited))`.
### 10.3 Runtime constants
`MaxExternalValidators` can remain a defensive bound, but final active enforcement must guarantee max 32 authorities.
## 11. Rollout Plan
1. Merge/deploy PR #433 baseline first (submitter role + era-target checks).
2. Deploy ServiceManager upgrade with weighted ranking logic.
3. Backfill/confirm `strategiesAndMultipliers` for all currently supported strategies.
4. Deploy runtime changes for final total-cap enforcement.
5. Re-run submitter daemon unchanged (it still submits `targetEra = ActiveEra + 1`).
6. Monitor across multiple era cycles before production rollout.
## 12. Testing Plan
### 12.1 Solidity
1. Weighted stake computation across multiple strategies.
2. Deterministic tie-break behavior.
3. Top-32 selection when candidate count exceeds 32.
4. Behavior when candidate count is below 32.
5. Zero-stake filtering.
6. Missing multiplier entries are treated as zero contribution.
7. `addStrategies...` sets multipliers atomically via `StrategyAndMultiplier` struct.
8. `removeStrategies...` removes multiplier entries for removed strategies.
9. `getStrategiesAndMultipliers()` returns a list matching EigenLayer's operator set strategies.
11. Integration with `buildNewValidatorSetMessageForEra(targetEra)` and correct target era encoding.
### 12.2 Runtime
1. Existing PR #433 era-validation tests continue to pass unchanged.
2. Final active authority cap remains <= 32 with mixed whitelisted/external sets.
3. Composition logic preserves whitelisted priority while enforcing cap.
### 12.3 Integration / E2E
1. End-to-end submission through `sendNewValidatorSetForEra` with ranked validator output.
2. Delayed relay still fails with PR #433 semantics (no regressions).
3. Ranked selection outcome is deterministic across repeated runs at fixed state.
## 13. Security Considerations
1. Owner-managed strategy weights are governance-sensitive and should remain multisig/governance controlled.
2. Deterministic ordering prevents non-deterministic set drift.
3. Preserve PR #433 stale/duplicate/too-early rejection invariants.
4. Apply overflow checks in weighted arithmetic and any integer downcasts.
## 14. File Change Summary
1. `contracts/src/DataHavenServiceManager.sol`
- weighted stake computation and deterministic top selection in `buildNewValidatorSetMessageForEra`.
2. `contracts/src/interfaces/IDataHavenServiceManager.sol`
- `strategiesAndMultipliers` naming and add/remove strategy API signature updates with multipliers.
3. `operator/pallets/external-validators/src/lib.rs`
- final authority cap enforcement at composition time (while keeping PR #433 era validation behavior).
4. `contracts/test/*`, `operator/pallets/external-validators/src/tests.rs`, `test/e2e/suites/validator-set-update.test.ts`
- unit/runtime/e2e coverage for weighted selection + strategy/multiplier sync + cap behavior + non-regression on era-targeted flow.

View file

@ -2047,6 +2047,13 @@ export const dataHavenServiceManagerAbi = [
outputs: [{ name: '', internalType: 'string', type: 'string' }],
stateMutability: 'view',
},
{
type: 'function',
inputs: [],
name: 'MAX_ACTIVE_VALIDATORS',
outputs: [{ name: '', internalType: 'uint32', type: 'uint32' }],
stateMutability: 'view',
},
{
type: 'function',
inputs: [],
@ -2058,9 +2065,17 @@ export const dataHavenServiceManagerAbi = [
type: 'function',
inputs: [
{
name: '_strategies',
internalType: 'contract IStrategy[]',
type: 'address[]',
name: '_strategyMultipliers',
internalType: 'struct IRewardsCoordinatorTypes.StrategyAndMultiplier[]',
type: 'tuple[]',
components: [
{
name: 'strategy',
internalType: 'contract IStrategy',
type: 'address',
},
{ name: 'multiplier', internalType: 'uint96', type: 'uint96' },
],
},
],
name: 'addStrategiesToValidatorsSupportedStrategies',
@ -2102,15 +2117,44 @@ export const dataHavenServiceManagerAbi = [
outputs: [],
stateMutability: 'nonpayable',
},
{
type: 'function',
inputs: [],
name: 'getStrategiesAndMultipliers',
outputs: [
{
name: '',
internalType: 'struct IRewardsCoordinatorTypes.StrategyAndMultiplier[]',
type: 'tuple[]',
components: [
{
name: 'strategy',
internalType: 'contract IStrategy',
type: 'address',
},
{ name: 'multiplier', internalType: 'uint96', type: 'uint96' },
],
},
],
stateMutability: 'view',
},
{
type: 'function',
inputs: [
{ name: 'initialOwner', internalType: 'address', type: 'address' },
{ name: '_rewardsInitiator', internalType: 'address', type: 'address' },
{
name: 'validatorsStrategies',
internalType: 'contract IStrategy[]',
type: 'address[]',
name: 'validatorsStrategiesAndMultipliers',
internalType: 'struct IRewardsCoordinatorTypes.StrategyAndMultiplier[]',
type: 'tuple[]',
components: [
{
name: 'strategy',
internalType: 'contract IStrategy',
type: 'address',
},
{ name: 'multiplier', internalType: 'uint96', type: 'uint96' },
],
},
{
name: '_snowbridgeGatewayAddress',
@ -2213,6 +2257,27 @@ export const dataHavenServiceManagerAbi = [
outputs: [],
stateMutability: 'nonpayable',
},
{
type: 'function',
inputs: [
{
name: '_strategyMultipliers',
internalType: 'struct IRewardsCoordinatorTypes.StrategyAndMultiplier[]',
type: 'tuple[]',
components: [
{
name: 'strategy',
internalType: 'contract IStrategy',
type: 'address',
},
{ name: 'multiplier', internalType: 'uint96', type: 'uint96' },
],
},
],
name: 'setStrategiesAndMultipliers',
outputs: [],
stateMutability: 'nonpayable',
},
{
type: 'function',
inputs: [
@ -2252,6 +2317,13 @@ export const dataHavenServiceManagerAbi = [
outputs: [{ name: '', internalType: 'address', type: 'address' }],
stateMutability: 'view',
},
{
type: 'function',
inputs: [{ name: '', internalType: 'contract IStrategy', type: 'address' }],
name: 'strategiesAndMultipliers',
outputs: [{ name: '', internalType: 'uint96', type: 'uint96' }],
stateMutability: 'view',
},
{
type: 'function',
inputs: [
@ -2498,6 +2570,27 @@ export const dataHavenServiceManagerAbi = [
],
name: 'SolochainAddressUpdated',
},
{
type: 'event',
anonymous: false,
inputs: [
{
name: 'strategyMultipliers',
internalType: 'struct IRewardsCoordinatorTypes.StrategyAndMultiplier[]',
type: 'tuple[]',
components: [
{
name: 'strategy',
internalType: 'contract IStrategy',
type: 'address',
},
{ name: 'multiplier', internalType: 'uint96', type: 'uint96' },
],
indexed: false,
},
],
name: 'StrategiesAndMultipliersSet',
},
{
type: 'event',
anonymous: false,
@ -2580,6 +2673,7 @@ export const dataHavenServiceManagerAbi = [
{ type: 'error', inputs: [], name: 'OnlyValidatorSetSubmitter' },
{ type: 'error', inputs: [], name: 'OperatorNotInAllowlist' },
{ type: 'error', inputs: [], name: 'SolochainAddressAlreadyAssigned' },
{ type: 'error', inputs: [], name: 'StrategyNotInOperatorSet' },
{ type: 'error', inputs: [], name: 'UnknownSolochainAddress' },
{ type: 'error', inputs: [], name: 'ZeroAddress' },
] as const
@ -10719,6 +10813,15 @@ export const readDataHavenServiceManagerDatahavenAvsMetadata =
functionName: 'DATAHAVEN_AVS_METADATA',
})
/**
* Wraps __{@link readContract}__ with `abi` set to __{@link dataHavenServiceManagerAbi}__ and `functionName` set to `"MAX_ACTIVE_VALIDATORS"`
*/
export const readDataHavenServiceManagerMaxActiveValidators =
/*#__PURE__*/ createReadContract({
abi: dataHavenServiceManagerAbi,
functionName: 'MAX_ACTIVE_VALIDATORS',
})
/**
* Wraps __{@link readContract}__ with `abi` set to __{@link dataHavenServiceManagerAbi}__ and `functionName` set to `"VALIDATORS_SET_ID"`
*/
@ -10737,6 +10840,15 @@ export const readDataHavenServiceManagerBuildNewValidatorSetMessageForEra =
functionName: 'buildNewValidatorSetMessageForEra',
})
/**
* Wraps __{@link readContract}__ with `abi` set to __{@link dataHavenServiceManagerAbi}__ and `functionName` set to `"getStrategiesAndMultipliers"`
*/
export const readDataHavenServiceManagerGetStrategiesAndMultipliers =
/*#__PURE__*/ createReadContract({
abi: dataHavenServiceManagerAbi,
functionName: 'getStrategiesAndMultipliers',
})
/**
* Wraps __{@link readContract}__ with `abi` set to __{@link dataHavenServiceManagerAbi}__ and `functionName` set to `"owner"`
*/
@ -10764,6 +10876,15 @@ export const readDataHavenServiceManagerSnowbridgeGateway =
functionName: 'snowbridgeGateway',
})
/**
* Wraps __{@link readContract}__ with `abi` set to __{@link dataHavenServiceManagerAbi}__ and `functionName` set to `"strategiesAndMultipliers"`
*/
export const readDataHavenServiceManagerStrategiesAndMultipliers =
/*#__PURE__*/ createReadContract({
abi: dataHavenServiceManagerAbi,
functionName: 'strategiesAndMultipliers',
})
/**
* Wraps __{@link readContract}__ with `abi` set to __{@link dataHavenServiceManagerAbi}__ and `functionName` set to `"supportsAVS"`
*/
@ -10933,6 +11054,15 @@ export const writeDataHavenServiceManagerSetSnowbridgeGateway =
functionName: 'setSnowbridgeGateway',
})
/**
* Wraps __{@link writeContract}__ with `abi` set to __{@link dataHavenServiceManagerAbi}__ and `functionName` set to `"setStrategiesAndMultipliers"`
*/
export const writeDataHavenServiceManagerSetStrategiesAndMultipliers =
/*#__PURE__*/ createWriteContract({
abi: dataHavenServiceManagerAbi,
functionName: 'setStrategiesAndMultipliers',
})
/**
* Wraps __{@link writeContract}__ with `abi` set to __{@link dataHavenServiceManagerAbi}__ and `functionName` set to `"setValidatorSetSubmitter"`
*/
@ -11101,6 +11231,15 @@ export const simulateDataHavenServiceManagerSetSnowbridgeGateway =
functionName: 'setSnowbridgeGateway',
})
/**
* Wraps __{@link simulateContract}__ with `abi` set to __{@link dataHavenServiceManagerAbi}__ and `functionName` set to `"setStrategiesAndMultipliers"`
*/
export const simulateDataHavenServiceManagerSetStrategiesAndMultipliers =
/*#__PURE__*/ createSimulateContract({
abi: dataHavenServiceManagerAbi,
functionName: 'setStrategiesAndMultipliers',
})
/**
* Wraps __{@link simulateContract}__ with `abi` set to __{@link dataHavenServiceManagerAbi}__ and `functionName` set to `"setValidatorSetSubmitter"`
*/
@ -11242,6 +11381,15 @@ export const watchDataHavenServiceManagerSolochainAddressUpdatedEvent =
eventName: 'SolochainAddressUpdated',
})
/**
* Wraps __{@link watchContractEvent}__ with `abi` set to __{@link dataHavenServiceManagerAbi}__ and `eventName` set to `"StrategiesAndMultipliersSet"`
*/
export const watchDataHavenServiceManagerStrategiesAndMultipliersSetEvent =
/*#__PURE__*/ createWatchContractEvent({
abi: dataHavenServiceManagerAbi,
eventName: 'StrategiesAndMultipliersSet',
})
/**
* Wraps __{@link watchContractEvent}__ with `abi` set to __{@link dataHavenServiceManagerAbi}__ and `eventName` set to `"ValidatorAddedToAllowlist"`
*/

View file

@ -7,11 +7,13 @@ import { $ } from "bun";
import {
allocationManagerAbi,
dataHavenServiceManagerAbi,
delegationManagerAbi
delegationManagerAbi,
strategyManagerAbi
} from "contract-bindings";
import { type Deployments, logger, waitForContainerToStart } from "utils";
import { DEFAULT_SUBSTRATE_WS_PORT } from "utils/constants";
import { getPublicPort } from "utils/docker";
import { erc20Abi } from "viem";
import { privateKeyToAccount } from "viem/accounts";
import validatorSet from "../../configs/validator-set.json";
import type { LaunchedNetwork } from "../../launcher/types/launchedNetwork";
@ -123,9 +125,50 @@ export async function registerOperator(
const { connectors, deployments } = options;
const validator = getValidator(validatorName);
const account = privateKeyToAccount(validator.privateKey as `0x${string}`);
const { publicClient, walletClient } = connectors;
// Deposit tokens into deployed strategies
const deployedStrategies = deployments.DeployedStrategies ?? [];
for (const strategy of deployedStrategies) {
const balance = await publicClient.readContract({
address: strategy.underlyingToken as `0x${string}`,
abi: erc20Abi,
functionName: "balanceOf",
args: [account.address]
});
if (balance > 0n) {
const depositAmount = balance / 10n;
const approveHash = await walletClient.writeContract({
address: strategy.underlyingToken as `0x${string}`,
abi: erc20Abi,
functionName: "approve",
args: [deployments.StrategyManager, depositAmount],
account,
chain: null
});
await publicClient.waitForTransactionReceipt({ hash: approveHash });
const depositHash = await walletClient.writeContract({
address: deployments.StrategyManager,
abi: strategyManagerAbi,
functionName: "depositIntoStrategy",
args: [
strategy.address as `0x${string}`,
strategy.underlyingToken as `0x${string}`,
depositAmount
],
account,
chain: null
});
await publicClient.waitForTransactionReceipt({ hash: depositHash });
logger.debug(`Deposited ${depositAmount} tokens into strategy ${strategy.address}`);
}
}
// Register as EigenLayer operator
const operatorHash = await connectors.walletClient.writeContract({
const operatorHash = await walletClient.writeContract({
address: deployments.DelegationManager as `0x${string}`,
abi: delegationManagerAbi,
functionName: "registerAsOperator",
@ -134,7 +177,7 @@ export async function registerOperator(
chain: null
});
const operatorReceipt = await connectors.publicClient.waitForTransactionReceipt({
const operatorReceipt = await publicClient.waitForTransactionReceipt({
hash: operatorHash
});
if (operatorReceipt.status !== "success") {
@ -142,7 +185,7 @@ export async function registerOperator(
}
// Register for operator sets
const hash = await connectors.walletClient.writeContract({
const registerHash = await walletClient.writeContract({
address: deployments.AllocationManager as `0x${string}`,
abi: allocationManagerAbi,
functionName: "registerForOperatorSets",
@ -158,10 +201,40 @@ export async function registerOperator(
chain: null
});
const receipt = await connectors.publicClient.waitForTransactionReceipt({ hash });
if (receipt.status !== "success") {
throw new Error(`Operator set registration failed: ${receipt.status}`);
const registerReceipt = await publicClient.waitForTransactionReceipt({ hash: registerHash });
if (registerReceipt.status !== "success") {
throw new Error(`Operator set registration failed: ${registerReceipt.status}`);
}
logger.debug(`Registered ${validatorName} as operator (gas: ${receipt.gasUsed})`);
// Allocate full magnitude to the validator operator set
const strategyAddresses = deployedStrategies.map((s) => s.address as `0x${string}`);
const newMagnitudes = strategyAddresses.map(() => BigInt(1e18));
const allocateHash = await walletClient.writeContract({
address: deployments.AllocationManager as `0x${string}`,
abi: allocationManagerAbi,
functionName: "modifyAllocations",
args: [
account.address,
[
{
operatorSet: {
avs: deployments.ServiceManager as `0x${string}`,
id: 0
},
strategies: strategyAddresses,
newMagnitudes
}
]
],
account,
chain: null
});
const allocateReceipt = await publicClient.waitForTransactionReceipt({ hash: allocateHash });
if (allocateReceipt.status !== "success") {
throw new Error(`Magnitude allocation failed: ${allocateReceipt.status}`);
}
logger.debug(`Registered ${validatorName} as operator (gas: ${registerReceipt.gasUsed})`);
}

View file

@ -69,6 +69,20 @@ describe("Validator Set Update", () => {
beforeAll(async () => {
deployments = await parseDeploymentsFile();
connectors = suite.getTestConnectors();
// Pause era rotation early so the active era stabilizes during tests 1-3 (~28s),
// avoiding the ~80s wait inside the cross-chain test.
// Tests 1-3 only touch Ethereum contracts and don't depend on era rotation.
const { dhApi } = connectors;
const pauseTx = dhApi.tx.Sudo.sudo({
call: dhApi.tx.ExternalValidators.force_era({
mode: { type: "ForceNone", value: undefined }
}).decodedCall
});
const pauseResult = await pauseTx.signAndSubmit(getPapiSigner("ALITH"));
if (!pauseResult.ok) {
throw new Error("Failed to pause era rotation");
}
});
it("should verify test environment", async () => {
@ -161,31 +175,18 @@ describe("Validator Set Update", () => {
async () => {
const { publicClient, walletClient, dhApi } = connectors;
// Pause era rotation so the active era doesn't advance while
// Snowbridge relays the message (relay latency > era duration with fast-runtime).
// DatahavenServiceManagerAddress is set during infrastructure setup by set-datahaven-parameters.
const setupTx = dhApi.tx.Sudo.sudo({
call: dhApi.tx.ExternalValidators.force_era({
mode: { type: "ForceNone", value: undefined }
}).decodedCall
});
const setupResult = await setupTx.signAndSubmit(getPapiSigner("ALITH"));
if (!setupResult.ok) {
throw new Error("Failed to pause era rotation");
}
// Wait for the active era to stabilize: ForceNone prevents new eras but
// an already-triggered era may still be pending activation at the next session boundary.
// Poll until CurrentEra == ActiveEra, meaning no pending era transition remains.
// Era rotation was paused in beforeAll. Wait for any pending transition to settle
// (ForceNone prevents new eras, but an in-progress one must finish first).
let stableEraIndex: number;
// eslint-disable-next-line no-constant-condition
while (true) {
await new Promise((r) => setTimeout(r, 12_000)); // ~2 substrate blocks
const activeEra = (await dhApi.query.ExternalValidators.ActiveEra.getValue())?.index ?? 0;
const currentEra = (await dhApi.query.ExternalValidators.CurrentEra.getValue()) ?? 0;
if (currentEra === activeEra) {
stableEraIndex = activeEra;
break;
}
await new Promise((r) => setTimeout(r, 6_000)); // ~1 substrate block
}
const targetEra = BigInt(stableEraIndex + 1);
@ -201,12 +202,22 @@ describe("Validator Set Update", () => {
chain: null
});
const receipt = await publicClient.waitForTransactionReceipt({ hash });
logger.info(
`sendNewValidatorSet tx status: ${receipt.status}, block: ${receipt.blockNumber}`
);
expect(receipt.status).toBe("success");
// Verify OutboundMessageAccepted event was emitted
const hasOutboundAccepted = (receipt.logs ?? []).some((log: any) => {
try {
const decoded = decodeEventLog({ abi: gatewayAbi, data: log.data, topics: log.topics });
const decoded = decodeEventLog({
abi: gatewayAbi,
data: log.data,
topics: log.topics
});
if (decoded.eventName === "OutboundMessageAccepted") {
logger.info(`OutboundMessageAccepted event: nonce=${(decoded.args as any)?.nonce}`);
}
return decoded.eventName === "OutboundMessageAccepted";
} catch {
return false;
@ -214,6 +225,7 @@ describe("Validator Set Update", () => {
});
expect(hasOutboundAccepted).toBe(true);
logger.info("Waiting for ExternalValidators.ExternalValidatorsSet event on DataHaven...");
// Wait for the validator set to be updated on Substrate
await waitForDataHavenEvent({
api: dhApi,

View file

@ -124,6 +124,29 @@ export const setupValidators = async (options: SetupValidatorsOptions): Promise<
logger.success(`Successfully registered validator ${validator.publicKey}`);
}
// Allocate stake for each validator (must run in a separate script because
// the allocation delay needs at least 1 block after registerAsOperator)
logger.info("📊 Allocating operator stake...");
for (const [i, validator] of validatorsToRegister.entries()) {
logger.info(`📊 Allocating stake for validator ${i} (${validator.publicKey})`);
const env = {
...process.env,
NETWORK: networkName,
OPERATOR_PRIVATE_KEY: validator.privateKey,
OPERATOR_SOLOCHAIN_ADDRESS: validator.solochainAddress || ""
};
const allocateCommand = `forge script script/transact/AllocateOperatorStake.s.sol --rpc-url ${rpcUrl} --broadcast --no-rpc-rate-limit --non-interactive`;
await runShellCommandWithLogger(allocateCommand, {
env,
cwd: "../contracts",
logLevel: "debug"
});
logger.success(`Successfully allocated stake for validator ${validator.publicKey}`);
}
return true;
};