@aztec/archiver 0.0.1-commit.993d240 → 0.0.1-commit.9a89641

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 (86) hide show
  1. package/dest/archiver.d.ts +44 -8
  2. package/dest/archiver.d.ts.map +1 -1
  3. package/dest/archiver.js +103 -35
  4. package/dest/config.d.ts +5 -4
  5. package/dest/config.d.ts.map +1 -1
  6. package/dest/config.js +28 -7
  7. package/dest/factory.d.ts +8 -1
  8. package/dest/factory.d.ts.map +1 -1
  9. package/dest/factory.js +41 -5
  10. package/dest/l1/calldata_retriever.d.ts +8 -1
  11. package/dest/l1/calldata_retriever.d.ts.map +1 -1
  12. package/dest/l1/calldata_retriever.js +7 -6
  13. package/dest/l1/data_retrieval.d.ts +7 -2
  14. package/dest/l1/data_retrieval.d.ts.map +1 -1
  15. package/dest/modules/contract_data_source_adapter.d.ts +2 -2
  16. package/dest/modules/contract_data_source_adapter.d.ts.map +1 -1
  17. package/dest/modules/contract_data_source_adapter.js +1 -9
  18. package/dest/modules/data_source_base.d.ts +5 -6
  19. package/dest/modules/data_source_base.d.ts.map +1 -1
  20. package/dest/modules/data_source_base.js +9 -38
  21. package/dest/modules/data_store_updater.d.ts +1 -1
  22. package/dest/modules/data_store_updater.d.ts.map +1 -1
  23. package/dest/modules/data_store_updater.js +4 -3
  24. package/dest/modules/instrumentation.d.ts +9 -1
  25. package/dest/modules/instrumentation.d.ts.map +1 -1
  26. package/dest/modules/instrumentation.js +23 -2
  27. package/dest/modules/l1_synchronizer.d.ts +3 -3
  28. package/dest/modules/l1_synchronizer.d.ts.map +1 -1
  29. package/dest/modules/l1_synchronizer.js +78 -41
  30. package/dest/modules/outbox_trees_resolver.d.ts +64 -0
  31. package/dest/modules/outbox_trees_resolver.d.ts.map +1 -0
  32. package/dest/modules/outbox_trees_resolver.js +184 -0
  33. package/dest/modules/validation.d.ts +29 -7
  34. package/dest/modules/validation.d.ts.map +1 -1
  35. package/dest/modules/validation.js +31 -13
  36. package/dest/store/block_store.d.ts +26 -22
  37. package/dest/store/block_store.d.ts.map +1 -1
  38. package/dest/store/block_store.js +105 -78
  39. package/dest/store/contract_class_store.d.ts +1 -1
  40. package/dest/store/contract_class_store.d.ts.map +1 -1
  41. package/dest/store/contract_class_store.js +19 -1
  42. package/dest/store/contract_instance_store.d.ts +1 -1
  43. package/dest/store/contract_instance_store.d.ts.map +1 -1
  44. package/dest/store/contract_instance_store.js +18 -1
  45. package/dest/store/log_store.d.ts +1 -1
  46. package/dest/store/log_store.d.ts.map +1 -1
  47. package/dest/store/log_store.js +3 -3
  48. package/dest/store/log_store_codec.d.ts +9 -1
  49. package/dest/store/log_store_codec.d.ts.map +1 -1
  50. package/dest/store/log_store_codec.js +9 -0
  51. package/dest/test/fake_l1_state.js +3 -3
  52. package/dest/test/mock_l1_to_l2_message_source.d.ts +1 -1
  53. package/dest/test/mock_l1_to_l2_message_source.d.ts.map +1 -1
  54. package/dest/test/mock_l1_to_l2_message_source.js +1 -2
  55. package/dest/test/mock_l2_block_source.d.ts +4 -11
  56. package/dest/test/mock_l2_block_source.d.ts.map +1 -1
  57. package/dest/test/mock_l2_block_source.js +15 -39
  58. package/dest/test/mock_structs.d.ts +1 -1
  59. package/dest/test/mock_structs.d.ts.map +1 -1
  60. package/dest/test/mock_structs.js +2 -2
  61. package/dest/test/noop_l1_archiver.d.ts +8 -3
  62. package/dest/test/noop_l1_archiver.d.ts.map +1 -1
  63. package/dest/test/noop_l1_archiver.js +12 -9
  64. package/package.json +13 -13
  65. package/src/archiver.ts +147 -42
  66. package/src/config.ts +34 -16
  67. package/src/factory.ts +48 -4
  68. package/src/l1/calldata_retriever.ts +13 -6
  69. package/src/l1/data_retrieval.ts +8 -1
  70. package/src/modules/contract_data_source_adapter.ts +1 -10
  71. package/src/modules/data_source_base.ts +17 -44
  72. package/src/modules/data_store_updater.ts +4 -3
  73. package/src/modules/instrumentation.ts +15 -2
  74. package/src/modules/l1_synchronizer.ts +120 -57
  75. package/src/modules/outbox_trees_resolver.ts +220 -0
  76. package/src/modules/validation.ts +71 -15
  77. package/src/store/block_store.ts +108 -101
  78. package/src/store/contract_class_store.ts +19 -1
  79. package/src/store/contract_instance_store.ts +18 -1
  80. package/src/store/log_store.ts +3 -2
  81. package/src/store/log_store_codec.ts +11 -0
  82. package/src/test/fake_l1_state.ts +3 -3
  83. package/src/test/mock_l1_to_l2_message_source.ts +0 -1
  84. package/src/test/mock_l2_block_source.ts +14 -53
  85. package/src/test/mock_structs.ts +5 -2
  86. package/src/test/noop_l1_archiver.ts +30 -11
@@ -1,16 +1,20 @@
1
1
  import type { EpochCache } from '@aztec/epoch-cache';
2
- import { EpochNumber } from '@aztec/foundation/branded-types';
2
+ import type { ViemCommitteeAttestations } from '@aztec/ethereum/contracts';
3
+ import { type CheckpointNumber, EpochNumber } from '@aztec/foundation/branded-types';
3
4
  import { compactArray } from '@aztec/foundation/collection';
5
+ import type { Fr } from '@aztec/foundation/curves/bn254';
4
6
  import type { Logger } from '@aztec/foundation/log';
5
7
  import {
6
8
  type AttestationInfo,
9
+ type CommitteeAttestation,
7
10
  type ValidateCheckpointNegativeResult,
8
11
  type ValidateCheckpointResult,
9
12
  getAttestationInfoFromPayload,
10
13
  } from '@aztec/stdlib/block';
11
- import type { PublishedCheckpoint } from '@aztec/stdlib/checkpoint';
14
+ import type { CheckpointInfo, PublishedCheckpoint } from '@aztec/stdlib/checkpoint';
12
15
  import { type L1RollupConstants, computeQuorum, getEpochAtSlot } from '@aztec/stdlib/epoch-helpers';
13
16
  import { ConsensusPayload, type CoordinationSignatureContext } from '@aztec/stdlib/p2p';
17
+ import type { CheckpointHeader } from '@aztec/stdlib/rollup';
14
18
 
15
19
  export type { ValidateCheckpointResult };
16
20
 
@@ -26,28 +30,79 @@ export function getAttestationInfoFromPublishedCheckpoint(
26
30
  return getAttestationInfoFromPayload(payload, attestations);
27
31
  }
28
32
 
33
+ /** The subset of a calldata-only checkpoint needed to validate its committee attestations. */
34
+ export type CalldataCheckpointForAttestations = {
35
+ checkpointNumber: CheckpointNumber;
36
+ archiveRoot: Fr;
37
+ feeAssetPriceModifier: bigint;
38
+ header: CheckpointHeader;
39
+ attestations: CommitteeAttestation[];
40
+ /** The exact packed attestations tuple from L1 calldata, carried verbatim for byte-faithful invalidation. */
41
+ verbatimAttestations: ViemCommitteeAttestations;
42
+ };
43
+
29
44
  /**
30
- * Validates the attestations submitted for the given checkpoint.
31
- * Returns true if the attestations are valid and sufficient, false otherwise.
45
+ * Validates the attestations of a checkpoint from L1 calldata only, without fetching or decoding its blobs.
46
+ * The signed consensus payload (header, archive root, fee asset price modifier) is fully available from
47
+ * calldata, so an invalid-attestation checkpoint can be rejected before any (possibly malformed) blob is
48
+ * fetched and decoded.
32
49
  */
33
- export async function validateCheckpointAttestations(
34
- publishedCheckpoint: PublishedCheckpoint,
50
+ export function validateCheckpointAttestationsFromCalldata(
51
+ checkpoint: CalldataCheckpointForAttestations,
35
52
  epochCache: EpochCache,
36
53
  constants: Pick<L1RollupConstants, 'epochDuration'>,
37
54
  signatureContext: CoordinationSignatureContext,
38
55
  logger?: Logger,
39
56
  ): Promise<ValidateCheckpointResult> {
40
- const attestorInfos = getAttestationInfoFromPublishedCheckpoint(publishedCheckpoint, signatureContext);
57
+ const payload = new ConsensusPayload(
58
+ checkpoint.header,
59
+ checkpoint.archiveRoot,
60
+ checkpoint.feeAssetPriceModifier,
61
+ signatureContext,
62
+ );
63
+ const checkpointInfo: CheckpointInfo = {
64
+ archive: checkpoint.archiveRoot,
65
+ lastArchive: checkpoint.header.lastArchiveRoot,
66
+ slotNumber: checkpoint.header.slotNumber,
67
+ checkpointNumber: checkpoint.checkpointNumber,
68
+ timestamp: checkpoint.header.timestamp,
69
+ };
70
+ return validateAttestations(
71
+ payload,
72
+ checkpoint.attestations,
73
+ checkpoint.verbatimAttestations,
74
+ checkpointInfo,
75
+ epochCache,
76
+ constants,
77
+ logger,
78
+ );
79
+ }
80
+
81
+ /**
82
+ * Core attestation validation over a consensus payload, its attestations, and checkpoint metadata --
83
+ * independent of whether the checkpoint's blocks have been decoded from blobs. Returns true if the
84
+ * attestations are valid and sufficient, false otherwise.
85
+ */
86
+ export async function validateAttestations(
87
+ payload: ConsensusPayload,
88
+ attestations: CommitteeAttestation[],
89
+ verbatimAttestations: ViemCommitteeAttestations,
90
+ checkpointInfo: CheckpointInfo,
91
+ epochCache: EpochCache,
92
+ constants: Pick<L1RollupConstants, 'epochDuration'>,
93
+ logger?: Logger,
94
+ ): Promise<ValidateCheckpointResult> {
95
+ const attestorInfos = getAttestationInfoFromPayload(payload, attestations);
41
96
  const attestors = compactArray(attestorInfos.map(info => ('address' in info ? info.address : undefined)));
42
- const { checkpoint, attestations } = publishedCheckpoint;
43
- const headerHash = checkpoint.header.hash();
44
- const archiveRoot = checkpoint.archive.root.toString();
45
- const slot = checkpoint.header.slotNumber;
97
+ const headerHash = payload.header.hash();
98
+ const archiveRoot = payload.archive.toString();
99
+ const slot = payload.header.slotNumber;
100
+ const checkpointNumber = checkpointInfo.checkpointNumber;
46
101
  const epoch: EpochNumber = getEpochAtSlot(slot, constants);
47
102
  const { committee, seed } = await epochCache.getCommitteeForEpoch(epoch);
48
- const logData = { checkpointNumber: checkpoint.number, slot, epoch, headerHash, archiveRoot };
103
+ const logData = { checkpointNumber, slot, epoch, headerHash, archiveRoot };
49
104
 
50
- logger?.debug(`Validating attestations for checkpoint ${checkpoint.number} at slot ${slot} in epoch ${epoch}`, {
105
+ logger?.debug(`Validating attestations for checkpoint ${checkpointNumber} at slot ${slot} in epoch ${epoch}`, {
51
106
  committee: (committee ?? []).map(member => member.toString()),
52
107
  recoveredAttestors: attestorInfos,
53
108
  postedAttestations: attestations.map(a => (a.address.isZero() ? a.signature : a.address).toString()),
@@ -72,12 +127,13 @@ export async function validateCheckpointAttestations(
72
127
  const failedValidationResult = <TReason extends ValidateCheckpointNegativeResult['reason']>(reason: TReason) => ({
73
128
  valid: false as const,
74
129
  reason,
75
- checkpoint: checkpoint.toCheckpointInfo(),
130
+ checkpoint: checkpointInfo,
76
131
  committee,
77
132
  seed,
78
133
  epoch,
79
134
  attestors,
80
135
  attestations,
136
+ verbatimAttestations,
81
137
  });
82
138
 
83
139
  for (let i = 0; i < attestorInfos.length; i++) {
@@ -123,7 +179,7 @@ export async function validateCheckpointAttestations(
123
179
  }
124
180
 
125
181
  logger?.debug(
126
- `Checkpoint attestations validated successfully for checkpoint ${checkpoint.number} at slot ${slot}`,
182
+ `Checkpoint attestations validated successfully for checkpoint ${checkpointNumber} at slot ${slot}`,
127
183
  logData,
128
184
  );
129
185
  return { valid: true };
@@ -30,7 +30,6 @@ import {
30
30
  type ProposedCheckpointInput,
31
31
  PublishedCheckpoint,
32
32
  } from '@aztec/stdlib/checkpoint';
33
- import { type L1RollupConstants, getEpochAtSlot } from '@aztec/stdlib/epoch-helpers';
34
33
  import { CheckpointHeader } from '@aztec/stdlib/rollup';
35
34
  import { AppendOnlyTreeSnapshot } from '@aztec/stdlib/trees';
36
35
  import {
@@ -38,8 +37,6 @@ import {
38
37
  type IndexedTxEffect,
39
38
  TxEffect,
40
39
  TxHash,
41
- TxReceipt,
42
- TxStatus,
43
40
  deserializeIndexedTxEffect,
44
41
  serializeIndexedTxEffect,
45
42
  } from '@aztec/stdlib/tx';
@@ -61,7 +58,7 @@ import {
61
58
  ProposedCheckpointPromotionNotSequentialError,
62
59
  } from '../errors.js';
63
60
 
64
- export { TxReceipt, type TxEffect, type TxHash } from '@aztec/stdlib/tx';
61
+ export type { TxEffect, TxHash, TxReceipt } from '@aztec/stdlib/tx';
65
62
 
66
63
  type BlockIndexValue = [blockNumber: number, index: number];
67
64
 
@@ -309,12 +306,18 @@ export class BlockStore {
309
306
 
310
307
  /**
311
308
  * Append new checkpoints to the store's list.
309
+ * Checkpoints at the start of the batch that are already stored (e.g. re-included by an L1 reorg)
310
+ * are accepted if their archive root matches: their L1 metadata is updated but they are not
311
+ * re-inserted, and they are excluded from the returned array.
312
312
  * @param checkpoints - The L2 checkpoints to be added to the store.
313
- * @returns True if the operation is successful.
313
+ * @returns The checkpoints that were actually inserted (excluding already-stored ones).
314
314
  */
315
- async addCheckpoints(checkpoints: PublishedCheckpoint[], opts: { force?: boolean } = {}): Promise<boolean> {
315
+ async addCheckpoints(
316
+ checkpoints: PublishedCheckpoint[],
317
+ opts: { force?: boolean } = {},
318
+ ): Promise<PublishedCheckpoint[]> {
316
319
  if (checkpoints.length === 0) {
317
- return true;
320
+ return [];
318
321
  }
319
322
 
320
323
  return await this.db.transactionAsync(async () => {
@@ -327,7 +330,7 @@ export class BlockStore {
327
330
  if (!opts.force && firstCheckpointNumber <= previousCheckpointNumber) {
328
331
  checkpoints = await this.skipOrUpdateAlreadyStoredCheckpoints(checkpoints, previousCheckpointNumber);
329
332
  if (checkpoints.length === 0) {
330
- return true;
333
+ return [];
331
334
  }
332
335
  // Re-check sequentiality after skipping
333
336
  const newFirstNumber = checkpoints[0].checkpoint.number;
@@ -390,7 +393,7 @@ export class BlockStore {
390
393
  }
391
394
 
392
395
  await this.advanceSynchedL1BlockNumber(checkpoints[checkpoints.length - 1].l1.blockNumber);
393
- return true;
396
+ return checkpoints;
394
397
  });
395
398
  }
396
399
 
@@ -521,6 +524,7 @@ export class BlockStore {
521
524
  l2BlockNumber: block.number,
522
525
  l2BlockHash: blockHash,
523
526
  txIndexInBlock: i,
527
+ slotNumber: block.header.globalVariables.slotNumber,
524
528
  };
525
529
  await this.#txEffects.set(txEffect.data.txHash.toString(), serializeIndexedTxEffect(txEffect));
526
530
  }
@@ -533,20 +537,58 @@ export class BlockStore {
533
537
  }
534
538
 
535
539
  /** Deletes a block and all associated data (tx effects, indices). */
536
- private async deleteBlock(block: L2Block): Promise<void> {
540
+ private async deleteBlock(blockNumber: number, blockStorage: BlockStorage): Promise<void> {
537
541
  // Delete the block from the main blocks map
538
- await this.#blocks.delete(block.number);
539
-
540
- // Delete all tx effects for this block
541
- await Promise.all(block.body.txEffects.map(tx => this.#txEffects.delete(tx.txHash.toString())));
542
+ await this.#blocks.delete(blockNumber);
543
+
544
+ const blockHash = bufferToHex(blockStorage.blockHash);
545
+
546
+ // Delete the tx effects of the block's txs, skipping entries that no longer point at this block: if
547
+ // another stored block also contains the tx, the entry points at that block, and deleting it here would
548
+ // orphan that block's index. Honest chains never store the same tx twice (duplicate nullifiers are
549
+ // rejected at build, re-execution, and world-state sync), so the store must not turn such a state into
550
+ // corruption of the surviving block if it ever appears.
551
+ const blockTxsBuffer = await this.#blockTxs.getAsync(blockHash);
552
+ if (blockTxsBuffer !== undefined) {
553
+ const reader = BufferReader.asReader(blockTxsBuffer);
554
+ const txHashes: string[] = [];
555
+ while (!reader.isEmpty()) {
556
+ txHashes.push(reader.readObject(TxHash).toString());
557
+ }
558
+ await Promise.all(txHashes.map(txHash => this.deleteTxEffect(txHash, blockStorage.blockHash)));
559
+ }
542
560
 
543
561
  // Delete block txs mapping
544
- const blockHash = (await block.hash()).toString();
545
562
  await this.#blockTxs.delete(blockHash);
546
563
 
547
564
  // Clean up indices
548
565
  await this.#blockHashIndex.delete(blockHash);
549
- await this.#blockArchiveIndex.delete(block.archive.root.toString());
566
+ await this.#blockArchiveIndex.delete(AppendOnlyTreeSnapshot.fromBuffer(blockStorage.archive).root.toString());
567
+ }
568
+
569
+ /** Deletes a tx effect only if it is still owned by (points at) the given block. */
570
+ private async deleteTxEffect(txHash: string, blockHash: Buffer): Promise<void> {
571
+ const stored = await this.#txEffects.getAsync(txHash);
572
+ if (stored === undefined) {
573
+ this.#log.warn(`Missing tx effect for tx ${txHash} while removing its block`, {
574
+ txHash,
575
+ blockHash: bufferToHex(blockHash),
576
+ });
577
+ return;
578
+ }
579
+ // An IndexedTxEffect starts with the owning block hash (32 bytes) — see getTxLocation.
580
+ if (!Buffer.from(stored.buffer, stored.byteOffset, 32).equals(blockHash)) {
581
+ // Fires only when two stored blocks listed the same tx — a state upstream validation should make
582
+ // unreachable. Keep the entry (it belongs to the surviving block) but flag the duplication loudly.
583
+ this.#log.warn(`Tx effect for tx ${txHash} is owned by a block other than the one being removed`, {
584
+ txHash,
585
+ removedBlockHash: bufferToHex(blockHash),
586
+ owningBlockHash: bufferToHex(Buffer.from(stored.buffer, stored.byteOffset, 32)),
587
+ });
588
+ return;
589
+ }
590
+
591
+ await this.#txEffects.delete(txHash);
550
592
  }
551
593
 
552
594
  /**
@@ -623,6 +665,26 @@ export class BlockStore {
623
665
  return checkpoints;
624
666
  }
625
667
 
668
+ /**
669
+ * Returns up to `limit` checkpoints anchored at `fromSlot`, ordered nearest-first, walking the slot index.
670
+ * With `reverse`, takes the checkpoints at or before `fromSlot` (descending by slot); otherwise the
671
+ * checkpoints at or after it (ascending). `limit: 1, reverse: true` yields the latest checkpoint at or
672
+ * before the slot in a single range scan.
673
+ */
674
+ async getCheckpointsBySlot(fromSlot: SlotNumber, limit: number, reverse: boolean): Promise<CheckpointData[]> {
675
+ // The KV range bounds are direction-dependent: forward is [start, end), reverse is (start, end], so a
676
+ // reverse scan uses `end: fromSlot` (inclusive) with no +1 to include the checkpoint at fromSlot itself.
677
+ const range = reverse ? { end: fromSlot, reverse: true, limit } : { start: fromSlot, limit };
678
+ const result: CheckpointData[] = [];
679
+ for await (const [, checkpointNumber] of this.#slotToCheckpoint.entriesAsync(range)) {
680
+ const checkpointStorage = await this.#checkpoints.getAsync(checkpointNumber);
681
+ if (checkpointStorage) {
682
+ result.push(this.checkpointDataFromCheckpointStorage(checkpointStorage));
683
+ }
684
+ }
685
+ return result;
686
+ }
687
+
626
688
  /** Returns checkpoint data for all checkpoints whose slot falls within the given range (inclusive). */
627
689
  async getCheckpointDataForSlotRange(startSlot: SlotNumber, endSlot: SlotNumber): Promise<CheckpointData[]> {
628
690
  const result: CheckpointData[] = [];
@@ -725,16 +787,24 @@ export class BlockStore {
725
787
 
726
788
  // Iterate from blockNumber + 1 to latestBlockNumber
727
789
  for (let bn = blockNumber + 1; bn <= latestBlockNumber; bn++) {
728
- const block = await this.getBlock({ number: BlockNumber(bn) });
790
+ const blockStorage = await this.#blocks.getAsync(bn);
729
791
 
730
- if (block === undefined) {
792
+ if (blockStorage === undefined) {
731
793
  this.#log.warn(`Cannot remove block ${bn} from the store since we don't have it`);
732
794
  continue;
733
795
  }
734
796
 
735
- removedBlocks.push(block);
736
- await this.deleteBlock(block);
737
- this.#log.debug(`Removed block ${bn} ${(await block.hash()).toString()}`);
797
+ // Load the full block for the returned list when possible, but clean up from the raw row regardless:
798
+ // a block whose body can no longer be fully loaded must still release its row, tx effects, and indices,
799
+ // or later inserts at this number leave stale tx-effect entries pointing into the new chain.
800
+ const block = await this.getBlockFromBlockStorage(bn, blockStorage);
801
+ if (block !== undefined) {
802
+ removedBlocks.push(block);
803
+ } else {
804
+ this.#log.warn(`Removing block ${bn} whose body could not be fully loaded`);
805
+ }
806
+ await this.deleteBlock(bn, blockStorage);
807
+ this.#log.debug(`Removed block ${bn} ${bufferToHex(blockStorage.blockHash)}`);
738
808
  }
739
809
 
740
810
  return removedBlocks;
@@ -1092,56 +1162,6 @@ export class BlockStore {
1092
1162
  return deserializeIndexedTxEffect(buffer);
1093
1163
  }
1094
1164
 
1095
- /**
1096
- * Gets a receipt of a settled tx.
1097
- * @param txHash - The hash of a tx we try to get the receipt for.
1098
- * @returns The requested tx receipt (or undefined if not found).
1099
- */
1100
- async getSettledTxReceipt(
1101
- txHash: TxHash,
1102
- l1Constants?: Pick<L1RollupConstants, 'epochDuration'>,
1103
- ): Promise<TxReceipt | undefined> {
1104
- const txEffect = await this.getTxEffect(txHash);
1105
- if (!txEffect) {
1106
- return undefined;
1107
- }
1108
-
1109
- const blockNumber = BlockNumber(txEffect.l2BlockNumber);
1110
-
1111
- // Use existing archiver methods to determine finalization level
1112
- const [provenBlockNumber, checkpointedBlockNumber, finalizedBlockNumber, blockData] = await Promise.all([
1113
- this.getProvenBlockNumber(),
1114
- this.getCheckpointedL2BlockNumber(),
1115
- this.getFinalizedL2BlockNumber(),
1116
- this.getBlockData({ number: blockNumber }),
1117
- ]);
1118
-
1119
- let status: TxStatus;
1120
- if (blockNumber <= finalizedBlockNumber) {
1121
- status = TxStatus.FINALIZED;
1122
- } else if (blockNumber <= provenBlockNumber) {
1123
- status = TxStatus.PROVEN;
1124
- } else if (blockNumber <= checkpointedBlockNumber) {
1125
- status = TxStatus.CHECKPOINTED;
1126
- } else {
1127
- status = TxStatus.PROPOSED;
1128
- }
1129
-
1130
- const epochNumber =
1131
- blockData && l1Constants ? getEpochAtSlot(blockData.header.globalVariables.slotNumber, l1Constants) : undefined;
1132
-
1133
- return new TxReceipt(
1134
- txHash,
1135
- status,
1136
- TxReceipt.executionResultFromRevertCode(txEffect.data.revertCode),
1137
- undefined,
1138
- txEffect.data.transactionFee.toBigInt(),
1139
- txEffect.l2BlockHash,
1140
- blockNumber,
1141
- epochNumber,
1142
- );
1143
- }
1144
-
1145
1165
  /**
1146
1166
  * Looks up which block included the requested tx effect.
1147
1167
  * @param txHash - The txHash of the tx.
@@ -1167,8 +1187,9 @@ export class BlockStore {
1167
1187
  * attach effect data on demand without paying for a full {@link TxEffect} deserialization.
1168
1188
  *
1169
1189
  * The on-disk `IndexedTxEffect` layout starts with a fixed-length header
1170
- * (`blockHash(32) + l2BlockNumber(4) + txIndexInBlock(4) + revertCode(1) + txHash(32) + transactionFee(32)` =
1171
- * 105 bytes), followed by `noteHashes` and `nullifiers` (both u8-length-prefixed `Fr` vectors). We
1190
+ * (`blockHash(32) + l2BlockNumber(4) + txIndexInBlock(4) + slotNumber(4) + revertCode(1) + txHash(32) +
1191
+ * transactionFee(32)` = 109 bytes), followed by `noteHashes` and `nullifiers` (both u8-length-prefixed `Fr`
1192
+ * vectors). We
1172
1193
  * skip the header, then read the two vectors, and stop — the large tail (`l2ToL1Msgs`,
1173
1194
  * `publicDataWrites`, `privateLogs`, `publicLogs`, `contractClassLogs`) is never touched.
1174
1195
  */
@@ -1180,8 +1201,9 @@ export class BlockStore {
1180
1201
  return [[], []];
1181
1202
  }
1182
1203
  const reader = BufferReader.asReader(buffer);
1183
- // Skip the fixed-length header: blockHash + l2BlockNumber + txIndexInBlock + revertCode + txHash + transactionFee.
1184
- reader.readBytes(32 + 4 + 4 + 1 + 32 + 32);
1204
+ // Skip the fixed-length header: blockHash + l2BlockNumber + txIndexInBlock + slotNumber + revertCode +
1205
+ // txHash + transactionFee.
1206
+ reader.readBytes(32 + 4 + 4 + 4 + 1 + 32 + 32);
1185
1207
  const noteHashes = reader.readVectorUint8Prefix(Fr);
1186
1208
  const nullifiers = reader.readVectorUint8Prefix(Fr);
1187
1209
  return [noteHashes, nullifiers];
@@ -1217,14 +1239,13 @@ export class BlockStore {
1217
1239
  }
1218
1240
 
1219
1241
  /**
1220
- * Resolves all five L2 chain tips (proposed, proposedCheckpoint, checkpointed, proven, finalized)
1221
- * in a single read-only transaction so the snapshot is internally consistent. Each underlying
1222
- * record is read at most once: latest block, latest confirmed checkpoint, and latest pending
1223
- * checkpoint are each loaded directly (no separate "find the number, then look up data" hop),
1224
- * the proven/finalized checkpoint singletons are read once and their storage entries are
1225
- * reused if they coincide with the latest checkpoint, and per-tip block hashes are deduped
1226
- * when two tips land on the same block (e.g. finalized == proven, or proposedCheckpoint falls
1227
- * back to checkpointed when no pending checkpoint exists).
1242
+ * Resolves all four L2 chain tips (proposed, checkpointed, proven, finalized) in a single
1243
+ * read-only transaction so the snapshot is internally consistent. Each underlying record is
1244
+ * read at most once: latest block and latest confirmed checkpoint are loaded directly (no
1245
+ * separate "find the number, then look up data" hop), the proven/finalized checkpoint
1246
+ * singletons are read once and their storage entries are reused if they coincide with the
1247
+ * latest checkpoint, and per-tip block hashes are deduped when two tips land on the same block
1248
+ * (e.g. finalized == proven).
1228
1249
  *
1229
1250
  * The result is guaranteed to satisfy `finalized <= proven <= checkpointed <= proposed` (by
1230
1251
  * block number). Genesis is represented by `(INITIAL_L2_BLOCK_NUM - 1)` and the supplied
@@ -1247,9 +1268,6 @@ export class BlockStore {
1247
1268
 
1248
1269
  // Load latest block and checkpoint entries
1249
1270
  const [latestBlockEntry] = await toArray(this.#blocks.entriesAsync({ reverse: true, limit: 1 }));
1250
- const [proposedCheckpointEntry] = await toArray(
1251
- this.#proposedCheckpoints.entriesAsync({ reverse: true, limit: 1 }),
1252
- );
1253
1271
  const [latestCheckpointEntry] = await toArray(this.#checkpoints.entriesAsync({ reverse: true, limit: 1 }));
1254
1272
  const latestCheckpointNumber = latestCheckpointEntry
1255
1273
  ? CheckpointNumber(latestCheckpointEntry[0])
@@ -1335,14 +1353,6 @@ export class BlockStore {
1335
1353
  const provenTip = await buildTipFromCheckpoint(provenCheckpoint);
1336
1354
  const finalizedTip = await buildTipFromCheckpoint(finalizedCheckpoint);
1337
1355
 
1338
- // Proposed checkpoint falls back to the checkpoint tip if it's not set. And if local storage is
1339
- // inconsistent and the proposed checkpoint is behind the checkpointed tip, we patch that and
1340
- // report the checkpointed tip as the proposed checkpoint to maintain the invariant.
1341
- const proposedCheckpointTip =
1342
- proposedCheckpointEntry === undefined || proposedCheckpointEntry[0] <= latestCheckpointNumber
1343
- ? checkpointedTip
1344
- : await buildTipFromCheckpoint(proposedCheckpointEntry[1]);
1345
-
1346
1356
  // A checkpointed block past the latest stored block would mean a checkpoint
1347
1357
  // references blocks that aren't in blocks.
1348
1358
  if (proposedBlockId.number < checkpointedTip.block.number) {
@@ -1354,11 +1364,10 @@ export class BlockStore {
1354
1364
  // Assert that checkpoint numbers are increasing
1355
1365
  if (
1356
1366
  finalizedTip.checkpoint.number > provenTip.checkpoint.number ||
1357
- provenTip.checkpoint.number > checkpointedTip.checkpoint.number ||
1358
- checkpointedTip.checkpoint.number > proposedCheckpointTip.checkpoint.number
1367
+ provenTip.checkpoint.number > checkpointedTip.checkpoint.number
1359
1368
  ) {
1360
1369
  throw new Error(
1361
- `Inconsistent checkpoint numbers in chain tips: finalized=${finalizedTip.checkpoint.number} proven=${provenTip.checkpoint.number} checkpointed=${checkpointedTip.checkpoint.number} proposed=${proposedCheckpointTip.checkpoint.number}`,
1370
+ `Inconsistent checkpoint numbers in chain tips: finalized=${finalizedTip.checkpoint.number} proven=${provenTip.checkpoint.number} checkpointed=${checkpointedTip.checkpoint.number}`,
1362
1371
  );
1363
1372
  }
1364
1373
 
@@ -1366,17 +1375,15 @@ export class BlockStore {
1366
1375
  if (
1367
1376
  finalizedTip.block.number > provenTip.block.number ||
1368
1377
  provenTip.block.number > checkpointedTip.block.number ||
1369
- checkpointedTip.block.number > proposedCheckpointTip.block.number ||
1370
- proposedCheckpointTip.block.number > proposedBlockId.number
1378
+ checkpointedTip.block.number > proposedBlockId.number
1371
1379
  ) {
1372
1380
  throw new Error(
1373
- `Inconsistent block numbers in chain tips: finalized=${finalizedTip.block.number} proven=${provenTip.block.number} checkpointed=${checkpointedTip.block.number} proposedCheckpoint=${proposedCheckpointTip.block.number} proposed=${proposedBlockId.number}`,
1381
+ `Inconsistent block numbers in chain tips: finalized=${finalizedTip.block.number} proven=${provenTip.block.number} checkpointed=${checkpointedTip.block.number} proposed=${proposedBlockId.number}`,
1374
1382
  );
1375
1383
  }
1376
1384
 
1377
1385
  return {
1378
1386
  proposed: proposedBlockId,
1379
- proposedCheckpoint: proposedCheckpointTip,
1380
1387
  checkpointed: checkpointedTip,
1381
1388
  proven: provenTip,
1382
1389
  finalized: finalizedTip,
@@ -2,6 +2,7 @@ import { Fr } from '@aztec/foundation/curves/bn254';
2
2
  import { toArray } from '@aztec/foundation/iterable';
3
3
  import { BufferReader, numToUInt8, serializeToBuffer } from '@aztec/foundation/serialize';
4
4
  import type { AztecAsyncKVStore, AztecAsyncMap } from '@aztec/kv-store';
5
+ import { isProtocolContractClass } from '@aztec/protocol-contracts';
5
6
  import type {
6
7
  ContractClassPublic,
7
8
  ContractClassPublicWithBlockNumber,
@@ -49,7 +50,19 @@ export class ContractClassStore {
49
50
  ): Promise<void> {
50
51
  await this.db.transactionAsync(async () => {
51
52
  const key = contractClass.id.toString();
52
- if (await this.#contractClasses.hasAsync(key)) {
53
+ const existing = await this.#contractClasses.getAsync(key);
54
+ if (existing !== undefined) {
55
+ // Protocol contracts are preloaded at block 0, so a later on-chain (re-)publish of a bundled
56
+ // protocol class id is valid and must be a no-op. Keep the existing block-0 entry untouched
57
+ // (do not bump its block number) so it survives reorgs of the publishing block.
58
+ if (isProtocolContractClass(contractClass.id)) {
59
+ return;
60
+ }
61
+ // An L1 reorg can re-present an already-stored checkpoint, replaying this class at the same
62
+ // block; treat that as a no-op. A duplicate at a different block still signals double-processing.
63
+ if (deserializeContractClassPublic(existing).l2BlockNumber === blockNumber) {
64
+ return;
65
+ }
53
66
  throw new Error(`Contract class ${key} already exists, cannot add again at block ${blockNumber}`);
54
67
  }
55
68
  await this.#contractClasses.set(
@@ -61,6 +74,11 @@ export class ContractClassStore {
61
74
  }
62
75
 
63
76
  async deleteContractClass(contractClass: ContractClassPublic, blockNumber: number): Promise<void> {
77
+ // Protocol contracts are preloaded at block 0 and must never be deleted, even when the block that
78
+ // (re-)published them on-chain is unwound by a reorg.
79
+ if (isProtocolContractClass(contractClass.id)) {
80
+ return;
81
+ }
64
82
  const restoredContractClass = await this.#contractClasses.getAsync(contractClass.id.toString());
65
83
  if (restoredContractClass && deserializeContractClassPublic(restoredContractClass).l2BlockNumber >= blockNumber) {
66
84
  await this.db.transactionAsync(async () => {
@@ -1,5 +1,6 @@
1
1
  import type { Fr } from '@aztec/foundation/curves/bn254';
2
2
  import type { AztecAsyncKVStore, AztecAsyncMap } from '@aztec/kv-store';
3
+ import { isProtocolContract } from '@aztec/protocol-contracts';
3
4
  import type { AztecAddress } from '@aztec/stdlib/aztec-address';
4
5
  import {
5
6
  type ContractInstanceUpdateWithAddress,
@@ -72,8 +73,19 @@ export class ContractInstanceStore {
72
73
  return this.db.transactionAsync(async () => {
73
74
  const key = contractInstance.address.toString();
74
75
  if (await this.#contractInstances.hasAsync(key)) {
76
+ // Protocol contracts are preloaded at block 0, so a later on-chain (re-)publish of a bundled
77
+ // protocol instance is valid and must be a no-op. Keep the existing block-0 entry untouched.
78
+ if (isProtocolContract(contractInstance.address)) {
79
+ return;
80
+ }
81
+ const existingBlockNumber = await this.#contractInstancePublishedAt.getAsync(key);
82
+ // An L1 reorg can re-present an already-stored checkpoint, replaying this instance at the same
83
+ // block; treat that as a no-op. A duplicate at a different block still signals double-processing.
84
+ if (existingBlockNumber === blockNumber) {
85
+ return;
86
+ }
75
87
  throw new Error(
76
- `Contract instance at ${key} already exists (deployed at block ${await this.#contractInstancePublishedAt.getAsync(key)}), cannot add again at block ${blockNumber}`,
88
+ `Contract instance at ${key} already exists (deployed at block ${existingBlockNumber}), cannot add again at block ${blockNumber}`,
77
89
  );
78
90
  }
79
91
  await this.#contractInstances.set(key, new SerializableContractInstance(contractInstance).toBuffer());
@@ -82,6 +94,11 @@ export class ContractInstanceStore {
82
94
  }
83
95
 
84
96
  deleteContractInstance(contractInstance: ContractInstanceWithAddress): Promise<void> {
97
+ // Protocol contracts are preloaded at block 0 and must never be deleted, even when the block that
98
+ // (re-)published them on-chain is unwound by a reorg.
99
+ if (isProtocolContract(contractInstance.address)) {
100
+ return Promise.resolve();
101
+ }
85
102
  return this.db.transactionAsync(async () => {
86
103
  await this.#contractInstances.delete(contractInstance.address.toString());
87
104
  await this.#contractInstancePublishedAt.delete(contractInstance.address.toString());
@@ -26,6 +26,7 @@ import {
26
26
  endOfTxRange,
27
27
  fieldHex,
28
28
  incKey,
29
+ tagHexForLog,
29
30
  } from './log_store_codec.js';
30
31
 
31
32
  /**
@@ -100,7 +101,7 @@ export class LogStore {
100
101
  let publicLogIndexWithinTx = 0;
101
102
 
102
103
  for (const log of txEffect.privateLogs) {
103
- const tagHex = fieldHex(log.fields[0]);
104
+ const tagHex = tagHexForLog(log.fields);
104
105
  const key = encodeKey(tagHex, blockNumber, txIndexWithinBlock, privateLogIndexWithinTx);
105
106
  const value = encodeValue({
106
107
  txHash,
@@ -115,7 +116,7 @@ export class LogStore {
115
116
 
116
117
  for (const log of txEffect.publicLogs) {
117
118
  const contractHex = fieldHex(log.contractAddress);
118
- const tagHex = fieldHex(log.fields[0]);
119
+ const tagHex = tagHexForLog(log.fields);
119
120
  const key = encodeKey(
120
121
  encodePublicPrefix(contractHex, tagHex),
121
122
  blockNumber,
@@ -37,6 +37,17 @@ export function fieldHex(value: Fr | { toString: () => string }): string {
37
37
  return value.toString().slice(2);
38
38
  }
39
39
 
40
+ /**
41
+ * Tag prefix for a log: the hex of its first field, or the empty string when the log carries no fields.
42
+ * A protocol-valid public log can have zero fields (e.g. a raw AVM `EMITPUBLICLOG` with `logSize = 0`),
43
+ * which has no tag to index by. Encoding it under the empty tag keeps it retrievable via the per-block
44
+ * read (and droppable on reorg) while never matching a real 64-hex-char tag query — instead of reading
45
+ * `fields[0]` off an empty array and aborting the whole block-ingestion transaction.
46
+ */
47
+ export function tagHexForLog(fields: Fr[]): string {
48
+ return fields.length > 0 ? fieldHex(fields[0]) : '';
49
+ }
50
+
40
51
  /** Encodes a number as 8-char zero-padded lowercase hex (matches a u32 big-endian byte buffer's lex order). */
41
52
  export function u32Hex(n: number): string {
42
53
  return n.toString(16).padStart(NUMERIC_HEX_LEN, '0');
@@ -682,7 +682,7 @@ export class FakeL1State {
682
682
  getHashedSignaturePayloadTypedData(attestationsAndSigners),
683
683
  );
684
684
 
685
- const packedAttestations = attestationsAndSigners.getPackedAttestations();
685
+ const verbatimAttestations = attestationsAndSigners.getPackedAttestations();
686
686
 
687
687
  const rollupInput = encodeFunctionData({
688
688
  abi: RollupAbi,
@@ -693,7 +693,7 @@ export class FakeL1State {
693
693
  archive,
694
694
  oracleInput: { feeAssetPriceModifier: 0n },
695
695
  },
696
- packedAttestations,
696
+ verbatimAttestations,
697
697
  attestationsAndSigners.getSigners().map(signer => signer.toString()),
698
698
  attestationsAndSignersSignature.toViemSignature(),
699
699
  blobInput,
@@ -716,7 +716,7 @@ export class FakeL1State {
716
716
 
717
717
  // Compute attestationsHash (same logic as CalldataRetriever)
718
718
  const attestationsHash = Buffer32.fromString(
719
- keccak256(encodeAbiParameters([this.getCommitteeAttestationsStructDef()], [packedAttestations])),
719
+ keccak256(encodeAbiParameters([this.getCommitteeAttestationsStructDef()], [verbatimAttestations])),
720
720
  );
721
721
 
722
722
  // Compute payloadDigest (same logic as CalldataRetriever)
@@ -44,7 +44,6 @@ export class MockL1ToL2MessageSource implements L1ToL2MessageSource {
44
44
  checkpointed: tip,
45
45
  proven: tip,
46
46
  finalized: tip,
47
- proposedCheckpoint: tip,
48
47
  });
49
48
  }
50
49
  }