From 9494fb4120b011295ebf85fcd232fbd6495c479b Mon Sep 17 00:00:00 2001 From: Santiago Palladino Date: Fri, 17 Apr 2026 15:44:35 -0300 Subject: [PATCH 1/3] feat(archiver): validate historical L1 log availability at startup Probe each configured L1 RPC for the OwnershipTransferred event emitted by the Rollup at deploy to detect providers that prune old logs. Abort startup with a detailed error (per-URL client version, required contract addresses, reth-specific guidance) unless ARCHIVER_SKIP_HISTORICAL_LOGS_CHECK=true. --- .../archiver/src/archiver-misc.test.ts | 2 + .../archiver/src/archiver-store.test.ts | 3 + .../archiver/src/archiver-sync.test.ts | 3 + yarn-project/archiver/src/archiver.ts | 18 ++- yarn-project/archiver/src/config.ts | 8 + yarn-project/archiver/src/factory.ts | 1 + .../src/l1/validate_historical_logs.test.ts | 134 +++++++++++++++++ .../src/l1/validate_historical_logs.ts | 140 ++++++++++++++++++ .../archiver/src/test/noop_l1_archiver.ts | 3 + yarn-project/ethereum/src/client.ts | 9 ++ .../ethereum/src/contracts/rollup.test.ts | 12 ++ yarn-project/ethereum/src/contracts/rollup.ts | 11 ++ yarn-project/foundation/src/config/env_var.ts | 1 + .../stdlib/src/interfaces/archiver.ts | 4 + 14 files changed, 348 insertions(+), 1 deletion(-) create mode 100644 yarn-project/archiver/src/l1/validate_historical_logs.test.ts create mode 100644 yarn-project/archiver/src/l1/validate_historical_logs.ts diff --git a/yarn-project/archiver/src/archiver-misc.test.ts b/yarn-project/archiver/src/archiver-misc.test.ts index 78de2d98cdef..d021953fa1f3 100644 --- a/yarn-project/archiver/src/archiver-misc.test.ts +++ b/yarn-project/archiver/src/archiver-misc.test.ts @@ -64,7 +64,9 @@ describe('Archiver misc', () => { publicClient, rollupContract, { + rollupAddress: EthAddress.random(), registryAddress: EthAddress.random(), + inboxAddress: EthAddress.random(), governanceProposerAddress: EthAddress.random(), slashingProposerAddress: EthAddress.random(), }, diff --git a/yarn-project/archiver/src/archiver-store.test.ts b/yarn-project/archiver/src/archiver-store.test.ts index c9d705efc4b4..c3bc47e6306e 100644 --- a/yarn-project/archiver/src/archiver-store.test.ts +++ b/yarn-project/archiver/src/archiver-store.test.ts @@ -33,6 +33,7 @@ import { makeChainedCheckpoints } from './test/mock_structs.js'; describe('Archiver Store', () => { const rollupAddress = EthAddress.random(); const registryAddress = EthAddress.random(); + const inboxAddress = EthAddress.random(); const governanceProposerAddress = EthAddress.random(); const slashingProposerAddress = EthAddress.random(); @@ -76,7 +77,9 @@ describe('Archiver Store', () => { }; const contractAddresses = { + rollupAddress, registryAddress, + inboxAddress, governanceProposerAddress, slashingProposerAddress, }; diff --git a/yarn-project/archiver/src/archiver-sync.test.ts b/yarn-project/archiver/src/archiver-sync.test.ts index cecfb3dfe5cf..9501538c6a36 100644 --- a/yarn-project/archiver/src/archiver-sync.test.ts +++ b/yarn-project/archiver/src/archiver-sync.test.ts @@ -100,7 +100,9 @@ describe('Archiver Sync', () => { archiverStore = new KVArchiverDataStore(await openTmpStore('archiver_sync_test'), 1000); const contractAddresses = { + rollupAddress, registryAddress, + inboxAddress, governanceProposerAddress, slashingProposerAddress, }; @@ -114,6 +116,7 @@ describe('Archiver Sync', () => { batchSize: 1000, maxAllowedEthClientDriftSeconds: 300, ethereumAllowNoDebugHosts: true, + skipHistoricalLogsCheck: true, }; // Create event emitter shared by archiver and synchronizer diff --git a/yarn-project/archiver/src/archiver.ts b/yarn-project/archiver/src/archiver.ts index 931395155957..0e440d8c9697 100644 --- a/yarn-project/archiver/src/archiver.ts +++ b/yarn-project/archiver/src/archiver.ts @@ -31,6 +31,7 @@ import { type TelemetryClient, type Traceable, type Tracer, trackSpan } from '@a import { type ArchiverConfig, mapArchiverConfig } from './config.js'; import { BlockAlreadyCheckpointedError, NoBlobBodiesFoundError } from './errors.js'; +import { validateAndLogHistoricalLogsAvailability } from './l1/validate_historical_logs.js'; import { validateAndLogTraceAvailability } from './l1/validate_trace.js'; import { ArchiverDataSourceBase } from './modules/data_source_base.js'; import { ArchiverDataStoreUpdater } from './modules/data_store_updater.js'; @@ -106,7 +107,10 @@ export class Archiver extends ArchiverDataSourceBase implements L2BlockSink, Tra private readonly publicClient: ViemPublicClient, private readonly debugClient: ViemPublicDebugClient, private readonly rollup: RollupContract, - private readonly l1Addresses: Pick & { + private readonly l1Addresses: Pick< + L1ContractAddresses, + 'rollupAddress' | 'registryAddress' | 'inboxAddress' | 'governanceProposerAddress' + > & { slashingProposerAddress: EthAddress; }, readonly dataStore: KVArchiverDataStore, @@ -116,6 +120,7 @@ export class Archiver extends ArchiverDataSourceBase implements L2BlockSink, Tra skipValidateCheckpointAttestations?: boolean; maxAllowedEthClientDriftSeconds: number; ethereumAllowNoDebugHosts?: boolean; + skipHistoricalLogsCheck?: boolean; }, private readonly blobClient: BlobClientInterface, instrumentation: ArchiverInstrumentation, @@ -172,6 +177,17 @@ export class Archiver extends ArchiverDataSourceBase implements L2BlockSink, Tra this.config.ethereumAllowNoDebugHosts ?? false, this.log.getBindings(), ); + await validateAndLogHistoricalLogsAvailability( + this.publicClient, + { + rollupAddress: this.l1Addresses.rollupAddress, + inboxAddress: this.l1Addresses.inboxAddress, + registryAddress: this.l1Addresses.registryAddress, + governanceProposerAddress: this.l1Addresses.governanceProposerAddress, + }, + this.config.skipHistoricalLogsCheck ?? false, + this.log.getBindings(), + ); // Log initial state for the archiver const { l1StartBlock } = this.l1Constants; diff --git a/yarn-project/archiver/src/config.ts b/yarn-project/archiver/src/config.ts index 93f3bdbe83f4..3e9780a9d706 100644 --- a/yarn-project/archiver/src/config.ts +++ b/yarn-project/archiver/src/config.ts @@ -67,6 +67,13 @@ export const archiverConfigMappings: ConfigMappingsType = { description: 'Whether to allow starting the archiver without debug/trace method support on Ethereum hosts', ...booleanConfigHelper(true), }, + archiverSkipHistoricalLogsCheck: { + env: 'ARCHIVER_SKIP_HISTORICAL_LOGS_CHECK', + description: + 'Skip the startup check that probes the L1 RPC for historical Rollup contract logs. ' + + 'Set to true to bypass the check when the connected RPC node is known to prune old logs.', + ...booleanConfigHelper(false), + }, ...chainConfigMappings, ...l1ReaderConfigMappings, viemPollingIntervalMS: { @@ -98,5 +105,6 @@ export function mapArchiverConfig(config: Partial) { skipValidateCheckpointAttestations: config.skipValidateCheckpointAttestations, maxAllowedEthClientDriftSeconds: config.maxAllowedEthClientDriftSeconds, ethereumAllowNoDebugHosts: config.ethereumAllowNoDebugHosts, + skipHistoricalLogsCheck: config.archiverSkipHistoricalLogsCheck, }; } diff --git a/yarn-project/archiver/src/factory.ts b/yarn-project/archiver/src/factory.ts index db7b06b5b1da..481638943414 100644 --- a/yarn-project/archiver/src/factory.ts +++ b/yarn-project/archiver/src/factory.ts @@ -121,6 +121,7 @@ export async function createArchiver( batchSize: 100, maxAllowedEthClientDriftSeconds: 300, ethereumAllowNoDebugHosts: false, + skipHistoricalLogsCheck: false, }, mapArchiverConfig(config), ); diff --git a/yarn-project/archiver/src/l1/validate_historical_logs.test.ts b/yarn-project/archiver/src/l1/validate_historical_logs.test.ts new file mode 100644 index 000000000000..6b4f791116a3 --- /dev/null +++ b/yarn-project/archiver/src/l1/validate_historical_logs.test.ts @@ -0,0 +1,134 @@ +import { RollupContract } from '@aztec/ethereum/contracts'; +import type { ViemPublicClient } from '@aztec/ethereum/types'; +import { EthAddress } from '@aztec/foundation/eth-address'; + +import { jest } from '@jest/globals'; +import { createPublicClient, fallback, http } from 'viem'; +import { foundry } from 'viem/chains'; + +import { + type HistoricalLogsContractAddresses, + validateAndLogHistoricalLogsAvailability, +} from './validate_historical_logs.js'; + +describe('validateAndLogHistoricalLogsAvailability', () => { + const url1 = 'http://fake-url-1:8545'; + const url2 = 'http://fake-url-2:8545'; + + let client: ViemPublicClient; + let addresses: HistoricalLogsContractAddresses; + let probeSpy: jest.SpiedFunction; + let fetchSpy: jest.Spied; + + beforeEach(() => { + // Build a real fallback-transport client over two fake URLs so the validator can extract them. + client = createPublicClient({ + chain: foundry, + transport: fallback([http(url1, { batch: false }), http(url2, { batch: false })]), + }) as ViemPublicClient; + + addresses = { + rollupAddress: EthAddress.random(), + inboxAddress: EthAddress.random(), + registryAddress: EthAddress.random(), + governanceProposerAddress: EthAddress.random(), + }; + + probeSpy = jest.spyOn(RollupContract.prototype, 'getOwnershipTransferredEventsAtDeploy'); + + // By default, make fetch (used by the per-URL client for web3_clientVersion) reject so we exercise + // the "Could not determine client version" branch without real network traffic. + fetchSpy = jest.spyOn(globalThis, 'fetch').mockRejectedValue(new Error('fetch disabled in tests')); + }); + + afterEach(() => { + jest.restoreAllMocks(); + }); + + it('resolves when every URL returns the OwnershipTransferred event', async () => { + probeSpy.mockResolvedValue([{ blockNumber: 10n } as any]); + await expect(validateAndLogHistoricalLogsAvailability(client, addresses, false)).resolves.toBeUndefined(); + expect(probeSpy).toHaveBeenCalledTimes(2); + }); + + it('throws on the first URL that returns no events, and includes that URL and all addresses', async () => { + probeSpy.mockResolvedValue([]); + const err = await validateAndLogHistoricalLogsAvailability(client, addresses, false).then( + () => new Error('expected to throw'), + (e: unknown) => e as Error, + ); + expect(err).toBeInstanceOf(Error); + expect(err.message).toMatch(/does not return historical logs/); + expect(err.message).toContain(url1); + expect(err.message).not.toContain(url2); + expect(err.message).toContain(addresses.rollupAddress.toString()); + expect(err.message).toContain(addresses.inboxAddress.toString()); + expect(err.message).toContain(addresses.registryAddress.toString()); + expect(err.message).toContain(addresses.governanceProposerAddress.toString()); + expect(probeSpy).toHaveBeenCalledTimes(1); + }); + + it('throws on the second URL when the first one succeeds', async () => { + probeSpy.mockResolvedValueOnce([{ blockNumber: 10n } as any]); + probeSpy.mockResolvedValueOnce([]); + const err = await validateAndLogHistoricalLogsAvailability(client, addresses, false).then( + () => new Error('expected to throw'), + (e: unknown) => e as Error, + ); + expect(err.message).toContain(url2); + expect(err.message).not.toContain(url1); + expect(probeSpy).toHaveBeenCalledTimes(2); + }); + + it('throws when the RPC event query itself fails', async () => { + probeSpy.mockRejectedValue(new Error('rpc exploded')); + const err = await validateAndLogHistoricalLogsAvailability(client, addresses, false).then( + () => new Error('expected to throw'), + (e: unknown) => e as Error, + ); + expect(err.message).toMatch(/rpc exploded/); + }); + + it('logs a warning and continues when skipCheck is true', async () => { + probeSpy.mockResolvedValue([]); + await expect(validateAndLogHistoricalLogsAvailability(client, addresses, true)).resolves.toBeUndefined(); + // Skip mode still probes every URL. + expect(probeSpy).toHaveBeenCalledTimes(2); + }); + + it('includes reth-specific guidance when the L1 client reports reth', async () => { + probeSpy.mockResolvedValue([]); + fetchSpy.mockImplementation(() => + Promise.resolve( + new Response(JSON.stringify({ jsonrpc: '2.0', id: 1, result: 'reth/v1.0.0-abcdef/linux-x86_64/1.75.0' }), { + status: 200, + headers: { 'Content-Type': 'application/json' }, + }), + ), + ); + const err = await validateAndLogHistoricalLogsAvailability(client, addresses, false).then( + () => new Error('expected to throw'), + (e: unknown) => e as Error, + ); + expect(err.message).toMatch(/Detected reth/); + expect(err.message).toMatch(/prune\.segments\.receipts_log_filter/); + }); + + it('falls back to generic guidance when the L1 client is not reth', async () => { + probeSpy.mockResolvedValue([]); + fetchSpy.mockImplementation(() => + Promise.resolve( + new Response(JSON.stringify({ jsonrpc: '2.0', id: 1, result: 'Geth/v1.14.0-stable/linux-amd64/go1.22.0' }), { + status: 200, + headers: { 'Content-Type': 'application/json' }, + }), + ), + ); + const err = await validateAndLogHistoricalLogsAvailability(client, addresses, false).then( + () => new Error('expected to throw'), + (e: unknown) => e as Error, + ); + expect(err.message).not.toMatch(/prune\.segments\.receipts_log_filter/); + expect(err.message).toMatch(/retains full log history/); + }); +}); diff --git a/yarn-project/archiver/src/l1/validate_historical_logs.ts b/yarn-project/archiver/src/l1/validate_historical_logs.ts new file mode 100644 index 000000000000..efd8e40e24b7 --- /dev/null +++ b/yarn-project/archiver/src/l1/validate_historical_logs.ts @@ -0,0 +1,140 @@ +import { getPublicClient, getRpcUrlsFromClient } from '@aztec/ethereum/client'; +import { RollupContract } from '@aztec/ethereum/contracts'; +import type { L1ContractAddresses } from '@aztec/ethereum/l1-contract-addresses'; +import type { ViemPublicClient } from '@aztec/ethereum/types'; +import { type Logger, type LoggerBindings, createLogger } from '@aztec/foundation/log'; + +/** Subset of L1 contract addresses whose historical logs the Aztec node relies on. */ +export type HistoricalLogsContractAddresses = Pick< + L1ContractAddresses, + 'rollupAddress' | 'inboxAddress' | 'registryAddress' | 'governanceProposerAddress' +>; + +/** Result of probing a single RPC URL. */ +type ProbeResult = { ok: true } | { ok: false; reason: string; clientVersion: string | undefined }; + +/** + * Validates that every configured L1 RPC URL returns historical logs for the Rollup contract. + * + * Some RPC providers prune old logs, which would cause L1 syncing to silently fail. To detect this, + * we query for the `OwnershipTransferred` event which every Rollup emits in its constructor (via + * Ownable) on the block it was deployed (`l1StartBlock`). The `client` is typically a viem fallback + * transport over several user-configured RPC URLs — checking only the first URL would miss a bad + * secondary, so we probe each URL independently. The first URL that fails aborts startup, unless + * the operator has explicitly opted out. + * + * @param client - The L1 public client built from the user-configured RPC URLs. + * @param addresses - The subset of L1 contract addresses we rely on for historical log retrieval. + * @param skipCheck - If true, log warnings instead of throwing. + * @param bindings - Optional logger bindings for context. + * @throws Error if any URL fails the probe and skipCheck is false. + */ +export async function validateAndLogHistoricalLogsAvailability( + client: ViemPublicClient, + addresses: HistoricalLogsContractAddresses, + skipCheck: boolean, + bindings?: LoggerBindings, +): Promise { + const logger = createLogger('archiver:validate_historical_logs', bindings); + logger.debug('Validating historical log availability on L1 RPCs'); + + const urls = getRpcUrlsFromClient(client); + if (urls.length === 0) { + logger.warn('Could not determine L1 RPC URLs from the public client; skipping historical logs check.'); + return; + } + + const chainId = client.chain?.id; + if (chainId === undefined) { + logger.warn('Could not determine L1 chain ID from the public client; skipping historical logs check.'); + return; + } + + for (const url of urls) { + const probeClient = getPublicClient({ l1RpcUrls: [url], l1ChainId: chainId }); + const rollup = new RollupContract(probeClient, addresses.rollupAddress.toString()); + const result = await probeRpcUrl(rollup, probeClient, logger); + + if (result.ok) { + logger.debug(`L1 RPC ${url} returned historical OwnershipTransferred log for the Rollup contract.`); + continue; + } + + const errorMessage = buildErrorMessage(url, result, addresses); + if (skipCheck) { + logger.warn(`${errorMessage}\nContinuing because ARCHIVER_SKIP_HISTORICAL_LOGS_CHECK is true.`); + continue; + } + + logger.error(errorMessage); + throw new Error(errorMessage); + } +} + +/** Runs the OwnershipTransferred probe against a single RPC and queries its client version. */ +async function probeRpcUrl(rollup: RollupContract, client: ViemPublicClient, logger: Logger): Promise { + let queryError: unknown; + try { + const logs = await rollup.getOwnershipTransferredEventsAtDeploy(); + if (logs.length > 0) { + return { ok: true }; + } + } catch (err) { + queryError = err; + } + + const clientVersion = await getClientVersion(client, logger); + + let reason: string; + if (queryError instanceof Error) { + reason = `Query for historical logs failed: ${queryError.message}`; + } else if (queryError !== undefined) { + reason = 'Query for historical logs failed with a non-Error value.'; + } else { + reason = 'No OwnershipTransferred event was returned by the L1 RPC for the Rollup deploy block.'; + } + return { ok: false, reason, clientVersion }; +} + +/** Builds the operator-facing error message for a failing RPC URL. */ +function buildErrorMessage( + url: string, + result: Extract, + addresses: HistoricalLogsContractAddresses, +): string { + return [ + `L1 RPC at ${url} does not return historical logs for the Rollup contract. ${result.reason}`, + `This likely means this Ethereum RPC node prunes old logs, which would cause the archiver ` + + `to silently miss data during L1 sync.`, + result.clientVersion + ? `Detected L1 client version for ${url}: ${result.clientVersion}.` + : `Could not determine L1 client version for ${url}.`, + `The following L1 contract addresses must have their historical logs retained by the RPC node:`, + ` - Rollup: ${addresses.rollupAddress.toString()}`, + ` - Inbox: ${addresses.inboxAddress.toString()}`, + ` - Registry: ${addresses.registryAddress.toString()}`, + ` - GovernanceProposer: ${addresses.governanceProposerAddress.toString()}`, + isReth(result.clientVersion) + ? `To retain logs for these contracts, configure reth with a ` + + `prune.segments.receipts_log_filter entry for each address above ` + + `so reth does not prune their receipts/logs. See https://reth.rs/run/pruning.html for details.` + : `Point this RPC endpoint at a node that retains full log history for the addresses above.`, + `Set ARCHIVER_SKIP_HISTORICAL_LOGS_CHECK=true to bypass this check at your own risk.`, + ].join('\n'); +} + +/** Queries `web3_clientVersion` on the L1 RPC. Returns undefined if the call fails or returns a non-string. */ +async function getClientVersion(client: ViemPublicClient, logger: Logger): Promise { + try { + const result = await client.request({ method: 'web3_clientVersion' }); + return typeof result === 'string' ? result : undefined; + } catch (err) { + logger.debug(`Failed to query web3_clientVersion: ${err instanceof Error ? err.message : err}`); + return undefined; + } +} + +/** Heuristic check for reth based on the web3_clientVersion string (reth returns e.g. "reth/v1.0.0-..."). */ +function isReth(clientVersion: string | undefined): boolean { + return !!clientVersion && /reth/i.test(clientVersion); +} diff --git a/yarn-project/archiver/src/test/noop_l1_archiver.ts b/yarn-project/archiver/src/test/noop_l1_archiver.ts index e1eccb37b5b6..3f70da3452d1 100644 --- a/yarn-project/archiver/src/test/noop_l1_archiver.ts +++ b/yarn-project/archiver/src/test/noop_l1_archiver.ts @@ -70,7 +70,9 @@ export class NoopL1Archiver extends Archiver { debugClient, rollup, { + rollupAddress: EthAddress.ZERO, registryAddress: EthAddress.ZERO, + inboxAddress: EthAddress.ZERO, governanceProposerAddress: EthAddress.ZERO, slashingProposerAddress: EthAddress.ZERO, }, @@ -81,6 +83,7 @@ export class NoopL1Archiver extends Archiver { skipValidateCheckpointAttestations: true, maxAllowedEthClientDriftSeconds: 300, ethereumAllowNoDebugHosts: true, // Skip trace validation + skipHistoricalLogsCheck: true, // Skip historical logs validation }, blobClient, instrumentation, diff --git a/yarn-project/ethereum/src/client.ts b/yarn-project/ethereum/src/client.ts index 3769b798719c..e4a26f778e1b 100644 --- a/yarn-project/ethereum/src/client.ts +++ b/yarn-project/ethereum/src/client.ts @@ -36,6 +36,15 @@ export function makeL1HttpTransport(rpcUrls: string[], opts?: { timeout?: number return fallback(rpcUrls.map(url => http(url, { batch: false, timeout: opts?.timeout }))); } +/** + * Returns the individual RPC URLs underlying a viem public client that was constructed with a + * fallback HTTP transport (see {@link makeL1HttpTransport}). Returns an empty array if the + * transport shape is not recognized. + */ +export function getRpcUrlsFromClient(client: ViemPublicClient): string[] { + return client.transport.transports.map(t => t?.value?.url).filter((url): url is string => typeof url === 'string'); +} + // TODO: Use these methods to abstract the creation of viem clients. /** Returns a viem public client given the L1 config. */ diff --git a/yarn-project/ethereum/src/contracts/rollup.test.ts b/yarn-project/ethereum/src/contracts/rollup.test.ts index a9ef1863314d..b2e0f3238e4b 100644 --- a/yarn-project/ethereum/src/contracts/rollup.test.ts +++ b/yarn-project/ethereum/src/contracts/rollup.test.ts @@ -355,6 +355,18 @@ describe('Rollup', () => { }); }); + describe('getOwnershipTransferredEventsAtDeploy', () => { + it('finds OwnershipTransferred event emitted at deploy block', async () => { + const logs = await rollup.getOwnershipTransferredEventsAtDeploy(); + expect(logs.length).toBeGreaterThan(0); + + const l1StartBlock = await rollup.getL1StartBlock(); + for (const log of logs) { + expect(log.blockNumber).toBe(l1StartBlock); + } + }); + }); + describe('compressFeeHeader', () => { it('compressed fee header can be read back by L1 getFeeHeader', async () => { const feeHeader: FeeHeader = { diff --git a/yarn-project/ethereum/src/contracts/rollup.ts b/yarn-project/ethereum/src/contracts/rollup.ts index df763e2a73ee..1dd381b926df 100644 --- a/yarn-project/ethereum/src/contracts/rollup.ts +++ b/yarn-project/ethereum/src/contracts/rollup.ts @@ -1266,6 +1266,17 @@ export class RollupContract { ); } + /** + * Fetches OwnershipTransferred events emitted on the L1 block this rollup was deployed on. + * The Rollup inherits from Ownable and emits this event in its constructor, so the event + * is guaranteed to exist on `l1StartBlock` for any correctly deployed rollup. Used as a + * probe to detect RPC nodes that prune historical logs. + */ + async getOwnershipTransferredEventsAtDeploy() { + const l1StartBlock = await this.getL1StartBlock(); + return await this.rollup.getEvents.OwnershipTransferred({}, { fromBlock: l1StartBlock, toBlock: l1StartBlock }); + } + /** Fetches CheckpointProposed events within the given block range. */ async getCheckpointProposedEvents(fromBlock: bigint, toBlock: bigint): Promise { const logs = await this.rollup.getEvents.CheckpointProposed({}, { fromBlock, toBlock }); diff --git a/yarn-project/foundation/src/config/env_var.ts b/yarn-project/foundation/src/config/env_var.ts index ec423746dcab..b4048381ecef 100644 --- a/yarn-project/foundation/src/config/env_var.ts +++ b/yarn-project/foundation/src/config/env_var.ts @@ -7,6 +7,7 @@ export type EnvVar = | 'API_PREFIX' | 'ARCHIVER_MAX_LOGS' | 'ARCHIVER_POLLING_INTERVAL_MS' + | 'ARCHIVER_SKIP_HISTORICAL_LOGS_CHECK' | 'ARCHIVER_URL' | 'ARCHIVER_VIEM_POLLING_INTERVAL_MS' | 'ARCHIVER_BATCH_SIZE' diff --git a/yarn-project/stdlib/src/interfaces/archiver.ts b/yarn-project/stdlib/src/interfaces/archiver.ts index cf5f22ebf026..d722819638f8 100644 --- a/yarn-project/stdlib/src/interfaces/archiver.ts +++ b/yarn-project/stdlib/src/interfaces/archiver.ts @@ -60,6 +60,9 @@ export type ArchiverSpecificConfig = { /** Whether to allow starting the archiver without debug/trace method support on Ethereum hosts */ ethereumAllowNoDebugHosts?: boolean; + /** Skip the startup check that probes the L1 RPC for historical logs on the Rollup contract. */ + archiverSkipHistoricalLogsCheck?: boolean; + /** Skip validating checkpoint attestations (for testing purposes only) */ skipValidateCheckpointAttestations?: boolean; }; @@ -72,6 +75,7 @@ export const ArchiverSpecificConfigSchema = z.object({ archiverStoreMapSizeKb: schemas.Integer.optional(), maxAllowedEthClientDriftSeconds: schemas.Integer.optional(), ethereumAllowNoDebugHosts: z.boolean().optional(), + archiverSkipHistoricalLogsCheck: z.boolean().optional(), skipValidateCheckpointAttestations: z.boolean().optional(), }); From 1016232630ea0da4da18feaead478e1b7fc95e70 Mon Sep 17 00:00:00 2001 From: Santiago Palladino Date: Fri, 17 Apr 2026 16:44:24 -0300 Subject: [PATCH 2/3] test(archiver): align reth guidance assertion with current error message --- yarn-project/archiver/src/l1/validate_historical_logs.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/yarn-project/archiver/src/l1/validate_historical_logs.test.ts b/yarn-project/archiver/src/l1/validate_historical_logs.test.ts index 6b4f791116a3..d9275e322048 100644 --- a/yarn-project/archiver/src/l1/validate_historical_logs.test.ts +++ b/yarn-project/archiver/src/l1/validate_historical_logs.test.ts @@ -110,7 +110,7 @@ describe('validateAndLogHistoricalLogsAvailability', () => { () => new Error('expected to throw'), (e: unknown) => e as Error, ); - expect(err.message).toMatch(/Detected reth/); + expect(err.message).toMatch(/Detected L1 client version for .*: reth/); expect(err.message).toMatch(/prune\.segments\.receipts_log_filter/); }); From 8ed7a23714449520e15870421284a66243e3f799 Mon Sep 17 00:00:00 2001 From: Santiago Palladino Date: Fri, 17 Apr 2026 17:27:09 -0300 Subject: [PATCH 3/3] fix(ethereum): guard getRpcUrlsFromClient against non-fallback transports The archiver's mock viem client used in archiver-sync tests does not carry a fallback transport, so transport.transports is undefined and accessing .map() throws. Handle both single-http transports and missing transports shape gracefully by returning an empty array when we cannot infer URLs. --- yarn-project/ethereum/src/client.ts | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/yarn-project/ethereum/src/client.ts b/yarn-project/ethereum/src/client.ts index e4a26f778e1b..010097d50894 100644 --- a/yarn-project/ethereum/src/client.ts +++ b/yarn-project/ethereum/src/client.ts @@ -39,10 +39,19 @@ export function makeL1HttpTransport(rpcUrls: string[], opts?: { timeout?: number /** * Returns the individual RPC URLs underlying a viem public client that was constructed with a * fallback HTTP transport (see {@link makeL1HttpTransport}). Returns an empty array if the - * transport shape is not recognized. + * transport shape is not recognized (e.g. mock clients in tests, or non-fallback transports). */ export function getRpcUrlsFromClient(client: ViemPublicClient): string[] { - return client.transport.transports.map(t => t?.value?.url).filter((url): url is string => typeof url === 'string'); + const transport = client.transport as unknown as { + transports?: { value?: { url?: string } }[]; + value?: { url?: string }; + url?: string; + }; + if (Array.isArray(transport?.transports)) { + return transport.transports.map(t => t?.value?.url).filter((url): url is string => typeof url === 'string'); + } + const singleUrl = transport?.value?.url ?? transport?.url; + return typeof singleUrl === 'string' ? [singleUrl] : []; } // TODO: Use these methods to abstract the creation of viem clients.