mirror of
https://github.com/datahaven-xyz/datahaven
synced 2026-05-24 01:38:32 +00:00
## 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`
84 lines
3.1 KiB
Bash
Executable file
84 lines
3.1 KiB
Bash
Executable file
#!/bin/bash
|
|
set -e
|
|
|
|
# Storage Layout Check Script
|
|
# Compares current storage layout against committed snapshot to detect unintended changes.
|
|
|
|
CONTRACT="${CONTRACT:-DataHavenServiceManager}"
|
|
SNAPSHOT_DIR="${SNAPSHOT_DIR:-storage-snapshots}"
|
|
SNAPSHOT="${SNAPSHOT:-${SNAPSHOT_DIR}/${CONTRACT}.storage.json}"
|
|
|
|
# Ensure we're in the contracts directory
|
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
cd "$SCRIPT_DIR/.."
|
|
|
|
# Check if snapshot exists
|
|
if [ ! -f "$SNAPSHOT" ]; then
|
|
echo "ERROR: Snapshot file not found: $SNAPSHOT"
|
|
echo "Generate it with: mkdir -p $SNAPSHOT_DIR && forge inspect $CONTRACT storage --json > $SNAPSHOT"
|
|
exit 1
|
|
fi
|
|
|
|
# Generate current layout
|
|
echo "Generating current storage layout for $CONTRACT..."
|
|
forge inspect "$CONTRACT" storage --json > /tmp/current_layout.json
|
|
|
|
# Normalize both files for comparison:
|
|
# - Remove astId (changes with compiler runs)
|
|
# - Remove contract field (contains full path)
|
|
# - Remove types section (contains unstable AST IDs)
|
|
# - Sort by slot number
|
|
normalize_json() {
|
|
jq 'del(.types)
|
|
| .storage
|
|
| map(
|
|
del(.astId, .contract)
|
|
# Remove unstable AST IDs from type strings (e.g., t_contract(IGatewayV2)12345, nested mappings)
|
|
| .type |= gsub("\\)[0-9]+"; ")")
|
|
)
|
|
| sort_by(.slot | tonumber)' "$1"
|
|
}
|
|
|
|
echo "Comparing storage layouts..."
|
|
normalize_json "$SNAPSHOT" > /tmp/snap_normalized.json
|
|
normalize_json /tmp/current_layout.json > /tmp/curr_normalized.json
|
|
|
|
if ! diff -q /tmp/snap_normalized.json /tmp/curr_normalized.json > /dev/null 2>&1; then
|
|
echo ""
|
|
echo "=========================================="
|
|
echo "ERROR: Storage layout has changed!"
|
|
echo "=========================================="
|
|
echo ""
|
|
echo "Differences found:"
|
|
diff /tmp/snap_normalized.json /tmp/curr_normalized.json || true
|
|
echo ""
|
|
echo "If this change is intentional, update the snapshot:"
|
|
echo " forge inspect $CONTRACT storage --json > $SNAPSHOT"
|
|
echo ""
|
|
echo "WARNING: Unintended storage layout changes can corrupt state during upgrades!"
|
|
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"
|