Merge remote-tracking branch 'origin/main' into feat/contracts-versioning-system

This commit is contained in:
Gonza Montiel 2026-02-25 15:04:54 +01:00
commit c8fc146b91
63 changed files with 4237 additions and 2374 deletions

View file

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

View file

@ -1 +1 @@
764d675e54fcb006d8b2b43a6fdf8782313e6509
9c861e3e1d290888127bc6d772fb1a3422bdf8b3

File diff suppressed because one or more lines are too long

11
contracts/foundry.lock Normal file
View file

@ -0,0 +1,11 @@
{
"lib/eigenlayer-contracts": {
"rev": "7ecc83c7b180850531bc5b8b953a7340adeecd43"
},
"lib/forge-std": {
"rev": "9530d9ec702df1b27b7f8f50c0a63a11b1b5fba9"
},
"lib/snowbridge": {
"rev": "13263fefa29a3f4af50e5650dcd93fe3afac44db"
}
}

View file

@ -20,6 +20,7 @@ contract Config {
address avsOwner;
address rewardsInitiator;
address[] validatorsStrategies;
address validatorSetSubmitter;
}
// EigenLayer parameters

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,8 +45,9 @@ import {ValidatorsUtils} from "../../script/utils/ValidatorsUtils.sol";
struct ServiceManagerInitParams {
address avsOwner;
address rewardsInitiator;
address[] validatorsStrategies;
IRewardsCoordinatorTypes.StrategyAndMultiplier[] validatorsStrategiesAndMultipliers;
address gateway;
address validatorSetSubmitter;
string initialVersion;
address versionUpdater;
}
@ -248,6 +253,16 @@ 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
});
}
// Read version from environment variable (passed by TypeScript wrapper)
string memory version = vm.envOr("DATAHAVEN_VERSION", string("0.1.0"));
console.log("| Version: %s", version);
@ -256,8 +271,9 @@ abstract contract DeployBase is Script, DeployParams, Accounts {
ServiceManagerInitParams memory initParams = ServiceManagerInitParams({
avsOwner: avsConfig.avsOwner,
rewardsInitiator: avsConfig.rewardsInitiator,
validatorsStrategies: avsConfig.validatorsStrategies,
validatorsStrategiesAndMultipliers: strategiesAndMultipliers,
gateway: address(gateway),
validatorSetSubmitter: avsConfig.validatorSetSubmitter,
initialVersion: version,
versionUpdater: _deployer
});

View file

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

View file

@ -206,8 +206,9 @@ contract DeployLocal is DeployBase {
DataHavenServiceManager.initialize.selector,
params.avsOwner,
params.rewardsInitiator,
params.validatorsStrategies,
params.validatorsStrategiesAndMultipliers,
params.gateway,
params.validatorSetSubmitter,
params.initialVersion,
params.versionUpdater
);
@ -359,11 +360,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

@ -79,6 +79,12 @@ contract DeployParams is Script, Config {
config.validatorsStrategies =
vm.parseJsonAddressArray(configJson, ".avs.validatorsStrategies");
try vm.parseJsonAddress(configJson, ".avs.validatorSetSubmitter") returns (address addr) {
config.validatorSetSubmitter = addr;
} catch {
config.validatorSetSubmitter = address(0);
}
return config;
}

View file

@ -16,9 +16,10 @@ contract DataHavenServiceManagerBadLayout is OwnableUpgradeable {
mapping(address => bool) public validatorsAllowlist;
IGatewayV2 private _snowbridgeGateway;
mapping(address => address) public validatorEthAddressToSolochainAddress;
mapping(address => address) public validatorSolochainAddressToEthAddress;
// Keep the original gap size to mirror shape, despite the shift
uint256[46] private __GAP;
uint256[45] private __GAP;
// Keep a compatible constructor signature for upgrade tests.
constructor(

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

@ -20,7 +20,7 @@ if [ "$EXIT_CODE" -eq 0 ]; then
exit 1
fi
if ! printf '%s\n' "$OUTPUT" | grep -q "ERROR: Storage layout has changed!"; then
if ! printf '%s\n' "$OUTPUT" | grep -qE "ERROR: (Storage layout has changed!|__GAP invariant violated!)"; then
echo "ERROR: Storage layout check failed, but not for the expected reason."
echo ""
echo "Output:"

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"
}
@ -59,4 +59,26 @@ if ! diff -q /tmp/snap_normalized.json /tmp/curr_normalized.json > /dev/null 2>&
exit 1
fi
# Verify gap invariant: __GAP slot + array size must equal a fixed constant.
# This catches cases where a new variable is added but __GAP is not shrunk accordingly.
EXPECTED_GAP_TOTAL=151
GAP_SLOT=$(jq '.storage[] | select(.label == "__GAP") | .slot | tonumber' /tmp/current_layout.json)
GAP_SIZE=$(jq -r '.storage[] | select(.label == "__GAP") | .type' /tmp/current_layout.json \
| grep -oE '[0-9]+' | tail -1)
if [ -n "$GAP_SLOT" ] && [ -n "$GAP_SIZE" ]; then
GAP_TOTAL=$((GAP_SLOT + GAP_SIZE))
if [ "$GAP_TOTAL" -ne "$EXPECTED_GAP_TOTAL" ]; then
echo ""
echo "=========================================="
echo "ERROR: __GAP invariant violated!"
echo "=========================================="
echo ""
echo " slot($GAP_SLOT) + size($GAP_SIZE) = $GAP_TOTAL, expected $EXPECTED_GAP_TOTAL"
echo ""
echo "If you added a new state variable, shrink __GAP by the same number of slots."
exit 1
fi
fi
echo "Storage layout OK - no changes detected"

View file

@ -43,6 +43,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
@ -65,6 +68,14 @@ contract DataHavenServiceManager is OwnableUpgradeable, IAVSRegistrar, IDataHave
/// @inheritdoc IDataHavenServiceManager
mapping(address => address) public validatorEthAddressToSolochainAddress;
mapping(address => address) public validatorSolochainAddressToEthAddress;
/// @inheritdoc IDataHavenServiceManager
address public validatorSetSubmitter;
/// @inheritdoc IDataHavenServiceManager
mapping(IStrategy => uint96) public strategiesAndMultipliers;
/// @notice Semantic version of the deployed DataHaven AVS stack.
/// Set during initialization based on deployment chain.
/// This should match the `version` field in the corresponding
@ -77,7 +88,7 @@ contract DataHavenServiceManager is OwnableUpgradeable, IAVSRegistrar, IDataHave
/// @notice Storage gap for upgradeability (must be at end of state variables)
// solhint-disable-next-line var-name-mixedcase
uint256[44] private __GAP;
uint256[41] private __GAP;
// ============ Modifiers ============
@ -99,6 +110,12 @@ contract DataHavenServiceManager is OwnableUpgradeable, IAVSRegistrar, IDataHave
_;
}
/// @notice Restricts function to the validator set submitter
modifier onlyValidatorSetSubmitter() {
_checkValidatorSetSubmitter();
_;
}
/// @notice Restricts function to the version updater or owner
modifier onlyVersionUpdater() {
_checkVersionUpdater();
@ -121,6 +138,10 @@ contract DataHavenServiceManager is OwnableUpgradeable, IAVSRegistrar, IDataHave
require(msg.sender == address(_ALLOCATION_MANAGER), OnlyAllocationManager());
}
function _checkValidatorSetSubmitter() internal view {
require(msg.sender == validatorSetSubmitter, OnlyValidatorSetSubmitter());
}
function _checkVersionUpdater() internal view {
require(
msg.sender == versionUpdater || msg.sender == owner(), "Only version updater or owner"
@ -145,8 +166,9 @@ contract DataHavenServiceManager is OwnableUpgradeable, IAVSRegistrar, IDataHave
function initialize(
address initialOwner,
address _rewardsInitiator,
IStrategy[] memory validatorsStrategies,
IRewardsCoordinatorTypes.StrategyAndMultiplier[] memory validatorsStrategiesAndMultipliers,
address _snowbridgeGatewayAddress,
address _validatorSetSubmitter,
string memory initialVersion,
address _versionUpdater
) public virtual initializer {
@ -169,16 +191,31 @@ 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);
// Set the Snowbridge Gateway address.
_snowbridgeGateway = IGatewayV2(_snowbridgeGatewayAddress);
// Set the validator set submitter if provided.
if (_validatorSetSubmitter != address(0)) {
validatorSetSubmitter = _validatorSetSubmitter;
emit ValidatorSetSubmitterUpdated(address(0), _validatorSetSubmitter);
}
}
// ============ View Functions ============
@ -192,38 +229,107 @@ contract DataHavenServiceManager is OwnableUpgradeable, IAVSRegistrar, IDataHave
// ============ External Functions ============
/// @inheritdoc IDataHavenServiceManager
function sendNewValidatorSet(
function setValidatorSetSubmitter(
address newSubmitter
) external onlyOwner {
require(newSubmitter != address(0), ZeroAddress());
address oldSubmitter = validatorSetSubmitter;
validatorSetSubmitter = newSubmitter;
emit ValidatorSetSubmitterUpdated(oldSubmitter, newSubmitter);
}
// ============ External Functions ============
/// @inheritdoc IDataHavenServiceManager
function sendNewValidatorSetForEra(
uint64 targetEra,
uint128 executionFee,
uint128 relayerFee
) external payable onlyOwner {
bytes memory message = buildNewValidatorSetMessage();
) external payable onlyValidatorSetSubmitter {
bytes memory message = buildNewValidatorSetMessageForEra(targetEra);
_snowbridgeGateway.v2_sendMessage{value: msg.value}(
message, new bytes[](0), bytes(""), executionFee, relayerFee
);
emit ValidatorSetMessageSubmitted(targetEra, keccak256(message), msg.sender);
}
/// @inheritdoc IDataHavenServiceManager
function buildNewValidatorSetMessage() public view returns (bytes memory) {
function buildNewValidatorSetMessageForEra(
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]);
}
}
// 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(
DataHavenSnowbridgeMessages.NewValidatorSetPayload({validators: newValidatorSet})
DataHavenSnowbridgeMessages.NewValidatorSetPayload({
validators: newValidatorSet, externalIndex: targetEra
})
);
}
@ -232,7 +338,20 @@ contract DataHavenServiceManager is OwnableUpgradeable, IAVSRegistrar, IDataHave
address solochainAddress
) external onlyValidator {
require(solochainAddress != address(0), ZeroAddress());
address existingEthOperator = validatorSolochainAddressToEthAddress[solochainAddress];
require(
existingEthOperator == address(0) || existingEthOperator == msg.sender,
SolochainAddressAlreadyAssigned()
);
address oldSolochainAddress = validatorEthAddressToSolochainAddress[msg.sender];
if (oldSolochainAddress != address(0) && oldSolochainAddress != solochainAddress) {
delete validatorSolochainAddressToEthAddress[oldSolochainAddress];
}
validatorEthAddressToSolochainAddress[msg.sender] = solochainAddress;
validatorSolochainAddressToEthAddress[solochainAddress] = msg.sender;
emit SolochainAddressUpdated(msg.sender, solochainAddress);
}
@ -263,7 +382,21 @@ contract DataHavenServiceManager is OwnableUpgradeable, IAVSRegistrar, IDataHave
require(operatorSetIds.length == 1, CantRegisterToMultipleOperatorSets());
require(operatorSetIds[0] == VALIDATORS_SET_ID, InvalidOperatorSetId());
require(validatorsAllowlist[operator], OperatorNotInAllowlist());
validatorEthAddressToSolochainAddress[operator] = _toAddress(data);
address solochainAddress = _toAddress(data);
address existingEthOperator = validatorSolochainAddressToEthAddress[solochainAddress];
require(
existingEthOperator == address(0) || existingEthOperator == operator,
SolochainAddressAlreadyAssigned()
);
address oldSolochainAddress = validatorEthAddressToSolochainAddress[operator];
if (oldSolochainAddress != address(0) && oldSolochainAddress != solochainAddress) {
delete validatorSolochainAddressToEthAddress[oldSolochainAddress];
}
validatorEthAddressToSolochainAddress[operator] = solochainAddress;
validatorSolochainAddressToEthAddress[solochainAddress] = operator;
emit OperatorRegistered(operator, operatorSetIds[0]);
}
@ -278,7 +411,11 @@ contract DataHavenServiceManager is OwnableUpgradeable, IAVSRegistrar, IDataHave
require(operatorSetIds.length == 1, CantDeregisterFromMultipleOperatorSets());
require(operatorSetIds[0] == VALIDATORS_SET_ID, InvalidOperatorSetId());
address oldSolochainAddress = validatorEthAddressToSolochainAddress[operator];
delete validatorEthAddressToSolochainAddress[operator];
if (oldSolochainAddress != address(0)) {
delete validatorSolochainAddressToEthAddress[oldSolochainAddress];
}
emit OperatorDeregistered(operator, operatorSetIds[0]);
}
@ -322,15 +459,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;
}
// ============ Version Management ============
@ -365,16 +558,22 @@ contract DataHavenServiceManager is OwnableUpgradeable, IAVSRegistrar, IDataHave
function submitRewards(
IRewardsCoordinatorTypes.OperatorDirectedRewardsSubmission calldata submission
) external override onlyRewardsInitiator {
IRewardsCoordinatorTypes.OperatorDirectedRewardsSubmission memory translatedSubmission =
submission;
uint256 totalAmount = 0;
for (uint256 i = 0; i < submission.operatorRewards.length; i++) {
totalAmount += submission.operatorRewards[i].amount;
for (uint256 i = 0; i < translatedSubmission.operatorRewards.length; i++) {
translatedSubmission.operatorRewards[i].operator =
_ethOperatorFromSolochain(translatedSubmission.operatorRewards[i].operator);
totalAmount += translatedSubmission.operatorRewards[i].amount;
}
_sortOperatorRewards(translatedSubmission.operatorRewards);
submission.token.safeIncreaseAllowance(address(_REWARDS_COORDINATOR), totalAmount);
IRewardsCoordinatorTypes.OperatorDirectedRewardsSubmission[] memory submissions =
new IRewardsCoordinatorTypes.OperatorDirectedRewardsSubmission[](1);
submissions[0] = submission;
submissions[0] = translatedSubmission;
OperatorSet memory operatorSet = OperatorSet({avs: address(this), id: VALIDATORS_SET_ID});
_REWARDS_COORDINATOR.createOperatorDirectedOperatorSetRewardsSubmission(
@ -425,9 +624,10 @@ contract DataHavenServiceManager is OwnableUpgradeable, IAVSRegistrar, IDataHave
SlashingRequest[] calldata slashings
) external onlyRewardsInitiator {
for (uint256 i = 0; i < slashings.length; i++) {
address ethOperator = _ethOperatorFromSolochain(slashings[i].operator);
IAllocationManagerTypes.SlashingParams memory slashingParams =
IAllocationManagerTypes.SlashingParams({
operator: slashings[i].operator,
operator: ethOperator,
operatorSetId: VALIDATORS_SET_ID,
strategies: slashings[i].strategies,
wadsToSlash: slashings[i].wadsToSlash,
@ -442,6 +642,26 @@ contract DataHavenServiceManager is OwnableUpgradeable, IAVSRegistrar, IDataHave
// ============ Internal Functions ============
/**
* @notice Sorts operator rewards array by operator address in ascending order using insertion sort
* @dev Insertion sort is optimal for small arrays (validator set capped at 32)
* @param rewards The operator rewards array to sort in-place
*/
function _sortOperatorRewards(
IRewardsCoordinatorTypes.OperatorReward[] memory rewards
) private pure {
uint256 len = rewards.length;
for (uint256 i = 1; i < len; i++) {
IRewardsCoordinatorTypes.OperatorReward memory key = rewards[i];
uint256 j = i;
while (j > 0 && rewards[j - 1].operator > key.operator) {
rewards[j] = rewards[j - 1];
j--;
}
rewards[j] = key;
}
}
/**
* @notice Safely converts a 20-byte array to an address
* @param data The bytes to convert (must be exactly 20 bytes)
@ -450,10 +670,43 @@ contract DataHavenServiceManager is OwnableUpgradeable, IAVSRegistrar, IDataHave
function _toAddress(
bytes memory data
) private pure returns (address result) {
require(data.length == 20, "Invalid address length");
require(data.length == 20, InvalidSolochainAddressLength());
assembly {
result := shr(96, mload(add(data, 32)))
}
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
*/
function _ethOperatorFromSolochain(
address solochainAddress
) internal view returns (address) {
address ethOperator = validatorSolochainAddressToEthAddress[solochainAddress];
require(ethOperator != address(0), UnknownSolochainAddress());
return ethOperator;
}
}

View file

@ -32,6 +32,19 @@ interface IDataHavenServiceManagerErrors {
error ZeroAddress();
/// @notice Thrown when the solochain address data length is not 20 bytes
error InvalidSolochainAddressLength();
/// @notice Thrown when the caller is not the authorized validator set submitter
error OnlyValidatorSetSubmitter();
/// @notice Thrown when trying to submit a validator set message with zero validators
error EmptyValidatorSet();
/// @notice Thrown when a Solochain address has not been mapped to an EigenLayer operator
error UnknownSolochainAddress();
/// @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();
}
/**
@ -88,6 +101,23 @@ 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
event ValidatorSetSubmitterUpdated(address indexed oldSubmitter, address indexed newSubmitter);
/// @notice Emitted when a validator set message is submitted for a target era
/// @param targetEra The target era for the validator set
/// @param payloadHash The keccak256 hash of the encoded message payload
/// @param submitter The address that submitted the validator set message
event ValidatorSetMessageSubmitted(
uint64 indexed targetEra, bytes32 payloadHash, address indexed submitter
);
}
/**
@ -127,11 +157,36 @@ interface IDataHavenServiceManager is
address validatorAddress
) external view returns (address);
/// @notice Returns the address authorized to submit validator set messages
/// @return The validator set submitter address
function validatorSetSubmitter() external view returns (address);
/**
* @notice Sets the address authorized to submit validator set messages
* @param newSubmitter The new validator set submitter address
* @dev Only callable by the owner
*/
function setValidatorSetSubmitter(
address newSubmitter
) external;
/**
* @notice Converts a Solochain validator address to the corresponding EigenLayer operator address
* @param solochainAddress The Solochain validator address to convert
* @return The corresponding EigenLayer operator address
*/
function validatorSolochainAddressToEthAddress(
address solochainAddress
) external view returns (address);
/**
* @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
* @param _snowbridgeGatewayAddress Address of the Snowbridge Gateway
* @param initialVersion Initial semantic version string (e.g., "1.0.0")
* @param _versionUpdater Address authorized to update the contract version
@ -139,31 +194,41 @@ interface IDataHavenServiceManager is
function initialize(
address initialOwner,
address rewardsInitiator,
IStrategy[] memory validatorsStrategies,
IRewardsCoordinatorTypes.StrategyAndMultiplier[] memory validatorsStrategiesAndMultipliers,
address _snowbridgeGatewayAddress,
address _validatorSetSubmitter,
string memory initialVersion,
address _versionUpdater
) external;
/**
* @notice Sends a new validator set to the Snowbridge Gateway
* @notice Sends a new validator set for a target era to the Snowbridge Gateway
* @dev The new validator set is made up of the Validators currently
* registered in the DataHaven Service Manager as operators of
* the Validators operator set (operatorSetId = VALIDATORS_SET_ID)
* @dev Only callable by the owner
* @dev Only callable by the validator set submitter
* @param targetEra The target era for the validator set submission
* @param executionFee The execution fee for the Snowbridge message
* @param relayerFee The relayer fee for the Snowbridge message
*/
function sendNewValidatorSet(
function sendNewValidatorSetForEra(
uint64 targetEra,
uint128 executionFee,
uint128 relayerFee
) external payable;
/**
* @notice Builds a new validator set message to be sent to the Snowbridge Gateway
* @return The encoded message bytes to be sent to the Snowbridge Gateway
* @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 SCALE-encoded message bytes to be sent to the Snowbridge Gateway
*/
function buildNewValidatorSetMessage() external view returns (bytes memory);
function buildNewValidatorSetMessageForEra(
uint64 targetEra
) external view returns (bytes memory);
/**
* @notice Updates the Solochain address for a Validator
@ -215,10 +280,39 @@ 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;
/**
@ -239,6 +333,15 @@ interface IDataHavenServiceManager is
address newVersionUpdater
) 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

@ -35,6 +35,8 @@ library DataHavenSnowbridgeMessages {
struct NewValidatorSetPayload {
/// @notice The list of validators in the DataHaven network.
address[] validators;
/// @notice The external index (target era) for the validator set.
uint64 externalIndex;
}
/**
@ -48,8 +50,6 @@ library DataHavenSnowbridgeMessages {
uint32 validatorsLen = uint32(payload.validators.length);
address[] memory validatorSet = payload.validators;
uint64 externalIndex = uint64(0);
// Flatten the validator set into a single bytes array
bytes memory validatorsFlattened;
for (uint32 i = 0; i < validatorSet.length; i++) {
@ -63,7 +63,7 @@ library DataHavenSnowbridgeMessages {
bytes1(uint8(OutboundCommandV1.ReceiveValidators)),
ScaleCodec.encodeCompactU32(validatorsLen),
validatorsFlattened,
ScaleCodec.encodeU64(externalIndex)
ScaleCodec.encodeU64(payload.externalIndex)
);
}
}

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,13 +65,37 @@
"type": "t_contract(IGatewayV2)23481"
},
{
"astId": 23785,
"astId": 23789,
"contract": "src/DataHavenServiceManager.sol:DataHavenServiceManager",
"label": "validatorEthAddressToSolochainAddress",
"offset": 0,
"slot": "104",
"type": "t_mapping(t_address,t_address)"
},
{
"astId": 23793,
"contract": "src/DataHavenServiceManager.sol:DataHavenServiceManager",
"label": "validatorSolochainAddressToEthAddress",
"offset": 0,
"slot": "105",
"type": "t_mapping(t_address,t_address)"
},
{
"astId": 23796,
"contract": "src/DataHavenServiceManager.sol:DataHavenServiceManager",
"label": "validatorSetSubmitter",
"offset": 0,
"slot": "106",
"type": "t_address"
},
{
"astId": 23802,
"contract": "src/DataHavenServiceManager.sol:DataHavenServiceManager",
"label": "strategiesAndMultipliers",
"offset": 0,
"slot": "107",
"type": "t_mapping(t_contract(IStrategy)7361,t_uint96)"
},
{
"astId": 23788,
"contract": "src/DataHavenServiceManager.sol:DataHavenServiceManager",
@ -93,8 +117,8 @@
"contract": "src/DataHavenServiceManager.sol:DataHavenServiceManager",
"label": "__GAP",
"offset": 0,
"slot": "107",
"type": "t_array(t_uint256)44_storage"
"slot": "105",
"type": "t_array(t_uint256)46_storage"
}
],
"types": {
@ -103,10 +127,10 @@
"label": "address",
"numberOfBytes": "20"
},
"t_array(t_uint256)44_storage": {
"t_array(t_uint256)46_storage": {
"encoding": "inplace",
"label": "uint256[44]",
"numberOfBytes": "1408",
"label": "uint256[46]",
"numberOfBytes": "1472",
"base": "t_uint256"
},
"t_array(t_uint256)49_storage": {
@ -131,6 +155,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,11 +174,6 @@
"numberOfBytes": "32",
"value": "t_bool"
},
"t_string_storage": {
"encoding": "bytes",
"label": "string",
"numberOfBytes": "32"
},
"t_uint256": {
"encoding": "inplace",
"label": "uint256",
@ -159,6 +183,11 @@
"encoding": "inplace",
"label": "uint8",
"numberOfBytes": "1"
},
"t_uint96": {
"encoding": "inplace",
"label": "uint96",
"numberOfBytes": "12"
}
}
}

View file

@ -15,7 +15,9 @@ contract MessageEncodingTest is Test {
address[] memory mockValidators = TestUtils.generateMockValidatorsAddresses(3);
DataHavenSnowbridgeMessages.NewValidatorSetPayload memory payload =
DataHavenSnowbridgeMessages.NewValidatorSetPayload({validators: mockValidators});
DataHavenSnowbridgeMessages.NewValidatorSetPayload({
validators: mockValidators, externalIndex: uint64(0)
});
bytes memory encodedMessage =
DataHavenSnowbridgeMessages.scaleEncodeNewValidatorSetMessagePayload(payload);

View file

@ -0,0 +1,158 @@
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.27;
import {AVSDeployer} from "./utils/AVSDeployer.sol";
import {DataHavenServiceManager} from "../src/DataHavenServiceManager.sol";
import {
IAllocationManagerTypes
} from "eigenlayer-contracts/src/contracts/interfaces/IAllocationManager.sol";
import {Test} from "forge-std/Test.sol";
contract OperatorAddressMappingsTest is AVSDeployer {
address public snowbridgeAgent = address(uint160(uint256(keccak256("snowbridgeAgent"))));
address internal operator1 = address(uint160(uint256(keccak256("operator1"))));
address internal operator2 = address(uint160(uint256(keccak256("operator2"))));
function setUp() public virtual {
_deployMockEigenLayerAndAVS();
// Configure the rewards initiator (not strictly needed for these tests,
// but keeps setup consistent with other suites).
vm.prank(avsOwner);
serviceManager.setRewardsInitiator(snowbridgeAgent);
}
function _registerOperator(
address ethOperator,
address solochainOperator
) internal {
vm.prank(avsOwner);
serviceManager.addValidatorToAllowlist(ethOperator);
vm.prank(ethOperator);
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(solochainOperator)
});
vm.prank(ethOperator);
allocationManager.registerForOperatorSets(ethOperator, registerParams);
}
function test_registerOperator_revertsIfSolochainAlreadyAssignedToDifferentOperator() public {
address sharedSolochain = address(0xBEEF);
_registerOperator(operator1, sharedSolochain);
// operator2 cannot claim the same solochain address
vm.prank(avsOwner);
serviceManager.addValidatorToAllowlist(operator2);
vm.prank(operator2);
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(sharedSolochain)
});
vm.prank(operator2);
vm.expectRevert(abi.encodeWithSignature("SolochainAddressAlreadyAssigned()"));
allocationManager.registerForOperatorSets(operator2, registerParams);
}
function test_updateSolochainAddressForValidator_revertsIfAlreadyAssignedToDifferentOperator()
public
{
address solo1 = address(0xBEEF);
address solo2 = address(0xCAFE);
_registerOperator(operator1, solo1);
_registerOperator(operator2, solo2);
// operator2 cannot update to operator1's solochain address
vm.prank(operator2);
vm.expectRevert(abi.encodeWithSignature("SolochainAddressAlreadyAssigned()"));
serviceManager.updateSolochainAddressForValidator(solo1);
}
function test_updateSolochainAddressForValidator_clearsOldReverseMapping() public {
address soloOld = address(0xBEEF);
address soloNew = address(0xCAFE);
_registerOperator(operator1, soloOld);
assertEq(
serviceManager.validatorEthAddressToSolochainAddress(operator1),
soloOld,
"forward mapping should be set"
);
assertEq(
serviceManager.validatorSolochainAddressToEthAddress(soloOld),
operator1,
"reverse mapping should be set"
);
vm.prank(operator1);
serviceManager.updateSolochainAddressForValidator(soloNew);
assertEq(
serviceManager.validatorEthAddressToSolochainAddress(operator1),
soloNew,
"forward mapping should update"
);
assertEq(
serviceManager.validatorSolochainAddressToEthAddress(soloNew),
operator1,
"reverse mapping should update"
);
assertEq(
serviceManager.validatorSolochainAddressToEthAddress(soloOld),
address(0),
"old reverse mapping should be cleared"
);
}
function test_registerOperator_replacesSolochainAndClearsOldReverseMapping() public {
address soloOld = address(0xBEEF);
address soloNew = address(0xCAFE);
_registerOperator(operator1, soloOld);
// simulate allocationManager registering operator1 again with a new solochain address
uint32[] memory operatorSetIds = new uint32[](1);
operatorSetIds[0] = serviceManager.VALIDATORS_SET_ID();
vm.prank(address(allocationManager));
serviceManager.registerOperator(
operator1, address(serviceManager), operatorSetIds, abi.encodePacked(soloNew)
);
assertEq(
serviceManager.validatorEthAddressToSolochainAddress(operator1),
soloNew,
"forward mapping should update"
);
assertEq(
serviceManager.validatorSolochainAddressToEthAddress(soloNew),
operator1,
"reverse mapping should update"
);
assertEq(
serviceManager.validatorSolochainAddressToEthAddress(soloOld),
address(0),
"old reverse mapping should be cleared"
);
}
}

View file

@ -6,8 +6,13 @@ pragma solidity ^0.8.13;
import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
import {
IRewardsCoordinator,
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 {AVSDeployer} from "./utils/AVSDeployer.sol";
import {ERC20FixedSupply} from "./utils/ERC20FixedSupply.sol";
@ -41,6 +46,30 @@ contract RewardsSubmitterTest is AVSDeployer {
IERC20(address(rewardToken)).safeTransfer(address(serviceManager), 100000e18);
}
function _registerOperator(
address ethOperator,
address solochainOperator
) internal {
// Allow our operator to register
vm.prank(avsOwner);
serviceManager.addValidatorToAllowlist(ethOperator);
vm.prank(ethOperator);
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(solochainOperator)
});
vm.prank(ethOperator);
allocationManager.registerForOperatorSets(ethOperator, registerParams);
}
// Helper function to build a submission
function _buildSubmission(
uint256 rewardAmount,
@ -91,6 +120,7 @@ contract RewardsSubmitterTest is AVSDeployer {
// ============ Access Control Tests ============
function test_submitRewards_revertsIfNotRewardsInitiator() public {
_registerOperator(operator1, operator1);
IRewardsCoordinatorTypes.OperatorDirectedRewardsSubmission memory submission =
_buildSubmission(1000e18, operator1);
@ -102,6 +132,7 @@ contract RewardsSubmitterTest is AVSDeployer {
// ============ Success Tests ============
function test_submitRewards_singleOperator() public {
_registerOperator(operator1, operator1);
uint256 rewardAmount = 1000e18;
IRewardsCoordinatorTypes.OperatorDirectedRewardsSubmission memory submission =
_buildSubmission(rewardAmount, operator1);
@ -116,6 +147,9 @@ contract RewardsSubmitterTest is AVSDeployer {
}
function test_submitRewards_multipleOperators() public {
_registerOperator(operator1, operator1);
_registerOperator(operator2, operator2);
// Build strategies
IRewardsCoordinatorTypes.StrategyAndMultiplier[] memory strategiesAndMultipliers =
new IRewardsCoordinatorTypes.StrategyAndMultiplier[](deployedStrategies.length);
@ -126,8 +160,8 @@ contract RewardsSubmitterTest is AVSDeployer {
}
// Ensure operators are sorted in ascending order (required by EigenLayer)
address opLow = address(0x1);
address opHigh = address(0x2);
(address opLow, address opHigh) =
operator1 < operator2 ? (operator1, operator2) : (operator2, operator1);
uint256 amount1 = 600e18;
uint256 amount2 = 400e18;
@ -160,6 +194,7 @@ contract RewardsSubmitterTest is AVSDeployer {
}
function test_submitRewards_multipleSubmissions() public {
_registerOperator(operator1, operator1);
uint32 duration = TEST_CALCULATION_INTERVAL;
// Submit for period 0
@ -188,6 +223,7 @@ contract RewardsSubmitterTest is AVSDeployer {
}
function test_submitRewards_withCustomDescription() public {
_registerOperator(operator1, operator1);
// Build submission with custom description
IRewardsCoordinatorTypes.StrategyAndMultiplier[] memory strategiesAndMultipliers =
new IRewardsCoordinatorTypes.StrategyAndMultiplier[](1);
@ -217,6 +253,7 @@ contract RewardsSubmitterTest is AVSDeployer {
}
function test_submitRewards_withDifferentToken() public {
_registerOperator(operator1, operator1);
// Deploy a different token
ERC20FixedSupply otherToken =
new ERC20FixedSupply("Other", "OTHER", 1000000e18, address(this));
@ -251,4 +288,153 @@ contract RewardsSubmitterTest is AVSDeployer {
emit IDataHavenServiceManagerEvents.RewardsSubmitted(500e18, 1);
serviceManager.submitRewards(submission);
}
function test_submitRewards_translatesSolochainOperatorToEthOperator() public {
address solochainOperator = address(0xBEEF);
_registerOperator(operator1, solochainOperator);
assertEq(
serviceManager.validatorEthAddressToSolochainAddress(operator1),
solochainOperator,
"forward mapping should be set"
);
assertEq(
serviceManager.validatorSolochainAddressToEthAddress(solochainOperator),
operator1,
"reverse mapping should be set"
);
uint256 rewardAmount = 1000e18;
IRewardsCoordinatorTypes.OperatorDirectedRewardsSubmission memory submission =
_buildSubmission(rewardAmount, solochainOperator);
// Warp to a time after the period ends
vm.warp(submission.startTimestamp + submission.duration + 1);
IRewardsCoordinatorTypes.OperatorReward[] memory expectedOperatorRewards =
new IRewardsCoordinatorTypes.OperatorReward[](1);
expectedOperatorRewards[0] =
IRewardsCoordinatorTypes.OperatorReward({operator: operator1, amount: rewardAmount});
IRewardsCoordinatorTypes.OperatorDirectedRewardsSubmission memory expectedSubmission =
IRewardsCoordinatorTypes.OperatorDirectedRewardsSubmission({
strategiesAndMultipliers: submission.strategiesAndMultipliers,
token: submission.token,
operatorRewards: expectedOperatorRewards,
startTimestamp: submission.startTimestamp,
duration: submission.duration,
description: submission.description
});
IRewardsCoordinatorTypes.OperatorDirectedRewardsSubmission[] memory submissions =
new IRewardsCoordinatorTypes.OperatorDirectedRewardsSubmission[](1);
submissions[0] = expectedSubmission;
OperatorSet memory operatorSet =
OperatorSet({avs: address(serviceManager), id: serviceManager.VALIDATORS_SET_ID()});
vm.expectCall(
address(rewardsCoordinator),
abi.encodeCall(
IRewardsCoordinator.createOperatorDirectedOperatorSetRewardsSubmission,
(operatorSet, submissions)
)
);
assertEq(
submission.operatorRewards[0].operator,
solochainOperator,
"submission should use solochain operator"
);
vm.prank(snowbridgeAgent);
serviceManager.submitRewards(submission);
}
function test_submitRewards_revertsIfUnknownSolochainAddress() public {
address unknownSolochainOperator = address(0xDEAD);
IRewardsCoordinatorTypes.OperatorDirectedRewardsSubmission memory submission =
_buildSubmission(1000e18, unknownSolochainOperator);
vm.prank(snowbridgeAgent);
vm.expectRevert(abi.encodeWithSignature("UnknownSolochainAddress()"));
serviceManager.submitRewards(submission);
}
function test_submitRewards_sortsTranslatedOperatorsByAddress() public {
(address ethLow, address ethHigh) =
operator1 < operator2 ? (operator1, operator2) : (operator2, operator1);
address solochainLow = address(0x1000);
address solochainHigh = address(0x2000);
_registerOperator(ethLow, solochainHigh);
_registerOperator(ethHigh, solochainLow);
IRewardsCoordinatorTypes.StrategyAndMultiplier[] memory strategiesAndMultipliers =
new IRewardsCoordinatorTypes.StrategyAndMultiplier[](deployedStrategies.length);
for (uint256 i = 0; i < deployedStrategies.length; i++) {
strategiesAndMultipliers[i] = IRewardsCoordinatorTypes.StrategyAndMultiplier({
strategy: deployedStrategies[i], multiplier: uint96((i + 1) * 1e18)
});
}
uint256 amountForEthLow = 600e18;
uint256 amountForEthHigh = 400e18;
uint256 totalAmount = amountForEthLow + amountForEthHigh;
IRewardsCoordinatorTypes.OperatorReward[] memory inputOperatorRewards =
new IRewardsCoordinatorTypes.OperatorReward[](2);
inputOperatorRewards[0] = IRewardsCoordinatorTypes.OperatorReward({
operator: solochainLow, amount: amountForEthHigh
});
inputOperatorRewards[1] = IRewardsCoordinatorTypes.OperatorReward({
operator: solochainHigh, amount: amountForEthLow
});
IRewardsCoordinatorTypes.OperatorDirectedRewardsSubmission memory submission =
IRewardsCoordinatorTypes.OperatorDirectedRewardsSubmission({
strategiesAndMultipliers: strategiesAndMultipliers,
token: IERC20(address(rewardToken)),
operatorRewards: inputOperatorRewards,
startTimestamp: GENESIS_REWARDS_TIMESTAMP,
duration: TEST_CALCULATION_INTERVAL,
description: "DataHaven rewards"
});
vm.warp(submission.startTimestamp + submission.duration + 1);
IRewardsCoordinatorTypes.OperatorReward[] memory expectedOperatorRewards =
new IRewardsCoordinatorTypes.OperatorReward[](2);
expectedOperatorRewards[0] =
IRewardsCoordinatorTypes.OperatorReward({operator: ethLow, amount: amountForEthLow});
expectedOperatorRewards[1] =
IRewardsCoordinatorTypes.OperatorReward({operator: ethHigh, amount: amountForEthHigh});
IRewardsCoordinatorTypes.OperatorDirectedRewardsSubmission memory expectedSubmission =
IRewardsCoordinatorTypes.OperatorDirectedRewardsSubmission({
strategiesAndMultipliers: strategiesAndMultipliers,
token: submission.token,
operatorRewards: expectedOperatorRewards,
startTimestamp: submission.startTimestamp,
duration: submission.duration,
description: submission.description
});
IRewardsCoordinatorTypes.OperatorDirectedRewardsSubmission[] memory submissions =
new IRewardsCoordinatorTypes.OperatorDirectedRewardsSubmission[](1);
submissions[0] = expectedSubmission;
OperatorSet memory operatorSet =
OperatorSet({avs: address(serviceManager), id: serviceManager.VALIDATORS_SET_ID()});
vm.expectCall(
address(rewardsCoordinator),
abi.encodeCall(
IRewardsCoordinator.createOperatorDirectedOperatorSetRewardsSubmission,
(operatorSet, submissions)
)
);
vm.prank(snowbridgeAgent);
vm.expectEmit(false, false, false, true);
emit IDataHavenServiceManagerEvents.RewardsSubmitted(totalAmount, 2);
serviceManager.submitRewards(submission);
}
}

View file

@ -26,6 +26,8 @@ contract SlashingTest is AVSDeployer {
}
function test_fulfilSlashingRequest() public {
address solochainOperator = address(0xBEEF);
// Allow our operator to register
vm.prank(avsOwner);
serviceManager.addValidatorToAllowlist(operator);
@ -43,7 +45,7 @@ contract SlashingTest is AVSDeployer {
IAllocationManagerTypes.RegisterParams({
avs: address(serviceManager),
operatorSetIds: operatorSetIds,
data: abi.encodePacked(address(operator))
data: abi.encodePacked(solochainOperator)
});
vm.prank(operator);
@ -61,7 +63,7 @@ contract SlashingTest is AVSDeployer {
IStrategy[] memory strategies = allocationManager.getStrategiesInOperatorSet(operatorSet);
slashings[0] = IDataHavenServiceManager.SlashingRequest(
operator, strategies, wadsToSlash, "Testing slashing"
solochainOperator, strategies, wadsToSlash, "Testing slashing"
);
console.log(block.number);
@ -83,6 +85,8 @@ contract SlashingTest is AVSDeployer {
}
function test_fulfilSlashingRequestForOnlyOneStrategy() public {
address solochainOperator = address(0xBEEF);
// Allow our operator to register
vm.prank(avsOwner);
serviceManager.addValidatorToAllowlist(operator);
@ -100,7 +104,7 @@ contract SlashingTest is AVSDeployer {
IAllocationManagerTypes.RegisterParams({
avs: address(serviceManager),
operatorSetIds: operatorSetIds,
data: abi.encodePacked(address(operator))
data: abi.encodePacked(solochainOperator)
});
vm.prank(operator);
@ -119,7 +123,7 @@ contract SlashingTest is AVSDeployer {
strategiesToSlash[0] = strategies[0];
slashings[0] = IDataHavenServiceManager.SlashingRequest(
operator, strategiesToSlash, wadsToSlash, "Testing slashing"
solochainOperator, strategiesToSlash, wadsToSlash, "Testing slashing"
);
console.log(block.number);
@ -139,4 +143,24 @@ contract SlashingTest is AVSDeployer {
emit IDataHavenServiceManagerEvents.SlashingComplete();
serviceManager.slashValidatorsOperator(slashings);
}
function test_fulfilSlashingRequest_revertsIfUnknownSolochainAddress() public {
// Configure the rewards initiator (because only the reward agent can submit slashing request)
vm.prank(avsOwner);
serviceManager.setRewardsInitiator(snowbridgeAgent);
address unknownSolochainOperator = address(0xDEAD);
DataHavenServiceManager.SlashingRequest[] memory slashings =
new DataHavenServiceManager.SlashingRequest[](1);
slashings[0] = IDataHavenServiceManager.SlashingRequest(
unknownSolochainOperator,
new IStrategy[](0),
new uint256[](0),
"Testing unknown solochain operator"
);
vm.prank(snowbridgeAgent);
vm.expectRevert(abi.encodeWithSignature("UnknownSolochainAddress()"));
serviceManager.slashValidatorsOperator(slashings);
}
}

View file

@ -10,8 +10,13 @@ import {OperatorSet} from "eigenlayer-contracts/src/contracts/libraries/Operator
import {SnowbridgeAndAVSDeployer} from "./utils/SnowbridgeAndAVSDeployer.sol";
contract SnowbridgeIntegrationTest is SnowbridgeAndAVSDeployer {
address public submitter = address(uint160(uint256(keccak256("submitter"))));
function setUp() public {
_deployMockAllContracts();
// Set up the validator set submitter
vm.prank(avsOwner);
serviceManager.setValidatorSetSubmitter(submitter);
}
function beforeTestSetup(
@ -19,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);
}
}
@ -36,11 +42,13 @@ contract SnowbridgeIntegrationTest is SnowbridgeAndAVSDeployer {
);
}
// Mock balance for the AVS owner
vm.deal(avsOwner, 1000000 ether);
uint64 targetEra = 42;
// Mock balance for the submitter
vm.deal(submitter, 1000000 ether);
// Send the new validator set message to the Snowbridge Gateway
bytes memory message = serviceManager.buildNewValidatorSetMessage();
bytes memory message = serviceManager.buildNewValidatorSetMessageForEra(targetEra);
Payload memory payload = Payload({
origin: address(serviceManager),
assets: new Asset[](0),
@ -52,7 +60,7 @@ contract SnowbridgeIntegrationTest is SnowbridgeAndAVSDeployer {
});
cheats.expectEmit();
emit IGatewayV2.OutboundMessageAccepted(1, payload);
cheats.prank(avsOwner);
serviceManager.sendNewValidatorSet{value: 2 ether}(1 ether, 1 ether);
cheats.prank(submitter);
serviceManager.sendNewValidatorSetForEra{value: 2 ether}(targetEra, 1 ether, 1 ether);
}
}

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

@ -0,0 +1,287 @@
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.13;
/* solhint-disable func-name-mixedcase */
import {SnowbridgeAndAVSDeployer} from "./utils/SnowbridgeAndAVSDeployer.sol";
import {
IDataHavenServiceManagerErrors,
IDataHavenServiceManagerEvents
} from "../src/interfaces/IDataHavenServiceManager.sol";
import {DataHavenServiceManager} from "../src/DataHavenServiceManager.sol";
import {
TransparentUpgradeableProxy
} from "@openzeppelin/contracts/proxy/transparent/TransparentUpgradeableProxy.sol";
import {
IRewardsCoordinatorTypes
} from "eigenlayer-contracts/src/contracts/interfaces/IRewardsCoordinator.sol";
contract ValidatorSetSubmitterTest is SnowbridgeAndAVSDeployer {
address public submitterA = address(uint160(uint256(keccak256("submitterA"))));
address public submitterB = address(uint160(uint256(keccak256("submitterB"))));
address public nonOwner = address(uint160(uint256(keccak256("nonOwner"))));
function setUp() public {
_deployMockAllContracts();
}
function beforeTestSetup(
bytes4 testSelector
) public pure returns (bytes[] memory beforeTestCalldata) {
if (
testSelector == this.test_sendNewValidatorSetForEra_success.selector
|| testSelector
== this.test_buildNewValidatorSetMessageForEra_encodesTargetEra.selector
|| testSelector == this.test_fuzz_sendNewValidatorSetForEra.selector
|| testSelector
== this.test_buildNewValidatorSetMessageForEra_exactEncoding.selector
) {
beforeTestCalldata = new bytes[](1);
beforeTestCalldata[0] =
abi.encodeWithSelector(this.setupValidatorsAsOperatorsWithAllocations.selector);
}
}
// ============ setValidatorSetSubmitter ============
function test_setValidatorSetSubmitter() public {
// After initialization, validatorSetSubmitter is already set to avsOwner
assertEq(
serviceManager.validatorSetSubmitter(),
avsOwner,
"validatorSetSubmitter should be set to avsOwner after init"
);
cheats.expectEmit();
emit IDataHavenServiceManagerEvents.ValidatorSetSubmitterUpdated(avsOwner, submitterA);
cheats.prank(avsOwner);
serviceManager.setValidatorSetSubmitter(submitterA);
assertEq(
serviceManager.validatorSetSubmitter(),
submitterA,
"validatorSetSubmitter should be set"
);
}
function test_setValidatorSetSubmitter_revertsIfNotOwner() public {
cheats.prank(nonOwner);
cheats.expectRevert();
serviceManager.setValidatorSetSubmitter(submitterA);
}
function test_setValidatorSetSubmitter_revertsOnZeroAddress() public {
cheats.prank(avsOwner);
cheats.expectRevert(
abi.encodeWithSelector(IDataHavenServiceManagerErrors.ZeroAddress.selector)
);
serviceManager.setValidatorSetSubmitter(address(0));
}
function test_setValidatorSetSubmitter_rotation() public {
// Set submitter A (rotating from avsOwner set during init)
cheats.prank(avsOwner);
serviceManager.setValidatorSetSubmitter(submitterA);
assertEq(serviceManager.validatorSetSubmitter(), submitterA);
// Rotate to submitter B
cheats.expectEmit();
emit IDataHavenServiceManagerEvents.ValidatorSetSubmitterUpdated(submitterA, submitterB);
cheats.prank(avsOwner);
serviceManager.setValidatorSetSubmitter(submitterB);
assertEq(serviceManager.validatorSetSubmitter(), submitterB);
// Old submitter A can no longer submit
vm.deal(submitterA, 10 ether);
cheats.prank(submitterA);
cheats.expectRevert(
abi.encodeWithSelector(
IDataHavenServiceManagerErrors.OnlyValidatorSetSubmitter.selector
)
);
serviceManager.sendNewValidatorSetForEra{value: 2 ether}(1, 1 ether, 1 ether);
}
// ============ sendNewValidatorSetForEra ============
function test_sendNewValidatorSetForEra_revertsIfNotSubmitter() public {
cheats.prank(avsOwner);
serviceManager.setValidatorSetSubmitter(submitterA);
vm.deal(nonOwner, 10 ether);
cheats.prank(nonOwner);
cheats.expectRevert(
abi.encodeWithSelector(
IDataHavenServiceManagerErrors.OnlyValidatorSetSubmitter.selector
)
);
serviceManager.sendNewValidatorSetForEra{value: 2 ether}(1, 1 ether, 1 ether);
}
function test_sendNewValidatorSetForEra_success() public {
cheats.prank(avsOwner);
serviceManager.setValidatorSetSubmitter(submitterA);
uint64 targetEra = 42;
vm.deal(submitterA, 1000000 ether);
bytes memory message = serviceManager.buildNewValidatorSetMessageForEra(targetEra);
bytes32 expectedHash = keccak256(message);
cheats.expectEmit();
emit IDataHavenServiceManagerEvents.ValidatorSetMessageSubmitted(
targetEra, expectedHash, submitterA
);
cheats.prank(submitterA);
serviceManager.sendNewValidatorSetForEra{value: 2 ether}(targetEra, 1 ether, 1 ether);
}
function test_sendNewValidatorSetForEra_revertsOnEmptyValidatorSet() public {
cheats.prank(avsOwner);
serviceManager.setValidatorSetSubmitter(submitterA);
vm.deal(submitterA, 10 ether);
cheats.prank(submitterA);
cheats.expectRevert(
abi.encodeWithSelector(IDataHavenServiceManagerErrors.EmptyValidatorSet.selector)
);
serviceManager.sendNewValidatorSetForEra{value: 2 ether}(1, 1 ether, 1 ether);
}
function test_ownerCannotCallSendNewValidatorSetForEra() public {
cheats.prank(avsOwner);
serviceManager.setValidatorSetSubmitter(submitterA);
vm.deal(avsOwner, 10 ether);
cheats.prank(avsOwner);
cheats.expectRevert(
abi.encodeWithSelector(
IDataHavenServiceManagerErrors.OnlyValidatorSetSubmitter.selector
)
);
serviceManager.sendNewValidatorSetForEra{value: 2 ether}(1, 1 ether, 1 ether);
}
// ============ buildNewValidatorSetMessageForEra ============
function test_buildNewValidatorSetMessageForEra_encodesTargetEra() public view {
bytes memory messageEra1 = serviceManager.buildNewValidatorSetMessageForEra(1);
bytes memory messageEra2 = serviceManager.buildNewValidatorSetMessageForEra(2);
bytes memory messageEra100 = serviceManager.buildNewValidatorSetMessageForEra(100);
// Different era values must produce different encoded output
assertTrue(
keccak256(messageEra1) != keccak256(messageEra2),
"Messages for different eras should differ"
);
assertTrue(
keccak256(messageEra1) != keccak256(messageEra100),
"Messages for different eras should differ"
);
}
function test_sendNewValidatorSetForEra_revertsWhenSubmitterIsZeroAddress() public {
// Deploy a fresh proxy with address(0) as the submitter
IRewardsCoordinatorTypes.StrategyAndMultiplier[] memory emptyStrategies =
new IRewardsCoordinatorTypes.StrategyAndMultiplier[](0);
cheats.startPrank(regularDeployer);
DataHavenServiceManager zeroSubmitterSM = DataHavenServiceManager(
address(
new TransparentUpgradeableProxy(
address(serviceManagerImplementation),
address(proxyAdmin),
abi.encodeWithSelector(
DataHavenServiceManager.initialize.selector,
avsOwner,
rewardsInitiator,
emptyStrategies,
address(snowbridgeGatewayMock),
address(0)
)
)
)
);
cheats.stopPrank();
assertEq(
zeroSubmitterSM.validatorSetSubmitter(),
address(0),
"validatorSetSubmitter should be address(0)"
);
vm.deal(submitterA, 10 ether);
cheats.prank(submitterA);
cheats.expectRevert(
abi.encodeWithSelector(
IDataHavenServiceManagerErrors.OnlyValidatorSetSubmitter.selector
)
);
zeroSubmitterSM.sendNewValidatorSetForEra{value: 2 ether}(1, 1 ether, 1 ether);
}
function test_fuzz_sendNewValidatorSetForEra(
uint64 targetEra
) public {
cheats.prank(avsOwner);
serviceManager.setValidatorSetSubmitter(submitterA);
vm.deal(submitterA, 1000000 ether);
bytes memory message = serviceManager.buildNewValidatorSetMessageForEra(targetEra);
bytes32 expectedHash = keccak256(message);
cheats.expectEmit();
emit IDataHavenServiceManagerEvents.ValidatorSetMessageSubmitted(
targetEra, expectedHash, submitterA
);
cheats.prank(submitterA);
serviceManager.sendNewValidatorSetForEra{value: 2 ether}(targetEra, 1 ether, 1 ether);
}
function test_buildNewValidatorSetMessageForEra_exactEncoding() public view {
uint64 targetEra = 42;
bytes memory message = serviceManager.buildNewValidatorSetMessageForEra(targetEra);
// Total: 4 (EL_MESSAGE_ID) + 1 (V0) + 1 (ReceiveValidators)
// + 1 (compact 10) + 10*20 (validators) + 8 (era) = 215
assertEq(message.length, 215, "Message length should be 215 bytes");
// First 4 bytes: EL_MESSAGE_ID = 0x70150038
assertEq(uint8(message[0]), 0x70, "EL_MESSAGE_ID byte 0");
assertEq(uint8(message[1]), 0x15, "EL_MESSAGE_ID byte 1");
assertEq(uint8(message[2]), 0x00, "EL_MESSAGE_ID byte 2");
assertEq(uint8(message[3]), 0x38, "EL_MESSAGE_ID byte 3");
// Byte 4: V0 = 0x00
assertEq(uint8(message[4]), 0x00, "V0 byte mismatch");
// Byte 5: ReceiveValidators = 0x00
assertEq(uint8(message[5]), 0x00, "ReceiveValidators byte mismatch");
// Byte 6: SCALE compact encoding of 10 validators = 10 << 2 = 40 = 0x28
assertEq(uint8(message[6]), 0x28, "Compact encoding of 10 validators");
// Last 8 bytes: era 42 in SCALE little-endian = 0x2A00000000000000
assertEq(uint8(message[207]), 0x2A, "Era LE byte 0");
assertEq(uint8(message[208]), 0x00, "Era LE byte 1");
assertEq(uint8(message[209]), 0x00, "Era LE byte 2");
assertEq(uint8(message[210]), 0x00, "Era LE byte 3");
assertEq(uint8(message[211]), 0x00, "Era LE byte 4");
assertEq(uint8(message[212]), 0x00, "Era LE byte 5");
assertEq(uint8(message[213]), 0x00, "Era LE byte 6");
assertEq(uint8(message[214]), 0x00, "Era LE byte 7");
}
// ============ Legacy function removed ============
function test_legacySendNewValidatorSet_removed() public {
// The old sendNewValidatorSet(uint128,uint128) selector should not be callable
bytes memory callData =
abi.encodeWithSelector(bytes4(keccak256("sendNewValidatorSet(uint128,uint128)")), 1, 1);
vm.deal(avsOwner, 10 ether);
cheats.prank(avsOwner);
(bool success,) = address(serviceManager).call{value: 2 ether}(callData);
assertFalse(success, "Legacy sendNewValidatorSet should not be callable");
}
}

View file

@ -21,10 +21,12 @@ contract StorageLayoutTest is AVSDeployer {
// 1. Populate state
address testValidator = address(0x1234);
address newRewardsInitiator = address(0x9999);
address testSubmitter = address(0x5678);
vm.startPrank(avsOwner);
serviceManager.addValidatorToAllowlist(testValidator);
serviceManager.setRewardsInitiator(newRewardsInitiator);
serviceManager.setValidatorSetSubmitter(testSubmitter);
vm.stopPrank();
// 2. Record state before upgrade
@ -32,6 +34,7 @@ contract StorageLayoutTest is AVSDeployer {
address rewardsInitiatorBefore = serviceManager.rewardsInitiator();
address ownerBefore = serviceManager.owner();
address gatewayBefore = serviceManager.snowbridgeGateway();
address submitterBefore = serviceManager.validatorSetSubmitter();
// 3. Deploy new implementation
DataHavenServiceManager newImpl =
@ -58,6 +61,11 @@ contract StorageLayoutTest is AVSDeployer {
gatewayBefore,
"snowbridgeGateway should be preserved"
);
assertEq(
serviceManager.validatorSetSubmitter(),
submitterBefore,
"validatorSetSubmitter should be preserved"
);
}
/// @notice Verifies validatorEthAddressToSolochainAddress mapping is preserved
@ -85,9 +93,12 @@ contract StorageLayoutTest is AVSDeployer {
bool inAllowlistBefore = serviceManager.validatorsAllowlist(testValidator);
address solochainAddressBefore =
serviceManager.validatorEthAddressToSolochainAddress(testValidator);
address ethOperatorBefore =
serviceManager.validatorSolochainAddressToEthAddress(testSolochainAddress);
// Verify the mapping was set correctly before upgrade
assertEq(solochainAddressBefore, testSolochainAddress, "Solochain address should be set");
assertEq(ethOperatorBefore, testValidator, "Eth operator should be set");
// Deploy new implementation and upgrade
DataHavenServiceManager newImpl =
@ -112,6 +123,16 @@ contract StorageLayoutTest is AVSDeployer {
testSolochainAddress,
"validatorEthAddressToSolochainAddress should have correct value after upgrade"
);
assertEq(
serviceManager.validatorSolochainAddressToEthAddress(testSolochainAddress),
ethOperatorBefore,
"validatorSolochainAddressToEthAddress mapping should be preserved after upgrade"
);
assertEq(
serviceManager.validatorSolochainAddressToEthAddress(testSolochainAddress),
testValidator,
"validatorSolochainAddressToEthAddress should have correct value after upgrade"
);
}
/// @notice Verifies multiple validators in allowlist are preserved

View file

@ -239,14 +239,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(
@ -256,8 +248,9 @@ contract AVSDeployer is Test {
DataHavenServiceManager.initialize.selector,
avsOwner,
rewardsInitiator,
validatorsStrategies,
defaultStrategyAndMultipliers,
address(snowbridgeGatewayMock),
avsOwner,
"v-mock",
versionUpdater
)

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

273
operator/Cargo.lock generated
View file

@ -1521,7 +1521,7 @@ dependencies = [
"pallet-message-queue",
"parity-scale-codec",
"scale-info",
"snowbridge-core 0.24.0",
"snowbridge-core 0.25.0",
"sp-core",
"sp-runtime",
"sp-std",
@ -2607,7 +2607,7 @@ dependencies = [
[[package]]
name = "datahaven-mainnet-runtime"
version = "0.24.0"
version = "0.25.0"
dependencies = [
"alloy-core",
"bridge-hub-common 0.13.1",
@ -2719,8 +2719,8 @@ dependencies = [
"shp-treasury-funding",
"shp-tx-implicits-runtime-api",
"smallvec",
"snowbridge-beacon-primitives 0.24.0",
"snowbridge-core 0.24.0",
"snowbridge-beacon-primitives 0.25.0",
"snowbridge-core 0.25.0",
"snowbridge-inbound-queue-primitives",
"snowbridge-merkle-tree",
"snowbridge-outbound-queue-primitives",
@ -2763,7 +2763,7 @@ dependencies = [
[[package]]
name = "datahaven-node"
version = "0.24.0"
version = "0.25.0"
dependencies = [
"async-channel 1.9.0",
"clap",
@ -2876,7 +2876,7 @@ dependencies = [
[[package]]
name = "datahaven-runtime-common"
version = "0.24.0"
version = "0.25.0"
dependencies = [
"alloy-core",
"fp-account",
@ -2910,7 +2910,7 @@ dependencies = [
[[package]]
name = "datahaven-stagenet-runtime"
version = "0.24.0"
version = "0.25.0"
dependencies = [
"alloy-core",
"bridge-hub-common 0.13.1",
@ -3022,8 +3022,8 @@ dependencies = [
"shp-treasury-funding",
"shp-tx-implicits-runtime-api",
"smallvec",
"snowbridge-beacon-primitives 0.24.0",
"snowbridge-core 0.24.0",
"snowbridge-beacon-primitives 0.25.0",
"snowbridge-core 0.25.0",
"snowbridge-inbound-queue-primitives",
"snowbridge-merkle-tree",
"snowbridge-outbound-queue-primitives",
@ -3066,7 +3066,7 @@ dependencies = [
[[package]]
name = "datahaven-testnet-runtime"
version = "0.24.0"
version = "0.25.0"
dependencies = [
"alloy-core",
"bridge-hub-common 0.13.1",
@ -3178,8 +3178,8 @@ dependencies = [
"shp-treasury-funding",
"shp-tx-implicits-runtime-api",
"smallvec",
"snowbridge-beacon-primitives 0.24.0",
"snowbridge-core 0.24.0",
"snowbridge-beacon-primitives 0.25.0",
"snowbridge-core 0.25.0",
"snowbridge-inbound-queue-primitives",
"snowbridge-merkle-tree",
"snowbridge-outbound-queue-primitives",
@ -3371,7 +3371,7 @@ dependencies = [
[[package]]
name = "dhp-bridge"
version = "0.24.0"
version = "0.25.0"
dependencies = [
"frame-support",
"frame-system",
@ -3379,7 +3379,7 @@ dependencies = [
"pallet-datahaven-native-transfer",
"pallet-external-validators",
"parity-scale-codec",
"snowbridge-core 0.24.0",
"snowbridge-core 0.25.0",
"snowbridge-inbound-queue-primitives",
"sp-core",
"sp-std",
@ -8639,8 +8639,8 @@ dependencies = [
[[package]]
name = "pallet-bucket-nfts"
version = "0.4.1"
source = "git+https://github.com/Moonsong-Labs/storage-hub.git?tag=v0.4.1#b5d6eb2ffa153d97e079d1fda382773b466f4702"
version = "0.4.2"
source = "git+https://github.com/Moonsong-Labs/storage-hub.git?tag=v0.4.2#5b52af21ca6c60db96bb7c3fe7c069075e941614"
dependencies = [
"frame-benchmarking",
"frame-support",
@ -8696,8 +8696,8 @@ dependencies = [
[[package]]
name = "pallet-cr-randomness"
version = "0.4.1"
source = "git+https://github.com/Moonsong-Labs/storage-hub.git?tag=v0.4.1#b5d6eb2ffa153d97e079d1fda382773b466f4702"
version = "0.4.2"
source = "git+https://github.com/Moonsong-Labs/storage-hub.git?tag=v0.4.2#5b52af21ca6c60db96bb7c3fe7c069075e941614"
dependencies = [
"frame-support",
"frame-system",
@ -8716,7 +8716,7 @@ dependencies = [
[[package]]
name = "pallet-datahaven-native-transfer"
version = "0.24.0"
version = "0.25.0"
dependencies = [
"frame-benchmarking",
"frame-support",
@ -8724,7 +8724,7 @@ dependencies = [
"pallet-balances",
"parity-scale-codec",
"scale-info",
"snowbridge-core 0.24.0",
"snowbridge-core 0.25.0",
"snowbridge-outbound-queue-primitives",
"sp-core",
"sp-io",
@ -8828,7 +8828,7 @@ dependencies = [
[[package]]
name = "pallet-evm-precompile-balances-erc20"
version = "0.24.0"
version = "0.25.0"
dependencies = [
"fp-evm",
"frame-support",
@ -8851,7 +8851,7 @@ dependencies = [
[[package]]
name = "pallet-evm-precompile-batch"
version = "0.24.0"
version = "0.25.0"
dependencies = [
"evm",
"fp-evm",
@ -8890,7 +8890,7 @@ dependencies = [
[[package]]
name = "pallet-evm-precompile-call-permit"
version = "0.24.0"
version = "0.25.0"
dependencies = [
"evm",
"fp-evm",
@ -8956,7 +8956,7 @@ dependencies = [
[[package]]
name = "pallet-evm-precompile-datahaven-native-transfer"
version = "0.24.0"
version = "0.25.0"
dependencies = [
"evm",
"fp-evm",
@ -8970,7 +8970,7 @@ dependencies = [
"parity-scale-codec",
"precompile-utils",
"scale-info",
"snowbridge-core 0.24.0",
"snowbridge-core 0.25.0",
"snowbridge-outbound-queue-primitives",
"sp-core",
"sp-io",
@ -8980,8 +8980,8 @@ dependencies = [
[[package]]
name = "pallet-evm-precompile-file-system"
version = "0.4.1"
source = "git+https://github.com/Moonsong-Labs/storage-hub.git?tag=v0.4.1#b5d6eb2ffa153d97e079d1fda382773b466f4702"
version = "0.4.2"
source = "git+https://github.com/Moonsong-Labs/storage-hub.git?tag=v0.4.2#5b52af21ca6c60db96bb7c3fe7c069075e941614"
dependencies = [
"fp-account",
"fp-evm",
@ -9049,7 +9049,7 @@ dependencies = [
[[package]]
name = "pallet-evm-precompile-proxy"
version = "0.24.0"
version = "0.25.0"
dependencies = [
"evm",
"fp-evm",
@ -9093,7 +9093,7 @@ dependencies = [
[[package]]
name = "pallet-evm-precompile-registry"
version = "0.24.0"
version = "0.25.0"
dependencies = [
"fp-evm",
"frame-support",
@ -9144,7 +9144,7 @@ dependencies = [
"parity-scale-codec",
"scale-info",
"serde",
"snowbridge-core 0.24.0",
"snowbridge-core 0.25.0",
"snowbridge-outbound-queue-primitives",
"sp-core",
"sp-io",
@ -9154,7 +9154,7 @@ dependencies = [
[[package]]
name = "pallet-external-validators"
version = "0.24.0"
version = "0.25.0"
dependencies = [
"frame-benchmarking",
"frame-support",
@ -9178,7 +9178,7 @@ dependencies = [
[[package]]
name = "pallet-external-validators-rewards"
version = "0.24.0"
version = "0.25.0"
dependencies = [
"frame-benchmarking",
"frame-support",
@ -9191,7 +9191,7 @@ dependencies = [
"pallet-timestamp",
"parity-scale-codec",
"scale-info",
"snowbridge-core 0.24.0",
"snowbridge-core 0.25.0",
"snowbridge-outbound-queue-primitives",
"sp-core",
"sp-io",
@ -9220,8 +9220,8 @@ dependencies = [
[[package]]
name = "pallet-file-system"
version = "0.4.1"
source = "git+https://github.com/Moonsong-Labs/storage-hub.git?tag=v0.4.1#b5d6eb2ffa153d97e079d1fda382773b466f4702"
version = "0.4.2"
source = "git+https://github.com/Moonsong-Labs/storage-hub.git?tag=v0.4.2#5b52af21ca6c60db96bb7c3fe7c069075e941614"
dependencies = [
"frame-benchmarking",
"frame-support",
@ -9249,8 +9249,8 @@ dependencies = [
[[package]]
name = "pallet-file-system-runtime-api"
version = "0.4.1"
source = "git+https://github.com/Moonsong-Labs/storage-hub.git?tag=v0.4.1#b5d6eb2ffa153d97e079d1fda382773b466f4702"
version = "0.4.2"
source = "git+https://github.com/Moonsong-Labs/storage-hub.git?tag=v0.4.2#5b52af21ca6c60db96bb7c3fe7c069075e941614"
dependencies = [
"parity-scale-codec",
"scale-info",
@ -9417,7 +9417,7 @@ dependencies = [
[[package]]
name = "pallet-outbound-commitment-store"
version = "0.24.0"
version = "0.25.0"
dependencies = [
"frame-support",
"frame-system",
@ -9445,8 +9445,8 @@ dependencies = [
[[package]]
name = "pallet-payment-streams"
version = "0.4.1"
source = "git+https://github.com/Moonsong-Labs/storage-hub.git?tag=v0.4.1#b5d6eb2ffa153d97e079d1fda382773b466f4702"
version = "0.4.2"
source = "git+https://github.com/Moonsong-Labs/storage-hub.git?tag=v0.4.2#5b52af21ca6c60db96bb7c3fe7c069075e941614"
dependencies = [
"frame-benchmarking",
"frame-support",
@ -9465,8 +9465,8 @@ dependencies = [
[[package]]
name = "pallet-payment-streams-runtime-api"
version = "0.4.1"
source = "git+https://github.com/Moonsong-Labs/storage-hub.git?tag=v0.4.1#b5d6eb2ffa153d97e079d1fda382773b466f4702"
version = "0.4.2"
source = "git+https://github.com/Moonsong-Labs/storage-hub.git?tag=v0.4.2#5b52af21ca6c60db96bb7c3fe7c069075e941614"
dependencies = [
"parity-scale-codec",
"scale-info",
@ -9493,8 +9493,8 @@ dependencies = [
[[package]]
name = "pallet-proofs-dealer"
version = "0.4.1"
source = "git+https://github.com/Moonsong-Labs/storage-hub.git?tag=v0.4.1#b5d6eb2ffa153d97e079d1fda382773b466f4702"
version = "0.4.2"
source = "git+https://github.com/Moonsong-Labs/storage-hub.git?tag=v0.4.2#5b52af21ca6c60db96bb7c3fe7c069075e941614"
dependencies = [
"frame-benchmarking",
"frame-support",
@ -9519,8 +9519,8 @@ dependencies = [
[[package]]
name = "pallet-proofs-dealer-runtime-api"
version = "0.4.1"
source = "git+https://github.com/Moonsong-Labs/storage-hub.git?tag=v0.4.1#b5d6eb2ffa153d97e079d1fda382773b466f4702"
version = "0.4.2"
source = "git+https://github.com/Moonsong-Labs/storage-hub.git?tag=v0.4.2#5b52af21ca6c60db96bb7c3fe7c069075e941614"
dependencies = [
"parity-scale-codec",
"scale-info",
@ -9541,7 +9541,7 @@ dependencies = [
[[package]]
name = "pallet-proxy-genesis-companion"
version = "0.24.0"
version = "0.25.0"
dependencies = [
"frame-support",
"frame-system",
@ -9558,8 +9558,8 @@ dependencies = [
[[package]]
name = "pallet-randomness"
version = "0.4.1"
source = "git+https://github.com/Moonsong-Labs/storage-hub.git?tag=v0.4.1#b5d6eb2ffa153d97e079d1fda382773b466f4702"
version = "0.4.2"
source = "git+https://github.com/Moonsong-Labs/storage-hub.git?tag=v0.4.2#5b52af21ca6c60db96bb7c3fe7c069075e941614"
dependencies = [
"frame-benchmarking",
"frame-support",
@ -9652,7 +9652,7 @@ dependencies = [
[[package]]
name = "pallet-session-benchmarking"
version = "0.24.0"
version = "0.25.0"
dependencies = [
"frame-benchmarking",
"frame-support",
@ -9696,8 +9696,8 @@ dependencies = [
[[package]]
name = "pallet-storage-providers"
version = "0.4.1"
source = "git+https://github.com/Moonsong-Labs/storage-hub.git?tag=v0.4.1#b5d6eb2ffa153d97e079d1fda382773b466f4702"
version = "0.4.2"
source = "git+https://github.com/Moonsong-Labs/storage-hub.git?tag=v0.4.2#5b52af21ca6c60db96bb7c3fe7c069075e941614"
dependencies = [
"frame-benchmarking",
"frame-support",
@ -9718,8 +9718,8 @@ dependencies = [
[[package]]
name = "pallet-storage-providers-runtime-api"
version = "0.4.1"
source = "git+https://github.com/Moonsong-Labs/storage-hub.git?tag=v0.4.1#b5d6eb2ffa153d97e079d1fda382773b466f4702"
version = "0.4.2"
source = "git+https://github.com/Moonsong-Labs/storage-hub.git?tag=v0.4.2#5b52af21ca6c60db96bb7c3fe7c069075e941614"
dependencies = [
"parity-scale-codec",
"scale-info",
@ -13881,8 +13881,8 @@ dependencies = [
[[package]]
name = "shc-actors-derive"
version = "0.4.1"
source = "git+https://github.com/Moonsong-Labs/storage-hub.git?tag=v0.4.1#b5d6eb2ffa153d97e079d1fda382773b466f4702"
version = "0.4.2"
source = "git+https://github.com/Moonsong-Labs/storage-hub.git?tag=v0.4.2#5b52af21ca6c60db96bb7c3fe7c069075e941614"
dependencies = [
"once_cell",
"proc-macro2",
@ -13894,8 +13894,8 @@ dependencies = [
[[package]]
name = "shc-actors-framework"
version = "0.4.1"
source = "git+https://github.com/Moonsong-Labs/storage-hub.git?tag=v0.4.1#b5d6eb2ffa153d97e079d1fda382773b466f4702"
version = "0.4.2"
source = "git+https://github.com/Moonsong-Labs/storage-hub.git?tag=v0.4.2#5b52af21ca6c60db96bb7c3fe7c069075e941614"
dependencies = [
"anyhow",
"bincode",
@ -13913,8 +13913,8 @@ dependencies = [
[[package]]
name = "shc-blockchain-service"
version = "0.4.1"
source = "git+https://github.com/Moonsong-Labs/storage-hub.git?tag=v0.4.1#b5d6eb2ffa153d97e079d1fda382773b466f4702"
version = "0.4.2"
source = "git+https://github.com/Moonsong-Labs/storage-hub.git?tag=v0.4.2#5b52af21ca6c60db96bb7c3fe7c069075e941614"
dependencies = [
"anyhow",
"array-bytes",
@ -13969,8 +13969,8 @@ dependencies = [
[[package]]
name = "shc-blockchain-service-db"
version = "0.4.1"
source = "git+https://github.com/Moonsong-Labs/storage-hub.git?tag=v0.4.1#b5d6eb2ffa153d97e079d1fda382773b466f4702"
version = "0.4.2"
source = "git+https://github.com/Moonsong-Labs/storage-hub.git?tag=v0.4.2#5b52af21ca6c60db96bb7c3fe7c069075e941614"
dependencies = [
"chrono",
"diesel",
@ -13993,8 +13993,8 @@ dependencies = [
[[package]]
name = "shc-client"
version = "0.4.1"
source = "git+https://github.com/Moonsong-Labs/storage-hub.git?tag=v0.4.1#b5d6eb2ffa153d97e079d1fda382773b466f4702"
version = "0.4.2"
source = "git+https://github.com/Moonsong-Labs/storage-hub.git?tag=v0.4.2#5b52af21ca6c60db96bb7c3fe7c069075e941614"
dependencies = [
"anyhow",
"array-bytes",
@ -14002,6 +14002,7 @@ dependencies = [
"async-trait",
"axum",
"axum-extra",
"bytes",
"chrono",
"frame-benchmarking",
"frame-benchmarking-cli",
@ -14067,12 +14068,13 @@ dependencies = [
[[package]]
name = "shc-common"
version = "0.4.1"
source = "git+https://github.com/Moonsong-Labs/storage-hub.git?tag=v0.4.1#b5d6eb2ffa153d97e079d1fda382773b466f4702"
version = "0.4.2"
source = "git+https://github.com/Moonsong-Labs/storage-hub.git?tag=v0.4.2#5b52af21ca6c60db96bb7c3fe7c069075e941614"
dependencies = [
"anyhow",
"bigdecimal",
"bincode",
"bytes",
"cumulus-primitives-core",
"cumulus-primitives-storage-weight-reclaim",
"fp-account",
@ -14131,8 +14133,8 @@ dependencies = [
[[package]]
name = "shc-file-manager"
version = "0.4.1"
source = "git+https://github.com/Moonsong-Labs/storage-hub.git?tag=v0.4.1#b5d6eb2ffa153d97e079d1fda382773b466f4702"
version = "0.4.2"
source = "git+https://github.com/Moonsong-Labs/storage-hub.git?tag=v0.4.2#5b52af21ca6c60db96bb7c3fe7c069075e941614"
dependencies = [
"bincode",
"hash-db",
@ -14140,6 +14142,7 @@ dependencies = [
"kvdb-memorydb",
"kvdb-rocksdb",
"log",
"lru 0.16.3",
"parity-scale-codec",
"serde_json",
"shc-common",
@ -14155,8 +14158,8 @@ dependencies = [
[[package]]
name = "shc-file-transfer-service"
version = "0.4.1"
source = "git+https://github.com/Moonsong-Labs/storage-hub.git?tag=v0.4.1#b5d6eb2ffa153d97e079d1fda382773b466f4702"
version = "0.4.2"
source = "git+https://github.com/Moonsong-Labs/storage-hub.git?tag=v0.4.2#5b52af21ca6c60db96bb7c3fe7c069075e941614"
dependencies = [
"anyhow",
"array-bytes",
@ -14164,6 +14167,7 @@ dependencies = [
"async-trait",
"chrono",
"futures",
"pallet-storage-providers-runtime-api",
"parity-scale-codec",
"prost 0.12.6",
"prost-build 0.12.6",
@ -14178,14 +14182,15 @@ dependencies = [
"shc-common",
"shp-file-key-verifier",
"shp-file-metadata",
"sp-api",
"thiserror 1.0.69",
"tokio",
]
[[package]]
name = "shc-fisherman-service"
version = "0.4.1"
source = "git+https://github.com/Moonsong-Labs/storage-hub.git?tag=v0.4.1#b5d6eb2ffa153d97e079d1fda382773b466f4702"
version = "0.4.2"
source = "git+https://github.com/Moonsong-Labs/storage-hub.git?tag=v0.4.2#5b52af21ca6c60db96bb7c3fe7c069075e941614"
dependencies = [
"async-trait",
"diesel",
@ -14215,8 +14220,8 @@ dependencies = [
[[package]]
name = "shc-forest-manager"
version = "0.4.1"
source = "git+https://github.com/Moonsong-Labs/storage-hub.git?tag=v0.4.1#b5d6eb2ffa153d97e079d1fda382773b466f4702"
version = "0.4.2"
source = "git+https://github.com/Moonsong-Labs/storage-hub.git?tag=v0.4.2#5b52af21ca6c60db96bb7c3fe7c069075e941614"
dependencies = [
"anyhow",
"async-trait",
@ -14241,8 +14246,8 @@ dependencies = [
[[package]]
name = "shc-indexer-db"
version = "0.4.1"
source = "git+https://github.com/Moonsong-Labs/storage-hub.git?tag=v0.4.1#b5d6eb2ffa153d97e079d1fda382773b466f4702"
version = "0.4.2"
source = "git+https://github.com/Moonsong-Labs/storage-hub.git?tag=v0.4.2#5b52af21ca6c60db96bb7c3fe7c069075e941614"
dependencies = [
"bigdecimal",
"chrono",
@ -14269,8 +14274,8 @@ dependencies = [
[[package]]
name = "shc-indexer-service"
version = "0.4.1"
source = "git+https://github.com/Moonsong-Labs/storage-hub.git?tag=v0.4.1#b5d6eb2ffa153d97e079d1fda382773b466f4702"
version = "0.4.2"
source = "git+https://github.com/Moonsong-Labs/storage-hub.git?tag=v0.4.2#5b52af21ca6c60db96bb7c3fe7c069075e941614"
dependencies = [
"anyhow",
"array-bytes",
@ -14320,8 +14325,8 @@ dependencies = [
[[package]]
name = "shc-rpc"
version = "0.4.1"
source = "git+https://github.com/Moonsong-Labs/storage-hub.git?tag=v0.4.1#b5d6eb2ffa153d97e079d1fda382773b466f4702"
version = "0.4.2"
source = "git+https://github.com/Moonsong-Labs/storage-hub.git?tag=v0.4.2#5b52af21ca6c60db96bb7c3fe7c069075e941614"
dependencies = [
"array-bytes",
"async-trait",
@ -14366,8 +14371,8 @@ dependencies = [
[[package]]
name = "shc-telemetry"
version = "0.4.1"
source = "git+https://github.com/Moonsong-Labs/storage-hub.git?tag=v0.4.1#b5d6eb2ffa153d97e079d1fda382773b466f4702"
version = "0.4.2"
source = "git+https://github.com/Moonsong-Labs/storage-hub.git?tag=v0.4.2#5b52af21ca6c60db96bb7c3fe7c069075e941614"
dependencies = [
"log",
"substrate-prometheus-endpoint",
@ -14383,8 +14388,8 @@ checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64"
[[package]]
name = "shp-constants"
version = "0.4.1"
source = "git+https://github.com/Moonsong-Labs/storage-hub.git?tag=v0.4.1#b5d6eb2ffa153d97e079d1fda382773b466f4702"
version = "0.4.2"
source = "git+https://github.com/Moonsong-Labs/storage-hub.git?tag=v0.4.2#5b52af21ca6c60db96bb7c3fe7c069075e941614"
dependencies = [
"sp-core",
"sp-runtime",
@ -14392,8 +14397,8 @@ dependencies = [
[[package]]
name = "shp-data-price-updater"
version = "0.4.1"
source = "git+https://github.com/Moonsong-Labs/storage-hub.git?tag=v0.4.1#b5d6eb2ffa153d97e079d1fda382773b466f4702"
version = "0.4.2"
source = "git+https://github.com/Moonsong-Labs/storage-hub.git?tag=v0.4.2#5b52af21ca6c60db96bb7c3fe7c069075e941614"
dependencies = [
"frame-support",
"parity-scale-codec",
@ -14407,8 +14412,8 @@ dependencies = [
[[package]]
name = "shp-file-key-verifier"
version = "0.4.1"
source = "git+https://github.com/Moonsong-Labs/storage-hub.git?tag=v0.4.1#b5d6eb2ffa153d97e079d1fda382773b466f4702"
version = "0.4.2"
source = "git+https://github.com/Moonsong-Labs/storage-hub.git?tag=v0.4.2#5b52af21ca6c60db96bb7c3fe7c069075e941614"
dependencies = [
"frame-support",
"parity-scale-codec",
@ -14425,8 +14430,8 @@ dependencies = [
[[package]]
name = "shp-file-metadata"
version = "0.4.1"
source = "git+https://github.com/Moonsong-Labs/storage-hub.git?tag=v0.4.1#b5d6eb2ffa153d97e079d1fda382773b466f4702"
version = "0.4.2"
source = "git+https://github.com/Moonsong-Labs/storage-hub.git?tag=v0.4.2#5b52af21ca6c60db96bb7c3fe7c069075e941614"
dependencies = [
"hex",
"num-bigint",
@ -14441,8 +14446,8 @@ dependencies = [
[[package]]
name = "shp-forest-verifier"
version = "0.4.1"
source = "git+https://github.com/Moonsong-Labs/storage-hub.git?tag=v0.4.1#b5d6eb2ffa153d97e079d1fda382773b466f4702"
version = "0.4.2"
source = "git+https://github.com/Moonsong-Labs/storage-hub.git?tag=v0.4.2#5b52af21ca6c60db96bb7c3fe7c069075e941614"
dependencies = [
"frame-support",
"parity-scale-codec",
@ -14458,16 +14463,16 @@ dependencies = [
[[package]]
name = "shp-opaque"
version = "0.4.1"
source = "git+https://github.com/Moonsong-Labs/storage-hub.git?tag=v0.4.1#b5d6eb2ffa153d97e079d1fda382773b466f4702"
version = "0.4.2"
source = "git+https://github.com/Moonsong-Labs/storage-hub.git?tag=v0.4.2#5b52af21ca6c60db96bb7c3fe7c069075e941614"
dependencies = [
"sp-runtime",
]
[[package]]
name = "shp-session-keys"
version = "0.4.1"
source = "git+https://github.com/Moonsong-Labs/storage-hub.git?tag=v0.4.1#b5d6eb2ffa153d97e079d1fda382773b466f4702"
version = "0.4.2"
source = "git+https://github.com/Moonsong-Labs/storage-hub.git?tag=v0.4.2#5b52af21ca6c60db96bb7c3fe7c069075e941614"
dependencies = [
"async-trait",
"parity-scale-codec",
@ -14481,8 +14486,8 @@ dependencies = [
[[package]]
name = "shp-traits"
version = "0.4.1"
source = "git+https://github.com/Moonsong-Labs/storage-hub.git?tag=v0.4.1#b5d6eb2ffa153d97e079d1fda382773b466f4702"
version = "0.4.2"
source = "git+https://github.com/Moonsong-Labs/storage-hub.git?tag=v0.4.2#5b52af21ca6c60db96bb7c3fe7c069075e941614"
dependencies = [
"frame-support",
"parity-scale-codec",
@ -14495,8 +14500,8 @@ dependencies = [
[[package]]
name = "shp-treasury-funding"
version = "0.4.1"
source = "git+https://github.com/Moonsong-Labs/storage-hub.git?tag=v0.4.1#b5d6eb2ffa153d97e079d1fda382773b466f4702"
version = "0.4.2"
source = "git+https://github.com/Moonsong-Labs/storage-hub.git?tag=v0.4.2#5b52af21ca6c60db96bb7c3fe7c069075e941614"
dependencies = [
"log",
"shp-traits",
@ -14506,8 +14511,8 @@ dependencies = [
[[package]]
name = "shp-tx-implicits-runtime-api"
version = "0.4.1"
source = "git+https://github.com/Moonsong-Labs/storage-hub.git?tag=v0.4.1#b5d6eb2ffa153d97e079d1fda382773b466f4702"
version = "0.4.2"
source = "git+https://github.com/Moonsong-Labs/storage-hub.git?tag=v0.4.2#5b52af21ca6c60db96bb7c3fe7c069075e941614"
dependencies = [
"parity-scale-codec",
"scale-info",
@ -14519,8 +14524,8 @@ dependencies = [
[[package]]
name = "shp-types"
version = "0.4.1"
source = "git+https://github.com/Moonsong-Labs/storage-hub.git?tag=v0.4.1#b5d6eb2ffa153d97e079d1fda382773b466f4702"
version = "0.4.2"
source = "git+https://github.com/Moonsong-Labs/storage-hub.git?tag=v0.4.2#5b52af21ca6c60db96bb7c3fe7c069075e941614"
dependencies = [
"sp-core",
"sp-runtime",
@ -14800,7 +14805,7 @@ dependencies = [
[[package]]
name = "snowbridge-beacon-primitives"
version = "0.24.0"
version = "0.25.0"
dependencies = [
"byte-slice-cast",
"frame-support",
@ -14845,7 +14850,7 @@ dependencies = [
[[package]]
name = "snowbridge-core"
version = "0.24.0"
version = "0.25.0"
dependencies = [
"bp-relayers",
"ethabi-decode",
@ -14922,8 +14927,8 @@ dependencies = [
"log",
"parity-scale-codec",
"scale-info",
"snowbridge-beacon-primitives 0.24.0",
"snowbridge-core 0.24.0",
"snowbridge-beacon-primitives 0.25.0",
"snowbridge-core 0.25.0",
"snowbridge-verification-primitives",
"sp-core",
"sp-io",
@ -14936,7 +14941,7 @@ dependencies = [
[[package]]
name = "snowbridge-merkle-tree"
version = "0.24.0"
version = "0.25.0"
dependencies = [
"array-bytes",
"hex",
@ -14977,7 +14982,7 @@ dependencies = [
[[package]]
name = "snowbridge-outbound-queue-primitives"
version = "0.24.0"
version = "0.25.0"
dependencies = [
"alloy-core",
"ethabi-decode",
@ -14989,7 +14994,7 @@ dependencies = [
"parity-scale-codec",
"polkadot-parachain-primitives",
"scale-info",
"snowbridge-core 0.24.0",
"snowbridge-core 0.25.0",
"snowbridge-verification-primitives",
"sp-arithmetic",
"sp-core",
@ -15003,12 +15008,12 @@ dependencies = [
[[package]]
name = "snowbridge-outbound-queue-v2-runtime-api"
version = "0.24.0"
version = "0.25.0"
dependencies = [
"frame-support",
"parity-scale-codec",
"scale-info",
"snowbridge-core 0.24.0",
"snowbridge-core 0.25.0",
"snowbridge-merkle-tree",
"snowbridge-outbound-queue-primitives",
"sp-api",
@ -15018,7 +15023,7 @@ dependencies = [
[[package]]
name = "snowbridge-pallet-ethereum-client"
version = "0.24.0"
version = "0.25.0"
dependencies = [
"frame-benchmarking",
"frame-support",
@ -15031,8 +15036,8 @@ dependencies = [
"scale-info",
"serde",
"serde_json",
"snowbridge-beacon-primitives 0.24.0",
"snowbridge-core 0.24.0",
"snowbridge-beacon-primitives 0.25.0",
"snowbridge-core 0.25.0",
"snowbridge-ethereum 0.3.0",
"snowbridge-inbound-queue-primitives",
"snowbridge-pallet-ethereum-client-fixtures",
@ -15048,8 +15053,8 @@ name = "snowbridge-pallet-ethereum-client-fixtures"
version = "0.9.0"
dependencies = [
"hex-literal 0.3.4",
"snowbridge-beacon-primitives 0.24.0",
"snowbridge-core 0.24.0",
"snowbridge-beacon-primitives 0.25.0",
"snowbridge-core 0.25.0",
"snowbridge-inbound-queue-primitives",
"sp-core",
"sp-std",
@ -15057,7 +15062,7 @@ dependencies = [
[[package]]
name = "snowbridge-pallet-inbound-queue-v2"
version = "0.24.0"
version = "0.25.0"
dependencies = [
"alloy-core",
"bp-relayers",
@ -15071,8 +15076,8 @@ dependencies = [
"parity-scale-codec",
"scale-info",
"serde",
"snowbridge-beacon-primitives 0.24.0",
"snowbridge-core 0.24.0",
"snowbridge-beacon-primitives 0.25.0",
"snowbridge-core 0.25.0",
"snowbridge-inbound-queue-primitives",
"snowbridge-pallet-ethereum-client",
"snowbridge-pallet-inbound-queue-v2-fixtures",
@ -15093,8 +15098,8 @@ name = "snowbridge-pallet-inbound-queue-v2-fixtures"
version = "0.10.0"
dependencies = [
"hex-literal 0.3.4",
"snowbridge-beacon-primitives 0.24.0",
"snowbridge-core 0.24.0",
"snowbridge-beacon-primitives 0.25.0",
"snowbridge-core 0.25.0",
"snowbridge-inbound-queue-primitives",
"sp-core",
"sp-std",
@ -15124,7 +15129,7 @@ dependencies = [
[[package]]
name = "snowbridge-pallet-outbound-queue-v2"
version = "0.24.0"
version = "0.25.0"
dependencies = [
"alloy-core",
"bp-relayers",
@ -15138,8 +15143,8 @@ dependencies = [
"parity-scale-codec",
"scale-info",
"serde",
"snowbridge-beacon-primitives 0.24.0",
"snowbridge-core 0.24.0",
"snowbridge-beacon-primitives 0.25.0",
"snowbridge-core 0.25.0",
"snowbridge-inbound-queue-primitives",
"snowbridge-merkle-tree",
"snowbridge-outbound-queue-primitives",
@ -15170,7 +15175,7 @@ dependencies = [
"parity-scale-codec",
"polkadot-primitives",
"scale-info",
"snowbridge-core 0.24.0",
"snowbridge-core 0.25.0",
"snowbridge-outbound-queue-primitives",
"snowbridge-pallet-outbound-queue",
"sp-core",
@ -15183,7 +15188,7 @@ dependencies = [
[[package]]
name = "snowbridge-pallet-system-v2"
version = "0.24.0"
version = "0.25.0"
dependencies = [
"frame-benchmarking",
"frame-support",
@ -15195,7 +15200,7 @@ dependencies = [
"parity-scale-codec",
"polkadot-primitives",
"scale-info",
"snowbridge-core 0.24.0",
"snowbridge-core 0.25.0",
"snowbridge-outbound-queue-primitives",
"snowbridge-pallet-outbound-queue-v2",
"snowbridge-pallet-system",
@ -15211,10 +15216,10 @@ dependencies = [
[[package]]
name = "snowbridge-system-v2-runtime-api"
version = "0.24.0"
version = "0.25.0"
dependencies = [
"parity-scale-codec",
"snowbridge-core 0.24.0",
"snowbridge-core 0.25.0",
"sp-api",
"sp-std",
"staging-xcm",
@ -15222,7 +15227,7 @@ dependencies = [
[[package]]
name = "snowbridge-test-utils"
version = "0.24.0"
version = "0.25.0"
dependencies = [
"frame-benchmarking",
"frame-support",
@ -15242,12 +15247,12 @@ dependencies = [
[[package]]
name = "snowbridge-verification-primitives"
version = "0.24.0"
version = "0.25.0"
dependencies = [
"frame-support",
"parity-scale-codec",
"scale-info",
"snowbridge-beacon-primitives 0.24.0",
"snowbridge-beacon-primitives 0.25.0",
"sp-core",
"sp-std",
]

View file

@ -5,7 +5,7 @@ edition = "2021"
homepage = "https://datahaven.xyz/"
license = "GPL-3"
repository = "https://github.com/datahavenxyz/datahaven"
version = "0.24.0"
version = "0.25.0"
[workspace]
members = [
@ -272,42 +272,42 @@ fc-storage = { git = "https://github.com/polkadot-evm/frontier", branch = "stabl
# StorageHub
## Runtime
pallet-bucket-nfts = { git = "https://github.com/Moonsong-Labs/storage-hub.git", tag = "v0.4.1", default-features = false }
pallet-cr-randomness = { git = "https://github.com/Moonsong-Labs/storage-hub.git", tag = "v0.4.1", default-features = false }
pallet-file-system = { git = "https://github.com/Moonsong-Labs/storage-hub.git", tag = "v0.4.1", default-features = false }
pallet-file-system-runtime-api = { git = "https://github.com/Moonsong-Labs/storage-hub.git", tag = "v0.4.1", default-features = false }
pallet-payment-streams = { git = "https://github.com/Moonsong-Labs/storage-hub.git", tag = "v0.4.1", default-features = false }
pallet-payment-streams-runtime-api = { git = "https://github.com/Moonsong-Labs/storage-hub.git", tag = "v0.4.1", default-features = false }
pallet-proofs-dealer = { git = "https://github.com/Moonsong-Labs/storage-hub.git", tag = "v0.4.1", default-features = false }
pallet-proofs-dealer-runtime-api = { git = "https://github.com/Moonsong-Labs/storage-hub.git", tag = "v0.4.1", default-features = false }
pallet-randomness = { git = "https://github.com/Moonsong-Labs/storage-hub.git", tag = "v0.4.1", default-features = false }
pallet-storage-providers = { git = "https://github.com/Moonsong-Labs/storage-hub.git", tag = "v0.4.1", default-features = false }
pallet-storage-providers-runtime-api = { git = "https://github.com/Moonsong-Labs/storage-hub.git", tag = "v0.4.1", default-features = false }
shp-constants = { git = "https://github.com/Moonsong-Labs/storage-hub.git", tag = "v0.4.1", default-features = false }
shp-data-price-updater = { git = "https://github.com/Moonsong-Labs/storage-hub.git", tag = "v0.4.1", default-features = false }
shp-file-key-verifier = { git = "https://github.com/Moonsong-Labs/storage-hub.git", tag = "v0.4.1", default-features = false }
shp-file-metadata = { git = "https://github.com/Moonsong-Labs/storage-hub.git", tag = "v0.4.1", default-features = false }
shp-forest-verifier = { git = "https://github.com/Moonsong-Labs/storage-hub.git", tag = "v0.4.1", default-features = false }
shp-traits = { git = "https://github.com/Moonsong-Labs/storage-hub.git", tag = "v0.4.1", default-features = false }
shp-treasury-funding = { git = "https://github.com/Moonsong-Labs/storage-hub.git", tag = "v0.4.1", default-features = false }
pallet-bucket-nfts = { git = "https://github.com/Moonsong-Labs/storage-hub.git", tag = "v0.4.2", default-features = false }
pallet-cr-randomness = { git = "https://github.com/Moonsong-Labs/storage-hub.git", tag = "v0.4.2", default-features = false }
pallet-file-system = { git = "https://github.com/Moonsong-Labs/storage-hub.git", tag = "v0.4.2", default-features = false }
pallet-file-system-runtime-api = { git = "https://github.com/Moonsong-Labs/storage-hub.git", tag = "v0.4.2", default-features = false }
pallet-payment-streams = { git = "https://github.com/Moonsong-Labs/storage-hub.git", tag = "v0.4.2", default-features = false }
pallet-payment-streams-runtime-api = { git = "https://github.com/Moonsong-Labs/storage-hub.git", tag = "v0.4.2", default-features = false }
pallet-proofs-dealer = { git = "https://github.com/Moonsong-Labs/storage-hub.git", tag = "v0.4.2", default-features = false }
pallet-proofs-dealer-runtime-api = { git = "https://github.com/Moonsong-Labs/storage-hub.git", tag = "v0.4.2", default-features = false }
pallet-randomness = { git = "https://github.com/Moonsong-Labs/storage-hub.git", tag = "v0.4.2", default-features = false }
pallet-storage-providers = { git = "https://github.com/Moonsong-Labs/storage-hub.git", tag = "v0.4.2", default-features = false }
pallet-storage-providers-runtime-api = { git = "https://github.com/Moonsong-Labs/storage-hub.git", tag = "v0.4.2", default-features = false }
shp-constants = { git = "https://github.com/Moonsong-Labs/storage-hub.git", tag = "v0.4.2", default-features = false }
shp-data-price-updater = { git = "https://github.com/Moonsong-Labs/storage-hub.git", tag = "v0.4.2", default-features = false }
shp-file-key-verifier = { git = "https://github.com/Moonsong-Labs/storage-hub.git", tag = "v0.4.2", default-features = false }
shp-file-metadata = { git = "https://github.com/Moonsong-Labs/storage-hub.git", tag = "v0.4.2", default-features = false }
shp-forest-verifier = { git = "https://github.com/Moonsong-Labs/storage-hub.git", tag = "v0.4.2", default-features = false }
shp-traits = { git = "https://github.com/Moonsong-Labs/storage-hub.git", tag = "v0.4.2", default-features = false }
shp-treasury-funding = { git = "https://github.com/Moonsong-Labs/storage-hub.git", tag = "v0.4.2", default-features = false }
## Client
shc-actors-derive = { git = "https://github.com/Moonsong-Labs/storage-hub.git", tag = "v0.4.1", default-features = false }
shc-actors-framework = { git = "https://github.com/Moonsong-Labs/storage-hub.git", tag = "v0.4.1", default-features = false }
shc-blockchain-service = { git = "https://github.com/Moonsong-Labs/storage-hub.git", tag = "v0.4.1", default-features = false }
shc-client = { git = "https://github.com/Moonsong-Labs/storage-hub.git", tag = "v0.4.1", default-features = false }
shc-common = { git = "https://github.com/Moonsong-Labs/storage-hub.git", tag = "v0.4.1", default-features = false }
shc-file-manager = { git = "https://github.com/Moonsong-Labs/storage-hub.git", tag = "v0.4.1", default-features = false }
shc-file-transfer-service = { git = "https://github.com/Moonsong-Labs/storage-hub.git", tag = "v0.4.1", default-features = false }
shc-fisherman-service = { git = "https://github.com/Moonsong-Labs/storage-hub.git", tag = "v0.4.1", default-features = false }
shc-forest-manager = { git = "https://github.com/Moonsong-Labs/storage-hub.git", tag = "v0.4.1", default-features = false }
shc-indexer-db = { git = "https://github.com/Moonsong-Labs/storage-hub.git", tag = "v0.4.1", default-features = false }
shc-indexer-service = { git = "https://github.com/Moonsong-Labs/storage-hub.git", tag = "v0.4.1", default-features = false }
shc-rpc = { git = "https://github.com/Moonsong-Labs/storage-hub.git", tag = "v0.4.1", default-features = false }
shp-opaque = { git = "https://github.com/Moonsong-Labs/storage-hub.git", tag = "v0.4.1", default-features = false }
shp-tx-implicits-runtime-api = { git = "https://github.com/Moonsong-Labs/storage-hub.git", tag = "v0.4.1", default-features = false }
shp-types = { git = "https://github.com/Moonsong-Labs/storage-hub.git", tag = "v0.4.1", default-features = false }
shc-actors-derive = { git = "https://github.com/Moonsong-Labs/storage-hub.git", tag = "v0.4.2", default-features = false }
shc-actors-framework = { git = "https://github.com/Moonsong-Labs/storage-hub.git", tag = "v0.4.2", default-features = false }
shc-blockchain-service = { git = "https://github.com/Moonsong-Labs/storage-hub.git", tag = "v0.4.2", default-features = false }
shc-client = { git = "https://github.com/Moonsong-Labs/storage-hub.git", tag = "v0.4.2", default-features = false }
shc-common = { git = "https://github.com/Moonsong-Labs/storage-hub.git", tag = "v0.4.2", default-features = false }
shc-file-manager = { git = "https://github.com/Moonsong-Labs/storage-hub.git", tag = "v0.4.2", default-features = false }
shc-file-transfer-service = { git = "https://github.com/Moonsong-Labs/storage-hub.git", tag = "v0.4.2", default-features = false }
shc-fisherman-service = { git = "https://github.com/Moonsong-Labs/storage-hub.git", tag = "v0.4.2", default-features = false }
shc-forest-manager = { git = "https://github.com/Moonsong-Labs/storage-hub.git", tag = "v0.4.2", default-features = false }
shc-indexer-db = { git = "https://github.com/Moonsong-Labs/storage-hub.git", tag = "v0.4.2", default-features = false }
shc-indexer-service = { git = "https://github.com/Moonsong-Labs/storage-hub.git", tag = "v0.4.2", default-features = false }
shc-rpc = { git = "https://github.com/Moonsong-Labs/storage-hub.git", tag = "v0.4.2", default-features = false }
shp-opaque = { git = "https://github.com/Moonsong-Labs/storage-hub.git", tag = "v0.4.2", default-features = false }
shp-tx-implicits-runtime-api = { git = "https://github.com/Moonsong-Labs/storage-hub.git", tag = "v0.4.2", default-features = false }
shp-types = { git = "https://github.com/Moonsong-Labs/storage-hub.git", tag = "v0.4.2", default-features = false }
## Precompiles
pallet-evm-precompile-file-system = { git = "https://github.com/Moonsong-Labs/storage-hub.git", tag = "v0.4.1", default-features = false }
pallet-evm-precompile-file-system = { git = "https://github.com/Moonsong-Labs/storage-hub.git", tag = "v0.4.2", default-features = false }
# Static linking

View file

@ -28,6 +28,7 @@ use shc_indexer_db::models::{FileFiltering, FileOrdering};
use shc_indexer_service::IndexerMode;
use shc_rpc::RpcConfig;
use shp_types::StorageDataUnit;
use sp_core::H256;
// Available Sealing methods.
#[derive(Copy, Clone, Debug, Default, ValueEnum)]
@ -73,6 +74,7 @@ pub struct Cli {
"pending_db_url",
"fisherman", "fisherman_database_url",
"trusted_file_transfer_server", "trusted_file_transfer_server_host", "trusted_file_transfer_server_port",
"trusted_file_transfer_batch_size_bytes", "trusted_msps",
])]
pub provider_config_file: Option<String>,
@ -497,6 +499,26 @@ pub struct ProviderConfigurations {
default_value = "7070"
)]
pub trusted_file_transfer_server_port: Option<u16>,
/// Batch size in bytes used by MSP trusted upload ingestion (default: 2MB).
#[arg(
long,
value_name = "BYTES",
help_heading = "Trusted File Transfer Server Options",
default_value = "2097152",
value_parser = clap::value_parser!(u64).range(1..)
)]
pub trusted_file_transfer_batch_size_bytes: Option<u64>,
/// Comma-separated list of trusted MSP IDs that this BSP accepts download requests from.
/// Only applicable when running as a BSP provider.
#[arg(
long = "trusted-msps",
value_delimiter = ',',
value_name = "MSP_ID",
help_heading = "BSP Download Authorisation"
)]
pub trusted_msps: Vec<H256>,
}
impl ProviderConfigurations {
@ -669,6 +691,8 @@ impl ProviderConfigurations {
trusted_file_transfer_server: self.trusted_file_transfer_server,
trusted_file_transfer_server_host: self.trusted_file_transfer_server_host.clone(),
trusted_file_transfer_server_port: self.trusted_file_transfer_server_port,
trusted_file_transfer_batch_size_bytes: self.trusted_file_transfer_batch_size_bytes,
trusted_msps: self.trusted_msps.clone(),
max_open_forests: self.max_open_forests,
// We don't support maintenance mode for now.
// maintenance_mode: self.maintenance_mode,

View file

@ -37,6 +37,7 @@ use shc_client::builder::{
};
use shc_rpc::RpcConfig;
use shp_types::StorageDataUnit;
use sp_core::H256;
/// Configuration for the provider.
#[derive(Debug, Clone, Deserialize)]
@ -92,6 +93,12 @@ pub struct ProviderOptions {
/// Port for trusted file transfer HTTP server.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub trusted_file_transfer_server_port: Option<u16>,
/// Batch size in bytes for trusted file transfer uploads.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub trusted_file_transfer_batch_size_bytes: Option<u64>,
/// List of trusted MSP IDs that BSP nodes accept download requests from.
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub trusted_msps: Vec<H256>,
// Whether the node is running in maintenance mode. We are not supporting maintenance mode.
// pub maintenance_mode: bool,
}

View file

@ -1192,19 +1192,10 @@ where
let task_spawner = TaskSpawner::new(task_manager.spawn_handle(), task_spawner_name);
let mut builder = StorageHubBuilder::<R, S, Runtime>::new(task_spawner, prometheus_registry);
// Setup file transfer service (common to all roles)
let (file_transfer_request_protocol_name, file_transfer_request_receiver) =
file_transfer_request_protocol
.expect("FileTransfer request protocol should already be initialised.");
builder
.with_file_transfer(
file_transfer_request_receiver,
file_transfer_request_protocol_name,
network.clone(),
)
.await;
// Role-specific configuration
let rpc_config = match role_options {
RoleOptions::Provider(ProviderOptions {
@ -1226,8 +1217,20 @@ where
trusted_file_transfer_server,
trusted_file_transfer_server_host,
trusted_file_transfer_server_port,
trusted_file_transfer_batch_size_bytes,
trusted_msps,
..
}) => {
// Setup file transfer service with trusted MSPs config
builder
.with_file_transfer(
client.clone(),
trusted_msps.clone(),
file_transfer_request_receiver,
file_transfer_request_protocol_name,
network.clone(),
)
.await;
info!(
"Starting as a Storage Provider. Storage path: {:?}, Max storage capacity: {:?}, Jump capacity: {:?}, MSP charging period: {:?}",
storage_path, max_storage_capacity, jump_capacity, msp_charging_period,
@ -1262,11 +1265,17 @@ where
}
if *trusted_file_transfer_server {
let batch_target_bytes = trusted_file_transfer_batch_size_bytes
.and_then(|size| usize::try_from(size).ok())
.unwrap_or(
shc_client::trusted_file_transfer::server::DEFAULT_BATCH_TARGET_BYTES,
);
let file_transfer_config = shc_client::trusted_file_transfer::server::Config {
host: trusted_file_transfer_server_host
.clone()
.unwrap_or_else(|| "127.0.0.1".to_string()),
port: trusted_file_transfer_server_port.unwrap_or(7070),
batch_target_bytes,
};
builder.with_trusted_file_transfer_server(file_transfer_config);
}
@ -1281,6 +1290,17 @@ where
rpc_config.clone()
}
RoleOptions::Fisherman(fisherman_options) => {
// Setup file transfer service (no trusted MSPs for fisherman)
builder
.with_file_transfer(
client.clone(),
vec![],
file_transfer_request_receiver,
file_transfer_request_protocol_name,
network.clone(),
)
.await;
// Validate configuration compatibility with indexer
if let Some(indexer_cfg) = indexer_options {
if indexer_cfg.indexer_mode == shc_indexer_service::IndexerMode::Lite {

View file

@ -309,6 +309,12 @@ pub mod pallet {
NoKeysRegistered,
/// Unable to derive validator id from account id
UnableToDeriveValidatorId,
/// The target era is too old (targetEra <= ActiveEra). Message arrived late.
TargetEraTooOld,
/// The target era is too far ahead (targetEra > ActiveEra + 1).
TargetEraTooNew,
/// The target era has already been seen (targetEra <= ExternalIndex). Duplicate or stale.
DuplicateOrStaleTargetEra,
}
#[pallet::call]
@ -419,6 +425,9 @@ pub mod pallet {
validators: Vec<T::ValidatorId>,
external_index: u64,
) -> DispatchResult {
// Validate the target era before accepting the validator set
Self::validate_target_era(external_index)?;
// If more validators than max, take the first n
let validators = BoundedVec::truncate_from(validators);
<ExternalValidators<T>>::put(&validators);
@ -431,6 +440,27 @@ pub mod pallet {
Ok(())
}
fn validate_target_era(target_era: u64) -> DispatchResult {
let active_era_index = Self::active_era()
.map(|info| info.index as u64)
.unwrap_or(0);
let current_external_index = ExternalIndex::<T>::get();
// Must target exactly the next era
if target_era <= active_era_index {
return Err(Error::<T>::TargetEraTooOld.into());
}
if target_era > active_era_index + 1 {
return Err(Error::<T>::TargetEraTooNew.into());
}
// Dedupe/stale guard
if target_era <= current_external_index {
return Err(Error::<T>::DuplicateOrStaleTargetEra.into());
}
Ok(())
}
/// Helper to set a new `ForceEra` mode.
pub(crate) fn set_force_era(mode: Forcing) {
log::info!("Setting force era mode {:?}.", mode);

View file

@ -345,10 +345,96 @@ fn era_hooks() {
});
}
#[test]
fn target_era_validation_accepts_next_era() {
new_test_ext().execute_with(|| {
// Advance to era 1 (session 6 starts era 1)
run_to_session(6);
// ActiveEra is now 1, so target era 2 (ActiveEra + 1) should succeed
assert_ok!(ExternalValidators::set_external_validators_inner(
vec![50, 51],
2
));
});
}
#[test]
fn target_era_validation_rejects_old_era() {
new_test_ext().execute_with(|| {
// Advance to era 1
run_to_session(6);
// target_era = 0 (ActiveEra - 1) should fail
assert_noop!(
ExternalValidators::set_external_validators_inner(vec![50, 51], 0),
Error::<Test>::TargetEraTooOld
);
// target_era = 1 (== ActiveEra) should also fail
assert_noop!(
ExternalValidators::set_external_validators_inner(vec![50, 51], 1),
Error::<Test>::TargetEraTooOld
);
});
}
#[test]
fn target_era_validation_rejects_too_new_era() {
new_test_ext().execute_with(|| {
// Advance to era 1
run_to_session(6);
// target_era = 3 (ActiveEra + 2) should fail
assert_noop!(
ExternalValidators::set_external_validators_inner(vec![50, 51], 3),
Error::<Test>::TargetEraTooNew
);
});
}
#[test]
fn target_era_validation_rejects_duplicate() {
new_test_ext().execute_with(|| {
// Advance to era 1
run_to_session(6);
// First submission with target_era = 2 should succeed
assert_ok!(ExternalValidators::set_external_validators_inner(
vec![50, 51],
2
));
// Second submission with same target_era = 2 should fail (duplicate)
assert_noop!(
ExternalValidators::set_external_validators_inner(vec![50, 51], 2),
Error::<Test>::DuplicateOrStaleTargetEra
);
});
}
#[test]
fn target_era_validation_at_genesis() {
new_test_ext().execute_with(|| {
// At genesis, ActiveEra = 0, so target_era = 1 (ActiveEra + 1) should succeed
assert_ok!(ExternalValidators::set_external_validators_inner(
vec![50, 51],
1
));
// target_era = 0 should fail (too old, <= ActiveEra)
assert_noop!(
ExternalValidators::set_external_validators_inner(vec![50, 51], 0),
Error::<Test>::TargetEraTooOld
);
});
}
#[test]
fn era_hooks_with_external_index() {
new_test_ext().execute_with(|| {
let first_external_index = 1000;
// ActiveEra starts at 0, so target era 1 (ActiveEra + 1) is valid
let first_external_index = 1;
assert_ok!(ExternalValidators::set_external_validators_inner(
vec![50, 51],
first_external_index
@ -356,7 +442,8 @@ fn era_hooks_with_external_index() {
run_to_session(8);
let second_external_index = 2000;
// ActiveEra is now 1, so target era 2 (ActiveEra + 1) is valid
let second_external_index = 2;
assert_ok!(ExternalValidators::set_external_validators_inner(
vec![50, 51],
@ -388,3 +475,81 @@ fn era_hooks_with_external_index() {
assert_eq!(Mock::mock().called_hooks, expected_calls);
});
}
#[test]
fn set_external_validators_extrinsic_rejects_bad_origin() {
new_test_ext().execute_with(|| {
// signed by an arbitrary non-root account → BadOrigin
assert_noop!(
ExternalValidators::set_external_validators(RuntimeOrigin::signed(1), vec![50, 51], 1),
BadOrigin
);
// unsigned → BadOrigin
assert_noop!(
ExternalValidators::set_external_validators(RuntimeOrigin::none(), vec![50, 51], 1),
BadOrigin
);
// root origin (requires signed(777) specifically, not sudo root) → BadOrigin
assert_noop!(
ExternalValidators::set_external_validators(RuntimeOrigin::root(), vec![50, 51], 1),
BadOrigin
);
// success with the correct signed origin
assert_ok!(ExternalValidators::set_external_validators(
RuntimeOrigin::signed(RootAccount::get()),
vec![50, 51],
1
));
});
}
#[test]
fn target_era_validation_rejects_u64_max() {
new_test_ext().execute_with(|| {
// At genesis, active_era = 0; u64::MAX is far above active_era + 1
assert_noop!(
ExternalValidators::set_external_validators_inner(vec![50, 51], u64::MAX),
Error::<Test>::TargetEraTooNew
);
});
}
#[test]
fn era_boundary_race_submit_advance_resubmit() {
new_test_ext().execute_with(|| {
// At genesis (active_era = 0), submit for era 1
assert_ok!(ExternalValidators::set_external_validators_inner(
vec![50, 51],
1
));
// Advance to era 1 (session 6 starts era 1)
run_to_session(6);
// Re-submit for era 1 now that active_era = 1 → TargetEraTooOld
assert_noop!(
ExternalValidators::set_external_validators_inner(vec![50, 51], 1),
Error::<Test>::TargetEraTooOld
);
});
}
#[test]
fn era_boundary_race_resubmit_without_advance() {
new_test_ext().execute_with(|| {
// At genesis (active_era = 0), submit for era 1
assert_ok!(ExternalValidators::set_external_validators_inner(
vec![50, 51],
1
));
// Immediately re-submit for era 1 without advancing → DuplicateOrStaleTargetEra
assert_noop!(
ExternalValidators::set_external_validators_inner(vec![50, 51], 1),
Error::<Test>::DuplicateOrStaleTargetEra
);
});
}

View file

@ -1838,7 +1838,7 @@ mod tests {
message_id: EL_MESSAGE_ID,
message: BridgeMessage::V1(InboundCommand::ReceiveValidators {
validators: Vec::new(),
external_index: 0,
external_index: 1,
}),
};

View file

@ -1819,7 +1819,7 @@ mod tests {
message_id: EL_MESSAGE_ID,
message: BridgeMessage::V1(InboundCommand::ReceiveValidators {
validators: Vec::new(),
external_index: 0,
external_index: 1,
}),
};

View file

@ -1841,7 +1841,7 @@ mod tests {
message_id: EL_MESSAGE_ID,
message: BridgeMessage::V1(InboundCommand::ReceiveValidators {
validators: Vec::new(),
external_index: 0,
external_index: 1,
}),
};

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

@ -0,0 +1,268 @@
# Validator Set Submission
**Status:** Accepted
**Owner:** DataHaven Protocol / AVS Integration
**Last Updated:** 2026-02-11
**Scope:** Ethereum -> Snowbridge -> DataHaven validator set synchronization
## Background
This specification defines an automation-first validator-set synchronization flow.
In this document:
- the validator-set submitter runs once per era window, and
- each message is valid only for the immediate next era.
The primary objective is to run an off-chain validator-set-submitter that automatically calls validator-set submission without manual intervention.
The design is:
1. Validator-set messages are permissioned on Ethereum by a dedicated submitter role.
2. The payload field `external_index` is used as `targetEra` (the era the message is intended for).
3. DataHaven accepts a message only if it targets the next era at receive time.
4. Delayed messages for past eras are rejected and never applied to later eras.
This enforces the invariant: **at most one canonical validator-set apply per target era, and no late-era spillover**.
### Current mechanism (as-is)
- Manual and one-shot submission flow is done via `test/scripts/update-validator-set.ts`.
- `sendNewValidatorSet(uint128 executionFee, uint128 relayerFee)` in `contracts/src/DataHavenServiceManager.sol` is owner-only.
- Message building currently does not carry explicit era intent.
- DataHaven inbound processing applies decoded `external_index` without era-target validation.
- Operational flow relies on fixed fee constants and has no automated submission pipeline.
## Problems addressed by this spec
- Manual operation for validator-set submission.
- Late relay can cause old messages to arrive after their intended era.
- Ambiguity between "message order" and "era intent".
- Owner-key usage for routine automated submissions.
## Goals
1. Run an off-chain component that automatically submits validator-set updates in the required era window.
2. Ensure each message is explicitly bound to a specific target era.
3. Accept a message only when it targets the immediate next era.
4. Reject delayed (past-era), duplicate, and too-far-ahead messages deterministically.
5. Accept that a failed submission for a given era is permanently missed (single submission window per era).
6. Avoid skipping era advancement even when validator addresses are unchanged.
### Non-goals
- Redesigning Snowbridge protocol internals.
- Replacing the existing owner/governance model outside submitter assignment.
- Building a multi-node HA control plane (single submitter process is acceptable initially).
## Terminology
- `ActiveEra`: era currently active on DataHaven.
- `NextEra`: `ActiveEra + 1`.
- `targetEra`: era this validator-set message is intended for.
- `external_index`: payload field; in this design, its value is `targetEra`.
- `ExternalIndex`: latest bridge-received `targetEra` accepted on DataHaven.
- `PendingExternalIndex`: staged external index applied when the next era starts.
- `CurrentExternalIndex`: external index currently applied to the active era.
- `Canonical apply`: the accepted validator-set apply for a specific `targetEra`.
## Proposed design
### High-level overview
The solution centers on a long-running off-chain validator-set-submitter under `test/tools/` that automatically submits validator-set updates.
Contract and runtime changes make the submitter service safe and deterministic:
- only the submitter role can send validator-set messages,
- payloads include explicit era intent (`targetEra`), and
- DataHaven accepts only messages targeting `NextEra`.
The submitter subscribes to finalized session changes via PAPI's `watchValue("finalized")` on `Session.CurrentIndex`. On each session change it evaluates whether submission is needed, and acts during the last session of the active era. Each era gets a single submission attempt — if it fails, the era is missed and the submitter moves on.
```
┌───────────────────────────────┐ submit (for era) ┌───────────────────────────────┐
│ Validator-Set-Submitter │ ──────────────────────────► │ ServiceManager (Ethereum) │
│ - watches session changes │ │ - submitter-gated API │
│ - computes targetEra │ │ - builds payload with target │
│ - single attempt per era │ └───────────────┬───────────────┘
└───────────────────────────────┘ │
│ Snowbridge message
┌────────────────────────────────────────────────────────────────────────────────────────────────┐
│ DataHaven inbound (`operator/primitives/bridge`) + external validators pallet │
│ - authorized origin check │
│ - era gate: targetEra == ActiveEra + 1 │
│ - duplicate/stale gate: targetEra > ExternalIndex │
│ - delayed messages for past eras are rejected │
└────────────────────────────────────────────────────────────────────────────────────────────────┘
```
### A) Ethereum contract changes
**Target contract**
- `contracts/src/DataHavenServiceManager.sol`
**Permissioned submitter role**
- Add state:
- `address public validatorSetSubmitter`
- Add admin API:
- `setValidatorSetSubmitter(address newSubmitter) external onlyOwner`
- `newSubmitter` MUST be non-zero
- emit `ValidatorSetSubmitterUpdated(oldSubmitter, newSubmitter)`
- Add modifier:
- `onlyValidatorSetSubmitter` (revert unless `msg.sender == validatorSetSubmitter`)
**Era-targeted submission**
- Add submission API:
- `sendNewValidatorSetForEra(uint64 targetEra, uint128 executionFee, uint128 relayerFee) external payable onlyValidatorSetSubmitter`
- builds validator payload with `targetEra`
- calls gateway `v2_sendMessage`
- emits `ValidatorSetMessageSubmitted`
- Add builder API:
- `buildNewValidatorSetMessageForEra(uint64 targetEra) public view returns (bytes memory)`
- encodes `targetEra` as `external_index`
**Legacy submission path**
- Legacy `sendNewValidatorSet(uint128,uint128)` must be removed from the production contract.
**Contract-side trust scope (this release)**
- No additional `lastSubmittedTargetEra` contract guard is required in this release.
- Rationale: submission is permissioned and runtime is the source of truth for era correctness (`targetEra == ActiveEra + 1`).
**Events**
- `event ValidatorSetSubmitterUpdated(address indexed oldSubmitter, address indexed newSubmitter);`
- `event ValidatorSetMessageSubmitted(uint64 indexed targetEra, bytes32 payloadHash, address indexed submitter);`
### B) Runtime changes (DataHaven)
**Target processor**
- `operator/primitives/bridge/src/lib.rs` in `EigenLayerMessageProcessor::process_message`
**Era-target validation rule**
Before `set_external_validators_inner`, validate `targetEra`:
1. Must satisfy `targetEra == ActiveEra + 1`
2. Must satisfy `targetEra > ExternalIndex` (dedupe/stale guard)
Reject cases:
- `targetEra <= ActiveEra`: delayed/past-era message.
- `targetEra > ActiveEra + 1`: too-far-ahead message.
- `targetEra <= ExternalIndex`: stale/duplicate message.
This ensures a delayed message cannot be applied to a later era.
**Error semantics**
Return deterministic dispatch errors, for example:
- `TargetEraTooOld`
- `TargetEraTooNew`
- `DuplicateOrStaleTargetEra`
**Authorization**
- Keep existing authorized-origin checks unchanged.
### C) Validator-set-submitter service (`test/tools/`)
**Location and runtime model**
- New component at `test/tools/validator-set-submitter/`
- Long-running daemon
- TypeScript + Bun
**Authoritative inputs**
- DataHaven:
- `ActiveEra`
- `ExternalIndex`
- `CurrentExternalIndex`
- `SessionsPerEra` and era-window session boundaries
- Ethereum:
- current validator set view from ServiceManager message-builder inputs
**Target era computation**
- `targetEra = ActiveEra + 1`
**Submission model**
- Submitter subscribes to finalized `Session.CurrentIndex` via PAPI `watchValue("finalized")`.
- On each session change, evaluates preconditions: `ActiveEra` set, `targetEra` not already processed, `ExternalIndex < targetEra`, and current session is the last session of the era.
- One submission attempt per era window. If the attempt fails (revert, missing event, or error), the era is marked as processed and permanently missed.
- Rationale: `validate_target_era` on the Substrate side rejects `targetEra <= activeEraIndex`, so once `ActiveEra` advances past the target, retries are impossible.
- Overlapping session emissions are dropped via RxJS `exhaustMap`.
**Delay/gap behavior (required)**
- If message for era `N` is delayed and arrives after `ActiveEra >= N`, it is rejected.
- If message for era `N` never relays, the system can still proceed by submitting for era `N+1` when `ActiveEra = N`.
- Out-of-order future messages are rejected until they become the next era target.
**Success criteria**
- Transaction receipt status is `success`.
- `OutboundMessageAccepted` event emitted in receipt logs.
**State model**
- Submitter is recoverable from chain state (reads `ActiveEra`, `ExternalIndex`, and session boundaries on each tick).
- In-memory state is limited to `submittedEra` (the last processed target era), held in a closure.
## API / interface changes
### Ethereum interface
- Add era-targeted submit function.
- Add submitter admin function + getter.
- Add era-targeted builder function.
### DataHaven runtime behavior
- Add next-era-only acceptance in inbound bridge path.
- Add explicit delayed/too-early/duplicate rejection paths.
### Tooling
- New daemon CLI entrypoint:
- `bun test/tools/validator-set-submitter/main.ts run`
- optional `--dry-run`
## Security considerations
- Submitter key compromise risk is reduced by dedicated role separation (vs broad owner use).
- Era-target checks prevent delayed-message replay into later eras.
- Authorized-origin restriction remains required and unchanged.
- Single-attempt model eliminates fee burn loops; a failed era is missed rather than retried.
## Observability and operations
Required metrics/log dimensions:
- `targetEra`
- current `ActiveEra` and `ExternalIndex`
- current session index
- outbound tx hash
- fee pair used
- submission outcome (success / revert / missing event / error)
Alert conditions:
- missed submission window (failed attempt logged as "era will be missed")
- repeated era misses across consecutive eras
- subscription errors on `Session.CurrentIndex`
## Testing
### Solidity tests
- submitter-only enforcement
- submitter rotation by owner
- payload encodes caller `targetEra`
- event fields emitted correctly
- zero-address submitter rejected
- legacy `sendNewValidatorSet` path is removed (no callable legacy submit path)
### Runtime tests
- accepts only `targetEra == ActiveEra + 1`
- rejects `targetEra <= ActiveEra` (late)
- rejects `targetEra > ActiveEra + 1` (too early)
- rejects `targetEra <= ExternalIndex` (duplicate/stale)
- origin authorization behavior unchanged
### Integration tests
- one canonical apply per target era
- delayed message for old era is rejected after era advances
- missing relay for era `N` does not block acceptance for era `N+1` when it becomes next
- boundary race: arrival at era transition behaves correctly (`N` stale, `N+1` accepted)
## Rollout
1. Implement and test contract + runtime changes.
2. Deploy to stagenet.
3. Run submitter service in dry-run mode and validate era-target decisions.
4. Enable active mode.
5. Monitor across multiple era cycles.
6. Promote to mainnet after stability criteria are met.
## Dependencies
- Existing manual script `test/scripts/update-validator-set.ts` may remain for emergency/manual use, but must be marked non-canonical.
- Legacy unscoped submit path `sendNewValidatorSet` must be removed in production.
## Possible improvements (future)
- Keep this release simple: `external_index` carries `targetEra`, and runtime enforces next-era-only acceptance.
- Add a generalized failure-handling strategy for the submitter, including retry behavior for transient issues while preserving safety and idempotency.
- Add generalized resiliency for event watching and connectivity, including recovery after disconnects and missed updates.
- Add production monitoring and operations dashboards (for example Prometheus/Grafana) covering service health, submission outcomes, retries, missed eras, and end-to-end latency.
- Add alerting/SLO definitions for validator-set submission reliability and response runbooks for incidents.
- Alternative direction: remove era dependency from payload and use an Ethereum-stamped freshness model:
- `ServiceManager` assigns message metadata on-chain (e.g., `issuedAt` timestamp and monotonic message nonce/ID).
- DataHaven accepts only fresh messages within a configured max relay delay and rejects expired ones.
- This reduces trust in submitter-provided era values while preserving deterministic stale/duplicate rejection.
## Acceptance criteria
This spec is accepted when:
- an off-chain validator-set-submitter runs unattended and automatically submits validator-set updates
- dedicated submitter role exists and is enforced
- era-targeted submission API is live
- runtime applies messages only when they target the next era
- delayed messages for past eras are rejected and not applied to later eras
- end-to-end tests pass for delayed/missing/out-of-order scenarios

View file

@ -1,5 +1,5 @@
{
"version": "0.1.0-autogenerated.17981369281038341211",
"version": "0.1.0-autogenerated.13357056092938763018",
"name": "@polkadot-api/descriptors",
"files": [
"dist"

Binary file not shown.

View file

@ -13,7 +13,7 @@ import { setParametersFromCollection } from "./parameters";
import { launchRelayers } from "./relayer";
import { launchStorageHubComponents } from "./storagehub";
import { performSummaryOperations } from "./summary";
import { performValidatorOperations } from "./validator";
import { performValidatorOperations, performValidatorSetUpdate } from "./validator";
export const NETWORK_ID = "cli-launch";
@ -43,6 +43,7 @@ export interface LaunchOptions {
deployContracts?: boolean;
fundValidators?: boolean;
setupValidators?: boolean;
updateValidatorSet?: boolean;
setParameters?: boolean;
relayer?: boolean;
relayerImageTag: string;
@ -84,8 +85,9 @@ const launchFunction = async (options: LaunchOptions, launchedNetwork: LaunchedN
}
// skip deploying contracts if we have injected it
let contractsDeployed = false;
if (options.deployContracts && !options.injectContracts) {
const contractsDeployed = await deployContracts({
contractsDeployed = await deployContracts({
rpcUrl: launchedNetwork.elRpcUrl,
verified: options.verified,
blockscoutBackendUrl,
@ -107,6 +109,8 @@ const launchFunction = async (options: LaunchOptions, launchedNetwork: LaunchedN
await launchRelayers(options, launchedNetwork);
await performValidatorSetUpdate(options, launchedNetwork.elRpcUrl, contractsDeployed);
await launchStorageHubComponents(options, launchedNetwork);
await performSummaryOperations(options, launchedNetwork);

View file

@ -75,11 +75,31 @@ export const performValidatorOperations = async (
* @returns Promise resolving when the operation is complete
*/
export const performValidatorSetUpdate = async (
options: LaunchOptions,
networkRpcUrl: string,
contractsDeployed: boolean
) => {
printHeader("Updating DataHaven Validator Set");
let shouldUpdateValidatorSet = options.updateValidatorSet;
if (shouldUpdateValidatorSet === undefined) {
shouldUpdateValidatorSet = await confirmWithTimeout(
"Do you want to update the validator set?",
true,
10
);
} else {
logger.info(
`🏳️ Using flag option: ${shouldUpdateValidatorSet ? "will update" : "will not update"} validator set`
);
}
if (!shouldUpdateValidatorSet) {
logger.info("👍 Skipping validator set update");
printDivider();
return;
}
if (!contractsDeployed) {
logger.warn(
"⚠️ Updating validator set but contracts were not deployed in this CLI run. Could have unexpected results."

View file

@ -2054,6 +2054,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: [],
@ -2065,9 +2072,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',
@ -2083,8 +2098,8 @@ export const dataHavenServiceManagerAbi = [
},
{
type: 'function',
inputs: [],
name: 'buildNewValidatorSetMessage',
inputs: [{ name: 'targetEra', internalType: 'uint64', type: 'uint64' }],
name: 'buildNewValidatorSetMessageForEra',
outputs: [{ name: '', internalType: 'bytes', type: 'bytes' }],
stateMutability: 'view',
},
@ -2109,21 +2124,55 @@ 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',
internalType: 'address',
type: 'address',
},
{
name: '_validatorSetSubmitter',
internalType: 'address',
type: 'address',
},
{ name: 'initialVersion', internalType: 'string', type: 'string' },
{ name: '_versionUpdater', internalType: 'address', type: 'address' },
],
@ -2187,10 +2236,11 @@ export const dataHavenServiceManagerAbi = [
{
type: 'function',
inputs: [
{ name: 'targetEra', internalType: 'uint64', type: 'uint64' },
{ name: 'executionFee', internalType: 'uint128', type: 'uint128' },
{ name: 'relayerFee', internalType: 'uint128', type: 'uint128' },
],
name: 'sendNewValidatorSet',
name: 'sendNewValidatorSetForEra',
outputs: [],
stateMutability: 'payable',
},
@ -2216,6 +2266,36 @@ 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: [
{ name: 'newSubmitter', internalType: 'address', type: 'address' },
],
name: 'setValidatorSetSubmitter',
outputs: [],
stateMutability: 'nonpayable',
},
{
type: 'function',
inputs: [
@ -2255,6 +2335,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: [
@ -2342,6 +2429,20 @@ export const dataHavenServiceManagerAbi = [
outputs: [{ name: '', internalType: 'address', type: 'address' }],
stateMutability: 'view',
},
{
type: 'function',
inputs: [],
name: 'validatorSetSubmitter',
outputs: [{ name: '', internalType: 'address', type: 'address' }],
stateMutability: 'view',
},
{
type: 'function',
inputs: [{ name: '', internalType: 'address', type: 'address' }],
name: 'validatorSolochainAddressToEthAddress',
outputs: [{ name: '', internalType: 'address', type: 'address' }],
stateMutability: 'view',
},
{
type: 'function',
inputs: [{ name: '', internalType: 'address', type: 'address' }],
@ -2501,6 +2602,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,
@ -2527,6 +2649,50 @@ export const dataHavenServiceManagerAbi = [
],
name: 'ValidatorRemovedFromAllowlist',
},
{
type: 'event',
anonymous: false,
inputs: [
{
name: 'targetEra',
internalType: 'uint64',
type: 'uint64',
indexed: true,
},
{
name: 'payloadHash',
internalType: 'bytes32',
type: 'bytes32',
indexed: false,
},
{
name: 'submitter',
internalType: 'address',
type: 'address',
indexed: true,
},
],
name: 'ValidatorSetMessageSubmitted',
},
{
type: 'event',
anonymous: false,
inputs: [
{
name: 'oldSubmitter',
internalType: 'address',
type: 'address',
indexed: true,
},
{
name: 'newSubmitter',
internalType: 'address',
type: 'address',
indexed: true,
},
],
name: 'ValidatorSetSubmitterUpdated',
},
{
type: 'event',
anonymous: false,
@ -2568,12 +2734,17 @@ export const dataHavenServiceManagerAbi = [
{ type: 'error', inputs: [], name: 'CallerIsNotValidator' },
{ type: 'error', inputs: [], name: 'CantDeregisterFromMultipleOperatorSets' },
{ type: 'error', inputs: [], name: 'CantRegisterToMultipleOperatorSets' },
{ type: 'error', inputs: [], name: 'EmptyValidatorSet' },
{ type: 'error', inputs: [], name: 'IncorrectAVSAddress' },
{ type: 'error', inputs: [], name: 'InvalidOperatorSetId' },
{ type: 'error', inputs: [], name: 'InvalidSolochainAddressLength' },
{ type: 'error', inputs: [], name: 'OnlyAllocationManager' },
{ type: 'error', inputs: [], name: 'OnlyRewardsInitiator' },
{ 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
@ -10721,6 +10892,15 @@ export const readDataHavenServiceManagerDatahavenVersion =
functionName: 'DATAHAVEN_VERSION',
})
/**
* 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"`
*/
@ -10731,12 +10911,21 @@ export const readDataHavenServiceManagerValidatorsSetId =
})
/**
* Wraps __{@link readContract}__ with `abi` set to __{@link dataHavenServiceManagerAbi}__ and `functionName` set to `"buildNewValidatorSetMessage"`
* Wraps __{@link readContract}__ with `abi` set to __{@link dataHavenServiceManagerAbi}__ and `functionName` set to `"buildNewValidatorSetMessageForEra"`
*/
export const readDataHavenServiceManagerBuildNewValidatorSetMessage =
export const readDataHavenServiceManagerBuildNewValidatorSetMessageForEra =
/*#__PURE__*/ createReadContract({
abi: dataHavenServiceManagerAbi,
functionName: 'buildNewValidatorSetMessage',
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',
})
/**
@ -10766,6 +10955,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"`
*/
@ -10784,6 +10982,24 @@ export const readDataHavenServiceManagerValidatorEthAddressToSolochainAddress =
functionName: 'validatorEthAddressToSolochainAddress',
})
/**
* Wraps __{@link readContract}__ with `abi` set to __{@link dataHavenServiceManagerAbi}__ and `functionName` set to `"validatorSetSubmitter"`
*/
export const readDataHavenServiceManagerValidatorSetSubmitter =
/*#__PURE__*/ createReadContract({
abi: dataHavenServiceManagerAbi,
functionName: 'validatorSetSubmitter',
})
/**
* Wraps __{@link readContract}__ with `abi` set to __{@link dataHavenServiceManagerAbi}__ and `functionName` set to `"validatorSolochainAddressToEthAddress"`
*/
export const readDataHavenServiceManagerValidatorSolochainAddressToEthAddress =
/*#__PURE__*/ createReadContract({
abi: dataHavenServiceManagerAbi,
functionName: 'validatorSolochainAddressToEthAddress',
})
/**
* Wraps __{@link readContract}__ with `abi` set to __{@link dataHavenServiceManagerAbi}__ and `functionName` set to `"validatorsAllowlist"`
*/
@ -10900,12 +11116,12 @@ export const writeDataHavenServiceManagerRenounceOwnership =
})
/**
* Wraps __{@link writeContract}__ with `abi` set to __{@link dataHavenServiceManagerAbi}__ and `functionName` set to `"sendNewValidatorSet"`
* Wraps __{@link writeContract}__ with `abi` set to __{@link dataHavenServiceManagerAbi}__ and `functionName` set to `"sendNewValidatorSetForEra"`
*/
export const writeDataHavenServiceManagerSendNewValidatorSet =
export const writeDataHavenServiceManagerSendNewValidatorSetForEra =
/*#__PURE__*/ createWriteContract({
abi: dataHavenServiceManagerAbi,
functionName: 'sendNewValidatorSet',
functionName: 'sendNewValidatorSetForEra',
})
/**
@ -10926,6 +11142,24 @@ 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"`
*/
export const writeDataHavenServiceManagerSetValidatorSetSubmitter =
/*#__PURE__*/ createWriteContract({
abi: dataHavenServiceManagerAbi,
functionName: 'setValidatorSetSubmitter',
})
/**
* Wraps __{@link writeContract}__ with `abi` set to __{@link dataHavenServiceManagerAbi}__ and `functionName` set to `"setVersionUpdater"`
*/
@ -11077,12 +11311,12 @@ export const simulateDataHavenServiceManagerRenounceOwnership =
})
/**
* Wraps __{@link simulateContract}__ with `abi` set to __{@link dataHavenServiceManagerAbi}__ and `functionName` set to `"sendNewValidatorSet"`
* Wraps __{@link simulateContract}__ with `abi` set to __{@link dataHavenServiceManagerAbi}__ and `functionName` set to `"sendNewValidatorSetForEra"`
*/
export const simulateDataHavenServiceManagerSendNewValidatorSet =
export const simulateDataHavenServiceManagerSendNewValidatorSetForEra =
/*#__PURE__*/ createSimulateContract({
abi: dataHavenServiceManagerAbi,
functionName: 'sendNewValidatorSet',
functionName: 'sendNewValidatorSetForEra',
})
/**
@ -11103,6 +11337,24 @@ 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"`
*/
export const simulateDataHavenServiceManagerSetValidatorSetSubmitter =
/*#__PURE__*/ createSimulateContract({
abi: dataHavenServiceManagerAbi,
functionName: 'setValidatorSetSubmitter',
})
/**
* Wraps __{@link simulateContract}__ with `abi` set to __{@link dataHavenServiceManagerAbi}__ and `functionName` set to `"setVersionUpdater"`
*/
@ -11253,6 +11505,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"`
*/
@ -11271,6 +11532,24 @@ export const watchDataHavenServiceManagerValidatorRemovedFromAllowlistEvent =
eventName: 'ValidatorRemovedFromAllowlist',
})
/**
* Wraps __{@link watchContractEvent}__ with `abi` set to __{@link dataHavenServiceManagerAbi}__ and `eventName` set to `"ValidatorSetMessageSubmitted"`
*/
export const watchDataHavenServiceManagerValidatorSetMessageSubmittedEvent =
/*#__PURE__*/ createWatchContractEvent({
abi: dataHavenServiceManagerAbi,
eventName: 'ValidatorSetMessageSubmitted',
})
/**
* Wraps __{@link watchContractEvent}__ with `abi` set to __{@link dataHavenServiceManagerAbi}__ and `eventName` set to `"ValidatorSetSubmitterUpdated"`
*/
export const watchDataHavenServiceManagerValidatorSetSubmitterUpdatedEvent =
/*#__PURE__*/ createWatchContractEvent({
abi: dataHavenServiceManagerAbi,
eventName: 'ValidatorSetSubmitterUpdated',
})
/**
* Wraps __{@link watchContractEvent}__ with `abi` set to __{@link dataHavenServiceManagerAbi}__ and `eventName` set to `"VersionUpdated"`
*/

View file

@ -1,4 +1,5 @@
export * from "./connectors";
export * from "./manager";
export * from "./submitter";
export * from "./suite";
export * from "./validators";

View file

@ -0,0 +1,135 @@
/**
* E2E test helper for managing the validator-set-submitter Docker container.
*
* The submitter daemon automates `sendNewValidatorSetForEra` calls on the
* ServiceManager contract. This module builds the image, launches the
* container on the shared Docker network, and tears it down after the test.
*/
import path from "node:path";
import { $ } from "bun";
import { ANVIL_FUNDED_ACCOUNTS, logger, waitForContainerToStart, waitForLog } from "utils";
import { RELAYER_CONFIG_DIR } from "../../launcher/relayers";
const SUBMITTER_IMAGE = "datahavenxyz/validator-set-submitter:local";
const SUBMITTER_READY_LOG = "Submitter started — watching session changes";
const SUBMITTER_READY_TIMEOUT_SECONDS = 30;
const SUBMITTER_LOG_TAIL_LINES = 200;
/**
* Builds the validator-set-submitter Docker image from the repo root.
*/
export async function buildSubmitterImage(): Promise<void> {
logger.debug("Building validator-set-submitter Docker image...");
const repoRoot = path.resolve(import.meta.dir, "../../..");
await $`docker build -f test/tools/validator-set-submitter/Dockerfile -t ${SUBMITTER_IMAGE} .`
.cwd(repoRoot)
.quiet();
logger.debug("Validator-set-submitter image built successfully");
}
export interface LaunchSubmitterOptions {
/** Docker network name (from launchedNetwork.networkName) */
networkName: string;
/** Network ID for container naming */
networkId: string;
/** Host-facing Ethereum RPC URL (e.g. http://127.0.0.1:32000) */
ethereumRpcUrl: string;
/** DataHaven container name for inter-container networking */
datahavenContainerName: string;
/** ServiceManager contract address from deployments */
serviceManagerAddress: string;
}
/**
* Launches the validator-set-submitter as a Docker container.
*
* Generates a YAML config, mounts it into the container, and connects
* it to the same Docker network as the DH nodes and relayers.
*/
export async function launchSubmitter(options: LaunchSubmitterOptions): Promise<{
containerName: string;
cleanup: () => Promise<void>;
}> {
const { networkName, networkId, ethereumRpcUrl, datahavenContainerName, serviceManagerAddress } =
options;
const containerName = `submitter-${networkId}`;
// Extract port from host-facing URL and rewrite for Docker inter-container access
const ethUrl = new URL(ethereumRpcUrl);
const dockerEthRpcUrl = `http://host.docker.internal:${ethUrl.port}`;
const dockerDhWsUrl = `ws://${datahavenContainerName}:9944`;
// Generate YAML config
const configContent = [
`ethereum_rpc_url: "${dockerEthRpcUrl}"`,
`datahaven_ws_url: "${dockerDhWsUrl}"`,
`service_manager_address: "${serviceManagerAddress}"`,
`network_id: "anvil"`,
`execution_fee: "0.1"`,
`relayer_fee: "0.2"`
].join("\n");
const configFileName = `submitter-config-${networkId}.yml`;
await $`mkdir -p ${RELAYER_CONFIG_DIR}`.quiet();
const hostConfigPath = path.resolve(path.join(RELAYER_CONFIG_DIR, configFileName));
await Bun.write(hostConfigPath, configContent);
logger.debug(`Submitter config written to ${hostConfigPath}`);
// Remove any existing container with the same name
await $`docker rm -f ${containerName}`.quiet().nothrow();
// Launch the container
const args = [
"run",
"-d",
"--name",
containerName,
"--network",
networkName,
"--add-host",
"host.docker.internal:host-gateway",
"-v",
`${hostConfigPath}:/config/config.yml:ro`,
"-e",
`SUBMITTER_PRIVATE_KEY=${ANVIL_FUNDED_ACCOUNTS[6].privateKey}`,
SUBMITTER_IMAGE
];
await $`docker ${args}`.quiet();
await waitForContainerToStart(containerName);
try {
await waitForLog({
containerName,
search: SUBMITTER_READY_LOG,
timeoutSeconds: SUBMITTER_READY_TIMEOUT_SECONDS
});
} catch (error) {
const logs =
(await $`docker logs --tail ${SUBMITTER_LOG_TAIL_LINES} ${containerName}`.nothrow().text()) ||
"<no logs captured>";
await stopSubmitter(containerName);
throw new Error(
`Submitter did not become ready. Expected log "${SUBMITTER_READY_LOG}". Last ${SUBMITTER_LOG_TAIL_LINES} log lines:\n${logs}`,
{ cause: error }
);
}
logger.debug(`Submitter container ${containerName} started`);
const cleanup = async () => {
await stopSubmitter(containerName);
};
return { containerName, cleanup };
}
/**
* Stops and removes the submitter container.
*/
export async function stopSubmitter(containerName: string): Promise<void> {
logger.debug(`Stopping submitter container ${containerName}...`);
await $`docker rm -f ${containerName}`.quiet().nothrow();
logger.debug(`Submitter container ${containerName} removed`);
}

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";
@ -120,9 +122,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",
@ -131,7 +174,7 @@ export async function registerOperator(
chain: null
});
const operatorReceipt = await connectors.publicClient.waitForTransactionReceipt({
const operatorReceipt = await publicClient.waitForTransactionReceipt({
hash: operatorHash
});
if (operatorReceipt.status !== "success") {
@ -139,7 +182,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",
@ -155,10 +198,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

@ -5,27 +5,29 @@
* - Start network and ensure 4 validator nodes are running (Alice, Bob, Charlie, Dave).
* - Confirm initial mapping exists only for Alice/Bob on `ServiceManager`.
* - Allowlist and register Charlie/Dave as operators on Ethereum.
* - Send updated validator set via `ServiceManager.sendNewValidatorSet`, assert Gateway `OutboundMessageAccepted`.
* - Send updated validator set via `ServiceManager.sendNewValidatorSetForEra`,
* assert Gateway `OutboundMessageAccepted`.
* - Observe `ExternalValidators.ExternalValidatorsSet` on DataHaven (substrate), confirming propagation.
*/
import { beforeAll, describe, expect, it } from "bun:test";
import { getOwnerAccount } from "launcher/validators";
import {
CROSS_CHAIN_TIMEOUTS,
type Deployments,
getPapiSigner,
logger,
parseDeploymentsFile,
ZERO_ADDRESS
} from "utils";
import { waitForDataHavenEvent } from "utils/events";
import { decodeEventLog, parseEther } from "viem";
import { dataHavenServiceManagerAbi, gatewayAbi } from "../../contract-bindings";
import { dataHavenServiceManagerAbi } from "../../contract-bindings";
import {
addValidatorToAllowlist,
BaseTestSuite,
buildSubmitterImage,
getValidator,
isValidatorRunning,
launchDatahavenValidator,
launchSubmitter,
registerOperator,
type TestConnectors
} from "../framework";
@ -48,11 +50,18 @@ class ValidatorSetUpdateTestSuite extends BaseTestSuite {
launchDatahavenValidator("charlie", { launchedNetwork }),
launchDatahavenValidator("dave", { launchedNetwork })
]);
// Build the submitter Docker image so it's ready for the test
await buildSubmitterImage();
}
public getNetworkId(): string {
return this.getConnectors().launchedNetwork.networkId;
}
public getLaunchedNetwork() {
return this.getConnectors().launchedNetwork;
}
}
// Create the test suite instance
@ -67,6 +76,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 () => {
@ -157,41 +180,60 @@ describe("Validator Set Update", () => {
it(
"should send updated validator set and verify on DataHaven",
async () => {
const { publicClient, walletClient, dhApi } = connectors;
const { dhApi } = connectors;
// Send the updated validator set via Snowbridge
const hash = await walletClient.writeContract({
address: deployments.ServiceManager as `0x${string}`,
abi: dataHavenServiceManagerAbi,
functionName: "sendNewValidatorSet",
args: [parseEther("0.1"), parseEther("0.2")],
value: parseEther("0.3"),
gas: 1000000n,
account: getOwnerAccount(),
chain: null
});
const receipt = await publicClient.waitForTransactionReceipt({ hash });
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 });
return decoded.eventName === "OutboundMessageAccepted";
} catch {
return false;
// 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) {
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;
}
});
expect(hasOutboundAccepted).toBe(true);
await new Promise((r) => setTimeout(r, 6_000)); // ~1 substrate block
}
// Wait for the validator set to be updated on Substrate
await waitForDataHavenEvent({
const targetEra = BigInt(stableEraIndex + 1);
const validatorSetUpdated = waitForDataHavenEvent({
api: dhApi,
pallet: "ExternalValidators",
event: "ExternalValidatorsSet",
filter: (event: { external_index: number | bigint }) =>
BigInt(event.external_index) === targetEra,
timeout: CROSS_CHAIN_TIMEOUTS.ETH_TO_DH_MS
});
// Prevent unhandled rejection if launchSubmitter fails before we await this promise.
void validatorSetUpdated.catch(() => undefined);
// Launch the submitter daemon — it will detect the last-session condition
// and automatically call sendNewValidatorSetForEra on the ServiceManager.
const launchedNetwork = suite.getLaunchedNetwork();
const { cleanup: cleanupSubmitter } = await launchSubmitter({
networkName: launchedNetwork.networkName,
networkId: suite.getNetworkId(),
ethereumRpcUrl: connectors.elRpcUrl,
datahavenContainerName: `datahaven-alice-${suite.getNetworkId()}`,
serviceManagerAddress: deployments.ServiceManager
});
try {
logger.info("Waiting for ExternalValidators.ExternalValidatorsSet event on DataHaven...");
// Wait for the validator set to be updated on Substrate
await validatorSetUpdated;
} finally {
await cleanupSubmitter();
}
// Resume era rotation
const resumeTx = dhApi.tx.Sudo.sudo({
call: dhApi.tx.ExternalValidators.force_era({
mode: { type: "NotForcing", value: undefined }
}).decodedCall
});
await resumeTx.signAndSubmit(getPapiSigner("ALITH"));
// Verify new validators are in storage
const validators = await dhApi.query.ExternalValidators.ExternalValidators.getValue();

View file

@ -441,8 +441,8 @@ export const launchRelayers = async (
await $`mkdir -p ${RELAYER_CONFIG_DIR}`.quiet();
const datastorePath = "tmp/datastore";
logger.debug(`Ensuring datastore directory exists: ${datastorePath}`);
await $`mkdir -p ${datastorePath}`.quiet();
logger.debug(`Clearing and recreating datastore directory: ${datastorePath}`);
await $`rm -rf ${datastorePath} && mkdir -p ${datastorePath}`.quiet();
const ethWsPort = await getPortFromKurtosis("el-1-reth-lodestar", "ws", kurtosisEnclaveName);
const ethHttpPort = await getPortFromKurtosis("cl-1-lodestar-reth", "http", kurtosisEnclaveName);

View file

@ -30,6 +30,8 @@
"test:e2e:parallel": "bun scripts/test-parallel.ts",
"moonwall:test": "moonwall test dev_datahaven",
"moonwall:run": "moonwall run dev_datahaven",
"submitter": "bun run tools/validator-set-submitter/main.ts run",
"submitter:dry-run": "bun run tools/validator-set-submitter/main.ts run --dry-run",
"typecheck": "tsc --noEmit",
"tsgo": "tsgo tsc --noEmit --pretty --skipLibCheck",
"postinstall": "papi"

View file

@ -179,33 +179,29 @@ export const updateParameters = async (
parameterCollection: ParameterCollection,
chain?: string
) => {
try {
const deployments = await parseDeploymentsFile(chain);
const gatewayAddress = deployments.Gateway;
const serviceManagerAddress = deployments.ServiceManager;
const deployments = await parseDeploymentsFile(chain);
const gatewayAddress = deployments.Gateway;
const serviceManagerAddress = deployments.ServiceManager;
if (gatewayAddress) {
logger.debug(`📝 Adding EthereumGatewayAddress parameter: ${gatewayAddress}`);
if (gatewayAddress) {
logger.debug(`📝 Adding EthereumGatewayAddress parameter: ${gatewayAddress}`);
parameterCollection.addParameter({
name: "EthereumGatewayAddress",
value: gatewayAddress
});
} else {
logger.warn("⚠️ Gateway address not found in deployments file");
}
parameterCollection.addParameter({
name: "EthereumGatewayAddress",
value: gatewayAddress
});
} else {
logger.warn("⚠️ Gateway address not found in deployments file");
}
if (serviceManagerAddress) {
logger.debug(`📝 Adding DatahavenServiceManagerAddress parameter: ${serviceManagerAddress}`);
parameterCollection.addParameter({
name: "DatahavenServiceManagerAddress",
value: serviceManagerAddress
});
} else {
logger.warn("⚠️ ServiceManager address not found in deployments file");
}
} catch (error) {
logger.error(`Failed to read parameters from deployment: ${error}`);
if (serviceManagerAddress) {
logger.debug(`📝 Adding DatahavenServiceManagerAddress parameter: ${serviceManagerAddress}`);
parameterCollection.addParameter({
name: "DatahavenServiceManagerAddress",
value: serviceManagerAddress
});
} else {
logger.warn("⚠️ ServiceManager address not found in deployments file");
}
};

View file

@ -125,7 +125,7 @@ async function formatStateDiff(): Promise<void> {
// Use a higher max size (3MB) to handle the large state-diff.json file
const result =
await $`bun run biome format --files-max-size=3000000 --write ${STATE_DIFF_PATH}`.quiet();
await $`bun run biome format --files-max-size=4000000 --write ${STATE_DIFF_PATH}`.quiet();
if (result.exitCode !== 0) {
logger.warn("⚠️ Biome formatting had issues, but continuing...");

View file

@ -50,8 +50,20 @@ export const setDataHavenParameters = async (
const result = await tx.signAndSubmit(signer);
if (!result.ok) {
logger.error(`❌ Transaction failed: ${result.block.hash}`);
// sudo always returns Ok at the extrinsic level — check the Sudid event
// for the inner call result
const sudidEvent = result.events.find(
(e: any) => e.type === "Sudo" && e.value?.type === "Sudid"
);
if (!sudidEvent) {
logger.error("❌ Sudo.Sudid event not found in transaction events");
return false;
}
const sudoResult = (sudidEvent.value as any).value.sudo_result;
if (sudoResult.type === "Err") {
logger.error(`❌ Sudo inner call failed: ${JSON.stringify(sudoResult)}`);
return false;
}

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;
};

View file

@ -7,6 +7,7 @@ import { logger } from "../utils/index";
interface UpdateValidatorSetOptions {
rpcUrl: string;
targetEra?: bigint;
}
/**
@ -51,8 +52,15 @@ export const updateValidatorSet = async (options: UpdateValidatorSetOptions): Pr
const executionFee = "100000000000000000"; // 0.1 ETH
const relayerFee = "200000000000000000"; // 0.2 ETH
const value = "300000000000000000"; // 0.3 ETH (sum of fees)
const targetEra = options.targetEra ?? 1n;
const sendCommand = `printf '%s\\n' "\${PRIVATE_KEY}" | ${castExecutable} send --interactive --value ${value} ${serviceManagerAddress} "sendNewValidatorSet(uint128,uint128)" ${executionFee} ${relayerFee} --rpc-url ${rpcUrl}`;
if (options.targetEra === undefined) {
logger.warn(
"No target era specified; defaulting to era 1. Use --target-era for already-running networks."
);
}
const sendCommand = `${castExecutable} send --private-key ${ownerPrivateKey} --value ${value} ${serviceManagerAddress} "sendNewValidatorSetForEra(uint64,uint128,uint128)" ${targetEra} ${executionFee} ${relayerFee} --rpc-url ${rpcUrl}`;
logger.debug(`Running command: ${sendCommand}`);
@ -94,6 +102,7 @@ if (import.meta.main) {
const args = process.argv.slice(2);
const options: {
rpcUrl?: string;
targetEra?: bigint;
} = {};
// Extract RPC URL
@ -102,6 +111,12 @@ if (import.meta.main) {
options.rpcUrl = args[rpcUrlIndex + 1];
}
// Extract target era
const targetEraIndex = args.indexOf("--target-era");
if (targetEraIndex !== -1 && targetEraIndex + 1 < args.length) {
options.targetEra = BigInt(args[targetEraIndex + 1]);
}
// Check required parameters
if (!options.rpcUrl) {
console.error("Error: --rpc-url parameter is required");
@ -110,7 +125,8 @@ if (import.meta.main) {
// Run update
updateValidatorSet({
rpcUrl: options.rpcUrl
rpcUrl: options.rpcUrl,
targetEra: options.targetEra
}).catch((error) => {
console.error("Validator set update failed:", error);
process.exit(1);

View file

@ -0,0 +1,37 @@
# Validator Set Submitter image
#
# Build from the repository root:
# docker build -f test/tools/validator-set-submitter/Dockerfile \
# -t datahavenxyz/validator-set-submitter:local .
#
# Runtime expectations:
# - Mount a config file at /config/config.yml
# - Provide SUBMITTER_PRIVATE_KEY (or pass --submitter-private-key)
# - Set service_manager_address in config.yml (contracts/deployments is not in the image)
FROM oven/bun:1.3.3-slim AS deps
WORKDIR /app
COPY test/package.json test/bun.lock test/tsconfig.json ./
COPY test/.papi ./.papi
RUN bun install --frozen-lockfile --production
FROM oven/bun:1.3.3-slim
WORKDIR /app
RUN useradd -m -u 1001 -U -s /bin/sh -d /submitter submitter
COPY --from=deps /app/node_modules ./node_modules
COPY test/tsconfig.json test/bunfig.toml ./
COPY test/tools/validator-set-submitter/ ./tools/validator-set-submitter/
COPY test/contract-bindings/ ./contract-bindings/
COPY test/utils/ ./utils/
ENV NODE_ENV=production
USER submitter
ENTRYPOINT ["bun", "run", "tools/validator-set-submitter/main.ts", "run"]
CMD ["--config", "/config/config.yml"]

View file

@ -0,0 +1,108 @@
# Validator Set Submitter
Long-running daemon that automatically submits validator-set updates from Ethereum to DataHaven each era via Snowbridge.
## How it works
The submitter subscribes to finalized `Session.CurrentIndex` changes on DataHaven. On each session change it evaluates:
1. Is `ActiveEra` set?
2. Has `targetEra` (`ActiveEra + 1`) already been processed?
3. Is `ExternalIndex` already at or past `targetEra`?
4. Is the current session the last session of the era?
If all preconditions are met, it calls `sendNewValidatorSetForEra` on the ServiceManager contract. Each era gets a single submission attempt — if it fails, the era is missed and the submitter moves on to the next.
## Prerequisites
- The submitter account must be registered on-chain via `setValidatorSetSubmitter` on the ServiceManager.
- An Ethereum RPC endpoint and a DataHaven WebSocket endpoint must be reachable.
- Dependencies installed: `bun i` from the `test/` directory.
## Configuration
Copy `config.yml` and fill in your values:
```yaml
# Connections
ethereum_rpc_url: "http://127.0.0.1:8545"
datahaven_ws_url: "ws://127.0.0.1:9944"
# Optional if provided via --submitter-private-key or SUBMITTER_PRIVATE_KEY env var
# The private key of the account authorized as validatorSetSubmitter
submitter_private_key: "0x..."
# Optional — falls back to contracts/deployments/{network_id}.json
# service_manager_address: "0x..."
network_id: "anvil"
# Fees (in ETH, sent as msg.value to cover Snowbridge relay costs)
execution_fee: "0.1"
relayer_fee: "0.2"
```
## Usage
From the `test/` directory:
```bash
# Start the submitter
bun tools/validator-set-submitter/main.ts run
# With a custom config path
bun tools/validator-set-submitter/main.ts run --config ./path/to/config.yml
# Provide private key via environment variable
SUBMITTER_PRIVATE_KEY=0x... bun tools/validator-set-submitter/main.ts run
# Provide private key via CLI argument
bun tools/validator-set-submitter/main.ts run --submitter-private-key 0x...
# Dry run — logs what would be submitted without sending transactions
bun tools/validator-set-submitter/main.ts run --dry-run
```
Private key precedence is: `--submitter-private-key` > `SUBMITTER_PRIVATE_KEY` > `submitter_private_key` in config file.
## Docker
Build the image from the repository root:
```bash
docker build -f test/tools/validator-set-submitter/Dockerfile \
-t datahavenxyz/validator-set-submitter:local .
```
Run the submitter with mounted config and env private key:
```bash
docker run --rm \
-v "$(pwd)/test/tools/validator-set-submitter/config.yml:/config/config.yml:ro" \
-e SUBMITTER_PRIVATE_KEY=0x... \
datahavenxyz/validator-set-submitter:local
```
Dry run:
```bash
docker run --rm \
-v "$(pwd)/test/tools/validator-set-submitter/config.yml:/config/config.yml:ro" \
-e SUBMITTER_PRIVATE_KEY=0x... \
datahavenxyz/validator-set-submitter:local --dry-run
```
The Docker image does not include `contracts/deployments/*.json`. In containerized runs, set `service_manager_address` in your config.
## Startup checks
On launch the submitter verifies:
- Ethereum RPC is reachable (fetches current block number).
- DataHaven WebSocket is reachable (fetches current block header).
- The configured private key matches the on-chain `validatorSetSubmitter` address.
If any check fails, the process exits immediately.
## Shutdown
Send `SIGINT` (Ctrl+C) or `SIGTERM`. The submitter unsubscribes from session changes and tears down connections cleanly.

View file

@ -0,0 +1,61 @@
import type { DataHavenApi } from "utils/papi";
import type { PublicClient } from "viem";
import { dataHavenServiceManagerAbi } from "../../contract-bindings";
/**
* Reads the current ActiveEra from the ExternalValidators pallet.
* Returns `{ index, start }` where `index` is the era number.
*/
export async function getActiveEra(dhApi: DataHavenApi) {
const era = await dhApi.query.ExternalValidators.ActiveEra.getValue();
return era;
}
/**
* Reads the ExternalIndex the latest era that has been confirmed on-chain
* via an inbound Snowbridge message.
*/
export async function getExternalIndex(dhApi: DataHavenApi): Promise<bigint> {
const index = await dhApi.query.ExternalValidators.ExternalIndex.getValue();
return BigInt(index);
}
/**
* The target era for the next submission is always ActiveEra + 1.
*/
export function computeTargetEra(activeEraIndex: number): bigint {
return BigInt(activeEraIndex + 1);
}
/**
* Reads the on-chain `validatorSetSubmitter` address from the ServiceManager contract.
*/
export async function getOnChainSubmitter(
publicClient: PublicClient,
serviceManagerAddress: `0x${string}`
): Promise<`0x${string}`> {
const submitter = await publicClient.readContract({
address: serviceManagerAddress,
abi: dataHavenServiceManagerAbi,
functionName: "validatorSetSubmitter"
});
return submitter as `0x${string}`;
}
/**
* Returns true if the current session is the last session of the active era.
* Uses the on-chain SessionsPerEra constant and ErasStartSessionIndex storage.
*/
export async function isLastSessionOfEra(dhApi: DataHavenApi): Promise<boolean> {
const activeEra = await dhApi.query.ExternalValidators.ActiveEra.getValue();
if (!activeEra) return false;
const sessionsPerEra = await dhApi.constants.ExternalValidators.SessionsPerEra();
const eraStartSession = await dhApi.query.ExternalValidators.ErasStartSessionIndex.getValue(
activeEra.index
);
if (eraStartSession === undefined) return false;
const currentSession = await dhApi.query.Session.CurrentIndex.getValue();
return currentSession >= eraStartSession + sessionsPerEra - 1;
}

View file

@ -0,0 +1,101 @@
import { parseDeploymentsFile } from "utils";
import { parseEther } from "viem";
import { parse as parseYaml } from "yaml";
export interface SubmitterConfig {
ethereumRpcUrl: string;
datahavenWsUrl: string;
submitterPrivateKey: `0x${string}`;
serviceManagerAddress: `0x${string}`;
networkId: string;
executionFee: bigint;
relayerFee: bigint;
dryRun: boolean;
}
interface CliOverrides {
dryRun?: boolean;
submitterPrivateKey?: string;
}
export async function loadConfig(
configPath: string,
cli: CliOverrides = {}
): Promise<SubmitterConfig> {
const file = Bun.file(configPath);
if (!(await file.exists())) {
throw new Error(`Config file not found: ${configPath}`);
}
const raw = parseYaml(await file.text()) as Record<string, unknown>;
const ethereumRpcUrl = requireString(raw, "ethereum_rpc_url");
const datahavenWsUrl = requireString(raw, "datahaven_ws_url");
const submitterPrivateKey = resolveSubmitterPrivateKey(raw, cli.submitterPrivateKey);
const networkId = optionalString(raw, "network_id") ?? "anvil";
let serviceManagerAddress = optionalHexString(raw, "service_manager_address");
if (!serviceManagerAddress) {
const deployments = await parseDeploymentsFile(networkId);
serviceManagerAddress = deployments.ServiceManager;
}
const executionFee = parseEther(optionalString(raw, "execution_fee") ?? "0.1");
const relayerFee = parseEther(optionalString(raw, "relayer_fee") ?? "0.2");
return {
ethereumRpcUrl,
datahavenWsUrl,
submitterPrivateKey,
serviceManagerAddress,
networkId,
executionFee,
relayerFee,
dryRun: cli.dryRun ?? false
};
}
function resolveSubmitterPrivateKey(
raw: Record<string, unknown>,
cliPrivateKey?: string
): `0x${string}` {
const submitterPrivateKey =
cliPrivateKey ??
process.env.SUBMITTER_PRIVATE_KEY ??
optionalString(raw, "submitter_private_key");
if (!submitterPrivateKey || submitterPrivateKey.length === 0) {
throw new Error(
"Missing submitter private key. Provide --submitter-private-key, SUBMITTER_PRIVATE_KEY, or submitter_private_key in config."
);
}
if (!/^0x[0-9a-fA-F]{64}$/.test(submitterPrivateKey)) {
throw new Error("Submitter private key must be a 66-character hex string (0x + 64 hex chars)");
}
return submitterPrivateKey as `0x${string}`;
}
function requireString(raw: Record<string, unknown>, key: string): string {
const val = raw[key];
if (typeof val !== "string" || val.length === 0) {
throw new Error(`Missing required config field: ${key}`);
}
return val;
}
function optionalString(raw: Record<string, unknown>, key: string): string | undefined {
const val = raw[key];
if (val === undefined || val === null) return undefined;
if (typeof val !== "string") return String(val);
return val;
}
function optionalHexString(raw: Record<string, unknown>, key: string): `0x${string}` | undefined {
const val = optionalString(raw, key);
if (!val) return undefined;
if (!val.startsWith("0x")) {
throw new Error(`Config field ${key} must start with 0x`);
}
return val as `0x${string}`;
}

View file

@ -0,0 +1,21 @@
# Validator Set Submitter Configuration
# Copy this file and update values for your environment.
# Connections
ethereum_rpc_url: "http://127.0.0.1:8545"
datahaven_ws_url: "ws://127.0.0.1:9944"
# Credentials
# Optional if provided via --submitter-private-key or SUBMITTER_PRIVATE_KEY env var
# The private key of the account authorized as validatorSetSubmitter on the ServiceManager
submitter_private_key: "0x..."
# Contract
# Optional — if omitted, falls back to contracts/deployments/{network_id}.json
# Note: Docker image does not include contracts/deployments; set this explicitly when running in Docker.
# service_manager_address: "0x..."
network_id: "anvil"
# Fees (in ETH, sent as msg.value to cover Snowbridge relay costs)
execution_fee: "0.1"
relayer_fee: "0.2"

View file

@ -0,0 +1,92 @@
import { Command } from "@commander-js/extra-typings";
import { logger } from "utils/logger";
import { privateKeyToAccount } from "viem/accounts";
import { getOnChainSubmitter } from "./chain";
import { loadConfig } from "./config";
import { createClients, startSubmitter } from "./submitter";
const program = new Command()
.name("validator-set-submitter")
.description("Automatically submits validator-set updates from Ethereum to DataHaven each era");
program
.command("run")
.description("Start the submitter daemon")
.option(
"--config <path>",
"Path to YAML config file",
"./tools/validator-set-submitter/config.yml"
)
.option(
"--submitter-private-key <key>",
"Override submitter private key (or use SUBMITTER_PRIVATE_KEY env var)"
)
.option("--dry-run", "Log what would be submitted without sending transactions", false)
.action(async (opts) => {
const config = await loadConfig(opts.config, {
dryRun: opts.dryRun,
submitterPrivateKey: opts.submitterPrivateKey
});
logger.info("Validator Set Submitter starting...");
logger.info(`Ethereum RPC: ${config.ethereumRpcUrl}`);
logger.info(`DataHaven WS: ${config.datahavenWsUrl}`);
logger.info(`ServiceManager: ${config.serviceManagerAddress}`);
logger.info(`Dry run: ${config.dryRun}`);
const clients = createClients(config);
// Startup self-checks
try {
const blockNumber = await clients.publicClient.getBlockNumber();
logger.info(`Ethereum connected — block #${blockNumber}`);
} catch (err) {
logger.error(`Cannot connect to Ethereum RPC: ${err}`);
process.exit(1);
}
try {
const header = await clients.papiClient.getBlockHeader();
logger.info(`DataHaven connected — block #${header.number}`);
} catch (err) {
logger.error(`Cannot connect to DataHaven WS: ${err}`);
process.exit(1);
}
// Verify our account is authorized on-chain
try {
const account = privateKeyToAccount(config.submitterPrivateKey);
const onChainSubmitter = await getOnChainSubmitter(
clients.publicClient,
config.serviceManagerAddress
);
if (onChainSubmitter.toLowerCase() !== account.address.toLowerCase()) {
logger.error(
`Account ${account.address} is not the authorized submitter (on-chain: ${onChainSubmitter})`
);
process.exit(1);
}
logger.info(`Authorized submitter verified: ${account.address}`);
} catch (err) {
logger.error(`Failed to verify submitter authorization: ${err}`);
process.exit(1);
}
// Graceful shutdown
const ac = new AbortController();
const shutdown = () => {
logger.info("Shutdown signal received, stopping...");
ac.abort();
};
process.on("SIGINT", shutdown);
process.on("SIGTERM", shutdown);
try {
await startSubmitter(clients, config, ac.signal);
} finally {
clients.papiClient.destroy();
logger.info("Submitter stopped, PAPI client destroyed");
}
});
program.parse();

View file

@ -0,0 +1,209 @@
import { EMPTY, exhaustMap } from "rxjs";
import { logger } from "utils/logger";
import { createPapiConnectors, type DataHavenApi } from "utils/papi";
import {
type Account,
createPublicClient,
createWalletClient,
decodeEventLog,
http,
type PublicClient,
type WalletClient
} from "viem";
import { privateKeyToAccount } from "viem/accounts";
import { dataHavenServiceManagerAbi, gatewayAbi } from "../../contract-bindings";
import { computeTargetEra, getActiveEra, getExternalIndex, isLastSessionOfEra } from "./chain";
import type { SubmitterConfig } from "./config";
interface SubmitterClients {
publicClient: PublicClient;
walletClient: WalletClient<ReturnType<typeof http>, undefined, Account>;
dhApi: DataHavenApi;
papiClient: ReturnType<typeof createPapiConnectors>["client"];
}
const RECEIPT_TIMEOUT_MS = 120_000;
export function createClients(config: SubmitterConfig): SubmitterClients {
const account = privateKeyToAccount(config.submitterPrivateKey);
const transport = http(config.ethereumRpcUrl);
const publicClient = createPublicClient({ transport });
const walletClient = createWalletClient({ account, transport });
const { client: papiClient, typedApi: dhApi } = createPapiConnectors(config.datahavenWsUrl);
return { publicClient, walletClient, dhApi, papiClient };
}
/**
* Returns a promise that resolves when the signal is aborted.
*/
function onAbort(signal: AbortSignal): Promise<void> {
if (signal.aborted) return Promise.resolve();
return new Promise((resolve) =>
signal.addEventListener("abort", () => resolve(), { once: true })
);
}
/**
* Waits for a transaction receipt with a hard timeout, and exits early on abort.
*/
async function waitForReceiptWithAbort(
publicClient: PublicClient,
hash: `0x${string}`,
signal: AbortSignal
) {
return Promise.race([
publicClient.waitForTransactionReceipt({
hash,
timeout: RECEIPT_TIMEOUT_MS
}),
onAbort(signal).then(() => {
throw signal.reason ?? new Error("Aborted while waiting for transaction receipt");
})
]);
}
/**
* Creates a tick handler that closes over submission state.
* Each call evaluates a session change and submits if eligible.
*/
function createTicker(clients: SubmitterClients, config: SubmitterConfig, signal: AbortSignal) {
let submittedEra: bigint | undefined;
return async (currentSession: number): Promise<void> => {
const { dhApi } = clients;
const activeEra = await getActiveEra(dhApi);
if (!activeEra) {
logger.warn("ActiveEra not set yet");
return;
}
const targetEra = computeTargetEra(activeEra.index);
if (submittedEra === targetEra) return;
const externalIndex = await getExternalIndex(dhApi);
if (externalIndex >= targetEra) {
submittedEra = targetEra;
return;
}
if (!(await isLastSessionOfEra(dhApi))) return;
logger.info(
`Session=${currentSession} ActiveEra=${activeEra.index} TargetEra=${targetEra} ExternalIndex=${externalIndex}`
);
const succeeded = await submitForEra(clients, config, targetEra, signal);
if (succeeded) submittedEra = targetEra;
};
}
/**
* Watches finalized session changes and submits validator sets when eligible.
* Runs until the signal is aborted.
*/
export async function startSubmitter(
clients: SubmitterClients,
config: SubmitterConfig,
signal: AbortSignal
): Promise<void> {
const { dhApi } = clients;
const tick = createTicker(clients, config, signal);
logger.info("Submitter started — watching session changes");
const sub = dhApi.query.Session.CurrentIndex.watchValue("finalized")
.pipe(
exhaustMap((currentSession) => {
if (signal.aborted) return EMPTY;
return tick(currentSession).catch((err) => {
if (!signal.aborted) logger.error(`Tick error: ${err}`);
});
})
)
.subscribe({
error: (err) => {
if (!signal.aborted) logger.error(`Session subscription error: ${err}`);
}
});
const done = new Promise<void>((resolve) => sub.add(() => resolve()));
await Promise.race([onAbort(signal), done]);
sub.unsubscribe();
logger.info("Submitter stopped");
}
/**
* Submits the validator set for a single target era.
* Logs success or failure internally.
*/
async function submitForEra(
clients: SubmitterClients,
config: SubmitterConfig,
targetEra: bigint,
signal: AbortSignal
): Promise<boolean> {
const { publicClient, walletClient } = clients;
const totalFee = config.executionFee + config.relayerFee;
logger.info(
`Submitting era ${targetEra} (execFee=${config.executionFee} relayerFee=${config.relayerFee})`
);
if (config.dryRun) {
const message = await publicClient.readContract({
address: config.serviceManagerAddress,
abi: dataHavenServiceManagerAbi,
functionName: "buildNewValidatorSetMessageForEra",
args: [targetEra]
});
logger.info(`[DRY RUN] Would send message: ${message}`);
return true;
}
try {
const hash = await walletClient.writeContract({
address: config.serviceManagerAddress,
abi: dataHavenServiceManagerAbi,
functionName: "sendNewValidatorSetForEra",
args: [targetEra, config.executionFee, config.relayerFee],
value: totalFee,
chain: null
});
logger.info(`Transaction sent: ${hash}`);
const receipt = await waitForReceiptWithAbort(publicClient, hash, signal);
if (receipt.status !== "success") {
logger.error(`Transaction reverted: ${hash}`);
return false;
}
const hasOutbound = receipt.logs.some((log) => {
try {
const decoded = decodeEventLog({
abi: gatewayAbi,
data: log.data,
topics: log.topics
});
return decoded.eventName === "OutboundMessageAccepted";
} catch {
return false;
}
});
if (!hasOutbound) {
logger.warn("Transaction succeeded but no OutboundMessageAccepted event found");
return false;
}
logger.info("OutboundMessageAccepted confirmed");
return true;
} catch (err: unknown) {
if (signal.aborted) return false;
logger.error(`Submission attempt failed: ${err}`);
return false;
}
}

View file

@ -43,6 +43,7 @@
"cli/**/*.ts",
"wagmi.config.ts",
"contract-bindings/*.ts",
"launcher/**/*.ts"
"launcher/**/*.ts",
"tools/**/*.ts"
]
}