## Overview Implements deterministic weighted-stake-based validator selection in `DataHavenServiceManager`, building on the era-targeting submitter model from PR #433. Previously, `buildNewValidatorSetMessage()` forwarded all registered operators in arbitrary membership order with no stake-based ranking, meaning high-stake operators could be displaced by lower-stake ones when downstream caps applied. This PR fixes that by computing a weighted stake score per operator and selecting the top-32 candidates before bridging the set to DataHaven. Spec: `specs/validator-set-selection/validator-set-selection.md` ## Contract Changes (`DataHavenServiceManager.sol`) **New state:** - `MAX_ACTIVE_VALIDATORS = 32` — cap on the outbound validator set - `mapping(IStrategy => uint96) public strategiesAndMultipliers` — per-strategy weight used in the selection formula **Updated `buildNewValidatorSetMessage()`:** 1. Fetches allocated stake for all operators × strategies from `AllocationManager` 2. Computes `weightedStake(op) = Σ(allocatedStake[op][j] × multiplier[j])` across all strategies 3. Filters operators with no solochain address mapping or zero weighted stake 4. Runs a partial selection sort to pick the top `min(candidateCount, 32)` by descending weighted stake; ties broken by lower operator address (deterministic) 5. Reverts with `EmptyValidatorSet()` if no eligible candidates remain **Admin API changes:** - `addStrategiesToValidatorsSupportedStrategies()` signature changed from `IStrategy[]` to `IRewardsCoordinatorTypes.StrategyAndMultiplier[]` — strategy and multiplier are stored atomically in one call, eliminating the risk of a strategy being registered without a multiplier - New `setStrategiesAndMultipliers(StrategyAndMultiplier[])` — updates multiplier weights for existing strategies without touching the EigenLayer strategy set - New `getStrategiesAndMultipliers()` — returns all strategies with their current multipliers - `removeStrategiesFromValidatorsSupportedStrategies()` now cleans up multiplier entries on removal **New error / event:** - `EmptyValidatorSet()` — reverts when no eligible candidates exist - `StrategiesAndMultipliersSet(StrategyAndMultiplier[])` — emitted on add or update of multipliers ## Tests (`ValidatorSetSelection.t.sol`) New 552-line Foundry test suite covering all cases from the spec: | Case | |------| | `addStrategies` stores multiplier atomically | | `removeStrategies` deletes multiplier | | `setStrategiesAndMultipliers` updates without touching the strategy set | | `getStrategiesAndMultipliers` returns correct pairs | | Weighted stake computed correctly across multiple strategies | | Operators with zero weighted stake are excluded | | Unset multiplier treated as 0 | | Top-32 selection when candidate count > 32 | | All candidates included when count < 32 | | Tie-breaking by lower operator address | | `EmptyValidatorSet` revert when no eligible operators | ## Deploy Scripts - **`DeployBase.s.sol`**: Sets a default multiplier of `1` for all configured validator strategies after AVS registration via `setStrategiesAndMultipliers` - **New `AllocateOperatorStake.s.sol`**: Forge script that allocates full magnitude (`1e18`) to the validator operator set for a given operator. Must be run at least one block after `SignUpValidator` to respect EigenLayer's allocation configuration delay. ## E2E Framework - **`validators.ts` — `registerOperator()`**: Extended to deposit tokens into each deployed strategy and allocate full magnitude to the DataHaven operator set after registration. Previously operators registered without staking, producing zero weighted stake and getting filtered out by the new selection logic. - **`setup-validators.ts`**: Added a stake allocation pass after the registration loop, invoking `AllocateOperatorStake.s.sol` per validator. - **`validator-set-update.test.ts`**: Added debug logging for transaction receipts and the `OutboundMessageAccepted` / `ExternalValidatorsSet` events. - **`generated.ts`**: Regenerated contract bindings to include new functions, events, and the `EmptyValidatorSet` error. ## ⚠️ Breaking Changes ⚠️ - `addStrategiesToValidatorsSupportedStrategies(IStrategy[])` → `addStrategiesToValidatorsSupportedStrategies(StrategyAndMultiplier[])`: callers must supply multipliers alongside strategies. - Operators with zero weighted stake are no longer included in the bridged validator set. ## Rollout Notes 1. PR #433 (era-targeting + submitter role) must be deployed first 2. Deploy this `ServiceManager` upgrade 3. Confirm `strategiesAndMultipliers` is set for all active strategies (default multiplier `1` applied automatically by `DeployBase`) 4. Deploy the runtime cap-enforcement changes (spec section 10.2) 5. Submitter daemon requires no changes — continues submitting `targetEra = ActiveEra + 1`
10 KiB
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:
- Ethereum computes weighted stake per operator.
- Ethereum deterministically sorts operators and selects top candidates.
- 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:
DataHavenServiceManager.sendNewValidatorSetForEra(uint64 targetEra, ...)is used for submission.- Submission is restricted to
validatorSetSubmitter(onlyValidatorSetSubmitter). external_indexin the Snowbridge payload is thetargetEra.- DataHaven runtime enforces era validity (
targetEraold/too-new/duplicate checks).
3. Goals
- Select external validators by weighted stake instead of raw member ordering.
- Keep selection deterministic (
same chain state -> same selected set). - Preserve PR #433 era-targeting invariants and submitter authorization flow.
- Enforce total active authority cap = 32 (
whitelisted + external). - Keep payload shape stable unless there is a hard requirement to version it.
4. Non-Goals
- Replacing PR #433 submitter-role model.
- Changing PR #433 era-target validation semantics.
- Redesigning Snowbridge transport internals.
- 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:
validatorsexternal_index(interpreted astargetEra)
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:
- Add strategy -> add multiplier in the same call via
IRewardsCoordinatorTypes.StrategyAndMultiplierstruct. - 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:
allocatedStakecomes from EigenLayer allocation data.multiplieris a per-strategy weight (no normalization divisor is applied during ranking).
7.1 Strategy Weight Semantics
- Every supported strategy should have an explicit multiplier entry for operational clarity.
- Missing multiplier entry is treated as
0multiplier. - 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
uint32 public constant MAX_ACTIVE_VALIDATORS = 32;
mapping(IStrategy => uint96) public strategiesAndMultipliers;
8.2 New/Updated Admin APIs
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:
- Read validator operator set members.
- Compute weighted stake per operator.
- Filter out operators with no solochain mapping.
- Resolve multiplier from
strategiesAndMultipliersfor each strategy used. - If any strategy is missing a multiplier entry, treat it as
0multiplier. - Filter out operators with zero weighted stake.
- 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). - 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:
[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:
w = whitelisted.len()external_budget = 32.saturating_sub(w)- Use at most
external_budgetexternal validators from the ranked list. - 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
- Merge/deploy PR #433 baseline first (submitter role + era-target checks).
- Deploy ServiceManager upgrade with weighted ranking logic.
- Backfill/confirm
strategiesAndMultipliersfor all currently supported strategies. - Deploy runtime changes for final total-cap enforcement.
- Re-run submitter daemon unchanged (it still submits
targetEra = ActiveEra + 1). - Monitor across multiple era cycles before production rollout.
12. Testing Plan
12.1 Solidity
- Weighted stake computation across multiple strategies.
- Deterministic tie-break behavior.
- Top-32 selection when candidate count exceeds 32.
- Behavior when candidate count is below 32.
- Zero-stake filtering.
- Missing multiplier entries are treated as zero contribution.
addStrategies...sets multipliers atomically viaStrategyAndMultiplierstruct.removeStrategies...removes multiplier entries for removed strategies.getStrategiesAndMultipliers()returns a list matching EigenLayer's operator set strategies.- Integration with
buildNewValidatorSetMessageForEra(targetEra)and correct target era encoding.
12.2 Runtime
- Existing PR #433 era-validation tests continue to pass unchanged.
- Final active authority cap remains <= 32 with mixed whitelisted/external sets.
- Composition logic preserves whitelisted priority while enforcing cap.
12.3 Integration / E2E
- End-to-end submission through
sendNewValidatorSetForErawith ranked validator output. - Delayed relay still fails with PR #433 semantics (no regressions).
- Ranked selection outcome is deterministic across repeated runs at fixed state.
13. Security Considerations
- Owner-managed strategy weights are governance-sensitive and should remain multisig/governance controlled.
- Deterministic ordering prevents non-deterministic set drift.
- Preserve PR #433 stale/duplicate/too-early rejection invariants.
- Apply overflow checks in weighted arithmetic and any integer downcasts.
14. File Change Summary
contracts/src/DataHavenServiceManager.sol- weighted stake computation and deterministic top selection in
buildNewValidatorSetMessageForEra.
- weighted stake computation and deterministic top selection in
contracts/src/interfaces/IDataHavenServiceManager.solstrategiesAndMultipliersnaming and add/remove strategy API signature updates with multipliers.
operator/pallets/external-validators/src/lib.rs- final authority cap enforcement at composition time (while keeping PR #433 era validation behavior).
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.