mirror of
https://github.com/datahaven-xyz/datahaven
synced 2026-05-24 09:50:01 +00:00
Merge remote-tracking branch 'origin/main' into feat/contracts-versioning-system
This commit is contained in:
commit
c8fc146b91
63 changed files with 4237 additions and 2374 deletions
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -1 +1 @@
|
|||
764d675e54fcb006d8b2b43a6fdf8782313e6509
|
||||
9c861e3e1d290888127bc6d772fb1a3422bdf8b3
|
||||
|
|
|
|||
File diff suppressed because one or more lines are too long
11
contracts/foundry.lock
Normal file
11
contracts/foundry.lock
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
{
|
||||
"lib/eigenlayer-contracts": {
|
||||
"rev": "7ecc83c7b180850531bc5b8b953a7340adeecd43"
|
||||
},
|
||||
"lib/forge-std": {
|
||||
"rev": "9530d9ec702df1b27b7f8f50c0a63a11b1b5fba9"
|
||||
},
|
||||
"lib/snowbridge": {
|
||||
"rev": "13263fefa29a3f4af50e5650dcd93fe3afac44db"
|
||||
}
|
||||
}
|
||||
|
|
@ -20,6 +20,7 @@ contract Config {
|
|||
address avsOwner;
|
||||
address rewardsInitiator;
|
||||
address[] validatorsStrategies;
|
||||
address validatorSetSubmitter;
|
||||
}
|
||||
|
||||
// EigenLayer parameters
|
||||
|
|
|
|||
|
|
@ -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
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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
|
||||
);
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
61
contracts/script/transact/AllocateOperatorStake.s.sol
Normal file
61
contracts/script/transact/AllocateOperatorStake.s.sol
Normal 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))
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -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:"
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 ============
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
158
contracts/test/OperatorAddressMappings.t.sol
Normal file
158
contracts/test/OperatorAddressMappings.t.sol
Normal 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"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
568
contracts/test/ValidatorSetSelection.t.sol
Normal file
568
contracts/test/ValidatorSetSelection.t.sol
Normal 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));
|
||||
}
|
||||
}
|
||||
287
contracts/test/ValidatorSetSubmitter.t.sol
Normal file
287
contracts/test/ValidatorSetSubmitter.t.sol
Normal 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");
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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
273
operator/Cargo.lock
generated
|
|
@ -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",
|
||||
]
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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
|
||||
);
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
}),
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
}),
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
}),
|
||||
};
|
||||
|
||||
|
|
|
|||
247
specs/validator-set-selection/validator-set-selection.md
Normal file
247
specs/validator-set-selection/validator-set-selection.md
Normal 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.
|
||||
268
specs/validator-set-submission/validator-set-submission.md
Normal file
268
specs/validator-set-submission/validator-set-submission.md
Normal 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
|
||||
|
|
@ -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.
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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."
|
||||
|
|
|
|||
|
|
@ -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"`
|
||||
*/
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
export * from "./connectors";
|
||||
export * from "./manager";
|
||||
export * from "./submitter";
|
||||
export * from "./suite";
|
||||
export * from "./validators";
|
||||
|
|
|
|||
135
test/e2e/framework/submitter.ts
Normal file
135
test/e2e/framework/submitter.ts
Normal 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`);
|
||||
}
|
||||
|
|
@ -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})`);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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");
|
||||
}
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -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...");
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
37
test/tools/validator-set-submitter/Dockerfile
Normal file
37
test/tools/validator-set-submitter/Dockerfile
Normal 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"]
|
||||
108
test/tools/validator-set-submitter/README.md
Normal file
108
test/tools/validator-set-submitter/README.md
Normal 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.
|
||||
61
test/tools/validator-set-submitter/chain.ts
Normal file
61
test/tools/validator-set-submitter/chain.ts
Normal 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;
|
||||
}
|
||||
101
test/tools/validator-set-submitter/config.ts
Normal file
101
test/tools/validator-set-submitter/config.ts
Normal 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}`;
|
||||
}
|
||||
21
test/tools/validator-set-submitter/config.yml
Normal file
21
test/tools/validator-set-submitter/config.yml
Normal 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"
|
||||
92
test/tools/validator-set-submitter/main.ts
Normal file
92
test/tools/validator-set-submitter/main.ts
Normal 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();
|
||||
209
test/tools/validator-set-submitter/submitter.ts
Normal file
209
test/tools/validator-set-submitter/submitter.ts
Normal 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;
|
||||
}
|
||||
}
|
||||
|
|
@ -43,6 +43,7 @@
|
|||
"cli/**/*.ts",
|
||||
"wagmi.config.ts",
|
||||
"contract-bindings/*.ts",
|
||||
"launcher/**/*.ts"
|
||||
"launcher/**/*.ts",
|
||||
"tools/**/*.ts"
|
||||
]
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue