diff --git a/yarn-project/foundation/src/config/env_var.ts b/yarn-project/foundation/src/config/env_var.ts index 41a34959a210..130c3033a966 100644 --- a/yarn-project/foundation/src/config/env_var.ts +++ b/yarn-project/foundation/src/config/env_var.ts @@ -79,6 +79,7 @@ export type EnvVar = | 'L1_CONSENSUS_HOST_URLS' | 'L1_CONSENSUS_HOST_API_KEYS' | 'L1_CONSENSUS_HOST_API_KEY_HEADERS' + | 'L1_TX_FAILED_STORE' | 'LOG_JSON' | 'LOG_MULTILINE' | 'LOG_NO_COLOR_PER_ACTOR' diff --git a/yarn-project/scripts/replay_failed_l1_tx.mjs b/yarn-project/scripts/replay_failed_l1_tx.mjs new file mode 100644 index 000000000000..54c21032cfb8 --- /dev/null +++ b/yarn-project/scripts/replay_failed_l1_tx.mjs @@ -0,0 +1,145 @@ +#!/usr/bin/env node +/** + * This script fetches a failed L1 transaction from the L1TxFailedStore and re-simulates it. + * Supports GCS, S3, R2, and local file paths. + * Usage: node scripts/replay_failed_l1_tx.mjs gs://bucket/path/simulation/0xabc123.json [--rpc-url URL] + */ +import { createLogger } from '@aztec/foundation/log'; +import { ErrorsAbi, RollupAbi } from '@aztec/l1-artifacts'; +import { createReadOnlyFileStore } from '@aztec/stdlib/file-store'; + +import { createPublicClient, decodeErrorResult, http } from 'viem'; + +const logger = createLogger('script:replay_failed_l1_tx'); + +// Parse arguments +const args = process.argv.slice(2); +let urlStr; +let rpcUrl; + +for (let i = 0; i < args.length; i++) { + if (args[i] === '--rpc-url' && args[i + 1]) { + rpcUrl = args[++i]; + } else if (!urlStr) { + urlStr = args[i]; + } +} + +if (!urlStr) { + logger.error('Usage: node scripts/replay_failed_l1_tx.mjs [--rpc-url URL]'); + logger.error('Supported URIs: gs://bucket/path, s3://bucket/path, file:///path, https://host/path'); + logger.error('Example: node scripts/replay_failed_l1_tx.mjs gs://my-bucket/failed-l1-txs/simulation/0xabc.json'); + process.exit(1); +} + +// Fetch the failed tx data from the store +const store = await createReadOnlyFileStore(urlStr, logger); +if (!store) { + logger.error(`Failed to create file store from: ${urlStr}`); + process.exit(1); +} + +logger.info(`Fetching failed L1 tx from: ${urlStr}`); + +let failedTx; +try { + const data = await store.read(urlStr); + failedTx = JSON.parse(data.toString()); +} catch (err) { + logger.error(`Failed to fetch tx from store: ${err.message}`); + process.exit(1); +} + +logger.info(`Loaded failed L1 tx:`, { + id: failedTx.id, + failureType: failedTx.failureType, + actions: failedTx.context.actions, + timestamp: new Date(failedTx.timestamp).toISOString(), + error: failedTx.error.message, +}); + +// Create a client to re-simulate +const defaultRpcUrl = process.env.ETHEREUM_HOST || 'http://localhost:8545'; +const effectiveRpcUrl = rpcUrl || defaultRpcUrl; +logger.info(`Using RPC URL: ${effectiveRpcUrl}`); + +// Try to detect chain from RPC +const client = createPublicClient({ + transport: http(effectiveRpcUrl), +}); + +const chainId = await client.getChainId(); +logger.info(`Connected to chain ID: ${chainId}`); + +// Re-simulate the transaction +logger.info(`Re-simulating transaction...`); +logger.info(`To: ${failedTx.request.to}`); +logger.info(`Data: ${failedTx.request.data.slice(0, 66)}... (${failedTx.request.data.length} chars)`); +if (failedTx.l1BlockNumber) { + logger.info(`L1 block number: ${failedTx.l1BlockNumber}`); +} + +const blockOverrides = failedTx.l1BlockNumber ? { blockOverrides: { number: BigInt(failedTx.l1BlockNumber) } } : {}; + +try { + // Try using eth_simulateV1 via simulateBlocks + const result = await client.simulateBlocks({ + validation: false, + blocks: [ + { + ...blockOverrides, + calls: [ + { + to: failedTx.request.to, + data: failedTx.request.data, + }, + ], + }, + ], + }); + + if (result[0].calls[0].status === 'failure') { + logger.error(`Simulation failed as expected:`); + const errorData = result[0].calls[0].data; + logger.error(`Raw error data: ${errorData}`); + + // Try to decode the error + const abis = [...ErrorsAbi, ...RollupAbi]; + try { + const decoded = decodeErrorResult({ abi: abis, data: errorData }); + logger.error(`Decoded error: ${decoded.errorName}(${decoded.args?.join(', ')})`); + } catch (decodeErr) { + logger.warn(`Could not decode error: ${decodeErr.message}`); + } + } else { + logger.info(`Simulation succeeded! The issue may have been resolved.`); + logger.info(`Gas used: ${result[0].gasUsed}`); + } +} catch (err) { + logger.error(`Simulation threw an error: ${err.message}`); + + // Try to extract more details + if (err.cause) { + logger.error(`Cause: ${err.cause.message || err.cause}`); + } +} + +// Print summary +logger.info(`\n--- Summary ---`); +logger.info(`Failed tx ID: ${failedTx.id}`); +logger.info(`Failure type: ${failedTx.failureType}`); +logger.info(`Actions: ${failedTx.context.actions.join(', ')}`); +if (failedTx.context.checkpointNumber) { + logger.info(`Checkpoint: ${failedTx.context.checkpointNumber}`); +} +if (failedTx.context.slot) { + logger.info(`Slot: ${failedTx.context.slot}`); +} +logger.info(`Original error: ${failedTx.error.message}`); +if (failedTx.error.name) { + logger.info(`Error name: ${failedTx.error.name}`); +} +if (failedTx.receipt) { + logger.info(`Receipt tx hash: ${failedTx.receipt.transactionHash}`); + logger.info(`Receipt block: ${failedTx.receipt.blockNumber}`); +} diff --git a/yarn-project/sequencer-client/src/publisher/config.ts b/yarn-project/sequencer-client/src/publisher/config.ts index 50d4cd8ff16b..ba250c31f0d1 100644 --- a/yarn-project/sequencer-client/src/publisher/config.ts +++ b/yarn-project/sequencer-client/src/publisher/config.ts @@ -48,6 +48,8 @@ export type PublisherConfig = L1TxUtilsConfig & fishermanMode?: boolean; /** Address of the forwarder contract to wrap all L1 transactions through (for testing purposes only) */ publisherForwarderAddress?: EthAddress; + /** Store for failed L1 transaction inputs (test networks only). Format: gs://bucket/path */ + l1TxFailedStore?: string; }; export type ProverPublisherConfig = L1TxUtilsConfig & @@ -62,6 +64,8 @@ export type SequencerPublisherConfig = L1TxUtilsConfig & fishermanMode?: boolean; sequencerPublisherAllowInvalidStates?: boolean; sequencerPublisherForwarderAddress?: EthAddress; + /** Store for failed L1 transaction inputs (test networks only). Format: gs://bucket/path */ + l1TxFailedStore?: string; }; export function getPublisherConfigFromProverConfig(config: ProverPublisherConfig): PublisherConfig { @@ -77,6 +81,7 @@ export function getPublisherConfigFromSequencerConfig(config: SequencerPublisher ...config, publisherAllowInvalidStates: config.sequencerPublisherAllowInvalidStates, publisherForwarderAddress: config.sequencerPublisherForwarderAddress, + l1TxFailedStore: config.l1TxFailedStore, }; } @@ -133,6 +138,10 @@ export const sequencerPublisherConfigMappings: ConfigMappingsType (val ? EthAddress.fromString(val) : undefined), }, + l1TxFailedStore: { + env: 'L1_TX_FAILED_STORE', + description: 'Store for failed L1 transaction inputs (test networks only). Format: gs://bucket/path', + }, }; export const proverPublisherConfigMappings: ConfigMappingsType = { diff --git a/yarn-project/sequencer-client/src/publisher/index.ts b/yarn-project/sequencer-client/src/publisher/index.ts index 08f9a6e5ca08..42e67e527a0e 100644 --- a/yarn-project/sequencer-client/src/publisher/index.ts +++ b/yarn-project/sequencer-client/src/publisher/index.ts @@ -3,3 +3,6 @@ export { SequencerPublisherFactory } from './sequencer-publisher-factory.js'; // Used for tests export { SequencerPublisherMetrics } from './sequencer-publisher-metrics.js'; + +// Failed L1 tx store (optional, for test networks) +export { type FailedL1Tx, type FailedL1TxUri, type L1TxFailedStore } from './l1_tx_failed_store/index.js'; diff --git a/yarn-project/sequencer-client/src/publisher/l1_tx_failed_store/factory.ts b/yarn-project/sequencer-client/src/publisher/l1_tx_failed_store/factory.ts new file mode 100644 index 000000000000..83c179c32a6c --- /dev/null +++ b/yarn-project/sequencer-client/src/publisher/l1_tx_failed_store/factory.ts @@ -0,0 +1,32 @@ +import { type Logger, createLogger } from '@aztec/foundation/log'; +import { createFileStore } from '@aztec/stdlib/file-store'; + +import type { L1TxFailedStore } from './failed_tx_store.js'; +import { FileStoreL1TxFailedStore } from './file_store_failed_tx_store.js'; + +/** + * Creates an L1TxFailedStore from a config string. + * Supports any backend that FileStore supports (GCS, S3, R2, local filesystem). + * @param config - Config string (e.g., 'gs://bucket/path', 's3://bucket/path', 'file:///path'). If undefined, returns undefined. + * @param logger - Optional logger. + * @returns The store instance, or undefined if config is not provided. + */ +export async function createL1TxFailedStore( + config: string | undefined, + logger: Logger = createLogger('sequencer:l1-tx-failed-store'), +): Promise { + if (!config) { + return undefined; + } + + const fileStore = await createFileStore(config, logger); + if (!fileStore) { + throw new Error( + `Failed to create file store from config: '${config}'. ` + + `Supported formats: 'gs://bucket/path', 's3://bucket/path', 'file:///path'.`, + ); + } + + logger.info(`Created L1 tx failed store`, { config }); + return new FileStoreL1TxFailedStore(fileStore, logger); +} diff --git a/yarn-project/sequencer-client/src/publisher/l1_tx_failed_store/failed_tx_store.ts b/yarn-project/sequencer-client/src/publisher/l1_tx_failed_store/failed_tx_store.ts new file mode 100644 index 000000000000..d6f7d5f49f2c --- /dev/null +++ b/yarn-project/sequencer-client/src/publisher/l1_tx_failed_store/failed_tx_store.ts @@ -0,0 +1,55 @@ +import type { Hex } from 'viem'; + +/** URI pointing to a stored failed L1 transaction. */ +export type FailedL1TxUri = string & { __brand: 'FailedL1TxUri' }; + +/** A failed L1 transaction captured for debugging and replay. */ +export type FailedL1Tx = { + /** Tx hash (for reverts) or keccak256(request.data) (for simulation/send failures). */ + id: Hex; + /** Unix timestamp (ms) when failure occurred. */ + timestamp: number; + /** Whether the failure was during simulation or after sending. */ + failureType: 'simulation' | 'revert' | 'send-error'; + /** The actual L1 transaction for replay (multicall-encoded for bundled txs). */ + request: { + to: Hex; + data: Hex; + value?: string; // bigint as string + }; + /** Raw blob data as hex for replay. */ + blobData?: Hex[]; + /** L1 block number at time of failure (simulation target or receipt block). */ + l1BlockNumber: string; // bigint as string + /** Receipt info (present only for on-chain reverts). */ + receipt?: { + transactionHash: Hex; + blockNumber: string; // bigint as string + gasUsed: string; // bigint as string + status: 'reverted'; + }; + /** Error information. */ + error: { + message: string; + /** Decoded error name (e.g., 'Rollup__InvalidProposer'). */ + name?: string; + }; + /** Context metadata. */ + context: { + /** Actions involved (e.g., ['propose', 'governance-signal']). */ + actions: string[]; + /** Individual request data for each action (metadata, not used for replay). */ + requests?: Array<{ action: string; to: Hex; data: Hex }>; + checkpointNumber?: number; + slot?: number; + sender: Hex; + }; +}; + +/** Store for failed L1 transactions for debugging purposes. */ +export interface L1TxFailedStore { + /** Saves a failed transaction and returns its URI. */ + saveFailedTx(tx: FailedL1Tx): Promise; + /** Retrieves a failed transaction by its URI. */ + getFailedTx(uri: FailedL1TxUri): Promise; +} diff --git a/yarn-project/sequencer-client/src/publisher/l1_tx_failed_store/file_store_failed_tx_store.ts b/yarn-project/sequencer-client/src/publisher/l1_tx_failed_store/file_store_failed_tx_store.ts new file mode 100644 index 000000000000..3c74cf23a8a7 --- /dev/null +++ b/yarn-project/sequencer-client/src/publisher/l1_tx_failed_store/file_store_failed_tx_store.ts @@ -0,0 +1,46 @@ +import { type Logger, createLogger } from '@aztec/foundation/log'; +import type { FileStore } from '@aztec/stdlib/file-store'; + +import type { FailedL1Tx, FailedL1TxUri, L1TxFailedStore } from './failed_tx_store.js'; + +/** + * L1TxFailedStore implementation using the FileStore abstraction. + * Supports any backend that FileStore supports (GCS, S3, R2, local filesystem). + */ +export class FileStoreL1TxFailedStore implements L1TxFailedStore { + private readonly log: Logger; + + constructor( + private readonly fileStore: FileStore, + logger?: Logger, + ) { + this.log = logger ?? createLogger('sequencer:l1-tx-failed-store'); + } + + public async saveFailedTx(tx: FailedL1Tx): Promise { + const prefix = tx.receipt ? 'tx' : 'data'; + const path = `${tx.failureType}/${prefix}-${tx.id}.json`; + const json = JSON.stringify(tx, null, 2); + + const uri = await this.fileStore.save(path, Buffer.from(json), { + metadata: { + 'content-type': 'application/json', + actions: tx.context.actions.join(','), + 'failure-type': tx.failureType, + }, + }); + + this.log.info(`Saved failed L1 tx to ${uri}`, { + id: tx.id, + failureType: tx.failureType, + actions: tx.context.actions.join(','), + }); + + return uri as FailedL1TxUri; + } + + public async getFailedTx(uri: FailedL1TxUri): Promise { + const data = await this.fileStore.read(uri); + return JSON.parse(data.toString()) as FailedL1Tx; + } +} diff --git a/yarn-project/sequencer-client/src/publisher/l1_tx_failed_store/index.ts b/yarn-project/sequencer-client/src/publisher/l1_tx_failed_store/index.ts new file mode 100644 index 000000000000..930e34977b8e --- /dev/null +++ b/yarn-project/sequencer-client/src/publisher/l1_tx_failed_store/index.ts @@ -0,0 +1,3 @@ +export { type FailedL1Tx, type FailedL1TxUri, type L1TxFailedStore } from './failed_tx_store.js'; +export { createL1TxFailedStore } from './factory.js'; +export { FileStoreL1TxFailedStore } from './file_store_failed_tx_store.js'; diff --git a/yarn-project/sequencer-client/src/publisher/sequencer-publisher.ts b/yarn-project/sequencer-client/src/publisher/sequencer-publisher.ts index a88d42f05029..d87e1ca84ce7 100644 --- a/yarn-project/sequencer-client/src/publisher/sequencer-publisher.ts +++ b/yarn-project/sequencer-client/src/publisher/sequencer-publisher.ts @@ -45,9 +45,19 @@ import type { CheckpointHeader } from '@aztec/stdlib/rollup'; import type { L1PublishCheckpointStats } from '@aztec/stdlib/stats'; import { type TelemetryClient, type Tracer, getTelemetryClient, trackSpan } from '@aztec/telemetry-client'; -import { type StateOverride, type TransactionReceipt, type TypedDataDefinition, encodeFunctionData, toHex } from 'viem'; +import { + type Hex, + type StateOverride, + type TransactionReceipt, + type TypedDataDefinition, + encodeFunctionData, + keccak256, + multicall3Abi, + toHex, +} from 'viem'; import type { SequencerPublisherConfig } from './config.js'; +import { type FailedL1Tx, type L1TxFailedStore, createL1TxFailedStore } from './l1_tx_failed_store/index.js'; import { SequencerPublisherMetrics } from './sequencer-publisher-metrics.js'; /** Arguments to the process method of the rollup contract */ @@ -109,6 +119,7 @@ export class SequencerPublisher { private interrupted = false; private metrics: SequencerPublisherMetrics; public epochCache: EpochCache; + private failedTxStore?: Promise; protected governanceLog = createLogger('sequencer:publisher:governance'); protected slashingLog = createLogger('sequencer:publisher:slashing'); @@ -149,7 +160,7 @@ export class SequencerPublisher { protected requests: RequestWithExpiry[] = []; constructor( - private config: Pick & + private config: Pick & Pick & { l1ChainId: number }, deps: { telemetry?: TelemetryClient; @@ -205,6 +216,31 @@ export class SequencerPublisher { this.rollupContract, createLogger('sequencer:publisher:price-oracle'), ); + + // Initialize failed L1 tx store (optional, for test networks) + this.failedTxStore = createL1TxFailedStore(config.l1TxFailedStore, this.log); + } + + /** + * Backs up a failed L1 transaction to the configured store for debugging. + * Does nothing if no store is configured. + */ + private backupFailedTx(failedTx: Omit): void { + if (!this.failedTxStore) { + return; + } + + const tx: FailedL1Tx = { + ...failedTx, + timestamp: Date.now(), + }; + + // Fire and forget - don't block on backup + void this.failedTxStore + .then(store => store?.saveFailedTx(tx)) + .catch(err => { + this.log.warn(`Failed to backup failed L1 tx to store`, err); + }); } public getRollupContract(): RollupContract { @@ -386,6 +422,21 @@ export class SequencerPublisher { validRequests.sort((a, b) => compareActions(a.action, b.action)); try { + // Capture context for failed tx backup before sending + const l1BlockNumber = await this.l1TxUtils.getBlockNumber(); + const multicallData = encodeFunctionData({ + abi: multicall3Abi, + functionName: 'aggregate3', + args: [ + validRequests.map(r => ({ + target: r.request.to!, + callData: r.request.data!, + allowFailure: true, + })), + ], + }); + const blobDataHex = blobConfig?.blobs?.map(b => toHex(b)) as Hex[] | undefined; + this.log.debug('Forwarding transactions', { validRequests: validRequests.map(request => request.action), txConfig, @@ -398,7 +449,12 @@ export class SequencerPublisher { this.rollupContract.address, this.log, ); - const { successfulActions = [], failedActions = [] } = this.callbackBundledTransactions(validRequests, result); + const txContext = { multicallData, blobData: blobDataHex, l1BlockNumber }; + const { successfulActions = [], failedActions = [] } = this.callbackBundledTransactions( + validRequests, + result, + txContext, + ); return { result, expiredActions, sentActions: validActions, successfulActions, failedActions }; } catch (err) { const viemError = formatViemError(err); @@ -418,11 +474,25 @@ export class SequencerPublisher { private callbackBundledTransactions( requests: RequestWithExpiry[], - result?: { receipt: TransactionReceipt } | FormattedViemError, + result: { receipt: TransactionReceipt; errorMsg?: string } | FormattedViemError | undefined, + txContext: { multicallData: Hex; blobData?: Hex[]; l1BlockNumber: bigint }, ) { const actionsListStr = requests.map(r => r.action).join(', '); if (result instanceof FormattedViemError) { this.log.error(`Failed to publish bundled transactions (${actionsListStr})`, result); + this.backupFailedTx({ + id: keccak256(txContext.multicallData), + failureType: 'send-error', + request: { to: MULTI_CALL_3_ADDRESS, data: txContext.multicallData }, + blobData: txContext.blobData, + l1BlockNumber: txContext.l1BlockNumber.toString(), + error: { message: result.message, name: result.name }, + context: { + actions: requests.map(r => r.action), + requests: requests.map(r => ({ action: r.action, to: r.request.to! as Hex, data: r.request.data! })), + sender: this.getSenderAddress().toString(), + }, + }); return { failedActions: requests.map(r => r.action) }; } else { this.log.verbose(`Published bundled transactions (${actionsListStr})`, { result, requests }); @@ -435,6 +505,30 @@ export class SequencerPublisher { failedActions.push(request.action); } } + // Single backup for the whole reverted tx + if (failedActions.length > 0 && result?.receipt?.status === 'reverted') { + this.backupFailedTx({ + id: result.receipt.transactionHash, + failureType: 'revert', + request: { to: MULTI_CALL_3_ADDRESS, data: txContext.multicallData }, + blobData: txContext.blobData, + l1BlockNumber: result.receipt.blockNumber.toString(), + receipt: { + transactionHash: result.receipt.transactionHash, + blockNumber: result.receipt.blockNumber.toString(), + gasUsed: (result.receipt.gasUsed ?? 0n).toString(), + status: 'reverted', + }, + error: { message: result.errorMsg ?? 'Transaction reverted' }, + context: { + actions: failedActions, + requests: requests + .filter(r => failedActions.includes(r.action)) + .map(r => ({ action: r.action, to: r.request.to! as Hex, data: r.request.data! })), + sender: this.getSenderAddress().toString(), + }, + }); + } return { successfulActions, failedActions }; } } @@ -546,6 +640,8 @@ export class SequencerPublisher { const request = this.buildInvalidateCheckpointRequest(validationResult); this.log.debug(`Simulating invalidate checkpoint ${checkpointNumber}`, { ...logData, request }); + const l1BlockNumber = await this.l1TxUtils.getBlockNumber(); + try { const { gasUsed } = await this.l1TxUtils.simulate( request, @@ -597,6 +693,18 @@ export class SequencerPublisher { // Otherwise, throw. We cannot build the next checkpoint if we cannot invalidate the previous one. this.log.error(`Simulation for invalidate checkpoint ${checkpointNumber} failed`, viemError, logData); + this.backupFailedTx({ + id: keccak256(request.data!), + failureType: 'simulation', + request: { to: request.to!, data: request.data!, value: request.value?.toString() }, + l1BlockNumber: l1BlockNumber.toString(), + error: { message: viemError.message, name: viemError.name }, + context: { + actions: [`invalidate-${reason}`], + checkpointNumber, + sender: this.getSenderAddress().toString(), + }, + }); throw new Error(`Failed to simulate invalidate checkpoint ${checkpointNumber}`, { cause: viemError }); } } @@ -744,11 +852,26 @@ export class SequencerPublisher { lastValidL2Slot: slotNumber, }); + const l1BlockNumber = await this.l1TxUtils.getBlockNumber(); + try { await this.l1TxUtils.simulate(request, { time: timestamp }, [], mergeAbis([request.abi ?? [], ErrorsAbi])); this.log.debug(`Simulation for ${action} at slot ${slotNumber} succeeded`, { request }); } catch (err) { - this.log.error(`Failed simulation for ${action} at slot ${slotNumber} (enqueuing the action anyway)`, err); + const viemError = formatViemError(err); + this.log.error(`Failed simulation for ${action} at slot ${slotNumber} (enqueuing the action anyway)`, viemError); + this.backupFailedTx({ + id: keccak256(request.data!), + failureType: 'simulation', + request: { to: request.to!, data: request.data!, value: request.value?.toString() }, + l1BlockNumber: l1BlockNumber.toString(), + error: { message: viemError.message, name: viemError.name }, + context: { + actions: [action], + slot: slotNumber, + sender: this.getSenderAddress().toString(), + }, + }); // Yes, we enqueue the request anyway, in case there was a bug with the simulation itself } @@ -1044,6 +1167,8 @@ export class SequencerPublisher { this.log.debug(`Simulating ${action} for slot ${slotNumber}`, logData); + const l1BlockNumber = await this.l1TxUtils.getBlockNumber(); + let gasUsed: bigint; const simulateAbi = mergeAbis([request.abi ?? [], ErrorsAbi]); try { @@ -1053,6 +1178,19 @@ export class SequencerPublisher { const viemError = formatViemError(err, simulateAbi); this.log.error(`Simulation for ${action} at ${slotNumber} failed`, viemError, logData); + this.backupFailedTx({ + id: keccak256(request.data!), + failureType: 'simulation', + request: { to: request.to!, data: request.data!, value: request.value?.toString() }, + l1BlockNumber: l1BlockNumber.toString(), + error: { message: viemError.message, name: viemError.name }, + context: { + actions: [action], + slot: slotNumber, + sender: this.getSenderAddress().toString(), + }, + }); + return false; } @@ -1136,9 +1274,27 @@ export class SequencerPublisher { kzg, }, ) - .catch(err => { - const { message, metaMessages } = formatViemError(err); - this.log.error(`Failed to validate blobs`, message, { metaMessages }); + .catch(async err => { + const viemError = formatViemError(err); + this.log.error(`Failed to validate blobs`, viemError.message, { metaMessages: viemError.metaMessages }); + const validateBlobsData = encodeFunctionData({ + abi: RollupAbi, + functionName: 'validateBlobs', + args: [blobInput], + }); + const l1BlockNumber = await this.l1TxUtils.getBlockNumber(); + this.backupFailedTx({ + id: keccak256(validateBlobsData), + failureType: 'simulation', + request: { to: this.rollupContract.address as Hex, data: validateBlobsData }, + blobData: encodedData.blobs.map(b => toHex(b.data)) as Hex[], + l1BlockNumber: l1BlockNumber.toString(), + error: { message: viemError.message, name: viemError.name }, + context: { + actions: ['validate-blobs'], + sender: this.getSenderAddress().toString(), + }, + }); throw new Error('Failed to validate blobs'); }); } @@ -1217,6 +1373,8 @@ export class SequencerPublisher { }); } + const l1BlockNumber = await this.l1TxUtils.getBlockNumber(); + const simulationResult = await this.l1TxUtils .simulate( { @@ -1250,6 +1408,18 @@ export class SequencerPublisher { }; } this.log.error(`Failed to simulate propose tx`, viemError); + this.backupFailedTx({ + id: keccak256(rollupData), + failureType: 'simulation', + request: { to: this.rollupContract.address, data: rollupData }, + l1BlockNumber: l1BlockNumber.toString(), + error: { message: viemError.message, name: viemError.name }, + context: { + actions: ['propose'], + slot: Number(args[0].header.slotNumber), + sender: this.getSenderAddress().toString(), + }, + }); throw err; });