Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
86 changes: 86 additions & 0 deletions yarn-project/aztec/src/cli/cmds/start_node.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,14 @@ import { Fr } from '@aztec/aztec.js/fields';
import { getSponsoredFPCAddress } from '@aztec/cli/cli-utils';
import { getL1Config } from '@aztec/cli/config';
import { getPublicClient } from '@aztec/ethereum/client';
import { RegistryContract, RollupContract } from '@aztec/ethereum/contracts';
import { SecretValue } from '@aztec/foundation/config';
import { EthAddress } from '@aztec/foundation/eth-address';
import type { NamespacedApiHandlers } from '@aztec/foundation/json-rpc/server';
import { startHttpRpcServer } from '@aztec/foundation/json-rpc/server';
import { Agent, makeUndiciFetch } from '@aztec/foundation/json-rpc/undici';
import type { LogFn } from '@aztec/foundation/log';
import { sleep } from '@aztec/foundation/sleep';
import { ProvingJobConsumerSchema, createProvingJobBrokerClient } from '@aztec/prover-client/broker';
import { type CliPXEOptions, type PXEConfig, allPxeConfigMappings } from '@aztec/pxe/config';
import { AztecNodeAdminApiSchema, AztecNodeApiSchema } from '@aztec/stdlib/interfaces/client';
Expand All @@ -21,6 +25,8 @@ import {
import { EmbeddedWallet } from '@aztec/wallets/embedded';
import { getGenesisValues } from '@aztec/world-state/testing';

import Koa from 'koa';

import { createAztecNode } from '../../local-network/index.js';
import {
extractNamespacedOptions,
Expand All @@ -31,6 +37,72 @@ import {
import { getVersions } from '../versioning.js';
import { startProverBroker } from './start_prover_broker.js';

const ROLLUP_POLL_INTERVAL_MS = 600_000;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would be in favour of reducing the interval to one minute. As it is nodes could potentially lose up to 25% of the first epoch (10 mins out of 38) depending on when they were started and when the switch over happens.


/**
* Waits until the canonical rollup's genesis archive root matches the expected local genesis root.
* If the rollup is not yet compatible (e.g. during L1 contract upgrades), enters standby mode:
* starts a lightweight HTTP server for K8s liveness probes and polls until a compatible rollup appears.
*/
async function waitForCompatibleRollup(
publicClient: ReturnType<typeof getPublicClient>,
registryAddress: EthAddress,
rollupVersion: number | 'canonical',
expectedGenesisRoot: Fr,
port: number | undefined,
userLog: LogFn,
): Promise<void> {
const registry = new RegistryContract(publicClient, registryAddress);
const rollupAddress = await registry.getRollupAddress(rollupVersion);
const rollup = new RollupContract(publicClient, rollupAddress.toString());

let l1GenesisRoot: Fr;
try {
l1GenesisRoot = await rollup.getGenesisArchiveTreeRoot();
} catch (err: any) {
throw new Error(
`Could not retrieve genesis archive root from canonical rollup at ${rollupAddress}: ${err.message}`,
);
}

if (l1GenesisRoot.equals(expectedGenesisRoot)) {
return;
}

userLog(
`Genesis root mismatch: expected ${expectedGenesisRoot}, got ${l1GenesisRoot} from rollup at ${rollupAddress}. ` +
`Entering standby mode. Will poll every ${ROLLUP_POLL_INTERVAL_MS / 1000}s for a compatible rollup...`,
);

const standbyServer = await startHttpRpcServer({ getApp: () => new Koa(), isHealthy: () => true }, { port });
userLog(`Standby status server listening on port ${standbyServer.port}`);

try {
while (true) {
await sleep(ROLLUP_POLL_INTERVAL_MS);
Comment on lines +80 to +82
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would use the retryUntil helper function we have in foundation but this is fine as it is.


const currentRollupAddress = await registry.getRollupAddress(rollupVersion);
const currentRollup = new RollupContract(publicClient, currentRollupAddress.toString());

try {
l1GenesisRoot = await currentRollup.getGenesisArchiveTreeRoot();
} catch {
userLog(`Failed to fetch genesis root from rollup at ${currentRollupAddress}. Retrying...`);
continue;
}

if (l1GenesisRoot.equals(expectedGenesisRoot)) {
userLog(`Compatible rollup found at ${currentRollupAddress}. Exiting standby mode.`);
return;
}

userLog(`Still waiting. Rollup at ${currentRollupAddress} has genesis root ${l1GenesisRoot}.`);
}
} finally {
await new Promise<void>((resolve, reject) => standbyServer.close(err => (err ? reject(err) : resolve())));
}
}

export async function startNode(
options: any,
signalHandlers: (() => Promise<void>)[],
Expand Down Expand Up @@ -96,6 +168,20 @@ export async function startNode(
if (!nodeConfig.l1Contracts.registryAddress || nodeConfig.l1Contracts.registryAddress.isZero()) {
throw new Error('L1 registry address is required to start Aztec Node');
}

// Wait for a compatible rollup before proceeding with full L1 config fetch.
// This prevents crashes when the canonical rollup hasn't been upgraded yet.
const publicClient = getPublicClient(nodeConfig);
const rollupVersion: number | 'canonical' = nodeConfig.rollupVersion ?? 'canonical';
await waitForCompatibleRollup(
publicClient,
nodeConfig.l1Contracts.registryAddress,
rollupVersion,
genesisArchiveRoot,
options.port,
userLog,
);

const { addresses, config } = await getL1Config(
nodeConfig.l1Contracts.registryAddress,
nodeConfig.l1RpcUrls,
Expand Down
Loading