run-validator
Start a validator to secure and verify transactions on a specified blockchain within the Hyperlane cross-chain network.
Instructions
Runs a validator for a specific chain.
Input Schema
TableJSON Schema
| Name | Required | Description | Default |
|---|---|---|---|
| chainName | Yes | Name of the chain to validate |
Implementation Reference
- src/index.ts:706-757 (registration)Registers the 'run-validator' MCP tool, including input schema (chainName) and handler function that initializes and runs a ValidatorRunner for the specified chain.server.tool( 'run-validator', 'Runs a validator for a specific chain.', { chainName: z.string().describe('Name of the chain to validate'), }, async ({ chainName }) => { server.server.sendLoggingMessage({ level: 'info', data: `Starting validator for chain: ${chainName}...`, }); const configFilePath = path.join( mcpDir, `agents/${chainName}-agent-config.json` ); server.server.sendLoggingMessage({ level: 'info', data: `Config file path: ${configFilePath}`, }); const validatorKey = process.env.PRIVATE_KEY; if (!validatorKey) { throw new Error('No private key provided'); } try { const validatorRunner = new ValidatorRunner( chainName, validatorKey, configFilePath ); await validatorRunner.run(); return { content: [ { type: 'text', text: `Validator started successfully for chain: ${chainName}`, }, ], }; } catch (error) { server.server.sendLoggingMessage({ level: 'error', data: `Error starting validator for chain ${chainName}: ${error}`, }); throw error; } } );
- src/index.ts:710-711 (schema)Input schema for the run-validator tool: requires chainName as string.chainName: z.string().describe('Name of the chain to validate'), },
- src/index.ts:712-756 (handler)Handler function for run-validator tool: logs, constructs config path, creates ValidatorRunner instance with chainName, private key, and config, then calls run().async ({ chainName }) => { server.server.sendLoggingMessage({ level: 'info', data: `Starting validator for chain: ${chainName}...`, }); const configFilePath = path.join( mcpDir, `agents/${chainName}-agent-config.json` ); server.server.sendLoggingMessage({ level: 'info', data: `Config file path: ${configFilePath}`, }); const validatorKey = process.env.PRIVATE_KEY; if (!validatorKey) { throw new Error('No private key provided'); } try { const validatorRunner = new ValidatorRunner( chainName, validatorKey, configFilePath ); await validatorRunner.run(); return { content: [ { type: 'text', text: `Validator started successfully for chain: ${chainName}`, }, ], }; } catch (error) { server.server.sendLoggingMessage({ level: 'error', data: `Error starting validator for chain ${chainName}: ${error}`, }); throw error; } }
- src/RunValidator.ts:18-206 (helper)ValidatorRunner class providing the core logic to run the validator: pulls Docker image, creates/starts container with validator command, mounts config/DB/signatures dirs, monitors logs.export class ValidatorRunner { private readonly chainName: ChainName; private readonly validatorKey: string; private readonly configFilePath: string; private readonly validatorSignaturesDir: string; private readonly validatorDbPath: string; private containerId: string | null = null; private latestTag: string = DEFAULT_VALIDATOR_TAG; constructor(chainName: string, validatorKey: string, configFilePath: string) { this.chainName = chainName; this.validatorKey = validatorKey; this.configFilePath = configFilePath; const logsPath = path.join( process.env.CACHE_DIR || process.env.HOME!, '.hyperlane-mcp/logs' ); createDirectory(logsPath); this.validatorSignaturesDir = path.resolve( `${logsPath}/hyperlane-validator-signatures-${chainName}` ); this.validatorDbPath = path.resolve( `${logsPath}/hyperlane_db_validator_${chainName}` ); // Ensure required directories exist createDirectory(this.validatorSignaturesDir); createDirectory(this.validatorDbPath); logger.info(`Validator config: ${JSON.stringify(this, null, 2)}`); } private async initializeLatestTag(): Promise<void> { if (this.latestTag !== DEFAULT_VALIDATOR_TAG) { return; } logger.info(`Initializing latest Docker image tag for validator...`); this.latestTag = getLatestImageTag(await fetchImageTags()) || DEFAULT_VALIDATOR_TAG; logger.info(`Latest Docker image tag in validator: ${this.latestTag}`); } async run(): Promise<void> { try { await this.initializeLatestTag(); await this.pullDockerImage(); await this.createAndStartContainer(); await this.monitorLogs(); } catch (error) { logger.error( `Error starting validator for chain: ${this.chainName} : ${error}` ); throw error; } } private async pullDockerImage(): Promise<void> { logger.info(`Pulling latest Hyperlane agent Docker image...`); await new Promise<void>((resolve, reject) => { docker.pull( `gcr.io/abacus-labs-dev/hyperlane-agent:${this.latestTag}`, { platform: 'linux/x86_64/v8', }, (err: Error | null, stream: NodeJS.ReadableStream | undefined) => { if (err) { reject(err); return; } if (!stream) { reject(new Error('Stream is undefined')); return; } docker.modem.followProgress( stream, (err: Error | null) => { if (err) reject(err); else resolve(); }, (event: any) => { logger.info( `Downloading Docker image... ${JSON.stringify(event, null, 2)}` ); } ); } ); }); } private async createAndStartContainer(): Promise<void> { logger.info( `Creating container for validator on chain: ${this.chainName}...` ); const container = await docker.createContainer({ Image: `gcr.io/abacus-labs-dev/hyperlane-agent:${this.latestTag}`, Env: [`CONFIG_FILES=${this.configFilePath}`], HostConfig: { NetworkMode: 'host', Mounts: [ { Source: path.resolve(this.configFilePath), Target: path.join( process.env.CACHE_DIR || process.env.HOME!, '.hyperlane-mcp', 'agents', `${this.chainName}-agent-config.json` ), Type: 'bind', ReadOnly: true, }, { Source: this.validatorDbPath, Target: '/hyperlane_db', Type: 'bind', }, { Source: this.validatorSignaturesDir, Target: '/validator-signatures', Type: 'bind', }, ], }, Cmd: [ './validator', '--db', '/hyperlane_db', '--originChainName', this.chainName, '--checkpointSyncer.type', 'localStorage', '--checkpointSyncer.path', '/validator-signatures', '--validator.key', this.validatorKey, ], Tty: true, NetworkDisabled: false, }); this.containerId = container.id; logger.info(`Starting validator for chain: ${this.chainName}...`); await container.start(); logger.info(`Validator for chain: ${this.chainName} started successfully.`); } private async monitorLogs(): Promise<void> { if (!this.containerId) { throw new Error('Container ID not set'); } const container = docker.getContainer(this.containerId); logger.info('Fetching container logs...'); const logStream = await container.logs({ follow: true, stdout: true, stderr: true, }); logStream.on('data', (chunk) => { logger.info(chunk.toString()); }); logger.info('Validator is now running. Monitoring logs...'); } async checkStatus(): Promise<void> { try { const containers = await docker.listContainers({ all: true }); const runningContainer = containers.find( (c) => c.Id === this.containerId ); if (runningContainer) { logger.info(`Validator container is running: ${runningContainer.Id}`); } else { logger.info('Validator container is not running.'); } } catch (error) { logger.error(`Error checking container status: ${error}`); throw error; } } }