Skip to content
Merged
Show file tree
Hide file tree
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
1 change: 1 addition & 0 deletions yarn-project/foundation/src/config/env_var.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down
145 changes: 145 additions & 0 deletions yarn-project/scripts/replay_failed_l1_tx.mjs
Original file line number Diff line number Diff line change
@@ -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 <file-uri> [--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}`);
}
9 changes: 9 additions & 0 deletions yarn-project/sequencer-client/src/publisher/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 &
Expand All @@ -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 {
Expand All @@ -77,6 +81,7 @@ export function getPublisherConfigFromSequencerConfig(config: SequencerPublisher
...config,
publisherAllowInvalidStates: config.sequencerPublisherAllowInvalidStates,
publisherForwarderAddress: config.sequencerPublisherForwarderAddress,
l1TxFailedStore: config.l1TxFailedStore,
};
}

Expand Down Expand Up @@ -133,6 +138,10 @@ export const sequencerPublisherConfigMappings: ConfigMappingsType<SequencerPubli
description: 'Address of the forwarder contract to wrap all L1 transactions through (for testing purposes only)',
parseEnv: (val: string) => (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<ProverPublisherConfig & L1TxUtilsConfig> = {
Expand Down
3 changes: 3 additions & 0 deletions yarn-project/sequencer-client/src/publisher/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Original file line number Diff line number Diff line change
@@ -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<L1TxFailedStore | undefined> {
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);
}
Original file line number Diff line number Diff line change
@@ -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<FailedL1TxUri>;
/** Retrieves a failed transaction by its URI. */
getFailedTx(uri: FailedL1TxUri): Promise<FailedL1Tx>;
}
Original file line number Diff line number Diff line change
@@ -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<FailedL1TxUri> {
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<FailedL1Tx> {
const data = await this.fileStore.read(uri);
return JSON.parse(data.toString()) as FailedL1Tx;
}
}
Original file line number Diff line number Diff line change
@@ -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';
Loading
Loading