datahaven/test/utils/validators.ts

245 lines
7.3 KiB
TypeScript
Raw Permalink Normal View History

test: Update validator set e2e test (#126) ## Add E2E validator-set update flow - feat: `test/utils/validators.ts` for on-demand validator orchestration. - feat: `test/suites/validator-set-update.test.ts` covering allowlist → register → update. - some minor launcher updates: avoid docker cache, add `--platform` when building datahaven image, avoid sending validator-set update on launch. - Helpers: ABI shortcut in `test/utils/contracts.ts`; config tweaks in `test/configs/validator-set.json`. - Minor cleanup/formatting across `test/launcher/*`, `test/scripts/setup-validators.ts`, and related tests. - added `keepAlive` flag to `BaseTestSuite`, in order to avoid tearing down the network while debugging. Defaults, obviously, to false. - added a `failOnTomeout` option on to waitForDataHavenEvents() so the test fails of the timeout is reached and no event was captured. ### Coverage - The test simulates an scenario in which we have two active authorities (alice and bob), which are running, and registered as operators, which is the normal state after the chain launches. Then: - It launches two more nodes (charlie and dave) - It add the nodes to allowlist and register them as operators - It sends the validator set update message - Checks that the validator update message was propagated through the gateway and arrived the external-validators pallet - Checks that the chain continues producing blocks ### Notes The last test case has a timeout of 10 minutes. This is to respect propagation times of the message through the relayers. We are testing that the external validators pallet actually updated the validator set. Locally, I could expect 5~6 minutes, I just wanted to be on the safe side. CI is passing showing that this was enough indeed. --------- Co-authored-by: Steve Degosserie <723552+stiiifff@users.noreply.github.com> Co-authored-by: Ahmad Kaouk <56095276+ahmadkaouk@users.noreply.github.com>
2025-10-02 11:23:40 +00:00
/**
* DataHaven utility functions for launching and managing validator nodes
*
* This module provides utilities for launching individual DataHaven validator nodes
* on demand, checking their status, and managing their lifecycle.
*
* @example
* ```typescript
* import { launchDatahavenValidator, TestAccounts } from "utils";
*
* // Launch a new Charlie validator node
* const charlieNode = await launchDatahavenValidator(TestAccounts.Charlie, {
* launchedNetwork: suite.getLaunchedNetwork()
* });
*
* console.log(`Charlie node launched on port ${charlieNode.publicPort}`);
* console.log(`WebSocket URL: ${charlieNode.wsUrl}`);
* ```
*
* @example
* ```typescript
* // Check if a node is already running before launching
* if (await isValidatorNodeRunning("charlie", "test-network")) {
* console.log("Charlie node is already running");
* } else {
* // Launch the node
* const node = await launchDatahavenValidator(TestAccounts.Charlie, options);
* }
* ```
*/
import { $ } from "bun";
import { dataHavenServiceManagerAbi } from "contract-bindings";
import { logger, waitForContainerToStart } from "utils";
import { DEFAULT_SUBSTRATE_WS_PORT } from "utils/constants";
import { getPublicPort } from "utils/docker";
import { privateKeyToAccount } from "viem/accounts";
import type { LaunchedNetwork } from "../launcher/types/launchedNetwork";
/**
* Enum for test account names that are prefunded in substrate
*/
export enum TestAccounts {
Alice = "alice",
Bob = "bob",
Charlie = "charlie",
Dave = "dave",
Eve = "eve",
Ferdie = "ferdie"
}
export interface ValidatorInfo {
publicKey: string;
privateKey: string;
solochainAddress: string;
solochainPrivateKey: string;
solochainAuthorityName: string;
isActive: boolean;
}
/**
* Information about a launched DataHaven validator node
*/
export interface LaunchedValidatorInfo {
nodeId: string;
containerName: string;
rpcUrl: string;
wsUrl: string;
publicPort: number;
internalPort: number;
}
/**
* Options for launching a DataHaven validator
*/
export interface LaunchValidatorOptions {
datahavenImageTag?: string;
launchedNetwork: LaunchedNetwork;
}
export const COMMON_LAUNCH_ARGS = [
"--unsafe-force-node-key-generation",
"--tmp",
"--validator",
"--discover-local",
"--no-prometheus",
"--unsafe-rpc-external",
"--rpc-cors=all",
"--force-authoring",
"--no-telemetry",
"--enable-offchain-indexing=true"
];
/**
* Checks if a DataHaven validator node is already running
* @param nodeId - The node identifier (e.g., "alice", "bob")
* @param networkId - The network identifier
* @returns True if the node is running, false otherwise
*/
export const isValidatorNodeRunning = async (
nodeId: string,
networkId: string
): Promise<boolean> => {
const containerName = `datahaven-${nodeId}-${networkId}`;
const dockerPsOutput = await $`docker ps -q --filter "name=^${containerName}"`.text();
return dockerPsOutput.trim().length > 0;
};
/**
* Launches a single DataHaven validator node on demand
* @param name - The test account name to launch
* @param options - Configuration options for launching the node
* @returns Information about the launched node
*/
export const launchDatahavenValidator = async (
name: TestAccounts,
options: LaunchValidatorOptions
): Promise<LaunchedValidatorInfo> => {
const nodeId = name.toLowerCase();
const networkId = options.launchedNetwork.networkId;
const datahavenImageTag = options.datahavenImageTag || "datahavenxyz/datahaven:local";
const containerName = `datahaven-${nodeId}-${networkId}`;
// Check if node is already running
if (await isValidatorNodeRunning(nodeId, networkId)) {
logger.warn(`⚠️ Node ${nodeId} is already running in network ${networkId}`);
// Get existing node info
const publicPort = await getPublicPort(containerName, DEFAULT_SUBSTRATE_WS_PORT);
return {
nodeId,
containerName,
rpcUrl: `http://127.0.0.1:${publicPort}`,
wsUrl: `ws://127.0.0.1:${publicPort}`,
publicPort,
internalPort: DEFAULT_SUBSTRATE_WS_PORT
};
}
logger.info(`🚀 Launching DataHaven validator node: ${nodeId}...`);
// Get port mapping for the node
const portMapping = getPortMappingForNode(nodeId, networkId);
const command: string[] = [
"docker",
"run",
"-d",
"--name",
containerName,
"--network",
options.launchedNetwork.networkName,
...portMapping,
datahavenImageTag,
`--${nodeId}`,
...COMMON_LAUNCH_ARGS
];
logger.debug(await $`sh -c "${command.join(" ")}"`.text());
await waitForContainerToStart(containerName);
// Get the dynamic port and register in the network
const publicPort = await getPublicPort(containerName, DEFAULT_SUBSTRATE_WS_PORT);
// Add container to the launched network
options.launchedNetwork.addContainer(
containerName,
{ ws: publicPort },
{ ws: DEFAULT_SUBSTRATE_WS_PORT }
);
logger.success(`DataHaven validator node ${nodeId} launched successfully on port ${publicPort}`);
return {
nodeId,
containerName,
rpcUrl: `http://127.0.0.1:${publicPort}`,
wsUrl: `ws://127.0.0.1:${publicPort}`,
publicPort,
internalPort: DEFAULT_SUBSTRATE_WS_PORT
};
};
/**
* Determines the port mapping for a DataHaven node based on the network type.
* Reused from launcher/datahaven.ts
* @param nodeId - The node identifier (e.g., "alice", "bob")
* @param networkId - The network identifier
* @returns Array of port mapping arguments for Docker run command
*/
const getPortMappingForNode = (nodeId: string, networkId: string): string[] => {
const isCliLaunch = networkId === "cli-launch";
if (isCliLaunch && nodeId === "alice") {
// For CLI-launch networks, only alice gets the fixed port mapping
return ["-p", `${DEFAULT_SUBSTRATE_WS_PORT}:${DEFAULT_SUBSTRATE_WS_PORT}`];
}
// For other networks or non-alice nodes, only expose internal port
// Docker will assign a random external port
return ["-p", `${DEFAULT_SUBSTRATE_WS_PORT}`];
};
/**
* Get node info by account name from validator set JSON
* @param validatorSetJson - Validator set JSON
* @param account - Test account name
* @returns Node info
*/
export const getValidatorInfoByName = (
validatorSetJson: any,
account: TestAccounts
): ValidatorInfo => {
const validatorsRaw = validatorSetJson.validators as Array<ValidatorInfo>;
const node = validatorsRaw.find((v) => v.solochainAuthorityName === account.toLowerCase());
if (!node) {
throw new Error(`Node ${account} not found in validator set`);
}
return node;
};
/**
* Adds a validator to the EigenLayer allowlist
* @param connectors - The connectors to use
* @param validator - The validator to add to the allowlist
*/
export const addValidatorToAllowlist = async (
connectors: any,
validator: ValidatorInfo,
deployments: any
) => {
logger.info(`Adding validator ${validator.publicKey} to allowlist...`);
const hash = await connectors.walletClient.writeContract({
address: deployments.ServiceManager as `0x${string}`,
abi: dataHavenServiceManagerAbi,
functionName: "addValidatorToAllowlist",
args: [validator.publicKey as `0x${string}`],
account: privateKeyToAccount(validator.privateKey as `0x${string}`),
chain: null
});
await connectors.publicClient.waitForTransactionReceipt({ hash });
logger.info(`✅ Validator ${validator.publicKey} added to allowlist`);
};