@aztec/archiver 2.1.8 → 2.1.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (57) hide show
  1. package/dest/archiver/archiver.d.ts +6 -5
  2. package/dest/archiver/archiver.d.ts.map +1 -1
  3. package/dest/archiver/archiver.js +26 -9
  4. package/dest/archiver/config.d.ts.map +1 -1
  5. package/dest/archiver/config.js +5 -0
  6. package/dest/archiver/instrumentation.d.ts +2 -0
  7. package/dest/archiver/instrumentation.d.ts.map +1 -1
  8. package/dest/archiver/instrumentation.js +11 -0
  9. package/dest/archiver/l1/bin/retrieve-calldata.d.ts +3 -0
  10. package/dest/archiver/l1/bin/retrieve-calldata.d.ts.map +1 -0
  11. package/dest/archiver/l1/bin/retrieve-calldata.js +149 -0
  12. package/dest/archiver/l1/calldata_retriever.d.ts +87 -0
  13. package/dest/archiver/l1/calldata_retriever.d.ts.map +1 -0
  14. package/dest/archiver/l1/calldata_retriever.js +406 -0
  15. package/dest/archiver/{data_retrieval.d.ts → l1/data_retrieval.d.ts} +13 -6
  16. package/dest/archiver/l1/data_retrieval.d.ts.map +1 -0
  17. package/dest/archiver/{data_retrieval.js → l1/data_retrieval.js} +22 -99
  18. package/dest/archiver/l1/debug_tx.d.ts +19 -0
  19. package/dest/archiver/l1/debug_tx.d.ts.map +1 -0
  20. package/dest/archiver/l1/debug_tx.js +73 -0
  21. package/dest/archiver/l1/spire_proposer.d.ts +70 -0
  22. package/dest/archiver/l1/spire_proposer.d.ts.map +1 -0
  23. package/dest/archiver/l1/spire_proposer.js +157 -0
  24. package/dest/archiver/l1/trace_tx.d.ts +97 -0
  25. package/dest/archiver/l1/trace_tx.d.ts.map +1 -0
  26. package/dest/archiver/l1/trace_tx.js +91 -0
  27. package/dest/archiver/l1/types.d.ts +12 -0
  28. package/dest/archiver/l1/types.d.ts.map +1 -0
  29. package/dest/archiver/l1/types.js +3 -0
  30. package/dest/archiver/l1/validate_trace.d.ts +29 -0
  31. package/dest/archiver/l1/validate_trace.d.ts.map +1 -0
  32. package/dest/archiver/l1/validate_trace.js +150 -0
  33. package/dest/index.d.ts +1 -1
  34. package/dest/index.d.ts.map +1 -1
  35. package/dest/index.js +1 -1
  36. package/package.json +15 -14
  37. package/src/archiver/archiver.ts +42 -12
  38. package/src/archiver/config.ts +5 -0
  39. package/src/archiver/instrumentation.ts +14 -0
  40. package/src/archiver/l1/README.md +98 -0
  41. package/src/archiver/l1/bin/retrieve-calldata.ts +186 -0
  42. package/src/archiver/l1/calldata_retriever.ts +528 -0
  43. package/src/archiver/{data_retrieval.ts → l1/data_retrieval.ts} +45 -155
  44. package/src/archiver/l1/debug_tx.ts +99 -0
  45. package/src/archiver/l1/spire_proposer.ts +160 -0
  46. package/src/archiver/l1/trace_tx.ts +128 -0
  47. package/src/archiver/l1/types.ts +13 -0
  48. package/src/archiver/l1/validate_trace.ts +211 -0
  49. package/src/index.ts +1 -1
  50. package/src/test/fixtures/debug_traceTransaction-multicall3.json +88 -0
  51. package/src/test/fixtures/debug_traceTransaction-multiplePropose.json +153 -0
  52. package/src/test/fixtures/debug_traceTransaction-proxied.json +122 -0
  53. package/src/test/fixtures/trace_transaction-multicall3.json +65 -0
  54. package/src/test/fixtures/trace_transaction-multiplePropose.json +319 -0
  55. package/src/test/fixtures/trace_transaction-proxied.json +128 -0
  56. package/src/test/fixtures/trace_transaction-randomRevert.json +216 -0
  57. package/dest/archiver/data_retrieval.d.ts.map +0 -1
@@ -0,0 +1,150 @@
1
+ import { createLogger } from '@aztec/foundation/log';
2
+ import { callTraceSchema } from './debug_tx.js';
3
+ import { traceTransactionResponseSchema } from './trace_tx.js';
4
+ const logger = createLogger('aztec:archiver:validate_trace');
5
+ /**
6
+ * Helper function to test a trace method with validation
7
+ *
8
+ * @param client - The Viem public debug client
9
+ * @param txHash - Transaction hash to trace
10
+ * @param schema - Zod schema to validate the response
11
+ * @param method - Name of the RPC method ('debug_traceTransaction' or 'trace_transaction')
12
+ * @param blockType - Type of block being tested ('recent' or 'old')
13
+ * @returns true if the method works and validation passes, false otherwise
14
+ */ async function testTraceMethod(client, txHash, schema, method, blockType) {
15
+ try {
16
+ // Make request with appropriate params based on method name
17
+ const result = await client.request(method === 'debug_traceTransaction' ? {
18
+ method,
19
+ params: [
20
+ txHash,
21
+ {
22
+ tracer: 'callTracer'
23
+ }
24
+ ]
25
+ } : {
26
+ method,
27
+ params: [
28
+ txHash
29
+ ]
30
+ });
31
+ schema.parse(result);
32
+ logger.debug(`${method} works for ${blockType} blocks`);
33
+ return true;
34
+ } catch (error) {
35
+ logger.warn(`${method} failed for ${blockType} blocks: ${error}`);
36
+ return false;
37
+ }
38
+ }
39
+ /**
40
+ * Validates the availability of debug/trace methods on the Ethereum client.
41
+ *
42
+ * @param client - The Viem public debug client
43
+ * @returns Object indicating which trace methods are available for recent and old blocks
44
+ */ export async function validateTraceAvailability(client) {
45
+ const result = {
46
+ debugTraceRecent: false,
47
+ traceTransactionRecent: false,
48
+ debugTraceOld: false,
49
+ traceTransactionOld: false
50
+ };
51
+ try {
52
+ // Get the latest block
53
+ let latestBlock = await client.getBlock({
54
+ blockTag: 'latest'
55
+ });
56
+ let attempts = 10;
57
+ // Loop back to find a block with transactions (handles dev/test scenarios)
58
+ while(!hasTxs(latestBlock) && latestBlock.number && latestBlock.number > 0n && --attempts > 0){
59
+ logger.debug(`Block ${latestBlock.number} has no transactions, checking previous block`);
60
+ latestBlock = await client.getBlock({
61
+ blockNumber: latestBlock.number - 1n
62
+ });
63
+ }
64
+ if (!hasTxs(latestBlock)) {
65
+ logger.warn('No blocks with transactions found from latest back to genesis, cannot validate trace availability');
66
+ return result;
67
+ }
68
+ // Get a transaction from the found block
69
+ const recentTxHash = latestBlock.transactions[0];
70
+ // Test debug_traceTransaction with recent block
71
+ result.debugTraceRecent = await testTraceMethod(client, recentTxHash, callTraceSchema, 'debug_traceTransaction', 'recent');
72
+ // Test trace_transaction with recent block
73
+ result.traceTransactionRecent = await testTraceMethod(client, recentTxHash, traceTransactionResponseSchema, 'trace_transaction', 'recent');
74
+ // Get a block from 512 blocks ago
75
+ const oldBlockNumber = latestBlock.number ? latestBlock.number - 512n : null;
76
+ if (!oldBlockNumber || oldBlockNumber < 0n) {
77
+ logger.debug('Cannot test old blocks, blockchain too short');
78
+ return result;
79
+ }
80
+ let oldBlock = await client.getBlock({
81
+ blockNumber: oldBlockNumber
82
+ });
83
+ attempts = 10;
84
+ // Loop back to find a block with transactions (handles dev/test scenarios)
85
+ while(!hasTxs(oldBlock) && oldBlock.number && oldBlock.number > 0n && --attempts > 0){
86
+ logger.debug(`Block ${oldBlock.number} has no transactions, checking previous block`);
87
+ oldBlock = await client.getBlock({
88
+ blockNumber: oldBlock.number - 1n
89
+ });
90
+ }
91
+ if (!hasTxs(oldBlock)) {
92
+ logger.debug('No blocks with transactions found from old block back to genesis, cannot validate trace availability for old blocks');
93
+ return result;
94
+ }
95
+ const oldTxHash = oldBlock.transactions[0];
96
+ // Test debug_traceTransaction with old block
97
+ result.debugTraceOld = await testTraceMethod(client, oldTxHash, callTraceSchema, 'debug_traceTransaction', 'old');
98
+ // Test trace_transaction with old block
99
+ result.traceTransactionOld = await testTraceMethod(client, oldTxHash, traceTransactionResponseSchema, 'trace_transaction', 'old');
100
+ } catch (error) {
101
+ logger.warn(`Error validating debug_traceTransaction and trace_transaction availability: ${error}`);
102
+ }
103
+ return result;
104
+ }
105
+ function hasTxs(block) {
106
+ return Array.isArray(block.transactions) && block.transactions.length > 0;
107
+ }
108
+ /**
109
+ * Validates trace availability and logs appropriate messages based on the results.
110
+ * Optionally throws an error if no trace methods are available and ethereumAllowNoDebugHosts is false.
111
+ *
112
+ * @param client - The Viem public debug client
113
+ * @param ethereumAllowNoDebugHosts - If false, throws an error when no trace methods are available
114
+ * @throws Error if ethereumAllowNoDebugHosts is false and no trace methods are available
115
+ */ export async function validateAndLogTraceAvailability(client, ethereumAllowNoDebugHosts) {
116
+ logger.info('Validating trace/debug method availability...');
117
+ const availability = await validateTraceAvailability(client);
118
+ // Check if we have support for old blocks (either debug or trace)
119
+ const hasOldBlockSupport = availability.debugTraceOld || availability.traceTransactionOld;
120
+ if (hasOldBlockSupport) {
121
+ // Ideal case: we have trace support for old blocks
122
+ const methods = [];
123
+ if (availability.debugTraceOld) {
124
+ methods.push('debug_traceTransaction');
125
+ }
126
+ if (availability.traceTransactionOld) {
127
+ methods.push('trace_transaction');
128
+ }
129
+ logger.info(`Ethereum client supports trace methods for old blocks (${methods.join(', ')}). Archiver can retrieve historical transaction traces.`);
130
+ } else if (availability.debugTraceRecent || availability.traceTransactionRecent) {
131
+ // Warning case: only recent block support
132
+ const methods = [];
133
+ if (availability.debugTraceRecent) {
134
+ methods.push('debug_traceTransaction');
135
+ }
136
+ if (availability.traceTransactionRecent) {
137
+ methods.push('trace_transaction');
138
+ }
139
+ logger.warn(`Ethereum client only supports trace methods for recent blocks (${methods.join(', ')}). Historical transaction traces may not be available.`);
140
+ } else {
141
+ // Error case: no support at all
142
+ const errorMessage = 'Ethereum debug client does not support debug_traceTransaction or trace_transaction methods. Transaction tracing will not be available. This may impact archiver syncing.';
143
+ if (ethereumAllowNoDebugHosts) {
144
+ logger.warn(`${errorMessage} Continuing because ETHEREUM_ALLOW_NO_DEBUG_HOSTS is set to true.`);
145
+ } else {
146
+ logger.error(errorMessage);
147
+ throw new Error(`${errorMessage} Set ETHEREUM_ALLOW_NO_DEBUG_HOSTS=true to bypass this check.`);
148
+ }
149
+ }
150
+ }
package/dest/index.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  export * from './archiver/index.js';
2
2
  export * from './factory.js';
3
3
  export * from './rpc/index.js';
4
- export { retrieveBlocksFromRollup, retrieveL2ProofVerifiedEvents } from './archiver/data_retrieval.js';
4
+ export { retrieveBlocksFromRollup, retrieveL2ProofVerifiedEvents } from './archiver/l1/data_retrieval.js';
5
5
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,qBAAqB,CAAC;AACpC,cAAc,cAAc,CAAC;AAC7B,cAAc,gBAAgB,CAAC;AAE/B,OAAO,EAAE,wBAAwB,EAAE,6BAA6B,EAAE,MAAM,8BAA8B,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,qBAAqB,CAAC;AACpC,cAAc,cAAc,CAAC;AAC7B,cAAc,gBAAgB,CAAC;AAE/B,OAAO,EAAE,wBAAwB,EAAE,6BAA6B,EAAE,MAAM,iCAAiC,CAAC"}
package/dest/index.js CHANGED
@@ -1,4 +1,4 @@
1
1
  export * from './archiver/index.js';
2
2
  export * from './factory.js';
3
3
  export * from './rpc/index.js';
4
- export { retrieveBlocksFromRollup, retrieveL2ProofVerifiedEvents } from './archiver/data_retrieval.js';
4
+ export { retrieveBlocksFromRollup, retrieveL2ProofVerifiedEvents } from './archiver/l1/data_retrieval.js';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aztec/archiver",
3
- "version": "2.1.8",
3
+ "version": "2.1.9",
4
4
  "type": "module",
5
5
  "exports": {
6
6
  ".": "./dest/index.js",
@@ -66,23 +66,24 @@
66
66
  ]
67
67
  },
68
68
  "dependencies": {
69
- "@aztec/blob-lib": "2.1.8",
70
- "@aztec/blob-sink": "2.1.8",
71
- "@aztec/constants": "2.1.8",
72
- "@aztec/epoch-cache": "2.1.8",
73
- "@aztec/ethereum": "2.1.8",
74
- "@aztec/foundation": "2.1.8",
75
- "@aztec/kv-store": "2.1.8",
76
- "@aztec/l1-artifacts": "2.1.8",
77
- "@aztec/noir-protocol-circuits-types": "2.1.8",
78
- "@aztec/protocol-contracts": "2.1.8",
79
- "@aztec/stdlib": "2.1.8",
80
- "@aztec/telemetry-client": "2.1.8",
69
+ "@aztec/blob-lib": "2.1.9",
70
+ "@aztec/blob-sink": "2.1.9",
71
+ "@aztec/constants": "2.1.9",
72
+ "@aztec/epoch-cache": "2.1.9",
73
+ "@aztec/ethereum": "2.1.9",
74
+ "@aztec/foundation": "2.1.9",
75
+ "@aztec/kv-store": "2.1.9",
76
+ "@aztec/l1-artifacts": "2.1.9",
77
+ "@aztec/noir-protocol-circuits-types": "2.1.9",
78
+ "@aztec/protocol-contracts": "2.1.9",
79
+ "@aztec/stdlib": "2.1.9",
80
+ "@aztec/telemetry-client": "2.1.9",
81
81
  "lodash.groupby": "^4.6.0",
82
82
  "lodash.omit": "^4.5.0",
83
83
  "tsc-watch": "^6.0.0",
84
84
  "tslib": "^2.5.0",
85
- "viem": "npm:@aztec/viem@2.38.2"
85
+ "viem": "npm:@aztec/viem@2.38.2",
86
+ "zod": "^3.23.8"
86
87
  },
87
88
  "devDependencies": {
88
89
  "@jest/globals": "^30.0.0",
@@ -4,8 +4,10 @@ import {
4
4
  BlockTagTooOldError,
5
5
  InboxContract,
6
6
  type L1BlockId,
7
+ type L1ContractAddresses,
7
8
  RollupContract,
8
9
  type ViemPublicClient,
10
+ type ViemPublicDebugClient,
9
11
  createEthereumChain,
10
12
  } from '@aztec/ethereum';
11
13
  import { maxBigint } from '@aztec/foundation/bigint';
@@ -77,14 +79,15 @@ import { type GetContractReturnType, type Hex, createPublicClient, fallback, htt
77
79
 
78
80
  import type { ArchiverDataStore, ArchiverL1SynchPoint } from './archiver_store.js';
79
81
  import type { ArchiverConfig } from './config.js';
82
+ import { InitialBlockNumberNotSequentialError, NoBlobBodiesFoundError } from './errors.js';
83
+ import { ArchiverInstrumentation } from './instrumentation.js';
80
84
  import {
81
85
  retrieveBlocksFromRollup,
82
86
  retrieveL1ToL2Message,
83
87
  retrieveL1ToL2Messages,
84
88
  retrievedBlockToPublishedL2Block,
85
- } from './data_retrieval.js';
86
- import { InitialBlockNumberNotSequentialError, NoBlobBodiesFoundError } from './errors.js';
87
- import { ArchiverInstrumentation } from './instrumentation.js';
89
+ } from './l1/data_retrieval.js';
90
+ import { validateAndLogTraceAvailability } from './l1/validate_trace.js';
88
91
  import type { InboxMessage } from './structs/inbox_message.js';
89
92
  import type { PublishedL2Block } from './structs/published.js';
90
93
  import { type ValidateBlockResult, validateBlockAttestations } from './validation.js';
@@ -107,6 +110,7 @@ function mapArchiverConfig(config: Partial<ArchiverConfig>) {
107
110
  batchSize: config.archiverBatchSize,
108
111
  skipValidateBlockAttestations: config.skipValidateBlockAttestations,
109
112
  maxAllowedEthClientDriftSeconds: config.maxAllowedEthClientDriftSeconds,
113
+ ethereumAllowNoDebugHosts: config.ethereumAllowNoDebugHosts,
110
114
  };
111
115
  }
112
116
 
@@ -144,6 +148,7 @@ export class Archiver extends (EventEmitter as new () => ArchiverEmitter) implem
144
148
  /**
145
149
  * Creates a new instance of the Archiver.
146
150
  * @param publicClient - A client for interacting with the Ethereum node.
151
+ * @param debugClient - A client for interacting with the Ethereum node for debug/trace methods.
147
152
  * @param rollupAddress - Ethereum address of the rollup contract.
148
153
  * @param inboxAddress - Ethereum address of the inbox contract.
149
154
  * @param registryAddress - Ethereum address of the registry contract.
@@ -153,13 +158,18 @@ export class Archiver extends (EventEmitter as new () => ArchiverEmitter) implem
153
158
  */
154
159
  constructor(
155
160
  private readonly publicClient: ViemPublicClient,
156
- private readonly l1Addresses: { rollupAddress: EthAddress; inboxAddress: EthAddress; registryAddress: EthAddress },
161
+ private readonly debugClient: ViemPublicDebugClient,
162
+ private readonly l1Addresses: Pick<
163
+ L1ContractAddresses,
164
+ 'rollupAddress' | 'inboxAddress' | 'registryAddress' | 'governanceProposerAddress' | 'slashFactoryAddress'
165
+ > & { slashingProposerAddress: EthAddress },
157
166
  readonly dataStore: ArchiverDataStore,
158
167
  private config: {
159
168
  pollingIntervalMs: number;
160
169
  batchSize: number;
161
170
  skipValidateBlockAttestations?: boolean;
162
171
  maxAllowedEthClientDriftSeconds: number;
172
+ ethereumAllowNoDebugHosts?: boolean;
163
173
  },
164
174
  private readonly blobSinkClient: BlobSinkClientInterface,
165
175
  private readonly epochCache: EpochCache,
@@ -207,14 +217,24 @@ export class Archiver extends (EventEmitter as new () => ArchiverEmitter) implem
207
217
  pollingInterval: config.viemPollingIntervalMS,
208
218
  });
209
219
 
220
+ // Create debug client using debug RPC URLs if available, otherwise fall back to regular RPC URLs
221
+ const debugRpcUrls = config.l1DebugRpcUrls.length > 0 ? config.l1DebugRpcUrls : config.l1RpcUrls;
222
+ const debugClient = createPublicClient({
223
+ chain: chain.chainInfo,
224
+ transport: fallback(debugRpcUrls.map(url => http(url))),
225
+ pollingInterval: config.viemPollingIntervalMS,
226
+ }) as ViemPublicDebugClient;
227
+
210
228
  const rollup = new RollupContract(publicClient, config.l1Contracts.rollupAddress);
211
229
 
212
- const [l1StartBlock, l1GenesisTime, proofSubmissionEpochs, genesisArchiveRoot] = await Promise.all([
213
- rollup.getL1StartBlock(),
214
- rollup.getL1GenesisTime(),
215
- rollup.getProofSubmissionEpochs(),
216
- rollup.getGenesisArchiveTreeRoot(),
217
- ] as const);
230
+ const [l1StartBlock, l1GenesisTime, proofSubmissionEpochs, genesisArchiveRoot, slashingProposerAddress] =
231
+ await Promise.all([
232
+ rollup.getL1StartBlock(),
233
+ rollup.getL1GenesisTime(),
234
+ rollup.getProofSubmissionEpochs(),
235
+ rollup.getGenesisArchiveTreeRoot(),
236
+ rollup.getSlashingProposerAddress(),
237
+ ] as const);
218
238
 
219
239
  const l1StartBlockHash = await publicClient
220
240
  .getBlock({ blockNumber: l1StartBlock, includeTransactions: false })
@@ -234,7 +254,12 @@ export class Archiver extends (EventEmitter as new () => ArchiverEmitter) implem
234
254
  };
235
255
 
236
256
  const opts = merge(
237
- { pollingIntervalMs: 10_000, batchSize: 100, maxAllowedEthClientDriftSeconds: 300 },
257
+ {
258
+ pollingIntervalMs: 10_000,
259
+ batchSize: 100,
260
+ maxAllowedEthClientDriftSeconds: 300,
261
+ ethereumAllowNoDebugHosts: false,
262
+ },
238
263
  mapArchiverConfig(config),
239
264
  );
240
265
 
@@ -243,7 +268,8 @@ export class Archiver extends (EventEmitter as new () => ArchiverEmitter) implem
243
268
 
244
269
  const archiver = new Archiver(
245
270
  publicClient,
246
- config.l1Contracts,
271
+ debugClient,
272
+ { ...config.l1Contracts, slashingProposerAddress },
247
273
  archiverStore,
248
274
  opts,
249
275
  deps.blobSinkClient,
@@ -272,6 +298,7 @@ export class Archiver extends (EventEmitter as new () => ArchiverEmitter) implem
272
298
 
273
299
  await this.blobSinkClient.testSources();
274
300
  await this.testEthereumNodeSynced();
301
+ await validateAndLogTraceAvailability(this.debugClient, this.config.ethereumAllowNoDebugHosts ?? false);
275
302
 
276
303
  // Log initial state for the archiver
277
304
  const { l1StartBlock } = this.l1constants;
@@ -832,9 +859,12 @@ export class Archiver extends (EventEmitter as new () => ArchiverEmitter) implem
832
859
  const retrievedBlocks = await retrieveBlocksFromRollup(
833
860
  this.rollup.getContract() as GetContractReturnType<typeof RollupAbi, ViemPublicClient>,
834
861
  this.publicClient,
862
+ this.debugClient,
835
863
  this.blobSinkClient,
836
864
  searchStartBlock, // TODO(palla/reorg): If the L2 reorg was due to an L1 reorg, we need to start search earlier
837
865
  searchEndBlock,
866
+ this.l1Addresses,
867
+ this.instrumentation,
838
868
  this.log,
839
869
  );
840
870
 
@@ -55,6 +55,11 @@ export const archiverConfigMappings: ConfigMappingsType<ArchiverConfig> = {
55
55
  description: 'Maximum allowed drift in seconds between the Ethereum client and current time.',
56
56
  ...numberConfigHelper(300),
57
57
  },
58
+ ethereumAllowNoDebugHosts: {
59
+ env: 'ETHEREUM_ALLOW_NO_DEBUG_HOSTS',
60
+ description: 'Whether to allow starting the archiver without debug/trace method support on Ethereum hosts',
61
+ ...booleanConfigHelper(true),
62
+ },
58
63
  ...chainConfigMappings,
59
64
  ...l1ReaderConfigMappings,
60
65
  viemPollingIntervalMS: {
@@ -34,6 +34,8 @@ export class ArchiverInstrumentation {
34
34
  private syncDurationPerMessage: Histogram;
35
35
  private syncMessageCount: UpDownCounter;
36
36
 
37
+ private blockProposalTxTargetCount: UpDownCounter;
38
+
37
39
  private log = createLogger('archiver:instrumentation');
38
40
 
39
41
  private constructor(
@@ -114,6 +116,11 @@ export class ArchiverInstrumentation {
114
116
  valueType: ValueType.INT,
115
117
  });
116
118
 
119
+ this.blockProposalTxTargetCount = meter.createUpDownCounter(Metrics.ARCHIVER_BLOCK_PROPOSAL_TX_TARGET_COUNT, {
120
+ description: 'Number of block proposals by tx target',
121
+ valueType: ValueType.INT,
122
+ });
123
+
117
124
  this.dbMetrics = new LmdbMetrics(
118
125
  meter,
119
126
  {
@@ -184,4 +191,11 @@ export class ArchiverInstrumentation {
184
191
  public updateL1BlockHeight(blockNumber: bigint) {
185
192
  this.l1BlockHeight.record(Number(blockNumber));
186
193
  }
194
+
195
+ public recordBlockProposalTxTarget(target: string, usedTrace: boolean) {
196
+ this.blockProposalTxTargetCount.add(1, {
197
+ [Attributes.L1_BLOCK_PROPOSAL_TX_TARGET]: target.toLowerCase(),
198
+ [Attributes.L1_BLOCK_PROPOSAL_USED_TRACE]: usedTrace,
199
+ });
200
+ }
187
201
  }
@@ -0,0 +1,98 @@
1
+ # Archiver L1 Data Retrieval
2
+
3
+ Modules and classes to handle data retrieval from L1 for the archiver.
4
+
5
+ ## Calldata Retriever
6
+
7
+ The sequencer publisher bundles multiple operations into a single multicall3 transaction for gas
8
+ efficiency. A typical transaction includes:
9
+
10
+ 1. Attestation invalidations (if needed): `invalidateBadAttestation`, `invalidateInsufficientAttestations`
11
+ 2. Block proposal: `propose` (exactly one per transaction to the rollup contract)
12
+ 3. Governance and slashing (if needed): votes, payload creation/execution
13
+
14
+ The archiver needs to extract the `propose` calldata from these bundled transactions to reconstruct
15
+ L2 blocks. This class needs to handle scenarios where the transaction was submitted via multicall3,
16
+ as well as alternative ways for submitting the `propose` call that other clients might use.
17
+
18
+ ### Multicall3 Validation and Decoding
19
+
20
+ First attempt to decode the transaction as a multicall3 `aggregate3` call with validation:
21
+
22
+ - Check if transaction is to multicall3 address (`0xcA11bde05977b3631167028862bE2a173976CA11`)
23
+ - Decode as `aggregate3(Call3[] calldata calls)`
24
+ - Allow calls to known addresses and methods (rollup, governance, slashing contracts, etc.)
25
+ - Find the single `propose` call to the rollup contract
26
+ - Verify exactly one `propose` call exists
27
+ - Extract and return the propose calldata
28
+
29
+ This step handles the common case efficiently without requiring expensive trace or debug RPC calls.
30
+ Any validation failure triggers fallback to the next step.
31
+
32
+ ### Direct Propose Call
33
+
34
+ Second attempt to decode the transaction as a direct `propose` call to the rollup contract:
35
+
36
+ - Check if transaction is to the rollup address
37
+ - Decode as `propose` function call
38
+ - Verify the function is indeed `propose`
39
+ - Return the transaction input as the propose calldata
40
+
41
+ This handles scenarios where clients submit transactions directly to the rollup contract without
42
+ using multicall3 for bundling. Any validation failure triggers fallback to the next step.
43
+
44
+ ### Spire Proposer Call
45
+
46
+ Given existing attempts to route the call via the Spire proposer, we also check if the tx is `to` the
47
+ proposer known address, and if so, we try decoding it as either a multicall3 or a direct call to the
48
+ rollup contract.
49
+
50
+ Similar as with the multicall3 check, we check that there are no other calls in the Spire proposer, so
51
+ we are absolutely sure that the only call is the successful one to the rollup. Any extraneous call would
52
+ imply an unexpected path to calling `propose` in the rollup contract, and since we cannot verify if the
53
+ calldata arguments we extracted are the correct ones (see the section below), we cannot know for sure which
54
+ one is the call that succeeded, so we don't know which calldata to process.
55
+
56
+ Furthermore, since the Spire proposer is upgradeable, we check if the implementation has not changed in
57
+ order to decode. As usual, any validation failure triggers fallback to the next step.
58
+
59
+ ### Verifying Multicall3 Arguments
60
+
61
+ **This is NOT implemented for simplicity's sake**
62
+
63
+ If the checks above don't hold, such as when there are multiple calls to `propose`, then we cannot
64
+ reliably extract the `propose` calldata from the multicall3 arguments alone. We can try a best-effort
65
+ where we try all `propose` calls we see and validate them against on-chain data. Note that we can use these
66
+ same strategies if we were to obtain the calldata from another source.
67
+
68
+ #### TempBlockLog Verification
69
+
70
+ Read the stored `TempBlockLog` for the L2 block number from L1 and verify it matches our decoded header hash,
71
+ since the `TempBlockLog` stores the hash of the proposed block header, the payload commitment, and the attestations.
72
+
73
+ However, `TempBlockLog` is only stored temporarily and deleted after proven, so this method only works for recent
74
+ blocks, not for historical data syncing.
75
+
76
+ #### Archive Verification
77
+
78
+ Verify that the archive root in the decoded propose is correct with regard to the block header. This requires
79
+ hashing the block header we have retrieved, inserting it into the archive tree, and checking the resulting root
80
+ against the one we got from L1.
81
+
82
+ However, this requires that the archive keeps a reference to world-state, which is not the case in the current
83
+ system.
84
+
85
+ #### Emit Commitments in Rollup Contract
86
+
87
+ Modify rollup contract to emit commitments to the block header in the `L2BlockProposed` event, allowing us to easily
88
+ verify the calldata we obtained vs the emitted event.
89
+
90
+ However, modifying the rollup contract is out of scope for this change. But we can implement this approach in `v2`.
91
+
92
+ ### Debug and Trace Transaction Fallback
93
+
94
+ Last, we use L1 node's trace/debug RPC methods to definitively identify the one successful `propose` call within the tx.
95
+ We can then extract the exact calldata that hit the `propose` function in the rollup contract.
96
+
97
+ This approach requires access to a debug-enabled L1 node, which may be more resource-intensive, so we only
98
+ use it as a fallback when the first step fails, which should be rare in practice.