datahaven/test/e2e/framework/submitter.ts
Ahmad Kaouk 39aea69e36
test: integrate validator-set-submitter Docker container into E2E test (#453)
## Summary

- Replace the manual `sendNewValidatorSetForEra` contract call in the
`validator-set-update` E2E test with the **validator-set-submitter
daemon** running as a Docker container
- Add `test/e2e/framework/submitter.ts` with helpers to build the image,
launch the container on the shared Docker network, and clean up after
the test
- Remove unused imports (`getOwnerAccount`, `decodeEventLog`,
`parseEther`, `gatewayAbi`) that were only needed for manual submission

The submitter automatically detects the last session of an era and
submits the validator set via Snowbridge, matching production behavior
more closely than the previous manual call.

## Test plan

- [x] `bun test e2e/suites/validator-set-update.test.ts --timeout
900000` passes (4/4 tests, 15 assertions)
- [x] Verify submitter container starts and connects to both Ethereum
and DataHaven
- [x] Verify `ExternalValidatorsSet` event is observed on DataHaven
- [x] Verify submitter container is cleaned up after the test
- [x] Verify Charlie and Dave appear in the final validator set
2026-02-24 18:31:49 +02:00

135 lines
4.6 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 repo root.
*/
export async function buildSubmitterImage(): Promise<void> {
logger.debug("Building validator-set-submitter Docker image...");
const repoRoot = path.resolve(import.meta.dir, "../../..");
await $`docker build -f test/tools/validator-set-submitter/Dockerfile -t ${SUBMITTER_IMAGE} .`
.cwd(repoRoot)
.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 logs =
(await $`docker logs --tail ${SUBMITTER_LOG_TAIL_LINES} ${containerName}`.nothrow().text()) ||
"<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`);
}