Skip to content

Commit

Permalink
Add bot to aztec start cli
Browse files Browse the repository at this point in the history
  • Loading branch information
spalladino committed Jul 24, 2024
1 parent 17f09b6 commit 71885e4
Show file tree
Hide file tree
Showing 10 changed files with 182 additions and 56 deletions.
5 changes: 4 additions & 1 deletion yarn-project/aztec/src/cli/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ export function injectAztecCommands(program: Command, userLog: LogFn, debugLogge
.option('-o, --prover-node [options]', cliTexts.proverNode)
.option('-p2p, --p2p-bootstrap [options]', cliTexts.p2pBootstrap)
.option('-t, --txe [options]', cliTexts.txe)
.option('--bot [options]', cliTexts.bot)
.action(async options => {
// list of 'stop' functions to call when process ends
const signalHandlers: Array<() => Promise<void>> = [];
Expand Down Expand Up @@ -66,10 +67,12 @@ export function injectAztecCommands(program: Command, userLog: LogFn, debugLogge
signalHandlers.push(stop);
services = [{ node: nodeServer }, { pxe: pxeServer }];
} else {
// Start Aztec Node
if (options.node) {
const { startNode } = await import('./cmds/start_node.js');
services = await startNode(options, signalHandlers, userLog);
} else if (options.bot) {
const { startBot } = await import('./cmds/start_bot.js');
services = await startBot(options, signalHandlers, userLog);
} else if (options.proverNode) {
const { startProverNode } = await import('./cmds/start_prover_node.js');
services = await startProverNode(options, signalHandlers, userLog);
Expand Down
43 changes: 43 additions & 0 deletions yarn-project/aztec/src/cli/cmds/start_bot.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import { type BotConfig, BotRunner, createBotRunnerRpcServer, getBotConfigFromEnv } from '@aztec/bot';
import { type PXE } from '@aztec/circuit-types';
import { type ServerList } from '@aztec/foundation/json-rpc/server';
import { type LogFn } from '@aztec/foundation/log';

import { mergeEnvVarsAndCliOptions, parseModuleOptions } from '../util.js';

export async function startBot(
options: any,
signalHandlers: (() => Promise<void>)[],
userLog: LogFn,
): Promise<ServerList> {
// Services that will be started in a single multi-rpc server
const services: ServerList = [];

const { proverNode, archiver, sequencer, p2pBootstrap, txe, prover } = options;
if (proverNode || archiver || sequencer || p2pBootstrap || txe || prover) {
userLog(
`Starting a bot with --prover-node, --prover, --archiver, --sequencer, --p2p-bootstrap, or --txe is not supported.`,
);
process.exit(1);
}

await addBot(options, services, signalHandlers);
return services;
}

export async function addBot(
options: any,
services: ServerList,
signalHandlers: (() => Promise<void>)[],
deps: { pxe?: PXE } = {},
) {
const envVars = getBotConfigFromEnv();
const cliOptions = parseModuleOptions(options.bot);
const config = mergeEnvVarsAndCliOptions<BotConfig>(envVars, cliOptions);

const botRunner = new BotRunner(config, { pxe: deps.pxe });
const botServer = createBotRunnerRpcServer(botRunner);
await botRunner.start();
services.push({ bot: botServer });
signalHandlers.push(botRunner.stop);
}
23 changes: 11 additions & 12 deletions yarn-project/aztec/src/cli/cmds/start_node.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,19 +3,19 @@ import {
createAztecNodeRpcServer,
getConfigEnvVars as getNodeConfigEnvVars,
} from '@aztec/aztec-node';
import { type PXE } from '@aztec/circuit-types';
import { NULL_KEY } from '@aztec/ethereum';
import { type ServerList } from '@aztec/foundation/json-rpc/server';
import { type LogFn } from '@aztec/foundation/log';
import { createProvingJobSourceServer } from '@aztec/prover-client/prover-agent';
import { type PXEServiceConfig, createPXERpcServer, getPXEServiceConfig } from '@aztec/pxe';
import {
createAndStartTelemetryClient,
getConfigEnvVars as getTelemetryClientConfig,
} from '@aztec/telemetry-client/start';

import { mnemonicToAccount, privateKeyToAccount } from 'viem/accounts';

import { MNEMONIC, createAztecNode, createAztecPXE, deployContractsToL1 } from '../../sandbox.js';
import { MNEMONIC, createAztecNode, deployContractsToL1 } from '../../sandbox.js';
import { mergeEnvVarsAndCliOptions, parseModuleOptions } from '../util.js';

const { DEPLOY_AZTEC_CONTRACTS } = process.env;
Expand Down Expand Up @@ -108,18 +108,17 @@ export const startNode = async (
// Add node stop function to signal handlers
signalHandlers.push(node.stop);

// Create a PXE client that connects to the node.
// Add a PXE client that connects to this node if requested
let pxe: PXE | undefined;
if (options.pxe) {
const pxeCliOptions = parseModuleOptions(options.pxe);
const pxeConfig = mergeEnvVarsAndCliOptions<PXEServiceConfig>(getPXEServiceConfig(), pxeCliOptions);
const pxe = await createAztecPXE(node, pxeConfig);
const pxeServer = createPXERpcServer(pxe);

// Add PXE to services list
services.push({ pxe: pxeServer });
const { addPXE } = await import('./start_pxe.js');
pxe = await addPXE(options, services, signalHandlers, userLog, { node });
}

// Add PXE stop function to signal handlers
signalHandlers.push(pxe.stop);
// Add a txs bot if requested
if (options.bot) {
const { addBot } = await import('./start_bot.js');
await addBot(options, services, signalHandlers, { pxe });
}

return services;
Expand Down
48 changes: 25 additions & 23 deletions yarn-project/aztec/src/cli/cmds/start_pxe.ts
Original file line number Diff line number Diff line change
@@ -1,38 +1,40 @@
import { createAztecNodeClient } from '@aztec/circuit-types';
import { type AztecNode, createAztecNodeClient } from '@aztec/circuit-types';
import { type ServerList } from '@aztec/foundation/json-rpc/server';
import { type LogFn } from '@aztec/foundation/log';
import { type PXEServiceConfig, createPXERpcServer, createPXEService, getPXEServiceConfig } from '@aztec/pxe';

import { mergeEnvVarsAndCliOptions, parseModuleOptions } from '../util.js';

const { AZTEC_NODE_URL } = process.env;

export const startPXE = async (options: any, signalHandlers: (() => Promise<void>)[], userLog: LogFn) => {
// Services that will be started in a single multi-rpc server
export async function startPXE(options: any, signalHandlers: (() => Promise<void>)[], userLog: LogFn) {
const services: ServerList = [];
// Starting a PXE with a remote node.
// get env vars first
const pxeConfigEnvVars = getPXEServiceConfig();
// get config from options
const pxeCliOptions = parseModuleOptions(options.pxe);
await addPXE(options, services, signalHandlers, userLog, {});
return services;
}

// Determine node url from options or env vars
const nodeUrl = pxeCliOptions.nodeUrl || AZTEC_NODE_URL;
// throw if no Aztec Node URL is provided
if (!nodeUrl) {
export async function addPXE(
options: any,
services: ServerList,
signalHandlers: (() => Promise<void>)[],
userLog: LogFn,
deps: { node?: AztecNode } = {},
) {
const pxeCliOptions = parseModuleOptions(options.pxe);
const pxeConfig = mergeEnvVarsAndCliOptions<PXEServiceConfig>(getPXEServiceConfig(), pxeCliOptions);
const nodeUrl = pxeCliOptions.nodeUrl ?? process.env.AZTEC_NODE_URL;
if (!nodeUrl && !deps.node) {
userLog('Aztec Node URL (nodeUrl | AZTEC_NODE_URL) option is required to start PXE without --node option');
throw new Error('Aztec Node URL (nodeUrl | AZTEC_NODE_URL) option is required to start PXE without --node option');
process.exit(1);
}

// merge env vars and cli options
const pxeConfig = mergeEnvVarsAndCliOptions<PXEServiceConfig>(pxeConfigEnvVars, pxeCliOptions);

// create a node client
const node = createAztecNodeClient(nodeUrl);

const node = deps.node ?? createAztecNodeClient(nodeUrl);
const pxe = await createPXEService(node, pxeConfig);
const pxeServer = createPXERpcServer(pxe);

// Add PXE to services list
services.push({ pxe: pxeServer });

// Add PXE stop function to signal handlers
signalHandlers.push(pxe.stop);
return services;
};

return pxe;
}
10 changes: 10 additions & 0 deletions yarn-project/aztec/src/cli/texts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -92,4 +92,14 @@ export const cliTexts = {
'Starts a TXE with options\n' +
'Available options are listed below as cliProperty:ENV_VARIABLE_NAME.\n' +
'txePort:TXE_PORT - number - The port on which the TXE should listen for connections. Default: 8081\n',
bot:
'Starts a bot that sends token transfer txs at regular intervals, using a local or remote PXE\n' +
'Available options are listed below as cliProperty:ENV_VARIABLE_NAME.\n' +
'feePaymentMethod:BOT_FEE_PAYMENT_METHOD - native | none - How to pay for fees for each tx.\n' +
'senderPrivateKey:BOT_PRIVATE_KEY - hex - Private key for sending txs.\n' +
'tokenSalt:BOT_TOKEN_SALT - hex - Deployment salt for the token contract.\n' +
'recipientEncryptionSecret:BOT_RECIPIENT_ENCRYPTION_SECRET - hex - Encryption secret key for the recipient account.\n' +
'txIntervalSeconds:BOT_TX_INTERVAL_SECONDS - number - Interval between txs are started. Too low a value may result in multiple txs in flight at a time. \n' +
'privateTransfersPerTx:BOT_PRIVATE_TRANSFERS_PER_TX - number - How many private transfers to execute per tx. \n' +
'publicTransfersPerTx:BOT_PUBLIC_TRANSFERS_PER_TX - number - How many public transfers to execute per tx.\n',
};
8 changes: 6 additions & 2 deletions yarn-project/aztec/src/cli/util.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
import { type ArchiverConfig } from '@aztec/archiver';
import { type AztecNodeConfig } from '@aztec/aztec-node';
import { type AccountManager, type Fr } from '@aztec/aztec.js';
import { type BotConfig } from '@aztec/bot';
import { type L1ContractAddresses, l1ContractsNames } from '@aztec/ethereum';
import { EthAddress } from '@aztec/foundation/eth-address';
import { type ServerList } from '@aztec/foundation/json-rpc/server';
import { type LogFn, createConsoleLogger } from '@aztec/foundation/log';
import { type P2PConfig } from '@aztec/p2p';
import { type ProverNodeConfig } from '@aztec/prover-node';
import { type PXEService, type PXEServiceConfig } from '@aztec/pxe';

export interface ServiceStarter<T = any> {
Expand Down Expand Up @@ -66,8 +68,10 @@ export const parseModuleOptions = (options: string): Record<string, string> => {
}, {});
};

export const mergeEnvVarsAndCliOptions = <T extends AztecNodeConfig | PXEServiceConfig | P2PConfig | ArchiverConfig>(
envVars: AztecNodeConfig | PXEServiceConfig | P2PConfig | ArchiverConfig,
export const mergeEnvVarsAndCliOptions = <
T extends AztecNodeConfig | PXEServiceConfig | P2PConfig | ArchiverConfig | BotConfig | ProverNodeConfig,
>(
envVars: AztecNodeConfig | PXEServiceConfig | P2PConfig | ArchiverConfig | BotConfig | ProverNodeConfig,
cliOptions: Record<string, string>,
contractsRequired = false,
userLog = createConsoleLogger(),
Expand Down
6 changes: 1 addition & 5 deletions yarn-project/bot/src/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,13 +34,9 @@ export function getBotConfigFromEnv(): BotConfig {
throw new Error(`Invalid bot fee payment method: ${BOT_FEE_PAYMENT_METHOD}`);
}

if (!BOT_PRIVATE_KEY) {
throw new Error('Bot private key is required');
}

return getBotDefaultConfig({
pxeUrl: process.env.BOT_PXE_URL,
senderPrivateKey: Fr.fromString(BOT_PRIVATE_KEY),
senderPrivateKey: BOT_PRIVATE_KEY ? Fr.fromString(BOT_PRIVATE_KEY) : undefined,
recipientEncryptionSecret: BOT_RECIPIENT_ENCRYPTION_SECRET
? Fr.fromString(BOT_RECIPIENT_ENCRYPTION_SECRET)
: undefined,
Expand Down
1 change: 1 addition & 0 deletions yarn-project/bot/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
export { Bot } from './bot.js';
export { BotRunner } from './runner.js';
export { BotConfig, getBotConfigFromEnv, getBotDefaultConfig } from './config.js';
export { createBotRunnerRpcServer } from './rpc.js';
16 changes: 16 additions & 0 deletions yarn-project/bot/src/rpc.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { TxHash } from '@aztec/circuit-types';
import { AztecAddress } from '@aztec/foundation/aztec-address';
import { EthAddress } from '@aztec/foundation/eth-address';
import { Fr } from '@aztec/foundation/fields';
import { JsonRpcServer } from '@aztec/foundation/json-rpc/server';

import { type BotRunner } from './runner.js';

/**
* Wraps a bot runner with a JSON RPC HTTP server.
* @param botRunner - The BotRunner.
* @returns An JSON-RPC HTTP server
*/
export function createBotRunnerRpcServer(botRunner: BotRunner) {
return new JsonRpcServer(botRunner, { AztecAddress, EthAddress, Fr, TxHash }, {}, []);
}
78 changes: 65 additions & 13 deletions yarn-project/bot/src/runner.ts
Original file line number Diff line number Diff line change
@@ -1,50 +1,102 @@
import { createDebugLogger } from '@aztec/aztec.js';
import { type PXE, createDebugLogger } from '@aztec/aztec.js';

import { Bot } from './bot.js';
import { type BotConfig } from './config.js';

export class BotRunner {
private log = createDebugLogger('aztec:bot');
private interval?: NodeJS.Timeout;
private bot: Promise<Bot>;
private bot?: Promise<Bot>;
private pxe?: PXE;
private running: Set<Promise<void>> = new Set();

protected constructor(private config: BotConfig) {
this.bot = Bot.create(this.config);
public constructor(private config: BotConfig, dependencies: { pxe?: PXE } = {}) {
this.pxe = dependencies.pxe;
}

/** Initializes the bot if needed. Blocks until the bot setup is finished. */
public async setup() {
if (!this.bot) {
this.log.verbose(`Setting up bot`);
await this.#createBot();
this.log.info(`Bot set up completed`);
}
}

/**
* Initializes the bot if needed and starts sending txs at regular intervals.
* Blocks until the bot setup is finished.
*/
public async start() {
await this.setup();
if (!this.interval) {
await this.bot;
this.log.info(`Starting bot with interval of ${this.config.txIntervalSeconds}s`);
this.interval = setInterval(() => this.run(), this.config.txIntervalSeconds * 1000);
}
}

public stop() {
/**
* Stops sending txs. Returns once all ongoing txs are finished.
*/
public async stop() {
if (this.interval) {
this.log.info(`Stopping bot`);
this.log.verbose(`Stopping bot`);
clearInterval(this.interval);
this.interval = undefined;
}
if (this.running.size > 0) {
this.log.verbose(`Waiting for ${this.running.size} running txs to finish`);
await Promise.all(this.running);
}
this.log.info(`Stopped bot`);
}

/** Returns whether the bot is running. */
public isRunning() {
return !!this.interval;
}

public update(config: BotConfig) {
/**
* Updates the bot config and recreates the bot. Will stop and restart the bot automatically if it was
* running when this method was called. Blocks until the new bot is set up.
*/
public async update(config: BotConfig) {
this.log.verbose(`Updating bot config`);
const wasRunning = this.isRunning();
if (wasRunning) {
this.stop();
await this.stop();
}
this.config = config;
this.bot = Bot.create(this.config);
this.config = { ...this.config, ...config };
await this.#createBot();
this.log.info(`Bot config updated`);
if (wasRunning) {
void this.start();
await this.start();
}
}

/**
* Triggers a single iteration of the bot. Requires the bot to be initialized.
* Blocks until the run is finished.
*/
public async run() {
if (!this.bot) {
throw new Error(`Bot is not initialized`);
}
this.log.verbose(`Manually triggered bot run`);
const bot = await this.bot;
await bot.run();
const promise = bot.run();
this.running.add(promise);
await promise;
this.running.delete(promise);
}

/** Returns the current configuration for the bot. */
public getConfig() {
return this.config;
}

async #createBot() {
this.bot = Bot.create(this.config, { pxe: this.pxe });
await this.bot;
}
}

0 comments on commit 71885e4

Please sign in to comment.