@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,406 @@
1
+ import { MULTI_CALL_3_ADDRESS } from '@aztec/ethereum';
2
+ import { EthAddress } from '@aztec/foundation/eth-address';
3
+ import { Fr } from '@aztec/foundation/fields';
4
+ import { EmpireSlashingProposerAbi, GovernanceProposerAbi, RollupAbi, SlashFactoryAbi, TallySlashingProposerAbi } from '@aztec/l1-artifacts';
5
+ import { CommitteeAttestation } from '@aztec/stdlib/block';
6
+ import { ProposedBlockHeader, StateReference } from '@aztec/stdlib/tx';
7
+ import { decodeFunctionData, hexToBytes, multicall3Abi, toFunctionSelector } from 'viem';
8
+ import { getSuccessfulCallsFromDebug } from './debug_tx.js';
9
+ import { getCallFromSpireProposer } from './spire_proposer.js';
10
+ import { getSuccessfulCallsFromTrace } from './trace_tx.js';
11
+ /**
12
+ * Extracts calldata to the `propose` method of the rollup contract from an L1 transaction
13
+ * in order to reconstruct an L2 block header.
14
+ */ export class CalldataRetriever {
15
+ publicClient;
16
+ debugClient;
17
+ targetCommitteeSize;
18
+ instrumentation;
19
+ logger;
20
+ /** Pre-computed valid contract calls for validation */ validContractCalls;
21
+ rollupAddress;
22
+ constructor(publicClient, debugClient, targetCommitteeSize, instrumentation, logger, contractAddresses){
23
+ this.publicClient = publicClient;
24
+ this.debugClient = debugClient;
25
+ this.targetCommitteeSize = targetCommitteeSize;
26
+ this.instrumentation = instrumentation;
27
+ this.logger = logger;
28
+ this.rollupAddress = contractAddresses.rollupAddress;
29
+ this.validContractCalls = computeValidContractCalls(contractAddresses);
30
+ }
31
+ /**
32
+ * Gets block header and metadata from the calldata of an L1 transaction.
33
+ * Tries multicall3 decoding, falls back to trace-based extraction.
34
+ * @param txHash - Hash of the tx that published it.
35
+ * @param l2BlockNumber - L2 block number.
36
+ * @returns L2 block header and metadata from the calldata, deserialized (without body)
37
+ */ async getBlockHeaderFromRollupTx(txHash, l2BlockNumber) {
38
+ this.logger.trace(`Fetching L2 block ${l2BlockNumber} from rollup tx ${txHash}`);
39
+ const tx = await this.publicClient.getTransaction({
40
+ hash: txHash
41
+ });
42
+ const proposeCalldata = await this.getProposeCallData(tx, l2BlockNumber);
43
+ return this.decodeAndBuildBlockHeader(proposeCalldata, tx.blockHash, l2BlockNumber);
44
+ }
45
+ /** Gets rollup propose calldata from a transaction */ async getProposeCallData(tx, l2BlockNumber) {
46
+ // Try to decode as multicall3 with validation
47
+ const proposeCalldata = this.tryDecodeMulticall3(tx);
48
+ if (proposeCalldata) {
49
+ this.logger.trace(`Decoded propose calldata from multicall3 for tx ${tx.hash}`);
50
+ this.instrumentation?.recordBlockProposalTxTarget(tx.to, false);
51
+ return proposeCalldata;
52
+ }
53
+ // Try to decode as direct propose call
54
+ const directProposeCalldata = this.tryDecodeDirectPropose(tx);
55
+ if (directProposeCalldata) {
56
+ this.logger.trace(`Decoded propose calldata from direct call for tx ${tx.hash}`);
57
+ this.instrumentation?.recordBlockProposalTxTarget(tx.to, false);
58
+ return directProposeCalldata;
59
+ }
60
+ // Try to decode as Spire Proposer multicall wrapper
61
+ const spireProposeCalldata = await this.tryDecodeSpireProposer(tx);
62
+ if (spireProposeCalldata) {
63
+ this.logger.trace(`Decoded propose calldata from Spire Proposer for tx ${tx.hash}`);
64
+ this.instrumentation?.recordBlockProposalTxTarget(tx.to, false);
65
+ return spireProposeCalldata;
66
+ }
67
+ // Fall back to trace-based extraction
68
+ this.logger.warn(`Failed to decode multicall3, direct propose, or Spire proposer for L1 tx ${tx.hash}, falling back to trace for L2 block ${l2BlockNumber}`);
69
+ this.instrumentation?.recordBlockProposalTxTarget(tx.to ?? EthAddress.ZERO.toString(), true);
70
+ return await this.extractCalldataViaTrace(tx.hash);
71
+ }
72
+ /**
73
+ * Attempts to decode a transaction as a Spire Proposer multicall wrapper.
74
+ * If successful, extracts the wrapped call and validates it as either multicall3 or direct propose.
75
+ * @param tx - The transaction to decode
76
+ * @returns The propose calldata if successfully decoded and validated, undefined otherwise
77
+ */ async tryDecodeSpireProposer(tx) {
78
+ // Try to decode as Spire Proposer multicall (extracts the wrapped call)
79
+ const spireWrappedCall = await getCallFromSpireProposer(tx, this.publicClient, this.logger);
80
+ if (!spireWrappedCall) {
81
+ return undefined;
82
+ }
83
+ this.logger.trace(`Decoded Spire Proposer wrapping for tx ${tx.hash}, inner call to ${spireWrappedCall.to}`);
84
+ // Now try to decode the wrapped call as either multicall3 or direct propose
85
+ const wrappedTx = {
86
+ to: spireWrappedCall.to,
87
+ input: spireWrappedCall.data,
88
+ hash: tx.hash
89
+ };
90
+ const multicall3Calldata = this.tryDecodeMulticall3(wrappedTx);
91
+ if (multicall3Calldata) {
92
+ this.logger.trace(`Decoded propose calldata from Spire Proposer to multicall3 for tx ${tx.hash}`);
93
+ return multicall3Calldata;
94
+ }
95
+ const directProposeCalldata = this.tryDecodeDirectPropose(wrappedTx);
96
+ if (directProposeCalldata) {
97
+ this.logger.trace(`Decoded propose calldata from Spire Proposer to direct propose for tx ${tx.hash}`);
98
+ return directProposeCalldata;
99
+ }
100
+ this.logger.warn(`Spire Proposer wrapped call could not be decoded as multicall3 or direct propose for tx ${tx.hash}`);
101
+ return undefined;
102
+ }
103
+ /**
104
+ * Attempts to decode transaction input as multicall3 and extract propose calldata.
105
+ * Returns undefined if validation fails.
106
+ * @param tx - The transaction-like object with to, input, and hash
107
+ * @returns The propose calldata if successfully validated, undefined otherwise
108
+ */ tryDecodeMulticall3(tx) {
109
+ const txHash = tx.hash;
110
+ try {
111
+ // Check if transaction is to Multicall3 address
112
+ if (!tx.to || !EthAddress.areEqual(tx.to, MULTI_CALL_3_ADDRESS)) {
113
+ this.logger.debug(`Transaction is not to Multicall3 address (to: ${tx.to})`, {
114
+ txHash,
115
+ to: tx.to
116
+ });
117
+ return undefined;
118
+ }
119
+ // Try to decode as multicall3 aggregate3 call
120
+ const { functionName: multicall3Fn, args: multicall3Args } = decodeFunctionData({
121
+ abi: multicall3Abi,
122
+ data: tx.input
123
+ });
124
+ // If not aggregate3, return undefined (not a multicall3 transaction)
125
+ if (multicall3Fn !== 'aggregate3') {
126
+ this.logger.warn(`Transaction is not multicall3 aggregate3 (got ${multicall3Fn})`, {
127
+ txHash
128
+ });
129
+ return undefined;
130
+ }
131
+ if (multicall3Args.length !== 1) {
132
+ this.logger.warn(`Unexpected number of arguments for multicall3 (got ${multicall3Args.length})`, {
133
+ txHash
134
+ });
135
+ return undefined;
136
+ }
137
+ const [calls] = multicall3Args;
138
+ // Validate all calls and find propose calls
139
+ const rollupAddressLower = this.rollupAddress.toString().toLowerCase();
140
+ const proposeCalls = [];
141
+ for(let i = 0; i < calls.length; i++){
142
+ const addr = calls[i].target.toLowerCase();
143
+ const callData = calls[i].callData;
144
+ // Extract function selector (first 4 bytes)
145
+ if (callData.length < 10) {
146
+ // "0x" + 8 hex chars = 10 chars minimum for a valid function call
147
+ this.logger.warn(`Invalid calldata length at index ${i} (${callData.length})`, {
148
+ txHash
149
+ });
150
+ return undefined;
151
+ }
152
+ const functionSelector = callData.slice(0, 10);
153
+ // Validate this call is allowed by searching through valid calls
154
+ const validCall = this.validContractCalls.find((vc)=>vc.address === addr && vc.functionSelector === functionSelector);
155
+ if (!validCall) {
156
+ this.logger.warn(`Invalid contract call detected in multicall3`, {
157
+ index: i,
158
+ targetAddress: addr,
159
+ functionSelector,
160
+ validCalls: this.validContractCalls.map((c)=>({
161
+ address: c.address,
162
+ selector: c.functionSelector
163
+ })),
164
+ txHash
165
+ });
166
+ return undefined;
167
+ }
168
+ this.logger.trace(`Valid call found to ${addr}`, {
169
+ validCall
170
+ });
171
+ // Collect propose calls specifically
172
+ if (addr === rollupAddressLower && validCall.functionName === 'propose') {
173
+ proposeCalls.push(callData);
174
+ }
175
+ }
176
+ // Validate exactly ONE propose call
177
+ if (proposeCalls.length === 0) {
178
+ this.logger.warn(`No propose calls found in multicall3`, {
179
+ txHash
180
+ });
181
+ return undefined;
182
+ }
183
+ if (proposeCalls.length > 1) {
184
+ this.logger.warn(`Multiple propose calls found in multicall3 (${proposeCalls.length})`, {
185
+ txHash
186
+ });
187
+ return undefined;
188
+ }
189
+ // Successfully extracted single propose call
190
+ return proposeCalls[0];
191
+ } catch (err) {
192
+ // Any decoding error triggers fallback to trace
193
+ this.logger.warn(`Failed to decode multicall3: ${err}`, {
194
+ txHash
195
+ });
196
+ return undefined;
197
+ }
198
+ }
199
+ /**
200
+ * Attempts to decode transaction as a direct propose call to the rollup contract.
201
+ * Returns undefined if validation fails.
202
+ * @param tx - The transaction-like object with to, input, and hash
203
+ * @returns The propose calldata if successfully validated, undefined otherwise
204
+ */ tryDecodeDirectPropose(tx) {
205
+ const txHash = tx.hash;
206
+ try {
207
+ // Check if transaction is to the rollup address
208
+ if (!tx.to || !EthAddress.areEqual(tx.to, this.rollupAddress)) {
209
+ this.logger.debug(`Transaction is not to rollup address (to: ${tx.to})`, {
210
+ txHash
211
+ });
212
+ return undefined;
213
+ }
214
+ // Try to decode as propose call
215
+ const { functionName } = decodeFunctionData({
216
+ abi: RollupAbi,
217
+ data: tx.input
218
+ });
219
+ // If not propose, return undefined
220
+ if (functionName !== 'propose') {
221
+ this.logger.warn(`Transaction to rollup is not propose (got ${functionName})`, {
222
+ txHash
223
+ });
224
+ return undefined;
225
+ }
226
+ // Successfully validated direct propose call
227
+ this.logger.trace(`Validated direct propose call to rollup`, {
228
+ txHash
229
+ });
230
+ return tx.input;
231
+ } catch (err) {
232
+ // Any decoding error means it's not a valid propose call
233
+ this.logger.warn(`Failed to decode as direct propose: ${err}`, {
234
+ txHash
235
+ });
236
+ return undefined;
237
+ }
238
+ }
239
+ /**
240
+ * Uses debug/trace RPC to extract the actual calldata from the successful propose call.
241
+ * This is the definitive fallback that works for any transaction pattern.
242
+ * Tries trace_transaction first, then falls back to debug_traceTransaction.
243
+ * @param txHash - The transaction hash to trace
244
+ * @returns The propose calldata from the successful call
245
+ */ async extractCalldataViaTrace(txHash) {
246
+ const rollupAddress = this.rollupAddress;
247
+ const selector = PROPOSE_SELECTOR;
248
+ let calls;
249
+ try {
250
+ // Try trace_transaction first (using Parity/OpenEthereum/Erigon RPC)
251
+ this.logger.debug(`Attempting to trace transaction ${txHash} using trace_transaction`);
252
+ calls = await getSuccessfulCallsFromTrace(this.debugClient, txHash, rollupAddress, selector, this.logger);
253
+ this.logger.debug(`Successfully traced using trace_transaction, found ${calls.length} calls`);
254
+ } catch (err) {
255
+ const traceError = err instanceof Error ? err : new Error(String(err));
256
+ this.logger.verbose(`Failed trace_transaction for ${txHash}`, {
257
+ traceError
258
+ });
259
+ try {
260
+ // Fall back to debug_traceTransaction (Geth RPC)
261
+ this.logger.debug(`Attempting to trace transaction ${txHash} using debug_traceTransaction`);
262
+ calls = await getSuccessfulCallsFromDebug(this.debugClient, txHash, rollupAddress, selector, this.logger);
263
+ this.logger.debug(`Successfully traced using debug_traceTransaction, found ${calls.length} calls`);
264
+ } catch (debugErr) {
265
+ const debugError = debugErr instanceof Error ? debugErr : new Error(String(debugErr));
266
+ this.logger.warn(`All tracing methods failed for tx ${txHash}`, {
267
+ traceError,
268
+ debugError,
269
+ txHash
270
+ });
271
+ throw new Error(`Failed to trace transaction ${txHash} to extract propose calldata`);
272
+ }
273
+ }
274
+ // Validate exactly ONE successful propose call
275
+ if (calls.length === 0) {
276
+ throw new Error(`No successful propose calls found in transaction ${txHash}`);
277
+ }
278
+ if (calls.length > 1) {
279
+ throw new Error(`Multiple successful propose calls found in transaction ${txHash} (${calls.length})`);
280
+ }
281
+ // Return the calldata from the single successful propose call
282
+ return calls[0].input;
283
+ }
284
+ /**
285
+ * Decodes propose calldata and builds the block header structure.
286
+ * @param proposeCalldata - The propose function calldata
287
+ * @param blockHash - The L1 block hash containing this transaction
288
+ * @param blobHashes - The blob hashes for this block
289
+ * @param l2BlockNumber - The L2 block number
290
+ * @returns The decoded block header and metadata
291
+ */ decodeAndBuildBlockHeader(proposeCalldata, blockHash, l2BlockNumber) {
292
+ const { functionName: rollupFunctionName, args: rollupArgs } = decodeFunctionData({
293
+ abi: RollupAbi,
294
+ data: proposeCalldata
295
+ });
296
+ if (rollupFunctionName !== 'propose') {
297
+ throw new Error(`Unexpected rollup method called ${rollupFunctionName}`);
298
+ }
299
+ const [decodedArgs, packedAttestations, _signers, _attestationsAndSignersSignature, _blobInput] = rollupArgs;
300
+ const attestations = CommitteeAttestation.fromPacked(packedAttestations, this.targetCommitteeSize);
301
+ this.logger.trace(`Decoded propose calldata`, {
302
+ l2BlockNumber,
303
+ archive: decodedArgs.archive,
304
+ stateReference: decodedArgs.stateReference,
305
+ header: decodedArgs.header,
306
+ l1BlockHash: blockHash,
307
+ attestations,
308
+ packedAttestations,
309
+ targetCommitteeSize: this.targetCommitteeSize
310
+ });
311
+ const header = ProposedBlockHeader.fromViem(decodedArgs.header);
312
+ const archiveRoot = new Fr(Buffer.from(hexToBytes(decodedArgs.archive)));
313
+ const stateReference = StateReference.fromViem(decodedArgs.stateReference);
314
+ return {
315
+ l2BlockNumber,
316
+ archiveRoot,
317
+ stateReference,
318
+ header,
319
+ attestations,
320
+ blockHash
321
+ };
322
+ }
323
+ }
324
+ /**
325
+ * Pre-computed function selectors for all valid contract calls.
326
+ * These are computed once at module load time from the ABIs.
327
+ * Based on analysis of sequencer-client/src/publisher/sequencer-publisher.ts
328
+ */ // Rollup contract function selectors (always valid)
329
+ const PROPOSE_SELECTOR = toFunctionSelector(RollupAbi.find((x)=>x.type === 'function' && x.name === 'propose'));
330
+ const INVALIDATE_BAD_ATTESTATION_SELECTOR = toFunctionSelector(RollupAbi.find((x)=>x.type === 'function' && x.name === 'invalidateBadAttestation'));
331
+ const INVALIDATE_INSUFFICIENT_ATTESTATIONS_SELECTOR = toFunctionSelector(RollupAbi.find((x)=>x.type === 'function' && x.name === 'invalidateInsufficientAttestations'));
332
+ // Governance proposer function selectors
333
+ const GOVERNANCE_SIGNAL_WITH_SIG_SELECTOR = toFunctionSelector(GovernanceProposerAbi.find((x)=>x.type === 'function' && x.name === 'signalWithSig'));
334
+ // Slash factory function selectors
335
+ const CREATE_SLASH_PAYLOAD_SELECTOR = toFunctionSelector(SlashFactoryAbi.find((x)=>x.type === 'function' && x.name === 'createSlashPayload'));
336
+ // Empire slashing proposer function selectors
337
+ const EMPIRE_SIGNAL_WITH_SIG_SELECTOR = toFunctionSelector(EmpireSlashingProposerAbi.find((x)=>x.type === 'function' && x.name === 'signalWithSig'));
338
+ const EMPIRE_SUBMIT_ROUND_WINNER_SELECTOR = toFunctionSelector(EmpireSlashingProposerAbi.find((x)=>x.type === 'function' && x.name === 'submitRoundWinner'));
339
+ // Tally slashing proposer function selectors
340
+ const TALLY_VOTE_SELECTOR = toFunctionSelector(TallySlashingProposerAbi.find((x)=>x.type === 'function' && x.name === 'vote'));
341
+ const TALLY_EXECUTE_ROUND_SELECTOR = toFunctionSelector(TallySlashingProposerAbi.find((x)=>x.type === 'function' && x.name === 'executeRound'));
342
+ /**
343
+ * All valid contract calls that the sequencer publisher can make.
344
+ * Builds the list of valid (address, selector) pairs for validation.
345
+ *
346
+ * Alternatively, if we are absolutely sure that no code path from any of these
347
+ * contracts can eventually land on another call to `propose`, we can remove the
348
+ * function selectors.
349
+ */ function computeValidContractCalls(addresses) {
350
+ const { rollupAddress, governanceProposerAddress, slashFactoryAddress, slashingProposerAddress } = addresses;
351
+ const calls = [];
352
+ // Rollup contract calls (always present)
353
+ calls.push({
354
+ address: rollupAddress.toString().toLowerCase(),
355
+ functionSelector: PROPOSE_SELECTOR,
356
+ functionName: 'propose'
357
+ }, {
358
+ address: rollupAddress.toString().toLowerCase(),
359
+ functionSelector: INVALIDATE_BAD_ATTESTATION_SELECTOR,
360
+ functionName: 'invalidateBadAttestation'
361
+ }, {
362
+ address: rollupAddress.toString().toLowerCase(),
363
+ functionSelector: INVALIDATE_INSUFFICIENT_ATTESTATIONS_SELECTOR,
364
+ functionName: 'invalidateInsufficientAttestations'
365
+ });
366
+ // Governance proposer calls (optional)
367
+ if (governanceProposerAddress && !governanceProposerAddress.isZero()) {
368
+ calls.push({
369
+ address: governanceProposerAddress.toString().toLowerCase(),
370
+ functionSelector: GOVERNANCE_SIGNAL_WITH_SIG_SELECTOR,
371
+ functionName: 'signalWithSig'
372
+ });
373
+ }
374
+ // Slash factory calls (optional)
375
+ if (slashFactoryAddress && !slashFactoryAddress.isZero()) {
376
+ calls.push({
377
+ address: slashFactoryAddress.toString().toLowerCase(),
378
+ functionSelector: CREATE_SLASH_PAYLOAD_SELECTOR,
379
+ functionName: 'createSlashPayload'
380
+ });
381
+ }
382
+ // Slashing proposer calls (optional, can be either Empire or Tally)
383
+ if (slashingProposerAddress && !slashingProposerAddress.isZero()) {
384
+ // Empire calls
385
+ calls.push({
386
+ address: slashingProposerAddress.toString().toLowerCase(),
387
+ functionSelector: EMPIRE_SIGNAL_WITH_SIG_SELECTOR,
388
+ functionName: 'signalWithSig (empire)'
389
+ }, {
390
+ address: slashingProposerAddress.toString().toLowerCase(),
391
+ functionSelector: EMPIRE_SUBMIT_ROUND_WINNER_SELECTOR,
392
+ functionName: 'submitRoundWinner'
393
+ });
394
+ // Tally calls
395
+ calls.push({
396
+ address: slashingProposerAddress.toString().toLowerCase(),
397
+ functionSelector: TALLY_VOTE_SELECTOR,
398
+ functionName: 'vote'
399
+ }, {
400
+ address: slashingProposerAddress.toString().toLowerCase(),
401
+ functionSelector: TALLY_EXECUTE_ROUND_SELECTOR,
402
+ functionName: 'executeRound'
403
+ });
404
+ }
405
+ return calls;
406
+ }
@@ -1,6 +1,6 @@
1
1
  import type { BlobSinkClientInterface } from '@aztec/blob-sink/client';
2
- import type { ViemClient, ViemPublicClient } from '@aztec/ethereum';
3
- import type { EthAddress } from '@aztec/foundation/eth-address';
2
+ import type { ViemClient, ViemPublicClient, ViemPublicDebugClient } from '@aztec/ethereum';
3
+ import { EthAddress } from '@aztec/foundation/eth-address';
4
4
  import { Fr } from '@aztec/foundation/fields';
5
5
  import { type Logger } from '@aztec/foundation/log';
6
6
  import { type InboxAbi, RollupAbi } from '@aztec/l1-artifacts';
@@ -8,9 +8,10 @@ import { Body, CommitteeAttestation, PublishedL2Block } from '@aztec/stdlib/bloc
8
8
  import { Proof } from '@aztec/stdlib/proofs';
9
9
  import { ProposedBlockHeader, StateReference } from '@aztec/stdlib/tx';
10
10
  import { type GetContractReturnType, type Hex } from 'viem';
11
- import type { DataRetrieval } from './structs/data_retrieval.js';
12
- import type { InboxMessage } from './structs/inbox_message.js';
13
- import type { L1PublishedData } from './structs/published.js';
11
+ import type { ArchiverInstrumentation } from '../instrumentation.js';
12
+ import type { DataRetrieval } from '../structs/data_retrieval.js';
13
+ import type { InboxMessage } from '../structs/inbox_message.js';
14
+ import type { L1PublishedData } from '../structs/published.js';
14
15
  export type RetrievedL2Block = {
15
16
  l2BlockNumber: number;
16
17
  archiveRoot: Fr;
@@ -26,14 +27,20 @@ export declare function retrievedBlockToPublishedL2Block(retrievedBlock: Retriev
26
27
  /**
27
28
  * Fetches new L2 blocks.
28
29
  * @param publicClient - The viem public client to use for transaction retrieval.
30
+ * @param debugClient - The viem debug client to use for trace/debug RPC methods (optional).
29
31
  * @param rollupAddress - The address of the rollup contract.
30
32
  * @param searchStartBlock - The block number to use for starting the search.
31
33
  * @param searchEndBlock - The highest block number that we should search up to.
32
34
  * @param expectedNextL2BlockNum - The next L2 block number that we expect to find.
33
35
  * @returns An array of block; as well as the next eth block to search from.
34
36
  */
35
- export declare function retrieveBlocksFromRollup(rollup: GetContractReturnType<typeof RollupAbi, ViemPublicClient>, publicClient: ViemPublicClient, blobSinkClient: BlobSinkClientInterface, searchStartBlock: bigint, searchEndBlock: bigint, logger?: Logger): Promise<RetrievedL2Block[]>;
37
+ export declare function retrieveBlocksFromRollup(rollup: GetContractReturnType<typeof RollupAbi, ViemPublicClient>, publicClient: ViemPublicClient, debugClient: ViemPublicDebugClient, blobSinkClient: BlobSinkClientInterface, searchStartBlock: bigint, searchEndBlock: bigint, contractAddresses: {
38
+ governanceProposerAddress: EthAddress;
39
+ slashFactoryAddress?: EthAddress;
40
+ slashingProposerAddress: EthAddress;
41
+ }, instrumentation: ArchiverInstrumentation, logger?: Logger): Promise<RetrievedL2Block[]>;
36
42
  export declare function getL1BlockTime(publicClient: ViemPublicClient, blockNumber: bigint): Promise<bigint>;
43
+ export declare function getBlockBodyFromBlobs(blobSinkClient: BlobSinkClientInterface, blockHash: string, blobHashes: Buffer<ArrayBufferLike>[], l2BlockNumber: number, logger: Logger): Promise<Body>;
37
44
  /** Given an L1 to L2 message, retrieves its corresponding event from the Inbox within a specific block range. */
38
45
  export declare function retrieveL1ToL2Message(inbox: GetContractReturnType<typeof InboxAbi, ViemClient>, leaf: Fr, fromBlock: bigint, toBlock: bigint): Promise<InboxMessage | undefined>;
39
46
  /**
@@ -0,0 +1 @@
1
+ {"version":3,"file":"data_retrieval.d.ts","sourceRoot":"","sources":["../../../src/archiver/l1/data_retrieval.ts"],"names":[],"mappings":"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"}
@@ -1,16 +1,18 @@
1
1
  import { Blob, BlobDeserializationError, EMPTY_BLOB_VERSIONED_HASH } from '@aztec/blob-lib';
2
2
  import { asyncPool } from '@aztec/foundation/async-pool';
3
3
  import { Buffer16, Buffer32 } from '@aztec/foundation/buffer';
4
+ import { EthAddress } from '@aztec/foundation/eth-address';
4
5
  import { Fr } from '@aztec/foundation/fields';
5
6
  import { createLogger } from '@aztec/foundation/log';
6
7
  import { bufferToHex } from '@aztec/foundation/string';
7
8
  import { RollupAbi } from '@aztec/l1-artifacts';
8
- import { Body, CommitteeAttestation, L2Block, PublishedL2Block } from '@aztec/stdlib/block';
9
+ import { Body, L2Block, PublishedL2Block } from '@aztec/stdlib/block';
9
10
  import { Proof } from '@aztec/stdlib/proofs';
10
11
  import { AppendOnlyTreeSnapshot } from '@aztec/stdlib/trees';
11
- import { BlockHeader, GlobalVariables, ProposedBlockHeader, StateReference } from '@aztec/stdlib/tx';
12
- import { decodeFunctionData, getAbiItem, hexToBytes, multicall3Abi } from 'viem';
13
- import { NoBlobBodiesFoundError } from './errors.js';
12
+ import { BlockHeader, GlobalVariables } from '@aztec/stdlib/tx';
13
+ import { decodeFunctionData, getAbiItem, hexToBytes } from 'viem';
14
+ import { NoBlobBodiesFoundError } from '../errors.js';
15
+ import { CalldataRetriever } from './calldata_retriever.js';
14
16
  export function retrievedBlockToPublishedL2Block(retrievedBlock) {
15
17
  const { l2BlockNumber, archiveRoot, stateReference, header: proposedHeader, body, l1, chainId, version, attestations } = retrievedBlock;
16
18
  const archive = new AppendOnlyTreeSnapshot(archiveRoot, l2BlockNumber + 1);
@@ -42,12 +44,13 @@ export function retrievedBlockToPublishedL2Block(retrievedBlock) {
42
44
  /**
43
45
  * Fetches new L2 blocks.
44
46
  * @param publicClient - The viem public client to use for transaction retrieval.
47
+ * @param debugClient - The viem debug client to use for trace/debug RPC methods (optional).
45
48
  * @param rollupAddress - The address of the rollup contract.
46
49
  * @param searchStartBlock - The block number to use for starting the search.
47
50
  * @param searchEndBlock - The highest block number that we should search up to.
48
51
  * @param expectedNextL2BlockNum - The next L2 block number that we expect to find.
49
52
  * @returns An array of block; as well as the next eth block to search from.
50
- */ export async function retrieveBlocksFromRollup(rollup, publicClient, blobSinkClient, searchStartBlock, searchEndBlock, logger = createLogger('archiver')) {
53
+ */ export async function retrieveBlocksFromRollup(rollup, publicClient, debugClient, blobSinkClient, searchStartBlock, searchEndBlock, contractAddresses, instrumentation, logger = createLogger('archiver')) {
51
54
  const retrievedBlocks = [];
52
55
  let rollupConstants;
53
56
  do {
@@ -75,7 +78,7 @@ export function retrievedBlockToPublishedL2Block(retrievedBlock) {
75
78
  targetCommitteeSize: Number(targetCommitteeSize)
76
79
  };
77
80
  }
78
- const newBlocks = await processL2BlockProposedLogs(rollup, publicClient, blobSinkClient, l2BlockProposedLogs, rollupConstants, logger);
81
+ const newBlocks = await processL2BlockProposedLogs(rollup, publicClient, debugClient, blobSinkClient, l2BlockProposedLogs, rollupConstants, contractAddresses, instrumentation, logger);
79
82
  retrievedBlocks.push(...newBlocks);
80
83
  searchStartBlock = lastLog.blockNumber + 1n;
81
84
  }while (searchStartBlock <= searchEndBlock)
@@ -86,10 +89,15 @@ export function retrievedBlockToPublishedL2Block(retrievedBlock) {
86
89
  * Processes newly received L2BlockProposed logs.
87
90
  * @param rollup - The rollup contract
88
91
  * @param publicClient - The viem public client to use for transaction retrieval.
92
+ * @param debugClient - The viem debug client to use for trace/debug RPC methods (optional).
89
93
  * @param logs - L2BlockProposed logs.
90
94
  * @returns - An array blocks.
91
- */ async function processL2BlockProposedLogs(rollup, publicClient, blobSinkClient, logs, { chainId, version, targetCommitteeSize }, logger) {
95
+ */ async function processL2BlockProposedLogs(rollup, publicClient, debugClient, blobSinkClient, logs, { chainId, version, targetCommitteeSize }, contractAddresses, instrumentation, logger) {
92
96
  const retrievedBlocks = [];
97
+ const calldataRetriever = new CalldataRetriever(publicClient, debugClient, targetCommitteeSize, instrumentation, logger, {
98
+ ...contractAddresses,
99
+ rollupAddress: EthAddress.fromString(rollup.address)
100
+ });
93
101
  await asyncPool(10, logs, async (log)=>{
94
102
  const l2BlockNumber = Number(log.args.blockNumber);
95
103
  const archive = log.args.archive;
@@ -99,14 +107,17 @@ export function retrievedBlockToPublishedL2Block(retrievedBlock) {
99
107
  const blobHashes = log.args.versionedBlobHashes.map((blobHash)=>Buffer.from(blobHash.slice(2), 'hex'));
100
108
  // The value from the event and contract will match only if the block is in the chain.
101
109
  if (archive === archiveFromChain) {
102
- const block = await getBlockFromRollupTx(publicClient, blobSinkClient, log.transactionHash, blobHashes, l2BlockNumber, rollup.address, targetCommitteeSize, logger);
110
+ const blockHeader = await calldataRetriever.getBlockHeaderFromRollupTx(log.transactionHash, l2BlockNumber);
111
+ const body = await getBlockBodyFromBlobs(blobSinkClient, blockHeader.blockHash, blobHashes, l2BlockNumber, logger);
103
112
  const l1 = {
104
113
  blockNumber: log.blockNumber,
105
114
  blockHash: log.blockHash,
106
115
  timestamp: await getL1BlockTime(publicClient, log.blockNumber)
107
116
  };
117
+ const { blockHash: _blockHash, ...blockData } = blockHeader;
108
118
  retrievedBlocks.push({
109
- ...block,
119
+ ...blockData,
120
+ body,
110
121
  l1,
111
122
  chainId,
112
123
  version
@@ -115,7 +126,7 @@ export function retrievedBlockToPublishedL2Block(retrievedBlock) {
115
126
  l1BlockNumber: log.blockNumber,
116
127
  l2BlockNumber,
117
128
  archive: archive.toString(),
118
- attestations: block.attestations
129
+ attestations: blockHeader.attestations
119
130
  });
120
131
  } else {
121
132
  logger.warn(`Ignoring L2 block ${l2BlockNumber} due to archive root mismatch`, {
@@ -133,95 +144,7 @@ export async function getL1BlockTime(publicClient, blockNumber) {
133
144
  });
134
145
  return block.timestamp;
135
146
  }
136
- /**
137
- * Extracts the first 'propose' method calldata from a multicall3 transaction's data.
138
- * @param multicall3Data - The multicall3 transaction input data
139
- * @param rollupAddress - The address of the rollup contract
140
- * @returns The calldata for the first 'propose' method call to the rollup contract
141
- */ function extractRollupProposeCalldata(multicall3Data, rollupAddress) {
142
- const { functionName: multicall3FunctionName, args: multicall3Args } = decodeFunctionData({
143
- abi: multicall3Abi,
144
- data: multicall3Data
145
- });
146
- if (multicall3FunctionName !== 'aggregate3') {
147
- throw new Error(`Unexpected multicall3 method called ${multicall3FunctionName}`);
148
- }
149
- if (multicall3Args.length !== 1) {
150
- throw new Error(`Unexpected number of arguments for multicall3`);
151
- }
152
- const [calls] = multicall3Args;
153
- // Find all rollup calls
154
- const rollupAddressLower = rollupAddress.toLowerCase();
155
- for(let i = 0; i < calls.length; i++){
156
- const addr = calls[i].target;
157
- if (addr.toLowerCase() !== rollupAddressLower) {
158
- continue;
159
- }
160
- const callData = calls[i].callData;
161
- try {
162
- const { functionName: rollupFunctionName } = decodeFunctionData({
163
- abi: RollupAbi,
164
- data: callData
165
- });
166
- if (rollupFunctionName === 'propose') {
167
- return callData;
168
- }
169
- } catch {
170
- continue;
171
- }
172
- }
173
- throw new Error(`Rollup address not found in multicall3 args`);
174
- }
175
- /**
176
- * Gets block from the calldata of an L1 transaction.
177
- * Assumes that the block was published from an EOA.
178
- * TODO: Add retries and error management.
179
- * @param publicClient - The viem public client to use for transaction retrieval.
180
- * @param txHash - Hash of the tx that published it.
181
- * @param l2BlockNumber - L2 block number.
182
- * @returns L2 block from the calldata, deserialized
183
- */ async function getBlockFromRollupTx(publicClient, blobSinkClient, txHash, blobHashes, l2BlockNumber, rollupAddress, targetCommitteeSize, logger) {
184
- logger.trace(`Fetching L2 block ${l2BlockNumber} from rollup tx ${txHash}`);
185
- const { input: forwarderData, blockHash } = await publicClient.getTransaction({
186
- hash: txHash
187
- });
188
- const rollupData = extractRollupProposeCalldata(forwarderData, rollupAddress);
189
- const { functionName: rollupFunctionName, args: rollupArgs } = decodeFunctionData({
190
- abi: RollupAbi,
191
- data: rollupData
192
- });
193
- if (rollupFunctionName !== 'propose') {
194
- throw new Error(`Unexpected rollup method called ${rollupFunctionName}`);
195
- }
196
- const [decodedArgs, packedAttestations, _signers, _blobInput] = rollupArgs;
197
- const attestations = CommitteeAttestation.fromPacked(packedAttestations, targetCommitteeSize);
198
- logger.trace(`Recovered propose calldata from tx ${txHash}`, {
199
- l2BlockNumber,
200
- archive: decodedArgs.archive,
201
- stateReference: decodedArgs.stateReference,
202
- header: decodedArgs.header,
203
- l1BlockHash: blockHash,
204
- blobHashes,
205
- attestations,
206
- packedAttestations,
207
- targetCommitteeSize
208
- });
209
- // TODO(md): why is the proposed block header different to the actual block header?
210
- // This is likely going to be a footgun
211
- const header = ProposedBlockHeader.fromViem(decodedArgs.header);
212
- const body = await getBlockBodyFromBlobs(blobSinkClient, blockHash, blobHashes, l2BlockNumber, logger);
213
- const archiveRoot = new Fr(Buffer.from(hexToBytes(decodedArgs.archive)));
214
- const stateReference = StateReference.fromViem(decodedArgs.stateReference);
215
- return {
216
- l2BlockNumber,
217
- archiveRoot,
218
- stateReference,
219
- header,
220
- body,
221
- attestations
222
- };
223
- }
224
- async function getBlockBodyFromBlobs(blobSinkClient, blockHash, blobHashes, l2BlockNumber, logger) {
147
+ export async function getBlockBodyFromBlobs(blobSinkClient, blockHash, blobHashes, l2BlockNumber, logger) {
225
148
  const blobBodies = await blobSinkClient.getBlobSidecar(blockHash, blobHashes);
226
149
  logger.trace(`Fetched ${blobBodies.length} blob bodies for L2 block ${l2BlockNumber}`, {
227
150
  blobHashes: blobHashes.map(bufferToHex),