datahaven/test/scripts/setup-validators.ts
Ahmad Kaouk 41788d56bb
test: refactor e2e tests (#365)
This PR significantly refactors and improves the end-to-end testing
framework and infrastructure. The primary focus was on simplifying the
test suites, improving reliability through better resource management,
and hardening the relayer infrastructure.

All E2E tests are now passing on the CI and demonstrate consistent
reliability when run locally.

### Key Changes

#### 1. E2E Test Suite Refactor & Cleanup
* **Simplified Test Logic**: Heavily refactored the core test suites
(`native-token-transfer.test.ts`, `rewards-message.test.ts`, and
`validator-set-update.test.ts`). The new implementation is much cleaner,
utilizing shared helpers to reduce boilerplate.
* **Utility Consolidation**: Removed redundant utility files
(`storage.ts`, `rewards-helpers.ts`) and simplified `events.ts`. Event
waiting now uses `rxjs` for Substrate and native `viem` watchers for
Ethereum, which is more robust and easier to maintain.
* **Better Connector Management**: Unified the creation and cleanup of
test clients in `ConnectorFactory`. It now handles the lifecycle of
WebSocket connections more gracefully, including clearing the
`socketClientCache` to prevent reconnection noise during teardown.

#### 2. Infrastructure & Stability
* **Relayer Relaunch Policy**: Added a restart policy for Snowbridge
relayer containers. They are now configured with `--restart
on-failure:5`, ensuring that relayers automatically relaunch if they
crash during the sensitive initialization phase.
*   **WebSocket Integration**: 
* Updated the `ConnectorFactory` to prefer **WebSockets** for the
Ethereum public client, which is essential for efficient, event-heavy
E2E testing.
* Enhanced `launchKurtosisNetwork` to correctly identify and register
the Execution Layer's WebSocket endpoint from Kurtosis.
* **Disabled Contract Injection**: This PR temporarily disables the
automatic injection of contracts into the genesis state by default.
* *Reason*: I encountered issues generating a valid `state-diff.json`
for the latest contract versions. Even after applying several
workarounds, the injected state remained unstable. As a result, I've
reverted to manual contract deployment during the launch sequence for
better reliability for now.

#### 3. Documentation & Maintenance
* Removed obsolete documentation (`event-utilities-guide.md`) that no
longer reflects the simplified event-handling API.
* Cleaned up `test/launcher/validators.ts` and moved logic into more
appropriate helpers.

---------

Co-authored-by: Steve Degosserie <723552+stiiifff@users.noreply.github.com>
2025-12-24 13:31:40 +01:00

190 lines
7.2 KiB
TypeScript

import fs from "node:fs";
import path from "node:path";
import invariant from "tiny-invariant";
import { logger, runShellCommandWithLogger } from "../utils/index";
interface SetupValidatorsOptions {
rpcUrl: string;
validatorsConfig?: string; // Path to JSON config file with validator addresses
executeSignup?: boolean;
networkName?: string; // Network name for default deployment path
deploymentPath?: string; // Optional custom deployment path
}
/**
* JSON structure for validator configuration
*/
interface ValidatorConfig {
validators: {
publicKey: string;
privateKey: string;
solochainAddress?: string;
solochainPrivateKey?: string;
solochainAuthorityName: string;
}[];
notes?: string;
}
/**
* Registers validators in EigenLayer based on a configuration file.
* This function reads validator details (public/private keys, optional solochain addresses)
* from a JSON file. If `executeSignup` is true (or confirmed by user prompt),
* it iterates through the configured validators and runs the
* `script/transact/SignUpValidator.s.sol` forge script for each to register them.
* Environment variables `OPERATOR_PRIVATE_KEY`, `OPERATOR_SOLOCHAIN_ADDRESS`, and `NETWORK`
* are set for the forge script execution.
*
* @param options - Configuration options for the validator setup process.
* @param options.rpcUrl - The RPC URL for the Ethereum network to interact with.
* @param options.validatorsConfig - Optional path to the JSON file containing validator configurations.
* Defaults to `../configs/validator-set.json` relative to this script.
* @param options.executeSignup - Optional. If true, proceeds with registration. If false, skips.
* If undefined, the user is prompted to confirm registration.
* @param options.networkName - Optional network name used when executing underlying scripts (e.g., for setting the `NETWORK` environment variable).
* Defaults to "anvil".
* @returns A Promise resolving to `true` if the validator registration process was executed
* (for all configured validators), or `false` if the registration was skipped
* (either due to the `executeSignup` option or user declining the prompt).
*/
export const setupValidators = async (options: SetupValidatorsOptions): Promise<boolean> => {
const { rpcUrl, validatorsConfig, networkName = "anvil" } = options;
// Validate RPC URL
invariant(rpcUrl, "❌ RPC URL is required");
// Load validator configuration - use default path if not specified
const configPath = validatorsConfig || path.resolve(__dirname, "../configs/validator-set.json");
// Ensure the configuration file exists
if (!fs.existsSync(configPath)) {
logger.error(`Validator configuration file not found: ${configPath}`);
throw new Error("Validator configuration file is required");
}
// Load and validate the validator configuration
logger.debug(`Loading validator configuration from ${configPath}`);
let config: ValidatorConfig;
try {
const fileContent = fs.readFileSync(configPath, "utf8");
config = JSON.parse(fileContent);
} catch (error) {
logger.error(`Failed to parse validator config file: ${error}`);
throw new Error("Invalid JSON format in validator configuration file");
}
// Validate the validators array
if (!config.validators || !Array.isArray(config.validators) || config.validators.length === 0) {
logger.error("Invalid validator configuration: 'validators' array is missing or empty");
throw new Error("Validator configuration must contain a non-empty 'validators' array");
}
// Validate each validator entry
for (const [index, validator] of config.validators.entries()) {
if (!validator.publicKey) {
throw new Error(`Validator at index ${index} is missing 'publicKey'`);
}
if (!validator.privateKey) {
throw new Error(`Validator at index ${index} is missing 'privateKey'`);
}
if (!validator.publicKey.startsWith("0x")) {
throw new Error(`Validator publicKey at index ${index} must start with '0x'`);
}
if (!validator.privateKey.startsWith("0x")) {
throw new Error(`Validator privateKey at index ${index} must start with '0x'`);
}
}
// Filter to only alice and bob validators
const validatorsToRegister = config.validators.filter((v) =>
["alice", "bob"].includes((v.solochainAuthorityName || "").toLowerCase())
);
logger.info(`🔎 Registering ${validatorsToRegister.length} validators`);
// Iterate through validators to register them
for (const [i, validator] of validatorsToRegister.entries()) {
logger.info(`🔧 Setting up validator ${i} (${validator.publicKey})`);
const env = {
...process.env,
NETWORK: networkName,
// OPERATOR_PRIVATE_KEY is what the script reads to set the operator
OPERATOR_PRIVATE_KEY: validator.privateKey,
// OPERATOR_SOLOCHAIN_ADDRESS is the validator's address on the substrate chain
OPERATOR_SOLOCHAIN_ADDRESS: validator.solochainAddress || ""
};
// Prepare command to register validator
const signupCommand = `forge script script/transact/SignUpValidator.s.sol --rpc-url ${rpcUrl} --broadcast --no-rpc-rate-limit --non-interactive`;
logger.debug(`Running command: ${signupCommand}`);
await runShellCommandWithLogger(signupCommand, { env, cwd: "../contracts", logLevel: "debug" });
logger.success(`Successfully registered validator ${validator.publicKey}`);
}
return true;
};
// Allow script to be run directly with CLI arguments
if (import.meta.main) {
const args = process.argv.slice(2);
const options: {
rpcUrl?: string;
validatorsConfig?: string;
executeSignup?: boolean;
networkName?: string;
deploymentPath?: string;
} = {
executeSignup: args.includes("--no-signup") ? false : undefined,
networkName: "anvil" // Default network name
};
// Extract RPC URL
const rpcUrlIndex = args.indexOf("--rpc-url");
if (rpcUrlIndex !== -1 && rpcUrlIndex + 1 < args.length) {
options.rpcUrl = args[rpcUrlIndex + 1];
}
// Extract validators config path
const configIndex = args.indexOf("--config");
if (configIndex !== -1 && configIndex + 1 < args.length) {
options.validatorsConfig = args[configIndex + 1];
}
// Extract network name
const networkIndex = args.indexOf("--network");
if (networkIndex !== -1 && networkIndex + 1 < args.length) {
options.networkName = args[networkIndex + 1];
}
// Extract custom deployment path
const deploymentPathIndex = args.indexOf("--deployment-path");
if (deploymentPathIndex !== -1 && deploymentPathIndex + 1 < args.length) {
options.deploymentPath = args[deploymentPathIndex + 1];
}
// Parse signup flag
if (args.includes("--signup")) {
options.executeSignup = true;
}
// Check required parameters
if (!options.rpcUrl) {
console.error("Error: --rpc-url parameter is required");
process.exit(1);
}
// Run setup
setupValidators({
rpcUrl: options.rpcUrl,
validatorsConfig: options.validatorsConfig,
executeSignup: options.executeSignup,
networkName: options.networkName,
deploymentPath: options.deploymentPath
}).catch((error) => {
console.error("Validator setup failed:", error);
process.exit(1);
});
}