datahaven/test/cli/handlers/launch/index.ts
Ahmad Kaouk 401f646286
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 10:31:44 +01:00

186 lines
6.2 KiB
TypeScript

import type { Command } from "@commander-js/extra-typings";
import { logger } from "utils";
import { DEFAULT_SUBSTRATE_WS_PORT } from "utils/constants";
import { createParameterCollection } from "utils/parameters";
import { getBlockscoutUrl } from "../../../launcher/kurtosis";
import { LaunchedNetwork } from "../../../launcher/types/launchedNetwork";
import { updateParameters } from "../../../scripts/deploy-contracts";
import { checkBaseDependencies } from "../common/checks";
import { deployContracts } from "./contracts";
import { launchDataHavenSolochain } from "./datahaven";
import { launchKurtosis } from "./kurtosis";
import { setParametersFromCollection } from "./parameters";
import { launchRelayers } from "./relayer";
import { launchStorageHubComponents } from "./storagehub";
import { performSummaryOperations } from "./summary";
import { performValidatorOperations, performValidatorSetUpdate } from "./validator";
export const NETWORK_ID = "cli-launch";
export interface NetworkOptions {
networkId: string;
dhInternalPort?: number;
}
export const CLI_NETWORK_OPTIONS: NetworkOptions = {
networkId: NETWORK_ID,
dhInternalPort: DEFAULT_SUBSTRATE_WS_PORT
};
// Non-optional properties should have default values set by the CLI
export interface LaunchOptions {
all?: boolean;
datahaven?: boolean;
buildDatahaven?: boolean;
datahavenBuildExtraArgs: string;
datahavenImageTag: string;
launchKurtosis?: boolean;
kurtosisEnclaveName: string;
slotTime?: number;
kurtosisNetworkArgs?: string;
verified?: boolean;
blockscout?: boolean;
deployContracts?: boolean;
fundValidators?: boolean;
setupValidators?: boolean;
updateValidatorSet?: boolean;
setParameters?: boolean;
relayer?: boolean;
relayerImageTag: string;
storagehub?: boolean;
cleanNetwork?: boolean;
injectContracts?: boolean;
}
// ===== Launch Handler Functions =====
const launchFunction = async (options: LaunchOptions, launchedNetwork: LaunchedNetwork) => {
logger.debug("Running with options:");
logger.debug(options);
const timeStart = performance.now();
await checkBaseDependencies();
// Create parameter collection to be used throughout the launch process
const parameterCollection = await createParameterCollection();
await launchDataHavenSolochain(options, launchedNetwork);
// Default injectContracts to true if not specified
const injectContracts = options.injectContracts !== undefined ? options.injectContracts : true;
await launchKurtosis({ ...options, injectContracts }, launchedNetwork);
logger.trace("Checking if Blockscout is enabled...");
let blockscoutBackendUrl: string | undefined;
if (options.blockscout === true) {
blockscoutBackendUrl = await getBlockscoutUrl(options.kurtosisEnclaveName);
logger.trace("Blockscout backend URL:", blockscoutBackendUrl);
} else if (options.verified) {
logger.warn(
"⚠️ Contract verification (--verified) requested, but Blockscout is disabled (--no-blockscout). Verification will be skipped."
);
}
// skip deploying contracts if we have injected it
let contractsDeployed = false;
if (options.deployContracts && !options.injectContracts) {
contractsDeployed = await deployContracts({
rpcUrl: launchedNetwork.elRpcUrl,
verified: options.verified,
blockscoutBackendUrl,
deployContracts: options.deployContracts,
parameterCollection
});
await performValidatorOperations(options, launchedNetwork.elRpcUrl, contractsDeployed);
} else {
// We are injecting contracts but we still need the addresses
await updateParameters(parameterCollection);
}
await setParametersFromCollection({
launchedNetwork,
collection: parameterCollection,
setParameters: options.setParameters
});
await launchRelayers(options, launchedNetwork);
await performValidatorSetUpdate(options, launchedNetwork.elRpcUrl, contractsDeployed);
await launchStorageHubComponents(options, launchedNetwork);
await performSummaryOperations(options, launchedNetwork);
const fullEnd = performance.now();
const fullMinutes = ((fullEnd - timeStart) / (1000 * 60)).toFixed(1);
logger.success(`Launch function completed successfully in ${fullMinutes} minutes`);
};
export const launch = async (options: LaunchOptions) => {
const run = new LaunchedNetwork();
await launchFunction(options, run);
};
export const launchPreActionHook = (
thisCmd: Command<[], LaunchOptions & { [key: string]: any }>
) => {
const {
all,
blockscout,
verified,
fundValidators,
setupValidators,
deployContracts,
datahaven,
buildDatahaven,
launchKurtosis,
relayer,
setParameters,
storagehub
} = thisCmd.opts();
// Check for conflicts with --all flag
if (
all &&
(datahaven === false ||
buildDatahaven === false ||
launchKurtosis === false ||
deployContracts === false ||
fundValidators === false ||
setupValidators === false ||
setParameters === false ||
relayer === false ||
storagehub === false)
) {
thisCmd.error(
"--all cannot be used with --no-datahaven, --no-build-datahaven, --no-launch-kurtosis, --no-deploy-contracts, --no-fund-validators, --no-setup-validators, --no-update-validator-set, --no-set-parameters, --no-relayer, or --no-storagehub"
);
}
// If --all is set, enable all components
if (all) {
thisCmd.setOptionValue("datahaven", true);
thisCmd.setOptionValue("buildDatahaven", true);
thisCmd.setOptionValue("launchKurtosis", true);
thisCmd.setOptionValue("deployContracts", true);
thisCmd.setOptionValue("fundValidators", true);
thisCmd.setOptionValue("setupValidators", true);
thisCmd.setOptionValue("setParameters", true);
thisCmd.setOptionValue("relayer", true);
thisCmd.setOptionValue("storagehub", true);
thisCmd.setOptionValue("cleanNetwork", true);
}
if (verified && !blockscout) {
thisCmd.error("--verified requires --blockscout to be set");
}
if (deployContracts === false && setupValidators) {
thisCmd.error("--setupValidators requires --deployContracts to be set");
}
if (deployContracts === false && fundValidators) {
thisCmd.error("--fundValidators requires --deployContracts to be set");
}
};