datahaven/contracts/storage-snapshots
Ahmad Kaouk eaf55fb414
feat: implement weighted top-32 validator selection (#443)
## 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`
2026-02-24 09:23:57 +01:00
..
DataHavenServiceManager.storage.json feat: implement weighted top-32 validator selection (#443) 2026-02-24 09:23:57 +01:00
README.md test: Add storage layout checks for upgradeable contracts (#420) 2026-02-05 11:08:35 +00:00

Storage Layout Snapshots

This directory contains storage layout snapshots for upgradeable contracts. These snapshots are used to detect unintended storage layout changes that could corrupt state during proxy upgrades.

How It Works

  1. Snapshot Comparison: CI compares the current storage layout against committed snapshots
  2. Upgrade Simulation: Foundry tests verify state preservation across upgrades

Updating Snapshots

When you intentionally modify the storage layout of a contract (e.g., adding new state variables), you must update the snapshot:

cd contracts
forge inspect DataHavenServiceManager storage --json > storage-snapshots/DataHavenServiceManager.storage.json

Important Guidelines

  • Never reorder existing variables - This corrupts existing state
  • Never change types of existing variables - This corrupts existing state
  • Always add new variables before the __GAP - This preserves upgrade safety
  • Reduce gap size when adding variables - Keep total slot count constant
  • Review snapshot diffs carefully - Ensure changes are intentional

Current Contracts

Contract Gap Size Gap Slot
DataHavenServiceManager 46 105

Verification Commands

# Check storage layout (CI script)
./scripts/check-storage-layout.sh

# Negative check (proves detector fails on broken layout)
./scripts/check-storage-layout-negative.sh

# Run upgrade simulation tests
forge test --match-contract StorageLayoutTest -vvv

# View human-readable layout
forge inspect DataHavenServiceManager storage --pretty

How Normalization Works

The snapshot comparison normalizes both files to avoid false positives:

  • Removes astId: Changes with each compiler run
  • Removes contract: Contains full file path
  • Removes .types section: Contains unstable AST IDs that cause false diffs
  • Normalizes type IDs: Strips unstable numeric suffixes from type (e.g., t_contract(IGatewayV2)12345)
  • Sorts by slot: Ensures deterministic comparison

This approach detects:

  • Variable reordering or slot changes
  • Top-level type changes (primitives, mappings, arrays)
  • Gap size modifications

Note on Struct Storage

If you add struct-typed storage variables in the future, be aware that internal struct field changes may not be detected by the snapshot diff. This is because:

  1. The .types section (which contains struct field definitions) is dropped to avoid unstable AST IDs
  2. The storage slot assignment for a struct variable doesn't change when its internal fields change

However, this does not break upgrades in the traditional sense. Struct field reordering or type changes within a struct would cause data misinterpretation (reading field A as field B), but the slot-level layout remains stable.

Mitigation: If adding struct storage, ensure the upgrade simulation tests (StorageLayoutTest) explicitly verify struct field values survive upgrades.

Current status: DataHavenServiceManager has no struct-typed storage variables, so this limitation does not apply.