mirror of
https://github.com/datahaven-xyz/datahaven
synced 2026-05-24 09:50:01 +00:00
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>
300 lines
10 KiB
TypeScript
300 lines
10 KiB
TypeScript
import { $ } from "bun";
|
|
import { getContainersMatchingImage, getPortFromKurtosis, logger } from "utils";
|
|
import { ParameterCollection } from "utils/parameters";
|
|
import { updateParameters } from "../../scripts/deploy-contracts";
|
|
import { deployContracts } from "../contracts";
|
|
import { launchLocalDataHavenSolochain } from "../datahaven";
|
|
import { getRunningKurtosisEnclaves, launchKurtosisNetwork } from "../kurtosis";
|
|
import { setDataHavenParameters } from "../parameters";
|
|
import { launchRelayers } from "../relayers";
|
|
import type { LaunchNetworkResult, NetworkLaunchOptions } from "../types";
|
|
import { LaunchedNetwork } from "../types/launchedNetwork";
|
|
import { checkBaseDependencies } from "../utils";
|
|
import { COMPONENTS } from "../utils/constants";
|
|
import { fundValidators, setupValidators } from "../validators";
|
|
|
|
// Authority IDs for test networks
|
|
const TEST_AUTHORITY_IDS = ["alice", "bob"] as const;
|
|
|
|
/**
|
|
* Validates that the network ID is unique and no resources with this ID exist.
|
|
* @throws {Error} if resources with the network ID already exist
|
|
*/
|
|
const validateNetworkIdUnique = async (networkId: string): Promise<void> => {
|
|
logger.info(`🔍 Validating network ID uniqueness: ${networkId}`);
|
|
|
|
// Check for existing DataHaven containers
|
|
const datahavenContainers = await getContainersMatchingImage(COMPONENTS.datahaven.imageName);
|
|
const conflictingDatahaven = datahavenContainers.filter((c) =>
|
|
c.Names.some((name) => name.includes(networkId))
|
|
);
|
|
if (conflictingDatahaven.length > 0) {
|
|
throw new Error(
|
|
`DataHaven containers with network ID '${networkId}' already exist. ` +
|
|
`Run 'bun cli stop --all' or remove containers manually.`
|
|
);
|
|
}
|
|
|
|
// Check for existing relayer containers
|
|
const relayerContainers = await getContainersMatchingImage(COMPONENTS.snowbridge.imageName);
|
|
const conflictingRelayers = relayerContainers.filter((c) =>
|
|
c.Names.some((name) => name.includes(networkId))
|
|
);
|
|
if (conflictingRelayers.length > 0) {
|
|
throw new Error(
|
|
`Relayer containers with network ID '${networkId}' already exist. ` +
|
|
`Run 'bun cli stop --all' or remove containers manually.`
|
|
);
|
|
}
|
|
|
|
// Check for existing Kurtosis enclaves
|
|
const enclaves = await getRunningKurtosisEnclaves();
|
|
const enclaveName = `eth-${networkId}`;
|
|
const conflictingEnclaves = enclaves.filter((e) => e.name === enclaveName);
|
|
if (conflictingEnclaves.length > 0) {
|
|
throw new Error(
|
|
`Kurtosis enclave '${enclaveName}' already exists. ` +
|
|
`Run 'kurtosis enclave rm ${enclaveName}' to remove it.`
|
|
);
|
|
}
|
|
|
|
// Check for existing Docker network
|
|
const dockerNetworkName = `datahaven-${networkId}`;
|
|
const networkOutput =
|
|
await $`docker network ls --filter "name=^${dockerNetworkName}$" --format "{{.Name}}"`.text();
|
|
if (networkOutput.trim()) {
|
|
throw new Error(
|
|
`Docker network '${dockerNetworkName}' already exists. ` +
|
|
`Run 'docker network rm ${dockerNetworkName}' to remove it.`
|
|
);
|
|
}
|
|
|
|
logger.success(`Network ID '${networkId}' is available`);
|
|
};
|
|
|
|
/**
|
|
* Creates a cleanup function for the test network.
|
|
*/
|
|
const createCleanupFunction = (networkId: string) => {
|
|
return async () => {
|
|
logger.info(`🧹 Cleaning up test network: ${networkId}`);
|
|
|
|
try {
|
|
// 1. Stop relayer containers
|
|
const relayerContainers = await getContainersMatchingImage(COMPONENTS.snowbridge.imageName);
|
|
const networkRelayers = relayerContainers.filter((c) =>
|
|
c.Names.some((name) => name.includes(networkId))
|
|
);
|
|
if (networkRelayers.length > 0) {
|
|
logger.info(`🔨 Stopping ${networkRelayers.length} relayer containers...`);
|
|
for (const container of networkRelayers) {
|
|
await $`docker stop ${container.Id}`.nothrow();
|
|
await $`docker rm ${container.Id}`.nothrow();
|
|
}
|
|
}
|
|
|
|
// 2. Stop DataHaven containers
|
|
const datahavenContainers = await getContainersMatchingImage(COMPONENTS.datahaven.imageName);
|
|
const networkDatahaven = datahavenContainers.filter((c) =>
|
|
c.Names.some((name) => name.includes(networkId))
|
|
);
|
|
if (networkDatahaven.length > 0) {
|
|
logger.info(`🔨 Stopping ${networkDatahaven.length} DataHaven containers...`);
|
|
for (const container of networkDatahaven) {
|
|
await $`docker stop ${container.Id}`.nothrow();
|
|
await $`docker rm ${container.Id}`.nothrow();
|
|
}
|
|
}
|
|
|
|
// 3. Remove Docker network
|
|
const dockerNetworkName = `datahaven-${networkId}`;
|
|
logger.info(`🔨 Removing Docker network: ${dockerNetworkName}`);
|
|
await $`docker network rm -f ${dockerNetworkName}`.nothrow();
|
|
|
|
// 4. Remove Kurtosis enclave
|
|
const enclaveName = `eth-${networkId}`;
|
|
logger.info(`🔨 Removing Kurtosis enclave: ${enclaveName}`);
|
|
await $`kurtosis enclave rm ${enclaveName} -f`.nothrow();
|
|
|
|
logger.success(`Cleanup completed for network: ${networkId}`);
|
|
} catch (error) {
|
|
logger.error(`❌ Cleanup failed for network ${networkId}:`, error);
|
|
// Continue cleanup, don't throw
|
|
}
|
|
};
|
|
};
|
|
|
|
/**
|
|
* Launches a complete network stack for E2E testing.
|
|
*
|
|
* This function orchestrates the launch of all network components:
|
|
* 1. DataHaven blockchain nodes
|
|
* 2. Kurtosis Ethereum network
|
|
* 3. Smart contracts deployment
|
|
* 4. Validator setup
|
|
* 5. Runtime parameter configuration
|
|
* 6. Relayer services
|
|
* 7. Validator set update
|
|
*
|
|
* @param options - Configuration options for the network launch
|
|
* @returns NetworkConnectors with cleanup function
|
|
* @throws {Error} if network ID is not unique or any component fails to launch
|
|
*/
|
|
export const launchNetwork = async (
|
|
options: NetworkLaunchOptions
|
|
): Promise<LaunchNetworkResult> => {
|
|
const networkId = options.networkId;
|
|
const launchedNetwork = new LaunchedNetwork();
|
|
launchedNetwork.networkName = networkId;
|
|
let injectContracts = false;
|
|
|
|
// Using env to check
|
|
if (process.env.INJECT_CONTRACTS === "true") {
|
|
injectContracts = true;
|
|
}
|
|
|
|
let cleanup: (() => Promise<void>) | undefined;
|
|
|
|
try {
|
|
logger.info(`🚀 Launching complete network stack with ID: ${networkId}`);
|
|
const startTime = performance.now();
|
|
|
|
// Check base dependencies
|
|
await checkBaseDependencies();
|
|
|
|
// Validate network ID is unique
|
|
await validateNetworkIdUnique(networkId);
|
|
|
|
// Create cleanup function
|
|
cleanup = createCleanupFunction(networkId);
|
|
|
|
// Create parameter collection for use throughout the launch
|
|
const parameterCollection = new ParameterCollection();
|
|
|
|
// 1. Launch DataHaven network
|
|
logger.info("📦 Launching DataHaven network...");
|
|
await launchLocalDataHavenSolochain(
|
|
{
|
|
networkId,
|
|
datahavenImageTag: options.datahavenImageTag || "datahavenxyz/datahaven:local",
|
|
relayerImageTag: options.relayerImageTag || "datahavenxyz/snowbridge-relay:latest",
|
|
authorityIds: TEST_AUTHORITY_IDS,
|
|
buildDatahaven: options.buildDatahaven ?? !isCI, // if not specified, default to false for CI, true for local testing
|
|
datahavenBuildExtraArgs: options.datahavenBuildExtraArgs || "--features=fast-runtime"
|
|
},
|
|
launchedNetwork
|
|
);
|
|
|
|
// 2. Launch Ethereum/Kurtosis network
|
|
logger.info("⚡️ Launching Kurtosis Ethereum network...");
|
|
const kurtosisEnclaveName = `eth-${networkId}`;
|
|
await launchKurtosisNetwork(
|
|
{
|
|
kurtosisEnclaveName: kurtosisEnclaveName,
|
|
blockscout: options.blockscout ?? false,
|
|
slotTime: options.slotTime || 1,
|
|
kurtosisNetworkArgs: options.kurtosisNetworkArgs,
|
|
injectContracts
|
|
},
|
|
launchedNetwork
|
|
);
|
|
|
|
// 3. Deploy contracts
|
|
if (injectContracts) {
|
|
logger.info("📄 Smart contracts injected.");
|
|
} else {
|
|
logger.info("📄 Deploying smart contracts...");
|
|
let blockscoutBackendUrl: string | undefined;
|
|
if (options.blockscout) {
|
|
const blockscoutPort = await getPortFromKurtosis("blockscout", "http", kurtosisEnclaveName);
|
|
blockscoutBackendUrl = `http://127.0.0.1:${blockscoutPort}`;
|
|
}
|
|
|
|
await deployContracts({
|
|
rpcUrl: launchedNetwork.elRpcUrl,
|
|
verified: options.verified ?? false,
|
|
blockscoutBackendUrl,
|
|
parameterCollection
|
|
});
|
|
}
|
|
|
|
if (!launchedNetwork.elRpcUrl) {
|
|
throw new Error("Ethereum RPC URL not available");
|
|
}
|
|
|
|
// 4. Fund validators
|
|
logger.info("💰 Funding validators...");
|
|
await fundValidators({
|
|
rpcUrl: launchedNetwork.elRpcUrl
|
|
});
|
|
|
|
// 5. Setup validators
|
|
logger.info("🔐 Setting up validators...");
|
|
await setupValidators({
|
|
rpcUrl: launchedNetwork.elRpcUrl
|
|
});
|
|
|
|
if (injectContracts) {
|
|
// We are injecting contracts but we still need the addresses
|
|
await updateParameters(parameterCollection);
|
|
}
|
|
|
|
// 6. Set DataHaven runtime parameters
|
|
logger.info("⚙️ Setting DataHaven parameters...");
|
|
await setDataHavenParameters({
|
|
launchedNetwork,
|
|
collection: parameterCollection
|
|
});
|
|
|
|
// 7. Launch relayers
|
|
logger.info("❄️ Launching Snowbridge relayers...");
|
|
if (!options.relayerImageTag) {
|
|
throw new Error("Relayer image tag not specified");
|
|
}
|
|
|
|
await launchRelayers(
|
|
{
|
|
networkId,
|
|
relayerImageTag: options.relayerImageTag,
|
|
kurtosisEnclaveName
|
|
},
|
|
launchedNetwork
|
|
);
|
|
|
|
// Log success
|
|
const endTime = performance.now();
|
|
const minutes = ((endTime - startTime) / (1000 * 60)).toFixed(1);
|
|
logger.success(`Network launched successfully in ${minutes} minutes`);
|
|
|
|
// Validate required endpoints
|
|
if (!launchedNetwork.clEndpoint) {
|
|
throw new Error("Consensus layer endpoint not available");
|
|
}
|
|
|
|
// Return connectors
|
|
const aliceContainerName = `datahaven-alice-${networkId}`;
|
|
const wsPort = launchedNetwork.getContainerPort(aliceContainerName);
|
|
// Use the WebSocket URL from LaunchedNetwork (set by registerServices from Kurtosis)
|
|
const ethereumWsUrl = launchedNetwork.elWsUrl;
|
|
return {
|
|
launchedNetwork,
|
|
dataHavenRpcUrl: `http://127.0.0.1:${wsPort}`,
|
|
ethereumRpcUrl: launchedNetwork.elRpcUrl,
|
|
ethereumWsUrl,
|
|
ethereumClEndpoint: launchedNetwork.clEndpoint,
|
|
cleanup
|
|
};
|
|
} catch (error) {
|
|
logger.error("❌ Failed to launch network", error);
|
|
|
|
// Run cleanup if we created it
|
|
if (cleanup) {
|
|
logger.info("🧹 Running cleanup due to launch failure...");
|
|
await cleanup();
|
|
}
|
|
|
|
throw error;
|
|
}
|
|
};
|
|
|
|
export const isCI = process.env.CI === "true" || process.env.GITHUB_ACTIONS === "true";
|