2026-02-05 11:08:35 +00:00
|
|
|
// SPDX-License-Identifier: GPL-3.0
|
|
|
|
|
pragma solidity ^0.8.27;
|
|
|
|
|
|
|
|
|
|
import {Test} from "forge-std/Test.sol";
|
|
|
|
|
import {
|
|
|
|
|
ITransparentUpgradeableProxy
|
|
|
|
|
} from "@openzeppelin/contracts/proxy/transparent/TransparentUpgradeableProxy.sol";
|
|
|
|
|
|
|
|
|
|
import {AVSDeployer} from "../utils/AVSDeployer.sol";
|
|
|
|
|
import {DataHavenServiceManager} from "../../src/DataHavenServiceManager.sol";
|
|
|
|
|
|
|
|
|
|
/// @title Storage Layout Tests for DataHavenServiceManager
|
|
|
|
|
/// @notice Verifies that proxy upgrades preserve state correctly
|
|
|
|
|
contract StorageLayoutTest is AVSDeployer {
|
|
|
|
|
function setUp() public {
|
|
|
|
|
_deployMockEigenLayerAndAVS();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// @notice Proves state is preserved across proxy upgrade
|
|
|
|
|
function test_upgradePreservesState() public {
|
|
|
|
|
// 1. Populate state
|
|
|
|
|
address testValidator = address(0x1234);
|
|
|
|
|
address newRewardsInitiator = address(0x9999);
|
feat: automated validator set submission with era targeting (#433)
## Era-targeted validator set submission with dedicated submitter role
> **Note:** This PR includes a detailed specification at
[`specs/validator-set-submission/validator-set-submission.md`](https://github.com/datahaven-xyz/datahaven/blob/feat/validator-set-submitter/specs/validator-set-submission/validator-set-submission.md)
that covers the design rationale, submission lifecycle, era-targeting
rules, and failure modes. Reading the spec first will make the contract,
pallet, and daemon changes easier to follow.
### Summary
- Introduce a dedicated `validatorSetSubmitter` role on
`DataHavenServiceManager`, separating validator set submission authority
from the contract owner
- Replace the unscoped `sendNewValidatorSet` with
`sendNewValidatorSetForEra`, which encodes a `targetEra` into the
Snowbridge message payload
- Add server-side era validation in the `external-validators` pallet to
reject stale, duplicate, or out-of-range submissions
- Add a long-running TypeScript daemon that watches session changes and
automatically submits each era's validator set at the right time
### Contract changes (`contracts/`)
- **New `validatorSetSubmitter` storage slot** — set during `initialize`
and rotatable via `setValidatorSetSubmitter` (owner-only). The storage
gap is decremented accordingly.
- **`sendNewValidatorSet` → `sendNewValidatorSetForEra`** — accepts a
`uint64 targetEra` parameter and is restricted to
`onlyValidatorSetSubmitter` instead of `onlyOwner`.
- **`buildNewValidatorSetMessageForEra`** — the
`NewValidatorSetPayload.externalIndex` is now caller-supplied instead of
hardcoded to `0`.
- **New events** — `ValidatorSetSubmitterUpdated`,
`ValidatorSetMessageSubmitted`.
- **New error** — `OnlyValidatorSetSubmitter`.
- **New test suite** — `ValidatorSetSubmitter.t.sol` covering submitter
set/rotate, access control, era encoding, and legacy function removal.
### Pallet changes (`operator/`)
- **`validate_target_era`** in `external-validators` — enforces
`activeEra < targetEra <= activeEra + 1` and `targetEra > ExternalIndex`
(dedup guard).
- **New errors** — `TargetEraTooOld`, `TargetEraTooNew`,
`DuplicateOrStaleTargetEra`.
- **Tests** — five new test cases for era boundary conditions (next-era
acceptance, old-era rejection, too-new rejection, duplicate rejection,
genesis behavior). Existing `era_hooks_with_external_index` test updated
to use valid target eras.
- **Runtime test fixes** — `external_index: 0` → `1` in
mainnet/stagenet/testnet EigenLayer message processor tests to satisfy
the new validation.
### Validator set submitter daemon
(`test/tools/validator-set-submitter/`)
- Event-driven service that subscribes to finalized
`Session.CurrentIndex` via Polkadot-API `watchValue`.
- Submits once per era during the last session, targeting `ActiveEra +
1`.
- Tracks submitted eras to avoid duplicates; skips if `ExternalIndex`
already covers the target.
- Startup self-checks: Ethereum connectivity, DataHaven connectivity,
on-chain submitter authorization.
- Supports `--dry-run` mode and YAML configuration.
- Graceful shutdown on `SIGINT`/`SIGTERM`.
### Test & tooling updates
- **E2E test** (`validator-set-update.test.ts`) — calls
`sendNewValidatorSetForEra` with a computed `targetEra` and filters the
substrate event by `external_index`.
- **`update-validator-set.ts` script** — accepts `--target-era` flag;
defaults to era 1 for fresh networks.
- **CLI launch** — wires validator set update as an interactive step
after relayer launch.
- **`package.json`** — new `submitter` and `submitter:dry-run` scripts.
- Regenerated contract bindings, PAPI metadata, state-diff, and storage
layout snapshots.
### Test plan
- [x] `forge test` — passes, including new `ValidatorSetSubmitter.t.sol`
- [x] `cargo test` — passes, including new era-validation tests in
`external-validators`
- [x] `bun test:e2e` — validator-set-update suite passes with
era-targeted flow
- [x] Manual: run submitter daemon against local network (`bun
submitter`), verify it submits once per era at the correct session
## ⚠️ Breaking Changes ⚠️
- **`sendNewValidatorSet` removed** — replaced by
`sendNewValidatorSetForEra(uint64 targetEra, ...)`. Callers must now
supply a `targetEra` parameter.
- **Access control changed** — validator set submission is now
restricted to the `validatorSetSubmitter` role instead of the contract
`owner`. The submitter address is set during `initialize` and rotatable
via `setValidatorSetSubmitter` (owner-only).
- **`external-validators` pallet now validates `targetEra`** — messages
with a stale, duplicate, or out-of-range `external_index` are rejected
on-chain. Existing integrations sending `external_index: 0` will fail
validation.
---------
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-02-20 09:31:44 +00:00
|
|
|
address testSubmitter = address(0x5678);
|
2026-02-05 11:08:35 +00:00
|
|
|
|
|
|
|
|
vm.startPrank(avsOwner);
|
|
|
|
|
serviceManager.addValidatorToAllowlist(testValidator);
|
|
|
|
|
serviceManager.setRewardsInitiator(newRewardsInitiator);
|
feat: automated validator set submission with era targeting (#433)
## Era-targeted validator set submission with dedicated submitter role
> **Note:** This PR includes a detailed specification at
[`specs/validator-set-submission/validator-set-submission.md`](https://github.com/datahaven-xyz/datahaven/blob/feat/validator-set-submitter/specs/validator-set-submission/validator-set-submission.md)
that covers the design rationale, submission lifecycle, era-targeting
rules, and failure modes. Reading the spec first will make the contract,
pallet, and daemon changes easier to follow.
### Summary
- Introduce a dedicated `validatorSetSubmitter` role on
`DataHavenServiceManager`, separating validator set submission authority
from the contract owner
- Replace the unscoped `sendNewValidatorSet` with
`sendNewValidatorSetForEra`, which encodes a `targetEra` into the
Snowbridge message payload
- Add server-side era validation in the `external-validators` pallet to
reject stale, duplicate, or out-of-range submissions
- Add a long-running TypeScript daemon that watches session changes and
automatically submits each era's validator set at the right time
### Contract changes (`contracts/`)
- **New `validatorSetSubmitter` storage slot** — set during `initialize`
and rotatable via `setValidatorSetSubmitter` (owner-only). The storage
gap is decremented accordingly.
- **`sendNewValidatorSet` → `sendNewValidatorSetForEra`** — accepts a
`uint64 targetEra` parameter and is restricted to
`onlyValidatorSetSubmitter` instead of `onlyOwner`.
- **`buildNewValidatorSetMessageForEra`** — the
`NewValidatorSetPayload.externalIndex` is now caller-supplied instead of
hardcoded to `0`.
- **New events** — `ValidatorSetSubmitterUpdated`,
`ValidatorSetMessageSubmitted`.
- **New error** — `OnlyValidatorSetSubmitter`.
- **New test suite** — `ValidatorSetSubmitter.t.sol` covering submitter
set/rotate, access control, era encoding, and legacy function removal.
### Pallet changes (`operator/`)
- **`validate_target_era`** in `external-validators` — enforces
`activeEra < targetEra <= activeEra + 1` and `targetEra > ExternalIndex`
(dedup guard).
- **New errors** — `TargetEraTooOld`, `TargetEraTooNew`,
`DuplicateOrStaleTargetEra`.
- **Tests** — five new test cases for era boundary conditions (next-era
acceptance, old-era rejection, too-new rejection, duplicate rejection,
genesis behavior). Existing `era_hooks_with_external_index` test updated
to use valid target eras.
- **Runtime test fixes** — `external_index: 0` → `1` in
mainnet/stagenet/testnet EigenLayer message processor tests to satisfy
the new validation.
### Validator set submitter daemon
(`test/tools/validator-set-submitter/`)
- Event-driven service that subscribes to finalized
`Session.CurrentIndex` via Polkadot-API `watchValue`.
- Submits once per era during the last session, targeting `ActiveEra +
1`.
- Tracks submitted eras to avoid duplicates; skips if `ExternalIndex`
already covers the target.
- Startup self-checks: Ethereum connectivity, DataHaven connectivity,
on-chain submitter authorization.
- Supports `--dry-run` mode and YAML configuration.
- Graceful shutdown on `SIGINT`/`SIGTERM`.
### Test & tooling updates
- **E2E test** (`validator-set-update.test.ts`) — calls
`sendNewValidatorSetForEra` with a computed `targetEra` and filters the
substrate event by `external_index`.
- **`update-validator-set.ts` script** — accepts `--target-era` flag;
defaults to era 1 for fresh networks.
- **CLI launch** — wires validator set update as an interactive step
after relayer launch.
- **`package.json`** — new `submitter` and `submitter:dry-run` scripts.
- Regenerated contract bindings, PAPI metadata, state-diff, and storage
layout snapshots.
### Test plan
- [x] `forge test` — passes, including new `ValidatorSetSubmitter.t.sol`
- [x] `cargo test` — passes, including new era-validation tests in
`external-validators`
- [x] `bun test:e2e` — validator-set-update suite passes with
era-targeted flow
- [x] Manual: run submitter daemon against local network (`bun
submitter`), verify it submits once per era at the correct session
## ⚠️ Breaking Changes ⚠️
- **`sendNewValidatorSet` removed** — replaced by
`sendNewValidatorSetForEra(uint64 targetEra, ...)`. Callers must now
supply a `targetEra` parameter.
- **Access control changed** — validator set submission is now
restricted to the `validatorSetSubmitter` role instead of the contract
`owner`. The submitter address is set during `initialize` and rotatable
via `setValidatorSetSubmitter` (owner-only).
- **`external-validators` pallet now validates `targetEra`** — messages
with a stale, duplicate, or out-of-range `external_index` are rejected
on-chain. Existing integrations sending `external_index: 0` will fail
validation.
---------
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-02-20 09:31:44 +00:00
|
|
|
serviceManager.setValidatorSetSubmitter(testSubmitter);
|
2026-02-05 11:08:35 +00:00
|
|
|
vm.stopPrank();
|
|
|
|
|
|
|
|
|
|
// 2. Record state before upgrade
|
|
|
|
|
bool allowlistBefore = serviceManager.validatorsAllowlist(testValidator);
|
|
|
|
|
address rewardsInitiatorBefore = serviceManager.rewardsInitiator();
|
|
|
|
|
address ownerBefore = serviceManager.owner();
|
|
|
|
|
address gatewayBefore = serviceManager.snowbridgeGateway();
|
feat: automated validator set submission with era targeting (#433)
## Era-targeted validator set submission with dedicated submitter role
> **Note:** This PR includes a detailed specification at
[`specs/validator-set-submission/validator-set-submission.md`](https://github.com/datahaven-xyz/datahaven/blob/feat/validator-set-submitter/specs/validator-set-submission/validator-set-submission.md)
that covers the design rationale, submission lifecycle, era-targeting
rules, and failure modes. Reading the spec first will make the contract,
pallet, and daemon changes easier to follow.
### Summary
- Introduce a dedicated `validatorSetSubmitter` role on
`DataHavenServiceManager`, separating validator set submission authority
from the contract owner
- Replace the unscoped `sendNewValidatorSet` with
`sendNewValidatorSetForEra`, which encodes a `targetEra` into the
Snowbridge message payload
- Add server-side era validation in the `external-validators` pallet to
reject stale, duplicate, or out-of-range submissions
- Add a long-running TypeScript daemon that watches session changes and
automatically submits each era's validator set at the right time
### Contract changes (`contracts/`)
- **New `validatorSetSubmitter` storage slot** — set during `initialize`
and rotatable via `setValidatorSetSubmitter` (owner-only). The storage
gap is decremented accordingly.
- **`sendNewValidatorSet` → `sendNewValidatorSetForEra`** — accepts a
`uint64 targetEra` parameter and is restricted to
`onlyValidatorSetSubmitter` instead of `onlyOwner`.
- **`buildNewValidatorSetMessageForEra`** — the
`NewValidatorSetPayload.externalIndex` is now caller-supplied instead of
hardcoded to `0`.
- **New events** — `ValidatorSetSubmitterUpdated`,
`ValidatorSetMessageSubmitted`.
- **New error** — `OnlyValidatorSetSubmitter`.
- **New test suite** — `ValidatorSetSubmitter.t.sol` covering submitter
set/rotate, access control, era encoding, and legacy function removal.
### Pallet changes (`operator/`)
- **`validate_target_era`** in `external-validators` — enforces
`activeEra < targetEra <= activeEra + 1` and `targetEra > ExternalIndex`
(dedup guard).
- **New errors** — `TargetEraTooOld`, `TargetEraTooNew`,
`DuplicateOrStaleTargetEra`.
- **Tests** — five new test cases for era boundary conditions (next-era
acceptance, old-era rejection, too-new rejection, duplicate rejection,
genesis behavior). Existing `era_hooks_with_external_index` test updated
to use valid target eras.
- **Runtime test fixes** — `external_index: 0` → `1` in
mainnet/stagenet/testnet EigenLayer message processor tests to satisfy
the new validation.
### Validator set submitter daemon
(`test/tools/validator-set-submitter/`)
- Event-driven service that subscribes to finalized
`Session.CurrentIndex` via Polkadot-API `watchValue`.
- Submits once per era during the last session, targeting `ActiveEra +
1`.
- Tracks submitted eras to avoid duplicates; skips if `ExternalIndex`
already covers the target.
- Startup self-checks: Ethereum connectivity, DataHaven connectivity,
on-chain submitter authorization.
- Supports `--dry-run` mode and YAML configuration.
- Graceful shutdown on `SIGINT`/`SIGTERM`.
### Test & tooling updates
- **E2E test** (`validator-set-update.test.ts`) — calls
`sendNewValidatorSetForEra` with a computed `targetEra` and filters the
substrate event by `external_index`.
- **`update-validator-set.ts` script** — accepts `--target-era` flag;
defaults to era 1 for fresh networks.
- **CLI launch** — wires validator set update as an interactive step
after relayer launch.
- **`package.json`** — new `submitter` and `submitter:dry-run` scripts.
- Regenerated contract bindings, PAPI metadata, state-diff, and storage
layout snapshots.
### Test plan
- [x] `forge test` — passes, including new `ValidatorSetSubmitter.t.sol`
- [x] `cargo test` — passes, including new era-validation tests in
`external-validators`
- [x] `bun test:e2e` — validator-set-update suite passes with
era-targeted flow
- [x] Manual: run submitter daemon against local network (`bun
submitter`), verify it submits once per era at the correct session
## ⚠️ Breaking Changes ⚠️
- **`sendNewValidatorSet` removed** — replaced by
`sendNewValidatorSetForEra(uint64 targetEra, ...)`. Callers must now
supply a `targetEra` parameter.
- **Access control changed** — validator set submission is now
restricted to the `validatorSetSubmitter` role instead of the contract
`owner`. The submitter address is set during `initialize` and rotatable
via `setValidatorSetSubmitter` (owner-only).
- **`external-validators` pallet now validates `targetEra`** — messages
with a stale, duplicate, or out-of-range `external_index` are rejected
on-chain. Existing integrations sending `external_index: 0` will fail
validation.
---------
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-02-20 09:31:44 +00:00
|
|
|
address submitterBefore = serviceManager.validatorSetSubmitter();
|
2026-02-05 11:08:35 +00:00
|
|
|
|
|
|
|
|
// 3. Deploy new implementation
|
|
|
|
|
DataHavenServiceManager newImpl =
|
|
|
|
|
new DataHavenServiceManager(rewardsCoordinator, allocationManager);
|
|
|
|
|
|
|
|
|
|
// 4. Upgrade proxy
|
|
|
|
|
vm.prank(proxyAdminOwner);
|
|
|
|
|
proxyAdmin.upgrade(ITransparentUpgradeableProxy(address(serviceManager)), address(newImpl));
|
|
|
|
|
|
|
|
|
|
// 5. Verify state preserved
|
|
|
|
|
assertEq(
|
|
|
|
|
serviceManager.validatorsAllowlist(testValidator),
|
|
|
|
|
allowlistBefore,
|
|
|
|
|
"validatorsAllowlist should be preserved"
|
|
|
|
|
);
|
|
|
|
|
assertEq(
|
|
|
|
|
serviceManager.rewardsInitiator(),
|
|
|
|
|
rewardsInitiatorBefore,
|
|
|
|
|
"rewardsInitiator should be preserved"
|
|
|
|
|
);
|
|
|
|
|
assertEq(serviceManager.owner(), ownerBefore, "owner should be preserved");
|
|
|
|
|
assertEq(
|
|
|
|
|
serviceManager.snowbridgeGateway(),
|
|
|
|
|
gatewayBefore,
|
|
|
|
|
"snowbridgeGateway should be preserved"
|
|
|
|
|
);
|
feat: automated validator set submission with era targeting (#433)
## Era-targeted validator set submission with dedicated submitter role
> **Note:** This PR includes a detailed specification at
[`specs/validator-set-submission/validator-set-submission.md`](https://github.com/datahaven-xyz/datahaven/blob/feat/validator-set-submitter/specs/validator-set-submission/validator-set-submission.md)
that covers the design rationale, submission lifecycle, era-targeting
rules, and failure modes. Reading the spec first will make the contract,
pallet, and daemon changes easier to follow.
### Summary
- Introduce a dedicated `validatorSetSubmitter` role on
`DataHavenServiceManager`, separating validator set submission authority
from the contract owner
- Replace the unscoped `sendNewValidatorSet` with
`sendNewValidatorSetForEra`, which encodes a `targetEra` into the
Snowbridge message payload
- Add server-side era validation in the `external-validators` pallet to
reject stale, duplicate, or out-of-range submissions
- Add a long-running TypeScript daemon that watches session changes and
automatically submits each era's validator set at the right time
### Contract changes (`contracts/`)
- **New `validatorSetSubmitter` storage slot** — set during `initialize`
and rotatable via `setValidatorSetSubmitter` (owner-only). The storage
gap is decremented accordingly.
- **`sendNewValidatorSet` → `sendNewValidatorSetForEra`** — accepts a
`uint64 targetEra` parameter and is restricted to
`onlyValidatorSetSubmitter` instead of `onlyOwner`.
- **`buildNewValidatorSetMessageForEra`** — the
`NewValidatorSetPayload.externalIndex` is now caller-supplied instead of
hardcoded to `0`.
- **New events** — `ValidatorSetSubmitterUpdated`,
`ValidatorSetMessageSubmitted`.
- **New error** — `OnlyValidatorSetSubmitter`.
- **New test suite** — `ValidatorSetSubmitter.t.sol` covering submitter
set/rotate, access control, era encoding, and legacy function removal.
### Pallet changes (`operator/`)
- **`validate_target_era`** in `external-validators` — enforces
`activeEra < targetEra <= activeEra + 1` and `targetEra > ExternalIndex`
(dedup guard).
- **New errors** — `TargetEraTooOld`, `TargetEraTooNew`,
`DuplicateOrStaleTargetEra`.
- **Tests** — five new test cases for era boundary conditions (next-era
acceptance, old-era rejection, too-new rejection, duplicate rejection,
genesis behavior). Existing `era_hooks_with_external_index` test updated
to use valid target eras.
- **Runtime test fixes** — `external_index: 0` → `1` in
mainnet/stagenet/testnet EigenLayer message processor tests to satisfy
the new validation.
### Validator set submitter daemon
(`test/tools/validator-set-submitter/`)
- Event-driven service that subscribes to finalized
`Session.CurrentIndex` via Polkadot-API `watchValue`.
- Submits once per era during the last session, targeting `ActiveEra +
1`.
- Tracks submitted eras to avoid duplicates; skips if `ExternalIndex`
already covers the target.
- Startup self-checks: Ethereum connectivity, DataHaven connectivity,
on-chain submitter authorization.
- Supports `--dry-run` mode and YAML configuration.
- Graceful shutdown on `SIGINT`/`SIGTERM`.
### Test & tooling updates
- **E2E test** (`validator-set-update.test.ts`) — calls
`sendNewValidatorSetForEra` with a computed `targetEra` and filters the
substrate event by `external_index`.
- **`update-validator-set.ts` script** — accepts `--target-era` flag;
defaults to era 1 for fresh networks.
- **CLI launch** — wires validator set update as an interactive step
after relayer launch.
- **`package.json`** — new `submitter` and `submitter:dry-run` scripts.
- Regenerated contract bindings, PAPI metadata, state-diff, and storage
layout snapshots.
### Test plan
- [x] `forge test` — passes, including new `ValidatorSetSubmitter.t.sol`
- [x] `cargo test` — passes, including new era-validation tests in
`external-validators`
- [x] `bun test:e2e` — validator-set-update suite passes with
era-targeted flow
- [x] Manual: run submitter daemon against local network (`bun
submitter`), verify it submits once per era at the correct session
## ⚠️ Breaking Changes ⚠️
- **`sendNewValidatorSet` removed** — replaced by
`sendNewValidatorSetForEra(uint64 targetEra, ...)`. Callers must now
supply a `targetEra` parameter.
- **Access control changed** — validator set submission is now
restricted to the `validatorSetSubmitter` role instead of the contract
`owner`. The submitter address is set during `initialize` and rotatable
via `setValidatorSetSubmitter` (owner-only).
- **`external-validators` pallet now validates `targetEra`** — messages
with a stale, duplicate, or out-of-range `external_index` are rejected
on-chain. Existing integrations sending `external_index: 0` will fail
validation.
---------
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-02-20 09:31:44 +00:00
|
|
|
assertEq(
|
|
|
|
|
serviceManager.validatorSetSubmitter(),
|
|
|
|
|
submitterBefore,
|
|
|
|
|
"validatorSetSubmitter should be preserved"
|
|
|
|
|
);
|
2026-02-05 11:08:35 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// @notice Verifies validatorEthAddressToSolochainAddress mapping is preserved
|
|
|
|
|
function test_upgradePreservesValidatorMappings() public {
|
|
|
|
|
address testValidator = address(0xABCD);
|
|
|
|
|
address testSolochainAddress = address(0xDEF0);
|
|
|
|
|
|
|
|
|
|
// Add validator to allowlist first
|
|
|
|
|
vm.prank(avsOwner);
|
|
|
|
|
serviceManager.addValidatorToAllowlist(testValidator);
|
|
|
|
|
|
|
|
|
|
// Register operator via allocationManager to set the solochain address mapping
|
|
|
|
|
uint32[] memory operatorSetIds = new uint32[](1);
|
|
|
|
|
operatorSetIds[0] = 0; // VALIDATORS_SET_ID
|
|
|
|
|
|
|
|
|
|
vm.prank(address(allocationManager));
|
|
|
|
|
serviceManager.registerOperator(
|
|
|
|
|
testValidator,
|
|
|
|
|
address(serviceManager),
|
|
|
|
|
operatorSetIds,
|
|
|
|
|
abi.encodePacked(testSolochainAddress)
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
// Record state before upgrade
|
|
|
|
|
bool inAllowlistBefore = serviceManager.validatorsAllowlist(testValidator);
|
|
|
|
|
address solochainAddressBefore =
|
|
|
|
|
serviceManager.validatorEthAddressToSolochainAddress(testValidator);
|
fix: 🩹 map validator address to operator address for rewards & slashes (#441)
## Summary
Slashing and rewards submissions were submitted through the bridge with
their **solochain address** , while EigenLayer expects the **ethereum
operator address**, the addresses were not being translated, so the
protocol was broken.
This PR adds a **reverse mapping** (Solochain address → Eth address) and
uses it in both the slashing and rewards paths so that:
- `slashValidatorsOperator` accepts requests where `operator` is a
Solochain address and translates it to the Eth operator before calling
EigenLayer.
- `submitRewards` translates each `operatorRewards[].operator` from
Solochain to Eth before calling the RewardsCoordinator.
- Unknown or unmapped solochain addresses cause a revert
(`UnknownSolochainAddress`) instead of silently failing.
## What's changed
### DataHavenServiceManager
- **Reverse mapping**: `mapping(address => address) public
validatorSolochainAddressToEthAddress` (Solochain → Eth), with `__GAP`
reduced by one slot for upgradeable layout.
- **Helper**: `_ethOperatorFromSolochain(address)` – returns Eth
operator for a Solochain address, reverts with
`UnknownSolochainAddress()` if unmapped.
- **Registration / lifecycle**:
- `registerOperator`: populates both forward and reverse mappings;
enforces uniqueness (one Solochain per operator) and clears old reverse
entry when an operator re-registers with a new Solochain.
- `deregisterOperator`: clears both forward and reverse entries.
- `updateSolochainAddressForValidator`: updates both mappings, enforces
uniqueness and clears the previous Solochain's reverse entry.
- **Slashing**: `slashValidatorsOperator` uses
`_ethOperatorFromSolochain(slashings[i].operator)` so requests keyed by
Solochain address are translated before calling EigenLayer.
- **Rewards**: `submitRewards` builds a translated copy of the
submission with each `operatorRewards[].operator` set via
`_ethOperatorFromSolochain(...)`; unmapped addresses revert.
### IDataHavenServiceManager
- New getter: `validatorSolochainAddressToEthAddress(address solochain)
external view returns (address)`.
- New errors: `UnknownSolochainAddress()`,
`SolochainAddressAlreadyAssigned()`.
### Storage and fixtures
- Storage snapshot updated for the new state variable.
- `DataHavenServiceManagerBadLayout.sol` updated (reverse mapping + gap)
for layout negative tests.
- Storage layout test extended to assert the reverse mapping is
preserved across proxy upgrade.
### Tests
- **Slashing.t.sol**: Slashing with Solochain address (translation and
emit of Eth operator); negative test for unmapped Solochain reverting
with `UnknownSolochainAddress()`.
- **RewardsSubmitter.t.sol**: Rewards submission with Solochain
addresses (translation to Eth in RewardsCoordinator calldata); negative
test for unmapped Solochain.
- **StorageLayout.t.sol**: Reverse mapping preserved after upgrade.
- **OperatorAddressMappings.t.sol** (new): Uniqueness (Solochain already
assigned to another operator), update/deregister clearing reverse
mapping, and getter behaviour.
## Testing
- **Unit tests**: `forge test` from `contracts/` (all existing and new
tests pass).
- **Storage**:
- `./scripts/check-storage-layout.sh`
- `./scripts/check-storage-layout-negative.sh`
- **Coverage**: Slashing path (Solochain → Eth translation + revert),
rewards path (translation + revert), registration/update/deregister
(reverse mapping and uniqueness), and storage layout upgrade
preservation.
---------
Co-authored-by: Ahmad Kaouk <56095276+ahmadkaouk@users.noreply.github.com>
Co-authored-by: Ahmad Kaouk <ahmadkaouk.93@gmail.com>
2026-02-18 19:38:13 +00:00
|
|
|
address ethOperatorBefore =
|
|
|
|
|
serviceManager.validatorSolochainAddressToEthAddress(testSolochainAddress);
|
2026-02-05 11:08:35 +00:00
|
|
|
|
|
|
|
|
// Verify the mapping was set correctly before upgrade
|
|
|
|
|
assertEq(solochainAddressBefore, testSolochainAddress, "Solochain address should be set");
|
fix: 🩹 map validator address to operator address for rewards & slashes (#441)
## Summary
Slashing and rewards submissions were submitted through the bridge with
their **solochain address** , while EigenLayer expects the **ethereum
operator address**, the addresses were not being translated, so the
protocol was broken.
This PR adds a **reverse mapping** (Solochain address → Eth address) and
uses it in both the slashing and rewards paths so that:
- `slashValidatorsOperator` accepts requests where `operator` is a
Solochain address and translates it to the Eth operator before calling
EigenLayer.
- `submitRewards` translates each `operatorRewards[].operator` from
Solochain to Eth before calling the RewardsCoordinator.
- Unknown or unmapped solochain addresses cause a revert
(`UnknownSolochainAddress`) instead of silently failing.
## What's changed
### DataHavenServiceManager
- **Reverse mapping**: `mapping(address => address) public
validatorSolochainAddressToEthAddress` (Solochain → Eth), with `__GAP`
reduced by one slot for upgradeable layout.
- **Helper**: `_ethOperatorFromSolochain(address)` – returns Eth
operator for a Solochain address, reverts with
`UnknownSolochainAddress()` if unmapped.
- **Registration / lifecycle**:
- `registerOperator`: populates both forward and reverse mappings;
enforces uniqueness (one Solochain per operator) and clears old reverse
entry when an operator re-registers with a new Solochain.
- `deregisterOperator`: clears both forward and reverse entries.
- `updateSolochainAddressForValidator`: updates both mappings, enforces
uniqueness and clears the previous Solochain's reverse entry.
- **Slashing**: `slashValidatorsOperator` uses
`_ethOperatorFromSolochain(slashings[i].operator)` so requests keyed by
Solochain address are translated before calling EigenLayer.
- **Rewards**: `submitRewards` builds a translated copy of the
submission with each `operatorRewards[].operator` set via
`_ethOperatorFromSolochain(...)`; unmapped addresses revert.
### IDataHavenServiceManager
- New getter: `validatorSolochainAddressToEthAddress(address solochain)
external view returns (address)`.
- New errors: `UnknownSolochainAddress()`,
`SolochainAddressAlreadyAssigned()`.
### Storage and fixtures
- Storage snapshot updated for the new state variable.
- `DataHavenServiceManagerBadLayout.sol` updated (reverse mapping + gap)
for layout negative tests.
- Storage layout test extended to assert the reverse mapping is
preserved across proxy upgrade.
### Tests
- **Slashing.t.sol**: Slashing with Solochain address (translation and
emit of Eth operator); negative test for unmapped Solochain reverting
with `UnknownSolochainAddress()`.
- **RewardsSubmitter.t.sol**: Rewards submission with Solochain
addresses (translation to Eth in RewardsCoordinator calldata); negative
test for unmapped Solochain.
- **StorageLayout.t.sol**: Reverse mapping preserved after upgrade.
- **OperatorAddressMappings.t.sol** (new): Uniqueness (Solochain already
assigned to another operator), update/deregister clearing reverse
mapping, and getter behaviour.
## Testing
- **Unit tests**: `forge test` from `contracts/` (all existing and new
tests pass).
- **Storage**:
- `./scripts/check-storage-layout.sh`
- `./scripts/check-storage-layout-negative.sh`
- **Coverage**: Slashing path (Solochain → Eth translation + revert),
rewards path (translation + revert), registration/update/deregister
(reverse mapping and uniqueness), and storage layout upgrade
preservation.
---------
Co-authored-by: Ahmad Kaouk <56095276+ahmadkaouk@users.noreply.github.com>
Co-authored-by: Ahmad Kaouk <ahmadkaouk.93@gmail.com>
2026-02-18 19:38:13 +00:00
|
|
|
assertEq(ethOperatorBefore, testValidator, "Eth operator should be set");
|
2026-02-05 11:08:35 +00:00
|
|
|
|
|
|
|
|
// Deploy new implementation and upgrade
|
|
|
|
|
DataHavenServiceManager newImpl =
|
|
|
|
|
new DataHavenServiceManager(rewardsCoordinator, allocationManager);
|
|
|
|
|
|
|
|
|
|
vm.prank(proxyAdminOwner);
|
|
|
|
|
proxyAdmin.upgrade(ITransparentUpgradeableProxy(address(serviceManager)), address(newImpl));
|
|
|
|
|
|
|
|
|
|
// Verify both mappings preserved after upgrade
|
|
|
|
|
assertEq(
|
|
|
|
|
serviceManager.validatorsAllowlist(testValidator),
|
|
|
|
|
inAllowlistBefore,
|
|
|
|
|
"validatorsAllowlist mapping should be preserved after upgrade"
|
|
|
|
|
);
|
|
|
|
|
assertEq(
|
|
|
|
|
serviceManager.validatorEthAddressToSolochainAddress(testValidator),
|
|
|
|
|
solochainAddressBefore,
|
|
|
|
|
"validatorEthAddressToSolochainAddress mapping should be preserved after upgrade"
|
|
|
|
|
);
|
|
|
|
|
assertEq(
|
|
|
|
|
serviceManager.validatorEthAddressToSolochainAddress(testValidator),
|
|
|
|
|
testSolochainAddress,
|
|
|
|
|
"validatorEthAddressToSolochainAddress should have correct value after upgrade"
|
|
|
|
|
);
|
fix: 🩹 map validator address to operator address for rewards & slashes (#441)
## Summary
Slashing and rewards submissions were submitted through the bridge with
their **solochain address** , while EigenLayer expects the **ethereum
operator address**, the addresses were not being translated, so the
protocol was broken.
This PR adds a **reverse mapping** (Solochain address → Eth address) and
uses it in both the slashing and rewards paths so that:
- `slashValidatorsOperator` accepts requests where `operator` is a
Solochain address and translates it to the Eth operator before calling
EigenLayer.
- `submitRewards` translates each `operatorRewards[].operator` from
Solochain to Eth before calling the RewardsCoordinator.
- Unknown or unmapped solochain addresses cause a revert
(`UnknownSolochainAddress`) instead of silently failing.
## What's changed
### DataHavenServiceManager
- **Reverse mapping**: `mapping(address => address) public
validatorSolochainAddressToEthAddress` (Solochain → Eth), with `__GAP`
reduced by one slot for upgradeable layout.
- **Helper**: `_ethOperatorFromSolochain(address)` – returns Eth
operator for a Solochain address, reverts with
`UnknownSolochainAddress()` if unmapped.
- **Registration / lifecycle**:
- `registerOperator`: populates both forward and reverse mappings;
enforces uniqueness (one Solochain per operator) and clears old reverse
entry when an operator re-registers with a new Solochain.
- `deregisterOperator`: clears both forward and reverse entries.
- `updateSolochainAddressForValidator`: updates both mappings, enforces
uniqueness and clears the previous Solochain's reverse entry.
- **Slashing**: `slashValidatorsOperator` uses
`_ethOperatorFromSolochain(slashings[i].operator)` so requests keyed by
Solochain address are translated before calling EigenLayer.
- **Rewards**: `submitRewards` builds a translated copy of the
submission with each `operatorRewards[].operator` set via
`_ethOperatorFromSolochain(...)`; unmapped addresses revert.
### IDataHavenServiceManager
- New getter: `validatorSolochainAddressToEthAddress(address solochain)
external view returns (address)`.
- New errors: `UnknownSolochainAddress()`,
`SolochainAddressAlreadyAssigned()`.
### Storage and fixtures
- Storage snapshot updated for the new state variable.
- `DataHavenServiceManagerBadLayout.sol` updated (reverse mapping + gap)
for layout negative tests.
- Storage layout test extended to assert the reverse mapping is
preserved across proxy upgrade.
### Tests
- **Slashing.t.sol**: Slashing with Solochain address (translation and
emit of Eth operator); negative test for unmapped Solochain reverting
with `UnknownSolochainAddress()`.
- **RewardsSubmitter.t.sol**: Rewards submission with Solochain
addresses (translation to Eth in RewardsCoordinator calldata); negative
test for unmapped Solochain.
- **StorageLayout.t.sol**: Reverse mapping preserved after upgrade.
- **OperatorAddressMappings.t.sol** (new): Uniqueness (Solochain already
assigned to another operator), update/deregister clearing reverse
mapping, and getter behaviour.
## Testing
- **Unit tests**: `forge test` from `contracts/` (all existing and new
tests pass).
- **Storage**:
- `./scripts/check-storage-layout.sh`
- `./scripts/check-storage-layout-negative.sh`
- **Coverage**: Slashing path (Solochain → Eth translation + revert),
rewards path (translation + revert), registration/update/deregister
(reverse mapping and uniqueness), and storage layout upgrade
preservation.
---------
Co-authored-by: Ahmad Kaouk <56095276+ahmadkaouk@users.noreply.github.com>
Co-authored-by: Ahmad Kaouk <ahmadkaouk.93@gmail.com>
2026-02-18 19:38:13 +00:00
|
|
|
assertEq(
|
|
|
|
|
serviceManager.validatorSolochainAddressToEthAddress(testSolochainAddress),
|
|
|
|
|
ethOperatorBefore,
|
|
|
|
|
"validatorSolochainAddressToEthAddress mapping should be preserved after upgrade"
|
|
|
|
|
);
|
|
|
|
|
assertEq(
|
|
|
|
|
serviceManager.validatorSolochainAddressToEthAddress(testSolochainAddress),
|
|
|
|
|
testValidator,
|
|
|
|
|
"validatorSolochainAddressToEthAddress should have correct value after upgrade"
|
|
|
|
|
);
|
2026-02-05 11:08:35 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// @notice Verifies multiple validators in allowlist are preserved
|
|
|
|
|
function test_upgradePreservesMultipleValidators() public {
|
|
|
|
|
address[] memory validators = new address[](3);
|
|
|
|
|
validators[0] = address(0x1111);
|
|
|
|
|
validators[1] = address(0x2222);
|
|
|
|
|
validators[2] = address(0x3333);
|
|
|
|
|
|
|
|
|
|
// Add multiple validators
|
|
|
|
|
vm.startPrank(avsOwner);
|
|
|
|
|
for (uint256 i = 0; i < validators.length; i++) {
|
|
|
|
|
serviceManager.addValidatorToAllowlist(validators[i]);
|
|
|
|
|
}
|
|
|
|
|
vm.stopPrank();
|
|
|
|
|
|
|
|
|
|
// Deploy new implementation and upgrade
|
|
|
|
|
DataHavenServiceManager newImpl =
|
|
|
|
|
new DataHavenServiceManager(rewardsCoordinator, allocationManager);
|
|
|
|
|
|
|
|
|
|
vm.prank(proxyAdminOwner);
|
|
|
|
|
proxyAdmin.upgrade(ITransparentUpgradeableProxy(address(serviceManager)), address(newImpl));
|
|
|
|
|
|
|
|
|
|
// Verify all validators still in allowlist
|
|
|
|
|
for (uint256 i = 0; i < validators.length; i++) {
|
|
|
|
|
assertTrue(
|
|
|
|
|
serviceManager.validatorsAllowlist(validators[i]),
|
|
|
|
|
"All validators should remain in allowlist after upgrade"
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// @notice Verifies that upgrade doesn't affect functionality
|
|
|
|
|
function test_functionalityAfterUpgrade() public {
|
|
|
|
|
// Deploy new implementation and upgrade
|
|
|
|
|
DataHavenServiceManager newImpl =
|
|
|
|
|
new DataHavenServiceManager(rewardsCoordinator, allocationManager);
|
|
|
|
|
|
|
|
|
|
vm.prank(proxyAdminOwner);
|
|
|
|
|
proxyAdmin.upgrade(ITransparentUpgradeableProxy(address(serviceManager)), address(newImpl));
|
|
|
|
|
|
|
|
|
|
// Verify functionality still works
|
|
|
|
|
address newValidator = address(0xBEEF);
|
|
|
|
|
|
|
|
|
|
vm.prank(avsOwner);
|
|
|
|
|
serviceManager.addValidatorToAllowlist(newValidator);
|
|
|
|
|
|
|
|
|
|
assertTrue(
|
|
|
|
|
serviceManager.validatorsAllowlist(newValidator),
|
|
|
|
|
"Should be able to add validators after upgrade"
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
vm.prank(avsOwner);
|
|
|
|
|
serviceManager.removeValidatorFromAllowlist(newValidator);
|
|
|
|
|
|
|
|
|
|
assertFalse(
|
|
|
|
|
serviceManager.validatorsAllowlist(newValidator),
|
|
|
|
|
"Should be able to remove validators after upgrade"
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
}
|