@aztec/archiver 2.1.0-rc.9 → 3.0.0-devnet.2

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 (38) hide show
  1. package/README.md +4 -4
  2. package/dest/archiver/archiver.d.ts +12 -0
  3. package/dest/archiver/archiver.d.ts.map +1 -1
  4. package/dest/archiver/archiver.js +35 -4
  5. package/dest/archiver/archiver_store.d.ts +20 -0
  6. package/dest/archiver/archiver_store.d.ts.map +1 -1
  7. package/dest/archiver/archiver_store_test_suite.d.ts.map +1 -1
  8. package/dest/archiver/archiver_store_test_suite.js +93 -3
  9. package/dest/archiver/config.js +1 -1
  10. package/dest/archiver/data_retrieval.d.ts +6 -5
  11. package/dest/archiver/data_retrieval.d.ts.map +1 -1
  12. package/dest/archiver/data_retrieval.js +30 -22
  13. package/dest/archiver/instrumentation.d.ts.map +1 -1
  14. package/dest/archiver/instrumentation.js +3 -0
  15. package/dest/archiver/kv_archiver_store/block_store.d.ts +26 -1
  16. package/dest/archiver/kv_archiver_store/block_store.d.ts.map +1 -1
  17. package/dest/archiver/kv_archiver_store/block_store.js +66 -4
  18. package/dest/archiver/kv_archiver_store/kv_archiver_store.d.ts +5 -1
  19. package/dest/archiver/kv_archiver_store/kv_archiver_store.d.ts.map +1 -1
  20. package/dest/archiver/kv_archiver_store/kv_archiver_store.js +13 -0
  21. package/dest/archiver/validation.d.ts.map +1 -1
  22. package/dest/archiver/validation.js +37 -42
  23. package/dest/factory.js +1 -1
  24. package/dest/test/mock_l2_block_source.d.ts +8 -1
  25. package/dest/test/mock_l2_block_source.d.ts.map +1 -1
  26. package/dest/test/mock_l2_block_source.js +56 -3
  27. package/package.json +14 -14
  28. package/src/archiver/archiver.ts +37 -3
  29. package/src/archiver/archiver_store.ts +24 -0
  30. package/src/archiver/archiver_store_test_suite.ts +104 -3
  31. package/src/archiver/config.ts +1 -1
  32. package/src/archiver/data_retrieval.ts +38 -26
  33. package/src/archiver/instrumentation.ts +3 -0
  34. package/src/archiver/kv_archiver_store/block_store.ts +80 -3
  35. package/src/archiver/kv_archiver_store/kv_archiver_store.ts +17 -1
  36. package/src/archiver/validation.ts +38 -45
  37. package/src/factory.ts +1 -1
  38. package/src/test/mock_l2_block_source.ts +61 -4
@@ -61,6 +61,18 @@ export interface ArchiverDataStore {
61
61
  */
62
62
  getPublishedBlock(number: number): Promise<PublishedL2Block | undefined>;
63
63
 
64
+ /**
65
+ * Returns the block for the given hash, or undefined if not exists.
66
+ * @param blockHash - The block hash to return.
67
+ */
68
+ getPublishedBlockByHash(blockHash: Fr): Promise<PublishedL2Block | undefined>;
69
+
70
+ /**
71
+ * Returns the block for the given archive root, or undefined if not exists.
72
+ * @param archive - The archive root to return.
73
+ */
74
+ getPublishedBlockByArchive(archive: Fr): Promise<PublishedL2Block | undefined>;
75
+
64
76
  /**
65
77
  * Gets up to `limit` amount of published L2 blocks starting from `from`.
66
78
  * @param from - Number of the first block to return (inclusive).
@@ -77,6 +89,18 @@ export interface ArchiverDataStore {
77
89
  */
78
90
  getBlockHeaders(from: number, limit: number): Promise<BlockHeader[]>;
79
91
 
92
+ /**
93
+ * Returns the block header for the given hash, or undefined if not exists.
94
+ * @param blockHash - The block hash to return.
95
+ */
96
+ getBlockHeaderByHash(blockHash: Fr): Promise<BlockHeader | undefined>;
97
+
98
+ /**
99
+ * Returns the block header for the given archive root, or undefined if not exists.
100
+ * @param archive - The archive root to return.
101
+ */
102
+ getBlockHeaderByArchive(archive: Fr): Promise<BlockHeader | undefined>;
103
+
80
104
  /**
81
105
  * Gets a tx effect.
82
106
  * @param txHash - The hash of the tx corresponding to the tx effect.
@@ -2,7 +2,6 @@ import {
2
2
  INITIAL_L2_BLOCK_NUM,
3
3
  NUMBER_OF_L1_L2_MESSAGES_PER_ROLLUP,
4
4
  PRIVATE_LOG_SIZE_IN_FIELDS,
5
- PUBLIC_LOG_SIZE_IN_FIELDS,
6
5
  } from '@aztec/constants';
7
6
  import { makeTuple } from '@aztec/foundation/array';
8
7
  import { Buffer16, Buffer32 } from '@aztec/foundation/buffer';
@@ -143,6 +142,28 @@ export function describeArchiverDataStore(
143
142
  await store.addBlocks(blocks);
144
143
  await expect(store.unwindBlocks(5, 1)).rejects.toThrow(/can only unwind blocks from the tip/i);
145
144
  });
145
+
146
+ it('unwound blocks and headers cannot be retrieved by hash or archive', async () => {
147
+ await store.addBlocks(blocks);
148
+ const lastBlock = blocks[blocks.length - 1];
149
+ const blockHash = await lastBlock.block.hash();
150
+ const archive = lastBlock.block.archive.root;
151
+
152
+ // Verify block and header exist before unwinding
153
+ expect(await store.getPublishedBlockByHash(blockHash)).toBeDefined();
154
+ expect(await store.getPublishedBlockByArchive(archive)).toBeDefined();
155
+ expect(await store.getBlockHeaderByHash(blockHash)).toBeDefined();
156
+ expect(await store.getBlockHeaderByArchive(archive)).toBeDefined();
157
+
158
+ // Unwind the block
159
+ await store.unwindBlocks(lastBlock.block.number, 1);
160
+
161
+ // Verify neither block nor header can be retrieved after unwinding
162
+ expect(await store.getPublishedBlockByHash(blockHash)).toBeUndefined();
163
+ expect(await store.getPublishedBlockByArchive(archive)).toBeUndefined();
164
+ expect(await store.getBlockHeaderByHash(blockHash)).toBeUndefined();
165
+ expect(await store.getBlockHeaderByArchive(archive)).toBeUndefined();
166
+ });
146
167
  });
147
168
 
148
169
  describe('getBlocks', () => {
@@ -180,6 +201,86 @@ export function describeArchiverDataStore(
180
201
  });
181
202
  });
182
203
 
204
+ describe('getPublishedBlockByHash', () => {
205
+ beforeEach(async () => {
206
+ await store.addBlocks(blocks);
207
+ });
208
+
209
+ it('retrieves a block by its hash', async () => {
210
+ const expectedBlock = blocks[5];
211
+ const blockHash = await expectedBlock.block.hash();
212
+ const retrievedBlock = await store.getPublishedBlockByHash(blockHash);
213
+
214
+ expect(retrievedBlock).toBeDefined();
215
+ expectBlocksEqual([retrievedBlock!], [expectedBlock]);
216
+ });
217
+
218
+ it('returns undefined for non-existent block hash', async () => {
219
+ const nonExistentHash = Fr.random();
220
+ await expect(store.getPublishedBlockByHash(nonExistentHash)).resolves.toBeUndefined();
221
+ });
222
+ });
223
+
224
+ describe('getPublishedBlockByArchive', () => {
225
+ beforeEach(async () => {
226
+ await store.addBlocks(blocks);
227
+ });
228
+
229
+ it('retrieves a block by its archive root', async () => {
230
+ const expectedBlock = blocks[3];
231
+ const archive = expectedBlock.block.archive.root;
232
+ const retrievedBlock = await store.getPublishedBlockByArchive(archive);
233
+
234
+ expect(retrievedBlock).toBeDefined();
235
+ expectBlocksEqual([retrievedBlock!], [expectedBlock]);
236
+ });
237
+
238
+ it('returns undefined for non-existent archive root', async () => {
239
+ const nonExistentArchive = Fr.random();
240
+ await expect(store.getPublishedBlockByArchive(nonExistentArchive)).resolves.toBeUndefined();
241
+ });
242
+ });
243
+
244
+ describe('getBlockHeaderByHash', () => {
245
+ beforeEach(async () => {
246
+ await store.addBlocks(blocks);
247
+ });
248
+
249
+ it('retrieves a block header by its hash', async () => {
250
+ const expectedBlock = blocks[7];
251
+ const blockHash = await expectedBlock.block.hash();
252
+ const retrievedHeader = await store.getBlockHeaderByHash(blockHash);
253
+
254
+ expect(retrievedHeader).toBeDefined();
255
+ expect(retrievedHeader!.equals(expectedBlock.block.getBlockHeader())).toBe(true);
256
+ });
257
+
258
+ it('returns undefined for non-existent block hash', async () => {
259
+ const nonExistentHash = Fr.random();
260
+ await expect(store.getBlockHeaderByHash(nonExistentHash)).resolves.toBeUndefined();
261
+ });
262
+ });
263
+
264
+ describe('getBlockHeaderByArchive', () => {
265
+ beforeEach(async () => {
266
+ await store.addBlocks(blocks);
267
+ });
268
+
269
+ it('retrieves a block header by its archive root', async () => {
270
+ const expectedBlock = blocks[2];
271
+ const archive = expectedBlock.block.archive.root;
272
+ const retrievedHeader = await store.getBlockHeaderByArchive(archive);
273
+
274
+ expect(retrievedHeader).toBeDefined();
275
+ expect(retrievedHeader!.equals(expectedBlock.block.getBlockHeader())).toBe(true);
276
+ });
277
+
278
+ it('returns undefined for non-existent archive root', async () => {
279
+ const nonExistentArchive = Fr.random();
280
+ await expect(store.getBlockHeaderByArchive(nonExistentArchive)).resolves.toBeUndefined();
281
+ });
282
+ });
283
+
183
284
  describe('getSyncedL2BlockNumber', () => {
184
285
  it('returns the block number before INITIAL_L2_BLOCK_NUM if no blocks have been added', async () => {
185
286
  await expect(store.getSynchedL2BlockNumber()).resolves.toEqual(INITIAL_L2_BLOCK_NUM - 1);
@@ -737,8 +838,8 @@ export function describeArchiverDataStore(
737
838
  const makePublicLog = (tag: Fr) =>
738
839
  PublicLog.from({
739
840
  contractAddress: AztecAddress.fromNumber(1),
740
- fields: makeTuple(PUBLIC_LOG_SIZE_IN_FIELDS, i => (!i ? tag : new Fr(tag.toNumber() + i))),
741
- emittedLength: PUBLIC_LOG_SIZE_IN_FIELDS,
841
+ // Arbitrary length
842
+ fields: new Array(10).fill(null).map((_, i) => (!i ? tag : new Fr(tag.toNumber() + i))),
742
843
  });
743
844
 
744
845
  const mockPrivateLogs = (blockNumber: number, txIndex: number) => {
@@ -44,7 +44,7 @@ export const archiverConfigMappings: ConfigMappingsType<ArchiverConfig> = {
44
44
  archiverStoreMapSizeKb: {
45
45
  env: 'ARCHIVER_STORE_MAP_SIZE_KB',
46
46
  parseEnv: (val: string | undefined) => (val ? +val : undefined),
47
- description: 'The maximum possible size of the archiver DB in KB. Overwrites the general dataStoreMapSizeKB.',
47
+ description: 'The maximum possible size of the archiver DB in KB. Overwrites the general dataStoreMapSizeKb.',
48
48
  },
49
49
  skipValidateBlockAttestations: {
50
50
  description: 'Whether to skip validating block attestations (use only for testing).',
@@ -1,4 +1,4 @@
1
- import { Blob, BlobDeserializationError } from '@aztec/blob-lib';
1
+ import { BlobDeserializationError, SpongeBlob, getBlobFieldsInCheckpoint } from '@aztec/blob-lib';
2
2
  import type { BlobSinkClientInterface } from '@aztec/blob-sink/client';
3
3
  import type {
4
4
  EpochProofPublicInputArgs,
@@ -15,10 +15,11 @@ import type { ViemSignature } from '@aztec/foundation/eth-signature';
15
15
  import { Fr } from '@aztec/foundation/fields';
16
16
  import { type Logger, createLogger } from '@aztec/foundation/log';
17
17
  import { type InboxAbi, RollupAbi } from '@aztec/l1-artifacts';
18
- import { Body, CommitteeAttestation, L2Block, PublishedL2Block } from '@aztec/stdlib/block';
18
+ import { Body, CommitteeAttestation, L2Block, L2BlockHeader, PublishedL2Block } from '@aztec/stdlib/block';
19
19
  import { Proof } from '@aztec/stdlib/proofs';
20
+ import { CheckpointHeader } from '@aztec/stdlib/rollup';
20
21
  import { AppendOnlyTreeSnapshot } from '@aztec/stdlib/trees';
21
- import { BlockHeader, GlobalVariables, ProposedBlockHeader, StateReference } from '@aztec/stdlib/tx';
22
+ import { GlobalVariables, StateReference } from '@aztec/stdlib/tx';
22
23
 
23
24
  import {
24
25
  type GetContractEventsReturnType,
@@ -39,21 +40,21 @@ export type RetrievedL2Block = {
39
40
  l2BlockNumber: number;
40
41
  archiveRoot: Fr;
41
42
  stateReference: StateReference;
42
- header: ProposedBlockHeader;
43
- body: Body;
43
+ header: CheckpointHeader;
44
+ blobFields: Fr[];
44
45
  l1: L1PublishedData;
45
46
  chainId: Fr;
46
47
  version: Fr;
47
48
  attestations: CommitteeAttestation[];
48
49
  };
49
50
 
50
- export function retrievedBlockToPublishedL2Block(retrievedBlock: RetrievedL2Block): PublishedL2Block {
51
+ export async function retrievedBlockToPublishedL2Block(retrievedBlock: RetrievedL2Block): Promise<PublishedL2Block> {
51
52
  const {
52
53
  l2BlockNumber,
53
54
  archiveRoot,
54
55
  stateReference,
55
- header: proposedHeader,
56
- body,
56
+ header: checkpointHeader,
57
+ blobFields,
57
58
  l1,
58
59
  chainId,
59
60
  version,
@@ -69,20 +70,32 @@ export function retrievedBlockToPublishedL2Block(retrievedBlock: RetrievedL2Bloc
69
70
  chainId,
70
71
  version,
71
72
  blockNumber: l2BlockNumber,
72
- slotNumber: proposedHeader.slotNumber,
73
- timestamp: proposedHeader.timestamp,
74
- coinbase: proposedHeader.coinbase,
75
- feeRecipient: proposedHeader.feeRecipient,
76
- gasFees: proposedHeader.gasFees,
73
+ slotNumber: checkpointHeader.slotNumber,
74
+ timestamp: checkpointHeader.timestamp,
75
+ coinbase: checkpointHeader.coinbase,
76
+ feeRecipient: checkpointHeader.feeRecipient,
77
+ gasFees: checkpointHeader.gasFees,
77
78
  });
78
79
 
79
- const header = BlockHeader.from({
80
- lastArchive: new AppendOnlyTreeSnapshot(proposedHeader.lastArchiveRoot, l2BlockNumber),
81
- contentCommitment: proposedHeader.contentCommitment,
80
+ // TODO(#17027)
81
+ // This works when there's only one block in the checkpoint.
82
+ // If there's more than one block, we need to build the spongeBlob from the endSpongeBlob of the previous block.
83
+ const spongeBlob = await SpongeBlob.init(blobFields.length);
84
+ // Skip the first field which is the checkpoint prefix indicating the number of total blob fields in a checkpoint.
85
+ const blockBlobFields = blobFields.slice(1);
86
+ await spongeBlob.absorb(blockBlobFields);
87
+ const spongeBlobHash = await spongeBlob.squeeze();
88
+
89
+ const body = Body.fromBlobFields(blockBlobFields);
90
+
91
+ const header = L2BlockHeader.from({
92
+ lastArchive: new AppendOnlyTreeSnapshot(checkpointHeader.lastArchiveRoot, l2BlockNumber),
93
+ contentCommitment: checkpointHeader.contentCommitment,
82
94
  state: stateReference,
83
95
  globalVariables,
84
96
  totalFees: body.txEffects.reduce((accum, txEffect) => accum.add(txEffect.transactionFee), Fr.ZERO),
85
- totalManaUsed: proposedHeader.totalManaUsed,
97
+ totalManaUsed: checkpointHeader.totalManaUsed,
98
+ spongeBlobHash,
86
99
  });
87
100
 
88
101
  const block = new L2Block(archive, header, body);
@@ -337,17 +350,19 @@ async function getBlockFromRollupTx(
337
350
  targetCommitteeSize,
338
351
  });
339
352
 
340
- // TODO(md): why is the proposed block header different to the actual block header?
341
- // This is likely going to be a footgun
342
- const header = ProposedBlockHeader.fromViem(decodedArgs.header);
353
+ const header = CheckpointHeader.fromViem(decodedArgs.header);
343
354
  const blobBodies = await blobSinkClient.getBlobSidecar(blockHash, blobHashes);
344
355
  if (blobBodies.length === 0) {
345
356
  throw new NoBlobBodiesFoundError(l2BlockNumber);
346
357
  }
347
358
 
348
- let blockFields: Fr[];
359
+ let blobFields: Fr[];
349
360
  try {
350
- blockFields = Blob.toEncodedFields(blobBodies.map(b => b.blob));
361
+ // Get the fields that were actually added in the checkpoint. And check the encoding of the fields.
362
+ blobFields = getBlobFieldsInCheckpoint(
363
+ blobBodies.map(b => b.blob),
364
+ true /* checkEncoding */,
365
+ );
351
366
  } catch (err: any) {
352
367
  if (err instanceof BlobDeserializationError) {
353
368
  logger.fatal(err.message);
@@ -357,9 +372,6 @@ async function getBlockFromRollupTx(
357
372
  throw err;
358
373
  }
359
374
 
360
- // The blob source gives us blockFields, and we must construct the body from them:
361
- const body = Body.fromBlobFields(blockFields);
362
-
363
375
  const archiveRoot = new Fr(Buffer.from(hexToBytes(decodedArgs.archive)));
364
376
 
365
377
  const stateReference = StateReference.fromViem(decodedArgs.stateReference);
@@ -369,7 +381,7 @@ async function getBlockFromRollupTx(
369
381
  archiveRoot,
370
382
  stateReference,
371
383
  header,
372
- body,
384
+ blobFields,
373
385
  attestations,
374
386
  };
375
387
  }
@@ -152,6 +152,9 @@ export class ArchiverInstrumentation {
152
152
  }
153
153
 
154
154
  public processNewMessages(count: number, syncPerMessageMs: number) {
155
+ if (count === 0) {
156
+ return;
157
+ }
155
158
  this.syncMessageCount.add(count);
156
159
  this.syncDurationPerMessage.record(Math.ceil(syncPerMessageMs));
157
160
  }
@@ -14,7 +14,7 @@ import {
14
14
  PublishedL2Block,
15
15
  type ValidateBlockResult,
16
16
  } from '@aztec/stdlib/block';
17
- import { deserializeValidateBlockResult, serializeValidateBlockResult } from '@aztec/stdlib/block';
17
+ import { L2BlockHeader, deserializeValidateBlockResult, serializeValidateBlockResult } from '@aztec/stdlib/block';
18
18
  import { AppendOnlyTreeSnapshot } from '@aztec/stdlib/trees';
19
19
  import {
20
20
  BlockHeader,
@@ -66,6 +66,12 @@ export class BlockStore {
66
66
  /** Index mapping a contract's address (as a string) to its location in a block */
67
67
  #contractIndex: AztecAsyncMap<string, BlockIndexValue>;
68
68
 
69
+ /** Index mapping block hash to block number */
70
+ #blockHashIndex: AztecAsyncMap<string, number>;
71
+
72
+ /** Index mapping block archive to block number */
73
+ #blockArchiveIndex: AztecAsyncMap<string, number>;
74
+
69
75
  #log = createLogger('archiver:block_store');
70
76
 
71
77
  constructor(private db: AztecAsyncKVStore) {
@@ -73,6 +79,8 @@ export class BlockStore {
73
79
  this.#blockTxs = db.openMap('archiver_block_txs');
74
80
  this.#txEffects = db.openMap('archiver_tx_effects');
75
81
  this.#contractIndex = db.openMap('archiver_contract_index');
82
+ this.#blockHashIndex = db.openMap('archiver_block_hash_index');
83
+ this.#blockArchiveIndex = db.openMap('archiver_block_archive_index');
76
84
  this.#lastSynchedL1Block = db.openSingleton('archiver_last_synched_l1_block');
77
85
  this.#lastProvenL2Block = db.openSingleton('archiver_last_proven_l2_block');
78
86
  this.#pendingChainValidationStatus = db.openSingleton('archiver_pending_chain_validation_status');
@@ -132,6 +140,10 @@ export class BlockStore {
132
140
  blockHash.toString(),
133
141
  Buffer.concat(block.block.body.txEffects.map(tx => tx.txHash.toBuffer())),
134
142
  );
143
+
144
+ // Update indices for block hash and archive
145
+ await this.#blockHashIndex.set(blockHash.toString(), block.block.number);
146
+ await this.#blockArchiveIndex.set(block.block.archive.root.toString(), block.block.number);
135
147
  }
136
148
 
137
149
  await this.#lastSynchedL1Block.set(blocks[blocks.length - 1].l1.blockNumber);
@@ -170,6 +182,11 @@ export class BlockStore {
170
182
  await Promise.all(block.block.body.txEffects.map(tx => this.#txEffects.delete(tx.txHash.toString())));
171
183
  const blockHash = (await block.block.hash()).toString();
172
184
  await this.#blockTxs.delete(blockHash);
185
+
186
+ // Clean up indices
187
+ await this.#blockHashIndex.delete(blockHash);
188
+ await this.#blockArchiveIndex.delete(block.block.archive.root.toString());
189
+
173
190
  this.#log.debug(`Unwound block ${blockNumber} ${blockHash}`);
174
191
  }
175
192
 
@@ -205,6 +222,66 @@ export class BlockStore {
205
222
  return this.getBlockFromBlockStorage(blockNumber, blockStorage);
206
223
  }
207
224
 
225
+ /**
226
+ * Gets an L2 block by its hash.
227
+ * @param blockHash - The hash of the block to return.
228
+ * @returns The requested L2 block.
229
+ */
230
+ async getBlockByHash(blockHash: L2BlockHash): Promise<PublishedL2Block | undefined> {
231
+ const blockNumber = await this.#blockHashIndex.getAsync(blockHash.toString());
232
+ if (blockNumber === undefined) {
233
+ return undefined;
234
+ }
235
+ return this.getBlock(blockNumber);
236
+ }
237
+
238
+ /**
239
+ * Gets an L2 block by its archive root.
240
+ * @param archive - The archive root of the block to return.
241
+ * @returns The requested L2 block.
242
+ */
243
+ async getBlockByArchive(archive: Fr): Promise<PublishedL2Block | undefined> {
244
+ const blockNumber = await this.#blockArchiveIndex.getAsync(archive.toString());
245
+ if (blockNumber === undefined) {
246
+ return undefined;
247
+ }
248
+ return this.getBlock(blockNumber);
249
+ }
250
+
251
+ /**
252
+ * Gets a block header by its hash.
253
+ * @param blockHash - The hash of the block to return.
254
+ * @returns The requested block header.
255
+ */
256
+ async getBlockHeaderByHash(blockHash: L2BlockHash): Promise<BlockHeader | undefined> {
257
+ const blockNumber = await this.#blockHashIndex.getAsync(blockHash.toString());
258
+ if (blockNumber === undefined) {
259
+ return undefined;
260
+ }
261
+ const blockStorage = await this.#blocks.getAsync(blockNumber);
262
+ if (!blockStorage || !blockStorage.header) {
263
+ return undefined;
264
+ }
265
+ return L2BlockHeader.fromBuffer(blockStorage.header).toBlockHeader();
266
+ }
267
+
268
+ /**
269
+ * Gets a block header by its archive root.
270
+ * @param archive - The archive root of the block to return.
271
+ * @returns The requested block header.
272
+ */
273
+ async getBlockHeaderByArchive(archive: Fr): Promise<BlockHeader | undefined> {
274
+ const blockNumber = await this.#blockArchiveIndex.getAsync(archive.toString());
275
+ if (blockNumber === undefined) {
276
+ return undefined;
277
+ }
278
+ const blockStorage = await this.#blocks.getAsync(blockNumber);
279
+ if (!blockStorage || !blockStorage.header) {
280
+ return undefined;
281
+ }
282
+ return L2BlockHeader.fromBuffer(blockStorage.header).toBlockHeader();
283
+ }
284
+
208
285
  /**
209
286
  * Gets the headers for a sequence of L2 blocks.
210
287
  * @param start - Number of the first block to return (inclusive).
@@ -213,7 +290,7 @@ export class BlockStore {
213
290
  */
214
291
  async *getBlockHeaders(start: number, limit: number): AsyncIterableIterator<BlockHeader> {
215
292
  for await (const [blockNumber, blockStorage] of this.getBlockStorages(start, limit)) {
216
- const header = BlockHeader.fromBuffer(blockStorage.header);
293
+ const header = L2BlockHeader.fromBuffer(blockStorage.header).toBlockHeader();
217
294
  if (header.getBlockNumber() !== blockNumber) {
218
295
  throw new Error(
219
296
  `Block number mismatch when retrieving block header from archive (expected ${blockNumber} but got ${header.getBlockNumber()})`,
@@ -240,7 +317,7 @@ export class BlockStore {
240
317
  blockNumber: number,
241
318
  blockStorage: BlockStorage,
242
319
  ): Promise<PublishedL2Block | undefined> {
243
- const header = BlockHeader.fromBuffer(blockStorage.header);
320
+ const header = L2BlockHeader.fromBuffer(blockStorage.header);
244
321
  const archive = AppendOnlyTreeSnapshot.fromBuffer(blockStorage.archive);
245
322
  const blockHash = blockStorage.blockHash;
246
323
  const blockHashString = bufferToHex(blockHash);
@@ -5,7 +5,7 @@ import { createLogger } from '@aztec/foundation/log';
5
5
  import type { AztecAsyncKVStore, CustomRange, StoreSize } from '@aztec/kv-store';
6
6
  import { FunctionSelector } from '@aztec/stdlib/abi';
7
7
  import type { AztecAddress } from '@aztec/stdlib/aztec-address';
8
- import type { L2Block, ValidateBlockResult } from '@aztec/stdlib/block';
8
+ import { type L2Block, L2BlockHash, type ValidateBlockResult } from '@aztec/stdlib/block';
9
9
  import type {
10
10
  ContractClassPublic,
11
11
  ContractDataSource,
@@ -204,6 +204,14 @@ export class KVArchiverDataStore implements ArchiverDataStore, ContractDataSourc
204
204
  return this.#blockStore.getBlock(number);
205
205
  }
206
206
 
207
+ getPublishedBlockByHash(blockHash: Fr): Promise<PublishedL2Block | undefined> {
208
+ return this.#blockStore.getBlockByHash(L2BlockHash.fromField(blockHash));
209
+ }
210
+
211
+ getPublishedBlockByArchive(archive: Fr): Promise<PublishedL2Block | undefined> {
212
+ return this.#blockStore.getBlockByArchive(archive);
213
+ }
214
+
207
215
  /**
208
216
  * Gets up to `limit` amount of L2 blocks starting from `from`.
209
217
  *
@@ -226,6 +234,14 @@ export class KVArchiverDataStore implements ArchiverDataStore, ContractDataSourc
226
234
  return toArray(this.#blockStore.getBlockHeaders(start, limit));
227
235
  }
228
236
 
237
+ getBlockHeaderByHash(blockHash: Fr): Promise<BlockHeader | undefined> {
238
+ return this.#blockStore.getBlockHeaderByHash(L2BlockHash.fromField(blockHash));
239
+ }
240
+
241
+ getBlockHeaderByArchive(archive: Fr): Promise<BlockHeader | undefined> {
242
+ return this.#blockStore.getBlockHeaderByArchive(archive);
243
+ }
244
+
229
245
  /**
230
246
  * Gets a tx effect.
231
247
  * @param txHash - The hash of the tx corresponding to the tx effect.
@@ -3,8 +3,9 @@ import { compactArray } from '@aztec/foundation/collection';
3
3
  import type { Logger } from '@aztec/foundation/log';
4
4
  import {
5
5
  type PublishedL2Block,
6
+ type ValidateBlockNegativeResult,
6
7
  type ValidateBlockResult,
7
- getAttestationsFromPublishedL2Block,
8
+ getAttestationInfoFromPublishedL2Block,
8
9
  } from '@aztec/stdlib/block';
9
10
  import { type L1RollupConstants, getEpochAtSlot } from '@aztec/stdlib/epoch-helpers';
10
11
 
@@ -20,8 +21,8 @@ export async function validateBlockAttestations(
20
21
  constants: Pick<L1RollupConstants, 'epochDuration'>,
21
22
  logger?: Logger,
22
23
  ): Promise<ValidateBlockResult> {
23
- const attestations = getAttestationsFromPublishedL2Block(publishedBlock);
24
- const attestors = compactArray(attestations.map(a => a?.tryGetSender()));
24
+ const attestorInfos = getAttestationInfoFromPublishedL2Block(publishedBlock);
25
+ const attestors = compactArray(attestorInfos.map(info => ('address' in info ? info.address : undefined)));
25
26
  const { block } = publishedBlock;
26
27
  const blockHash = await block.hash().then(hash => hash.toString());
27
28
  const archiveRoot = block.archive.root.toString();
@@ -32,10 +33,7 @@ export async function validateBlockAttestations(
32
33
 
33
34
  logger?.debug(`Validating attestations for block ${block.number} at slot ${slot} in epoch ${epoch}`, {
34
35
  committee: (committee ?? []).map(member => member.toString()),
35
- recoveredAttestors: attestors.map(member => member.toString()),
36
- invalidAttestations: attestations
37
- .filter(a => a !== undefined && a.tryGetSender() === undefined)
38
- .map(a => a?.toInspect()),
36
+ recoveredAttestors: attestorInfos,
39
37
  postedAttestations: publishedBlock.attestations.map(a => (a.address.isZero() ? a.signature : a.address).toString()),
40
38
  ...logData,
41
39
  });
@@ -48,57 +46,52 @@ export async function validateBlockAttestations(
48
46
  const committeeSet = new Set(committee.map(member => member.toString()));
49
47
  const requiredAttestationCount = Math.floor((committee.length * 2) / 3) + 1;
50
48
 
51
- for (let i = 0; i < attestations.length; i++) {
52
- const attestation = attestations[i];
49
+ const failedValidationResult = <TReason extends ValidateBlockNegativeResult['reason']>(reason: TReason) => ({
50
+ valid: false as const,
51
+ reason,
52
+ block: publishedBlock.block.toBlockInfo(),
53
+ committee,
54
+ seed,
55
+ epoch,
56
+ attestors,
57
+ attestations: publishedBlock.attestations,
58
+ });
53
59
 
54
- // Skip empty signatures (undefined entries)
55
- if (attestation === undefined) {
56
- continue;
57
- }
60
+ for (let i = 0; i < attestorInfos.length; i++) {
61
+ const info = attestorInfos[i];
58
62
 
59
- const signer = attestation.tryGetSender()?.toString();
60
- if (signer !== undefined && committeeSet.has(signer)) {
61
- continue;
63
+ // Fail on invalid signatures (no address recovered)
64
+ if (info.status === 'invalid-signature' || info.status === 'empty') {
65
+ logger?.warn(`Attestation with empty or invalid signature at slot ${slot}`, {
66
+ committee,
67
+ invalidIndex: i,
68
+ ...logData,
69
+ });
70
+ return { ...failedValidationResult('invalid-attestation'), invalidIndex: i };
62
71
  }
63
72
 
64
- if (signer === undefined) {
65
- logger?.warn(`Attestation with invalid signature at slot ${slot}`, { committee, ...logData });
66
- } else {
67
- logger?.warn(`Attestation from non-committee member ${signer} at slot ${slot}`, { committee, ...logData });
73
+ // Check if the attestor is in the committee
74
+ if (info.status === 'recovered-from-signature' || info.status === 'provided-as-address') {
75
+ const signer = info.address.toString();
76
+ if (!committeeSet.has(signer)) {
77
+ logger?.warn(`Attestation from non-committee member ${signer} at slot ${slot}`, {
78
+ committee,
79
+ invalidIndex: i,
80
+ ...logData,
81
+ });
82
+ return { ...failedValidationResult('invalid-attestation'), invalidIndex: i };
83
+ }
68
84
  }
69
-
70
- const reason = 'invalid-attestation';
71
- return {
72
- valid: false,
73
- reason,
74
- invalidIndex: i,
75
- block: publishedBlock.block.toBlockInfo(),
76
- committee,
77
- seed,
78
- epoch,
79
- attestors,
80
- attestations: publishedBlock.attestations,
81
- };
82
85
  }
83
86
 
84
- const validAttestationCount = compactArray(attestations).length;
87
+ const validAttestationCount = attestorInfos.filter(info => info.status === 'recovered-from-signature').length;
85
88
  if (validAttestationCount < requiredAttestationCount) {
86
89
  logger?.warn(`Insufficient attestations for block at slot ${slot}`, {
87
90
  requiredAttestations: requiredAttestationCount,
88
91
  actualAttestations: validAttestationCount,
89
92
  ...logData,
90
93
  });
91
- const reason = 'insufficient-attestations';
92
- return {
93
- valid: false,
94
- reason,
95
- block: publishedBlock.block.toBlockInfo(),
96
- committee,
97
- seed,
98
- epoch,
99
- attestors,
100
- attestations: publishedBlock.attestations,
101
- };
94
+ return failedValidationResult('insufficient-attestations');
102
95
  }
103
96
 
104
97
  logger?.debug(`Block attestations validated successfully for block ${block.number} at slot ${slot}`, logData);
package/src/factory.ts CHANGED
@@ -20,7 +20,7 @@ export async function createArchiverStore(
20
20
  ) {
21
21
  const config = {
22
22
  ...userConfig,
23
- dataStoreMapSizeKB: userConfig.archiverStoreMapSizeKb ?? userConfig.dataStoreMapSizeKB,
23
+ dataStoreMapSizeKb: userConfig.archiverStoreMapSizeKb ?? userConfig.dataStoreMapSizeKb,
24
24
  };
25
25
  const store = await createStore(ARCHIVER_STORE_NAME, ARCHIVER_DB_VERSION, config, createLogger('archiver:lmdb'));
26
26
  return new KVArchiverDataStore(store, config.maxLogs);