@aztec/archiver 0.0.1-commit.7ac86ea28 → 0.0.1-commit.7b86788

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.
@@ -157,11 +157,6 @@ export async function retrieveCheckpointsFromRollup(
157
157
  blobClient: BlobClientInterface,
158
158
  searchStartBlock: bigint,
159
159
  searchEndBlock: bigint,
160
- contractAddresses: {
161
- governanceProposerAddress: EthAddress;
162
- slashFactoryAddress?: EthAddress;
163
- slashingProposerAddress: EthAddress;
164
- },
165
160
  instrumentation: ArchiverInstrumentation,
166
161
  logger: Logger = createLogger('archiver'),
167
162
  isHistoricalSync: boolean = false,
@@ -205,7 +200,6 @@ export async function retrieveCheckpointsFromRollup(
205
200
  blobClient,
206
201
  checkpointProposedLogs,
207
202
  rollupConstants,
208
- contractAddresses,
209
203
  instrumentation,
210
204
  logger,
211
205
  isHistoricalSync,
@@ -226,7 +220,6 @@ export async function retrieveCheckpointsFromRollup(
226
220
  * @param blobClient - The blob client client for fetching blob data.
227
221
  * @param logs - CheckpointProposed logs.
228
222
  * @param rollupConstants - The rollup constants (chainId, version, targetCommitteeSize).
229
- * @param contractAddresses - The contract addresses (governanceProposerAddress, slashFactoryAddress, slashingProposerAddress).
230
223
  * @param instrumentation - The archiver instrumentation instance.
231
224
  * @param logger - The logger instance.
232
225
  * @param isHistoricalSync - Whether this is a historical sync.
@@ -239,11 +232,6 @@ async function processCheckpointProposedLogs(
239
232
  blobClient: BlobClientInterface,
240
233
  logs: CheckpointProposedLog[],
241
234
  { chainId, version, targetCommitteeSize }: { chainId: Fr; version: Fr; targetCommitteeSize: number },
242
- contractAddresses: {
243
- governanceProposerAddress: EthAddress;
244
- slashFactoryAddress?: EthAddress;
245
- slashingProposerAddress: EthAddress;
246
- },
247
235
  instrumentation: ArchiverInstrumentation,
248
236
  logger: Logger,
249
237
  isHistoricalSync: boolean,
@@ -255,7 +243,7 @@ async function processCheckpointProposedLogs(
255
243
  targetCommitteeSize,
256
244
  instrumentation,
257
245
  logger,
258
- { ...contractAddresses, rollupAddress: EthAddress.fromString(rollup.address) },
246
+ EthAddress.fromString(rollup.address),
259
247
  );
260
248
 
261
249
  await asyncPool(10, logs, async log => {
@@ -266,10 +254,9 @@ async function processCheckpointProposedLogs(
266
254
 
267
255
  // The value from the event and contract will match only if the checkpoint is in the chain.
268
256
  if (archive.equals(archiveFromChain)) {
269
- // Build expected hashes object (fields may be undefined for backwards compatibility with older events)
270
257
  const expectedHashes = {
271
- attestationsHash: log.args.attestationsHash?.toString(),
272
- payloadDigest: log.args.payloadDigest?.toString(),
258
+ attestationsHash: log.args.attestationsHash.toString() as Hex,
259
+ payloadDigest: log.args.payloadDigest.toString() as Hex,
273
260
  };
274
261
 
275
262
  const checkpoint = await calldataRetriever.getCheckpointFromRollupTx(
@@ -278,6 +265,9 @@ async function processCheckpointProposedLogs(
278
265
  checkpointNumber,
279
266
  expectedHashes,
280
267
  );
268
+ const { timestamp, parentBeaconBlockRoot } = await getL1Block(publicClient, log.l1BlockNumber);
269
+ const l1 = new L1PublishedData(log.l1BlockNumber, timestamp, log.l1BlockHash.toString());
270
+
281
271
  const checkpointBlobData = await getCheckpointBlobDataFromBlobs(
282
272
  blobClient,
283
273
  checkpoint.blockHash,
@@ -285,12 +275,8 @@ async function processCheckpointProposedLogs(
285
275
  checkpointNumber,
286
276
  logger,
287
277
  isHistoricalSync,
288
- );
289
-
290
- const l1 = new L1PublishedData(
291
- log.l1BlockNumber,
292
- await getL1BlockTime(publicClient, log.l1BlockNumber),
293
- log.l1BlockHash.toString(),
278
+ parentBeaconBlockRoot,
279
+ timestamp,
294
280
  );
295
281
 
296
282
  retrievedCheckpoints.push({ ...checkpoint, checkpointBlobData, l1, chainId, version });
@@ -311,9 +297,12 @@ async function processCheckpointProposedLogs(
311
297
  return retrievedCheckpoints;
312
298
  }
313
299
 
314
- export async function getL1BlockTime(publicClient: ViemPublicClient, blockNumber: bigint): Promise<bigint> {
300
+ export async function getL1Block(
301
+ publicClient: ViemPublicClient,
302
+ blockNumber: bigint,
303
+ ): Promise<{ timestamp: bigint; parentBeaconBlockRoot: string | undefined }> {
315
304
  const block = await publicClient.getBlock({ blockNumber, includeTransactions: false });
316
- return block.timestamp;
305
+ return { timestamp: block.timestamp, parentBeaconBlockRoot: block.parentBeaconBlockRoot };
317
306
  }
318
307
 
319
308
  export async function getCheckpointBlobDataFromBlobs(
@@ -323,8 +312,14 @@ export async function getCheckpointBlobDataFromBlobs(
323
312
  checkpointNumber: CheckpointNumber,
324
313
  logger: Logger,
325
314
  isHistoricalSync: boolean,
315
+ parentBeaconBlockRoot?: string,
316
+ l1BlockTimestamp?: bigint,
326
317
  ): Promise<CheckpointBlobData> {
327
- const blobBodies = await blobClient.getBlobSidecar(blockHash, blobHashes, { isHistoricalSync });
318
+ const blobBodies = await blobClient.getBlobSidecar(blockHash, blobHashes, {
319
+ isHistoricalSync,
320
+ parentBeaconBlockRoot,
321
+ l1BlockTimestamp,
322
+ });
328
323
  if (blobBodies.length === 0) {
329
324
  throw new NoBlobBodiesFoundError(checkpointNumber);
330
325
  }
@@ -87,17 +87,17 @@ export async function verifyProxyImplementation(
87
87
  /**
88
88
  * Attempts to decode transaction as a Spire Proposer Multicall.
89
89
  * Spire Proposer is a proxy contract that wraps multiple calls.
90
- * Returns the target address and calldata of the wrapped call if validation succeeds and there is a single call.
90
+ * Returns all wrapped calls if validation succeeds (caller handles hash matching to find the propose call).
91
91
  * @param tx - The transaction to decode
92
92
  * @param publicClient - The viem public client for proxy verification
93
93
  * @param logger - Logger instance
94
- * @returns Object with 'to' and 'data' of the wrapped call, or undefined if validation fails
94
+ * @returns Array of wrapped calls with 'to' and 'data', or undefined if not a valid Spire Proposer tx
95
95
  */
96
- export async function getCallFromSpireProposer(
96
+ export async function getCallsFromSpireProposer(
97
97
  tx: Transaction,
98
98
  publicClient: { getStorageAt: (params: { address: Hex; slot: Hex }) => Promise<Hex | undefined> },
99
99
  logger: Logger,
100
- ): Promise<{ to: Hex; data: Hex } | undefined> {
100
+ ): Promise<{ to: Hex; data: Hex }[] | undefined> {
101
101
  const txHash = tx.hash;
102
102
 
103
103
  try {
@@ -141,17 +141,9 @@ export async function getCallFromSpireProposer(
141
141
 
142
142
  const [calls] = spireArgs;
143
143
 
144
- // Validate exactly ONE call (see ./README.md for rationale)
145
- if (calls.length !== 1) {
146
- logger.warn(`Spire Proposer multicall must contain exactly one call (got ${calls.length})`, { txHash });
147
- return undefined;
148
- }
149
-
150
- const call = calls[0];
151
-
152
- // Successfully extracted the single wrapped call
153
- logger.trace(`Decoded Spire Proposer with single call to ${call.target}`, { txHash });
154
- return { to: call.target, data: call.data };
144
+ // Return all wrapped calls (hash matching in the caller determines which is the propose call)
145
+ logger.trace(`Decoded Spire Proposer with ${calls.length} call(s)`, { txHash });
146
+ return calls.map(call => ({ to: call.target, data: call.data }));
155
147
  } catch (err) {
156
148
  // Any decoding error triggers fallback to trace
157
149
  logger.warn(`Failed to decode Spire Proposer: ${err}`, { txHash });
@@ -1,6 +1,9 @@
1
+ import type { SlotNumber } from '@aztec/foundation/branded-types';
1
2
  import { createLogger } from '@aztec/foundation/log';
2
3
  import type { L2Block } from '@aztec/stdlib/block';
3
4
  import type { CheckpointData } from '@aztec/stdlib/checkpoint';
5
+ import type { L1RollupConstants } from '@aztec/stdlib/epoch-helpers';
6
+ import { getTimestampForSlot } from '@aztec/stdlib/epoch-helpers';
4
7
  import {
5
8
  Attributes,
6
9
  type Gauge,
@@ -38,6 +41,8 @@ export class ArchiverInstrumentation {
38
41
 
39
42
  private blockProposalTxTargetCount: UpDownCounter;
40
43
 
44
+ private checkpointL1InclusionDelay: Histogram;
45
+
41
46
  private log = createLogger('archiver:instrumentation');
42
47
 
43
48
  private constructor(
@@ -85,6 +90,8 @@ export class ArchiverInstrumentation {
85
90
  },
86
91
  );
87
92
 
93
+ this.checkpointL1InclusionDelay = meter.createHistogram(Metrics.ARCHIVER_CHECKPOINT_L1_INCLUSION_DELAY);
94
+
88
95
  this.dbMetrics = new LmdbMetrics(
89
96
  meter,
90
97
  {
@@ -161,4 +168,17 @@ export class ArchiverInstrumentation {
161
168
  [Attributes.L1_BLOCK_PROPOSAL_USED_TRACE]: usedTrace,
162
169
  });
163
170
  }
171
+
172
+ /**
173
+ * Records L1 inclusion timing for a checkpoint observed on L1 (seconds into the L2 slot).
174
+ */
175
+ public processCheckpointL1Timing(data: {
176
+ slotNumber: SlotNumber;
177
+ l1Timestamp: bigint;
178
+ l1Constants: Pick<L1RollupConstants, 'l1GenesisTime' | 'slotDuration'>;
179
+ }): void {
180
+ const slotStartTs = getTimestampForSlot(data.slotNumber, data.l1Constants);
181
+ const inclusionDelaySeconds = Number(data.l1Timestamp - slotStartTs);
182
+ this.checkpointL1InclusionDelay.record(inclusionDelaySeconds);
183
+ }
164
184
  }
@@ -1,7 +1,6 @@
1
1
  import type { BlobClientInterface } from '@aztec/blob-client/client';
2
2
  import { EpochCache } from '@aztec/epoch-cache';
3
3
  import { InboxContract, RollupContract } from '@aztec/ethereum/contracts';
4
- import type { L1ContractAddresses } from '@aztec/ethereum/l1-contract-addresses';
5
4
  import type { L1BlockId } from '@aztec/ethereum/l1-types';
6
5
  import type { ViemPublicClient, ViemPublicDebugClient } from '@aztec/ethereum/types';
7
6
  import { maxBigint } from '@aztec/foundation/bigint';
@@ -9,7 +8,6 @@ import { BlockNumber, CheckpointNumber, EpochNumber } from '@aztec/foundation/br
9
8
  import { Buffer32 } from '@aztec/foundation/buffer';
10
9
  import { pick } from '@aztec/foundation/collection';
11
10
  import { Fr } from '@aztec/foundation/curves/bn254';
12
- import { EthAddress } from '@aztec/foundation/eth-address';
13
11
  import { type Logger, createLogger } from '@aztec/foundation/log';
14
12
  import { count } from '@aztec/foundation/string';
15
13
  import { DateProvider, Timer, elapsed } from '@aztec/foundation/timer';
@@ -61,10 +59,6 @@ export class ArchiverL1Synchronizer implements Traceable {
61
59
  private readonly debugClient: ViemPublicDebugClient,
62
60
  private readonly rollup: RollupContract,
63
61
  private readonly inbox: InboxContract,
64
- private readonly l1Addresses: Pick<
65
- L1ContractAddresses,
66
- 'registryAddress' | 'governanceProposerAddress' | 'slashFactoryAddress'
67
- > & { slashingProposerAddress: EthAddress },
68
62
  private readonly store: KVArchiverDataStore,
69
63
  private config: {
70
64
  batchSize: number;
@@ -708,7 +702,6 @@ export class ArchiverL1Synchronizer implements Traceable {
708
702
  this.blobClient,
709
703
  searchStartBlock, // TODO(palla/reorg): If the L2 reorg was due to an L1 reorg, we need to start search earlier
710
704
  searchEndBlock,
711
- this.l1Addresses,
712
705
  this.instrumentation,
713
706
  this.log,
714
707
  !initialSyncComplete, // isHistoricalSync
@@ -803,6 +796,14 @@ export class ArchiverL1Synchronizer implements Traceable {
803
796
  );
804
797
  }
805
798
 
799
+ for (const published of validCheckpoints) {
800
+ this.instrumentation.processCheckpointL1Timing({
801
+ slotNumber: published.checkpoint.header.slotNumber,
802
+ l1Timestamp: published.l1.timestamp,
803
+ l1Constants: this.l1Constants,
804
+ });
805
+ }
806
+
806
807
  try {
807
808
  const updatedValidationResult =
808
809
  rollupStatus.validationResult === initialValidationResult ? undefined : rollupStatus.validationResult;
@@ -14,6 +14,7 @@ import { CommitteeAttestation, CommitteeAttestationsAndSigners, L2Block } from '
14
14
  import { Checkpoint } from '@aztec/stdlib/checkpoint';
15
15
  import { getSlotAtTimestamp } from '@aztec/stdlib/epoch-helpers';
16
16
  import { InboxLeaf } from '@aztec/stdlib/messaging';
17
+ import { ConsensusPayload, SignatureDomainSeparator } from '@aztec/stdlib/p2p';
17
18
  import {
18
19
  makeAndSignCommitteeAttestationsAndSigners,
19
20
  makeCheckpointAttestationFromCheckpoint,
@@ -22,7 +23,16 @@ import {
22
23
  import { AppendOnlyTreeSnapshot } from '@aztec/stdlib/trees';
23
24
 
24
25
  import { type MockProxy, mock } from 'jest-mock-extended';
25
- import { type FormattedBlock, type Transaction, encodeFunctionData, multicall3Abi, toHex } from 'viem';
26
+ import {
27
+ type AbiParameter,
28
+ type FormattedBlock,
29
+ type Transaction,
30
+ encodeAbiParameters,
31
+ encodeFunctionData,
32
+ keccak256,
33
+ multicall3Abi,
34
+ toHex,
35
+ } from 'viem';
26
36
 
27
37
  import { updateRollingHash } from '../structs/inbox_message.js';
28
38
 
@@ -87,6 +97,10 @@ type CheckpointData = {
87
97
  blobHashes: `0x${string}`[];
88
98
  blobs: Blob[];
89
99
  signers: Secp256k1Signer[];
100
+ /** Hash of the packed attestations, matching what the L1 event emits. */
101
+ attestationsHash: Buffer32;
102
+ /** Payload digest, matching what the L1 event emits. */
103
+ payloadDigest: Buffer32;
90
104
  /** If true, archiveAt will ignore it */
91
105
  pruned?: boolean;
92
106
  };
@@ -194,8 +208,8 @@ export class FakeL1State {
194
208
  // Store the messages internally so they match the checkpoint's inHash
195
209
  this.addMessages(checkpointNumber, messagesL1BlockNumber, messages);
196
210
 
197
- // Create the transaction and blobs
198
- const tx = await this.makeRollupTx(checkpoint, signers);
211
+ // Create the transaction, blobs, and event hashes
212
+ const { tx, attestationsHash, payloadDigest } = await this.makeRollupTx(checkpoint, signers);
199
213
  const blobHashes = await this.makeVersionedBlobHashes(checkpoint);
200
214
  const blobs = await this.makeBlobsFromCheckpoint(checkpoint);
201
215
 
@@ -208,6 +222,8 @@ export class FakeL1State {
208
222
  blobHashes,
209
223
  blobs,
210
224
  signers,
225
+ attestationsHash,
226
+ payloadDigest,
211
227
  });
212
228
 
213
229
  // Update last archive for auto-chaining
@@ -510,10 +526,8 @@ export class FakeL1State {
510
526
  checkpointNumber: cpData.checkpointNumber,
511
527
  archive: cpData.checkpoint.archive.root,
512
528
  versionedBlobHashes: cpData.blobHashes.map(h => Buffer.from(h.slice(2), 'hex')),
513
- // These are intentionally undefined to skip hash validation in the archiver
514
- // (validation is skipped when these fields are falsy)
515
- payloadDigest: undefined,
516
- attestationsHash: undefined,
529
+ attestationsHash: cpData.attestationsHash,
530
+ payloadDigest: cpData.payloadDigest,
517
531
  },
518
532
  }));
519
533
  }
@@ -539,7 +553,10 @@ export class FakeL1State {
539
553
  }));
540
554
  }
541
555
 
542
- private async makeRollupTx(checkpoint: Checkpoint, signers: Secp256k1Signer[]): Promise<Transaction> {
556
+ private async makeRollupTx(
557
+ checkpoint: Checkpoint,
558
+ signers: Secp256k1Signer[],
559
+ ): Promise<{ tx: Transaction; attestationsHash: Buffer32; payloadDigest: Buffer32 }> {
543
560
  const attestations = signers
544
561
  .map(signer => makeCheckpointAttestationFromCheckpoint(checkpoint, signer))
545
562
  .map(attestation => CommitteeAttestation.fromSignature(attestation.signature))
@@ -557,6 +574,8 @@ export class FakeL1State {
557
574
  signers[0],
558
575
  );
559
576
 
577
+ const packedAttestations = attestationsAndSigners.getPackedAttestations();
578
+
560
579
  const rollupInput = encodeFunctionData({
561
580
  abi: RollupAbi,
562
581
  functionName: 'propose',
@@ -566,7 +585,7 @@ export class FakeL1State {
566
585
  archive,
567
586
  oracleInput: { feeAssetPriceModifier: 0n },
568
587
  },
569
- attestationsAndSigners.getPackedAttestations(),
588
+ packedAttestations,
570
589
  attestationsAndSigners.getSigners().map(signer => signer.toString()),
571
590
  attestationsAndSignersSignature.toViemSignature(),
572
591
  blobInput,
@@ -587,12 +606,43 @@ export class FakeL1State {
587
606
  ],
588
607
  });
589
608
 
590
- return {
609
+ // Compute attestationsHash (same logic as CalldataRetriever)
610
+ const attestationsHash = Buffer32.fromString(
611
+ keccak256(encodeAbiParameters([this.getCommitteeAttestationsStructDef()], [packedAttestations])),
612
+ );
613
+
614
+ // Compute payloadDigest (same logic as CalldataRetriever)
615
+ const consensusPayload = ConsensusPayload.fromCheckpoint(checkpoint);
616
+ const payloadToSign = consensusPayload.getPayloadToSign(SignatureDomainSeparator.checkpointAttestation);
617
+ const payloadDigest = Buffer32.fromString(keccak256(payloadToSign));
618
+
619
+ const tx = {
591
620
  input: multiCallInput,
592
621
  hash: archive,
593
622
  blockHash: archive,
594
623
  to: MULTI_CALL_3_ADDRESS as `0x${string}`,
595
624
  } as Transaction<bigint, number>;
625
+
626
+ return { tx, attestationsHash, payloadDigest };
627
+ }
628
+
629
+ /** Extracts the CommitteeAttestations struct definition from RollupAbi for hash computation. */
630
+ private getCommitteeAttestationsStructDef(): AbiParameter {
631
+ const proposeFunction = RollupAbi.find(item => item.type === 'function' && item.name === 'propose') as
632
+ | { type: 'function'; name: string; inputs: readonly AbiParameter[] }
633
+ | undefined;
634
+
635
+ if (!proposeFunction) {
636
+ throw new Error('propose function not found in RollupAbi');
637
+ }
638
+
639
+ const attestationsParam = proposeFunction.inputs.find(param => param.name === '_attestations');
640
+ if (!attestationsParam) {
641
+ throw new Error('_attestations parameter not found in propose function');
642
+ }
643
+
644
+ const tupleParam = attestationsParam as unknown as { type: 'tuple'; components?: readonly AbiParameter[] };
645
+ return { type: 'tuple', components: tupleParam.components || [] } as AbiParameter;
596
646
  }
597
647
 
598
648
  private async makeVersionedBlobHashes(checkpoint: Checkpoint): Promise<`0x${string}`[]> {