datahaven/test/e2e/framework/submitter.ts
Ahmad Kaouk f5067ea842
fix(e2e): stabilize submitter CI and local relayer startup (#470)
## Summary
- fixes the untrusted CI failure in `e2e-tests / E2E Tests with Kurtosis
Ethereum Network`
- keeps validator-set-submitter startup actionable by avoiding test-only
contract config imports during container startup
- improves submitter readiness diagnostics by capturing both stdout and
stderr from container logs and making streamed log matching robust to
chunked UTF-8 output
- reduces validator-set-submitter Docker build time in CI by building
from `test/` and adding a tight `test/.dockerignore`
- makes local arm64 E2E runs use a native local Snowbridge relayer image
instead of forcing `linux/amd64` emulation
- auto-builds the local relayer image when needed for `:local` tags

## Why
The original failing untrusted test started as a submitter container
startup problem, but the branch now also addresses a second timeout path
that showed up while debugging:
- the submitter image was being built from the repository root with a
large Docker context, which made the `validator-set-update` suite spend
most of its hook timeout budget inside `docker build`
- on Apple Silicon, forcing `datahavenxyz/snowbridge-relay:latest`
through `linux/amd64` caused `generate-beacon-checkpoint` to segfault
during local runs

These changes make the submitter failure actionable, cut the CI Docker
build context down substantially, and keep local E2E runs reliable on
arm64.

## Validation
- `cd test && bun fmt`
- `cd test && bun x tsc --noEmit`
- `bun test e2e/suites/validator-set-update.test.ts --timeout 900000`
- `cd test && docker build -f tools/validator-set-submitter/Dockerfile
-t datahavenxyz/validator-set-submitter:local .`
2026-03-06 13:08:13 +01:00

137 lines
4.7 KiB
TypeScript

/**
* E2E test helper for managing the validator-set-submitter Docker container.
*
* The submitter daemon automates `sendNewValidatorSetForEra` calls on the
* ServiceManager contract. This module builds the image, launches the
* container on the shared Docker network, and tears it down after the test.
*/
import path from "node:path";
import { $ } from "bun";
import { ANVIL_FUNDED_ACCOUNTS, logger, waitForContainerToStart, waitForLog } from "utils";
import { RELAYER_CONFIG_DIR } from "../../launcher/relayers";
const SUBMITTER_IMAGE = "datahavenxyz/validator-set-submitter:local";
const SUBMITTER_READY_LOG = "Submitter started — watching session changes";
const SUBMITTER_READY_TIMEOUT_SECONDS = 30;
const SUBMITTER_LOG_TAIL_LINES = 200;
/**
* Builds the validator-set-submitter Docker image from the test directory.
*/
export async function buildSubmitterImage(): Promise<void> {
logger.debug("Building validator-set-submitter Docker image...");
const testRoot = path.resolve(import.meta.dir, "../..");
await $`docker build -f tools/validator-set-submitter/Dockerfile -t ${SUBMITTER_IMAGE} .`
.cwd(testRoot)
.quiet();
logger.debug("Validator-set-submitter image built successfully");
}
export interface LaunchSubmitterOptions {
/** Docker network name (from launchedNetwork.networkName) */
networkName: string;
/** Network ID for container naming */
networkId: string;
/** Host-facing Ethereum RPC URL (e.g. http://127.0.0.1:32000) */
ethereumRpcUrl: string;
/** DataHaven container name for inter-container networking */
datahavenContainerName: string;
/** ServiceManager contract address from deployments */
serviceManagerAddress: string;
}
/**
* Launches the validator-set-submitter as a Docker container.
*
* Generates a YAML config, mounts it into the container, and connects
* it to the same Docker network as the DH nodes and relayers.
*/
export async function launchSubmitter(options: LaunchSubmitterOptions): Promise<{
containerName: string;
cleanup: () => Promise<void>;
}> {
const { networkName, networkId, ethereumRpcUrl, datahavenContainerName, serviceManagerAddress } =
options;
const containerName = `submitter-${networkId}`;
// Extract port from host-facing URL and rewrite for Docker inter-container access
const ethUrl = new URL(ethereumRpcUrl);
const dockerEthRpcUrl = `http://host.docker.internal:${ethUrl.port}`;
const dockerDhWsUrl = `ws://${datahavenContainerName}:9944`;
// Generate YAML config
const configContent = [
`ethereum_rpc_url: "${dockerEthRpcUrl}"`,
`datahaven_ws_url: "${dockerDhWsUrl}"`,
`service_manager_address: "${serviceManagerAddress}"`,
`network_id: "anvil"`,
`execution_fee: "0.1"`,
`relayer_fee: "0.2"`
].join("\n");
const configFileName = `submitter-config-${networkId}.yml`;
await $`mkdir -p ${RELAYER_CONFIG_DIR}`.quiet();
const hostConfigPath = path.resolve(path.join(RELAYER_CONFIG_DIR, configFileName));
await Bun.write(hostConfigPath, configContent);
logger.debug(`Submitter config written to ${hostConfigPath}`);
// Remove any existing container with the same name
await $`docker rm -f ${containerName}`.quiet().nothrow();
// Launch the container
const args = [
"run",
"-d",
"--name",
containerName,
"--network",
networkName,
"--add-host",
"host.docker.internal:host-gateway",
"-v",
`${hostConfigPath}:/config/config.yml:ro`,
"-e",
`SUBMITTER_PRIVATE_KEY=${ANVIL_FUNDED_ACCOUNTS[6].privateKey}`,
SUBMITTER_IMAGE
];
await $`docker ${args}`.quiet();
await waitForContainerToStart(containerName);
try {
await waitForLog({
containerName,
search: SUBMITTER_READY_LOG,
timeoutSeconds: SUBMITTER_READY_TIMEOUT_SECONDS
});
} catch (error) {
const logResult = await $`docker logs --tail ${SUBMITTER_LOG_TAIL_LINES} ${containerName}`
.nothrow()
.quiet();
const logs =
`${logResult.stdout.toString()}${logResult.stderr.toString()}`.trim() || "<no logs captured>";
await stopSubmitter(containerName);
throw new Error(
`Submitter did not become ready. Expected log "${SUBMITTER_READY_LOG}". Last ${SUBMITTER_LOG_TAIL_LINES} log lines:\n${logs}`,
{ cause: error }
);
}
logger.debug(`Submitter container ${containerName} started`);
const cleanup = async () => {
await stopSubmitter(containerName);
};
return { containerName, cleanup };
}
/**
* Stops and removes the submitter container.
*/
export async function stopSubmitter(containerName: string): Promise<void> {
logger.debug(`Stopping submitter container ${containerName}...`);
await $`docker rm -f ${containerName}`.quiet().nothrow();
logger.debug(`Submitter container ${containerName} removed`);
}