mirror of
https://github.com/datahaven-xyz/datahaven
synced 2026-05-24 09:50:01 +00:00
## 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>
186 lines
6.2 KiB
TypeScript
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");
|
|
}
|
|
};
|