@aztec-labs/archiver 6.0.0-rc.1 → 6.0.0-rc.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 (42) hide show
  1. package/README.md +7 -1
  2. package/dest/errors.d.ts +20 -1
  3. package/dest/errors.d.ts.map +1 -1
  4. package/dest/errors.js +22 -0
  5. package/dest/l1/bin/retrieve-calldata.js +14 -4
  6. package/dest/l1/calldata_retriever.d.ts +15 -17
  7. package/dest/l1/calldata_retriever.d.ts.map +1 -1
  8. package/dest/l1/calldata_retriever.js +2 -9
  9. package/dest/l1/data_retrieval.d.ts +7 -3
  10. package/dest/l1/data_retrieval.d.ts.map +1 -1
  11. package/dest/l1/data_retrieval.js +14 -9
  12. package/dest/modules/data_store_updater.js +5 -5
  13. package/dest/modules/l1_synchronizer.d.ts +1 -1
  14. package/dest/modules/l1_synchronizer.d.ts.map +1 -1
  15. package/dest/modules/l1_synchronizer.js +228 -191
  16. package/dest/modules/validation.d.ts +14 -8
  17. package/dest/modules/validation.d.ts.map +1 -1
  18. package/dest/modules/validation.js +72 -22
  19. package/dest/store/block_store.d.ts +2 -1
  20. package/dest/store/block_store.d.ts.map +1 -1
  21. package/dest/store/block_store.js +34 -14
  22. package/dest/store/contract_class_store.d.ts +1 -1
  23. package/dest/store/contract_class_store.d.ts.map +1 -1
  24. package/dest/store/contract_class_store.js +4 -2
  25. package/dest/store/contract_instance_store.d.ts +5 -4
  26. package/dest/store/contract_instance_store.d.ts.map +1 -1
  27. package/dest/store/contract_instance_store.js +20 -9
  28. package/dest/test/fake_l1_state.d.ts +3 -1
  29. package/dest/test/fake_l1_state.d.ts.map +1 -1
  30. package/dest/test/fake_l1_state.js +4 -4
  31. package/package.json +13 -13
  32. package/src/errors.ts +32 -0
  33. package/src/l1/bin/retrieve-calldata.ts +15 -4
  34. package/src/l1/calldata_retriever.ts +14 -20
  35. package/src/l1/data_retrieval.ts +22 -14
  36. package/src/modules/data_store_updater.ts +4 -4
  37. package/src/modules/l1_synchronizer.ts +294 -261
  38. package/src/modules/validation.ts +87 -28
  39. package/src/store/block_store.ts +41 -13
  40. package/src/store/contract_class_store.ts +4 -4
  41. package/src/store/contract_instance_store.ts +24 -17
  42. package/src/test/fake_l1_state.ts +9 -3
@@ -34,7 +34,7 @@ import { type Traceable, type Tracer, execInSpan, trackSpan } from '@aztec-labs/
34
34
 
35
35
  import { InitialCheckpointNumberNotSequentialError } from '../errors.js';
36
36
  import {
37
- type RetrievedCheckpointFromCalldata,
37
+ type ResolvedCheckpointFromCalldata,
38
38
  getCheckpointBlobDataFromBlobs,
39
39
  retrieveCheckpointCalldataFromRollup,
40
40
  retrievedToPublishedCheckpoint,
@@ -45,7 +45,7 @@ import type { L2FrontierCache } from '../store/l2_frontier_cache.js';
45
45
  import { ArchiverDataStoreUpdater, blockLeafCount } from './data_store_updater.js';
46
46
  import { InboxMessageSynchronizer } from './inbox_message_synchronizer.js';
47
47
  import type { ArchiverInstrumentation } from './instrumentation.js';
48
- import { validateCheckpointAttestationsFromCalldata } from './validation.js';
48
+ import { resolveCheckpointAttestationsFromCalldata } from './validation.js';
49
49
 
50
50
  type RollupStatus = {
51
51
  provenCheckpointNumber: CheckpointNumber;
@@ -863,295 +863,328 @@ export class ArchiverL1Synchronizer implements Traceable {
863
863
  },
864
864
  );
865
865
 
866
- // Check if the last checkpoint matches a local pending entry (so we can skip blob fetch).
867
- // We only check the last one; if it matches, the blob fetch is skipped for that entry.
868
- // TODO(palla/pipelining): We may have more than a single checkpoint to promote
869
- const lastCalldataCheckpoint = calldataCheckpoints[calldataCheckpoints.length - 1];
870
- const promoteResult = await this.tryBuildPublishedCheckpointFromProposed(lastCalldataCheckpoint);
871
- const checkpointToPromote = promoteResult && !('diverged' in promoteResult) ? promoteResult : undefined;
872
- const evictProposedFrom =
873
- promoteResult && 'diverged' in promoteResult ? promoteResult.fromCheckpointNumber : undefined;
874
-
875
- // Validate attestations from CALLDATA before fetching any blobs. A checkpoint with invalid
876
- // attestations (or one descending from a rejected ancestor) is rejected here without fetching its
877
- // blobs, so a malformed blob does not throw during decode before the rejection path runs and
878
- // stall sync. The signed consensus payload (header, archive root, fee asset price
879
- // modifier) is fully available from calldata.
880
- const checkpointsToIngest: RetrievedCheckpointFromCalldata[] = [];
881
-
882
- for (const calldataCheckpoint of calldataCheckpoints) {
883
- // Check the attestations uploaded by the publisher to L1 are correct.
884
- // Rollup contract does not validate attestations to save on gas, so this
885
- // falls on the nodes to verify offchain and skip those checkpoints.
886
- const validationResult = this.config.skipValidateCheckpointAttestations
887
- ? { valid: true as const }
888
- : await validateCheckpointAttestationsFromCalldata(
889
- calldataCheckpoint,
890
- this.epochCache,
891
- this.l1Constants,
892
- this.getSignatureContext(),
893
- this.log,
894
- );
866
+ // Screening below records rejected checkpoints as it goes, and each such write advances the L1 sync point past
867
+ // the rejected checkpoint's L1 block before the batch's valid checkpoints are persisted. If anything in the batch
868
+ // throws before the batch is persisted, the sync point is restored to where it stood before the batch, so the
869
+ // next iteration processes the whole batch again instead of skipping its valid checkpoints. Once the batch is
870
+ // persisted the sync point is left alone: replaying it would refetch blobs for checkpoints already stored,
871
+ // including promoted ones this node never fetched blobs for, and stall if those blobs are unavailable.
872
+ const syncPointBeforeBatch = await this.stores.blocks.getSynchedL1BlockNumber();
873
+ let batchPersisted = false;
874
+ try {
875
+ // Decode and validate attestations from CALLDATA before fetching any blobs, so a checkpoint with invalid
876
+ // attestations is rejected without decoding a possibly malformed blob. Runs even when validation is
877
+ // disabled, since promotion and persistence need the decoded attestations.
878
+ const resolvedCheckpoints = await asyncPool(10, calldataCheckpoints, async calldataCheckpoint => {
879
+ const { attestations, validationResult } = await resolveCheckpointAttestationsFromCalldata(
880
+ calldataCheckpoint,
881
+ this.epochCache,
882
+ this.l1Constants,
883
+ this.getSignatureContext(),
884
+ this.log,
885
+ );
886
+ return { checkpoint: { ...calldataCheckpoint, attestations }, validationResult };
887
+ });
895
888
 
896
- // Also skip the checkpoint if it builds on a previously-rejected ancestor. Without
897
- // this, addCheckpoints would throw InitialCheckpointNumberNotSequentialError when the
898
- // ancestor was skipped earlier (e.g. due to invalid attestations), the catch handler
899
- // would roll back the L1 sync point, and the next iteration would re-fetch and re-throw.
900
- const rejectedAncestor = await this.stores.blocks.getRejectedCheckpointByArchiveRoot(
901
- calldataCheckpoint.header.lastArchiveRoot,
902
- );
889
+ // Check if the last checkpoint matches a local pending entry (so we can skip blob fetch).
890
+ // We only check the last one; if it matches, the blob fetch is skipped for that entry.
891
+ // TODO(palla/pipelining): We may have more than a single checkpoint to promote
892
+ const lastCalldataCheckpoint = resolvedCheckpoints[resolvedCheckpoints.length - 1].checkpoint;
893
+ const promoteResult = await this.tryBuildPublishedCheckpointFromProposed(lastCalldataCheckpoint);
894
+ const checkpointToPromote = promoteResult && !('diverged' in promoteResult) ? promoteResult : undefined;
895
+ const evictProposedFrom =
896
+ promoteResult && 'diverged' in promoteResult ? promoteResult.fromCheckpointNumber : undefined;
897
+
898
+ const checkpointsToIngest: ResolvedCheckpointFromCalldata[] = [];
899
+
900
+ for (const {
901
+ checkpoint: calldataCheckpoint,
902
+ validationResult: resolvedValidationResult,
903
+ } of resolvedCheckpoints) {
904
+ // Check the attestations uploaded by the publisher to L1 are correct.
905
+ // Rollup contract does not validate attestations to save on gas, so this
906
+ // falls on the nodes to verify offchain and skip those checkpoints.
907
+ const validationResult = this.config.skipValidateCheckpointAttestations
908
+ ? { valid: true as const }
909
+ : resolvedValidationResult;
910
+
911
+ // Also skip the checkpoint if it builds on a previously-rejected ancestor. Without
912
+ // this, addCheckpoints would throw InitialCheckpointNumberNotSequentialError when the
913
+ // ancestor was skipped earlier (e.g. due to invalid attestations), the catch handler
914
+ // would roll back the L1 sync point, and the next iteration would re-fetch and re-throw.
915
+ const rejectedAncestor = await this.stores.blocks.getRejectedCheckpointByArchiveRoot(
916
+ calldataCheckpoint.header.lastArchiveRoot,
917
+ );
903
918
 
904
- // Update the validation result if it has changed, so we can keep track of the first invalid checkpoint
905
- // in case there is a sequence of more than one invalid checkpoint, as we need to invalidate the first one.
906
- // There is an exception though: if a checkpoint is invalidated and replaced with another invalid checkpoint,
907
- // we need to update the validation result, since we need to be able to invalidate the new one.
908
- // See test 'chain progresses if an invalid checkpoint is invalidated with an invalid one' for more info.
909
- // Do not update the validation result if there is a rejected ancestor, since in that case we want to keep the
910
- // original invalidation, as the new checkpoint is extending from a previous invalid one.
911
- const validStatusChanged = rollupStatus.validationResult?.valid !== validationResult.valid;
912
- const invalidStatusWithSameCheckpointNumber =
913
- !validationResult.valid &&
914
- rollupStatus.validationResult &&
915
- !rollupStatus.validationResult.valid &&
916
- rollupStatus.validationResult.checkpoint.checkpointNumber === validationResult.checkpoint.checkpointNumber;
917
-
918
- if (!rejectedAncestor && (validStatusChanged || invalidStatusWithSameCheckpointNumber)) {
919
- rollupStatus.validationResult = validationResult;
920
- }
919
+ // Update the validation result if it has changed, so we can keep track of the first invalid checkpoint
920
+ // in case there is a sequence of more than one invalid checkpoint, as we need to invalidate the first one.
921
+ // There is an exception though: if a checkpoint is invalidated and replaced with another invalid checkpoint,
922
+ // we need to update the validation result, since we need to be able to invalidate the new one.
923
+ // See test 'chain progresses if an invalid checkpoint is invalidated with an invalid one' for more info.
924
+ // Do not update the validation result if there is a rejected ancestor, since in that case we want to keep the
925
+ // original invalidation, as the new checkpoint is extending from a previous invalid one.
926
+ const validStatusChanged = rollupStatus.validationResult?.valid !== validationResult.valid;
927
+ const invalidStatusWithSameCheckpointNumber =
928
+ !validationResult.valid &&
929
+ rollupStatus.validationResult &&
930
+ !rollupStatus.validationResult.valid &&
931
+ rollupStatus.validationResult.checkpoint.checkpointNumber === validationResult.checkpoint.checkpointNumber;
932
+
933
+ if (!rejectedAncestor && (validStatusChanged || invalidStatusWithSameCheckpointNumber)) {
934
+ rollupStatus.validationResult = validationResult;
935
+ }
921
936
 
922
- if (!validationResult.valid) {
923
- this.log.warn(`Skipping checkpoint ${calldataCheckpoint.checkpointNumber} due to invalid attestations`, {
924
- checkpointNumber: calldataCheckpoint.checkpointNumber,
925
- l1BlockNumber: calldataCheckpoint.l1.blockNumber,
926
- ...pick(validationResult, 'reason'),
927
- });
937
+ if (!validationResult.valid) {
938
+ this.log.warn(`Skipping checkpoint ${calldataCheckpoint.checkpointNumber} due to invalid attestations`, {
939
+ checkpointNumber: calldataCheckpoint.checkpointNumber,
940
+ l1BlockNumber: calldataCheckpoint.l1.blockNumber,
941
+ ...pick(validationResult, 'reason'),
942
+ });
943
+
944
+ // Emit event for invalid checkpoint detection
945
+ this.events.emit(L2BlockSourceEvents.InvalidAttestationsCheckpointDetected, {
946
+ type: L2BlockSourceEvents.InvalidAttestationsCheckpointDetected,
947
+ validationResult,
948
+ });
949
+
950
+ // Persist a rejected-ancestor entry so any later checkpoint that builds on this one
951
+ // is detected and skipped (rather than tripping the addCheckpoints consecutive-number
952
+ // check and causing the sync point to roll back in a loop).
953
+ await this.stores.blocks.addRejectedCheckpoint({
954
+ checkpointNumber: calldataCheckpoint.checkpointNumber,
955
+ archiveRoot: calldataCheckpoint.archiveRoot,
956
+ parentArchiveRoot: calldataCheckpoint.header.lastArchiveRoot,
957
+ slotNumber: calldataCheckpoint.header.slotNumber,
958
+ l1: calldataCheckpoint.l1,
959
+ reason: 'invalid-attestations' as const,
960
+ });
961
+
962
+ continue;
963
+ }
928
964
 
929
- // Emit event for invalid checkpoint detection
930
- this.events.emit(L2BlockSourceEvents.InvalidAttestationsCheckpointDetected, {
931
- type: L2BlockSourceEvents.InvalidAttestationsCheckpointDetected,
932
- validationResult,
933
- });
965
+ if (rejectedAncestor) {
966
+ const descendantInfo: CheckpointInfo = {
967
+ archive: calldataCheckpoint.archiveRoot,
968
+ lastArchive: calldataCheckpoint.header.lastArchiveRoot,
969
+ slotNumber: calldataCheckpoint.header.slotNumber,
970
+ checkpointNumber: calldataCheckpoint.checkpointNumber,
971
+ timestamp: calldataCheckpoint.header.timestamp,
972
+ };
973
+ this.log.warn(
974
+ `Skipping checkpoint ${calldataCheckpoint.checkpointNumber} as it is a descendant of ` +
975
+ `rejected checkpoint ${rejectedAncestor.checkpointNumber} (${rejectedAncestor.reason})`,
976
+ {
977
+ checkpointNumber: calldataCheckpoint.checkpointNumber,
978
+ l1BlockNumber: calldataCheckpoint.l1.blockNumber,
979
+ l1BlockHash: calldataCheckpoint.l1.blockHash,
980
+ ancestorCheckpointNumber: rejectedAncestor.checkpointNumber,
981
+ ancestorArchiveRoot: rejectedAncestor.archiveRoot.toString(),
982
+ ancestorReason: rejectedAncestor.reason,
983
+ },
984
+ );
934
985
 
935
- // Persist a rejected-ancestor entry so any later checkpoint that builds on this one
936
- // is detected and skipped (rather than tripping the addCheckpoints consecutive-number
937
- // check and causing the sync point to roll back in a loop).
938
- await this.stores.blocks.addRejectedCheckpoint({
939
- checkpointNumber: calldataCheckpoint.checkpointNumber,
940
- archiveRoot: calldataCheckpoint.archiveRoot,
941
- parentArchiveRoot: calldataCheckpoint.header.lastArchiveRoot,
942
- slotNumber: calldataCheckpoint.header.slotNumber,
943
- l1: calldataCheckpoint.l1,
944
- reason: 'invalid-attestations' as const,
945
- });
986
+ this.events.emit(L2BlockSourceEvents.DescendentOfInvalidAttestationsCheckpointDetected, {
987
+ type: L2BlockSourceEvents.DescendentOfInvalidAttestationsCheckpointDetected,
988
+ checkpoint: descendantInfo,
989
+ ancestorArchiveRoot: rejectedAncestor.archiveRoot,
990
+ ancestorCheckpointNumber: rejectedAncestor.checkpointNumber,
991
+ });
946
992
 
947
- continue;
993
+ // Persist this chainpoint as rejected as well, so we can construct a chain of
994
+ // skipped checkpoints starting from the first one with invalid attestations.
995
+ await this.stores.blocks.addRejectedCheckpoint({
996
+ checkpointNumber: calldataCheckpoint.checkpointNumber,
997
+ archiveRoot: calldataCheckpoint.archiveRoot,
998
+ parentArchiveRoot: calldataCheckpoint.header.lastArchiveRoot,
999
+ slotNumber: calldataCheckpoint.header.slotNumber,
1000
+ l1: calldataCheckpoint.l1,
1001
+ reason: 'descends-from-invalid-attestations' as const,
1002
+ });
1003
+
1004
+ continue;
1005
+ }
1006
+
1007
+ checkpointsToIngest.push(calldataCheckpoint);
948
1008
  }
949
1009
 
950
- if (rejectedAncestor) {
951
- const descendantInfo: CheckpointInfo = {
952
- archive: calldataCheckpoint.archiveRoot,
953
- lastArchive: calldataCheckpoint.header.lastArchiveRoot,
954
- slotNumber: calldataCheckpoint.header.slotNumber,
955
- checkpointNumber: calldataCheckpoint.checkpointNumber,
956
- timestamp: calldataCheckpoint.header.timestamp,
957
- };
958
- this.log.warn(
959
- `Skipping checkpoint ${calldataCheckpoint.checkpointNumber} as it is a descendant of ` +
960
- `rejected checkpoint ${rejectedAncestor.checkpointNumber} (${rejectedAncestor.reason})`,
1010
+ // Fetch blobs in parallel only for the surviving (attestation-valid, non-descendant) checkpoints,
1011
+ // then build the full published checkpoints. The last calldata checkpoint may be promotable from a
1012
+ // local proposed block (checkpointToPromote), in which case it carries no blob to fetch. A missing or
1013
+ // undecodable blob throws and propagates, and the batch guard restores the L1 sync point so the fetch is retried.
1014
+ const toFetchBlobs = checkpointToPromote
1015
+ ? checkpointsToIngest.filter(c => c.checkpointNumber !== checkpointToPromote.checkpoint.number)
1016
+ : checkpointsToIngest;
1017
+ const blobFetched = await asyncPool(10, toFetchBlobs, async checkpoint =>
1018
+ retrievedToPublishedCheckpoint({
1019
+ ...checkpoint,
1020
+ checkpointBlobData: await getCheckpointBlobDataFromBlobs(
1021
+ this.blobClient,
1022
+ checkpoint.l1.blockHash,
1023
+ checkpoint.blobHashes,
1024
+ checkpoint.checkpointNumber,
1025
+ this.log,
1026
+ !initialSyncComplete,
1027
+ checkpoint.parentBeaconBlockRoot,
1028
+ checkpoint.l1.timestamp,
1029
+ ),
1030
+ }),
1031
+ );
1032
+
1033
+ // Index the built checkpoints by number so we can ingest them in calldata order, slotting in the
1034
+ // promoted checkpoint (built from a local proposed block rather than blobs).
1035
+ const publishedByNumber = new Map(
1036
+ blobFetched.map(publishedCheckpoint => [publishedCheckpoint.checkpoint.number, publishedCheckpoint]),
1037
+ );
1038
+ if (checkpointToPromote) {
1039
+ publishedByNumber.set(checkpointToPromote.checkpoint.number, checkpointToPromote);
1040
+ }
1041
+
1042
+ const validCheckpoints: PublishedCheckpoint[] = [];
1043
+ for (const calldataCheckpoint of checkpointsToIngest) {
1044
+ const published = publishedByNumber.get(calldataCheckpoint.checkpointNumber)!;
1045
+
1046
+ validCheckpoints.push(published);
1047
+ this.log.debug(
1048
+ `Ingesting new checkpoint ${published.checkpoint.number} with ${published.checkpoint.blocks.length} blocks`,
961
1049
  {
962
- checkpointNumber: calldataCheckpoint.checkpointNumber,
963
- l1BlockNumber: calldataCheckpoint.l1.blockNumber,
964
- l1BlockHash: calldataCheckpoint.l1.blockHash,
965
- ancestorCheckpointNumber: rejectedAncestor.checkpointNumber,
966
- ancestorArchiveRoot: rejectedAncestor.archiveRoot.toString(),
967
- ancestorReason: rejectedAncestor.reason,
1050
+ checkpointHash: published.checkpoint.hash(),
1051
+ l1BlockNumber: published.l1.blockNumber,
1052
+ ...published.checkpoint.header.toInspect(),
1053
+ blocks: published.checkpoint.blocks.map(b => b.getStats()),
968
1054
  },
969
1055
  );
1056
+ }
970
1057
 
971
- this.events.emit(L2BlockSourceEvents.DescendentOfInvalidAttestationsCheckpointDetected, {
972
- type: L2BlockSourceEvents.DescendentOfInvalidAttestationsCheckpointDetected,
973
- checkpoint: descendantInfo,
974
- ancestorArchiveRoot: rejectedAncestor.archiveRoot,
975
- ancestorCheckpointNumber: rejectedAncestor.checkpointNumber,
976
- });
977
-
978
- // Persist this chainpoint as rejected as well, so we can construct a chain of
979
- // skipped checkpoints starting from the first one with invalid attestations.
980
- await this.stores.blocks.addRejectedCheckpoint({
981
- checkpointNumber: calldataCheckpoint.checkpointNumber,
982
- archiveRoot: calldataCheckpoint.archiveRoot,
983
- parentArchiveRoot: calldataCheckpoint.header.lastArchiveRoot,
984
- slotNumber: calldataCheckpoint.header.slotNumber,
985
- l1: calldataCheckpoint.l1,
986
- reason: 'descends-from-invalid-attestations' as const,
1058
+ for (const published of validCheckpoints) {
1059
+ this.instrumentation.processCheckpointL1Timing({
1060
+ slotNumber: published.checkpoint.header.slotNumber,
1061
+ l1Timestamp: published.l1.timestamp,
1062
+ l1Constants: this.l1Constants,
987
1063
  });
988
-
989
- continue;
990
1064
  }
991
1065
 
992
- checkpointsToIngest.push(calldataCheckpoint);
993
- }
1066
+ try {
1067
+ const updatedValidationResult =
1068
+ rollupStatus.validationResult === initialValidationResult ? undefined : rollupStatus.validationResult;
994
1069
 
995
- // Fetch blobs in parallel only for the surviving (attestation-valid, non-descendant) checkpoints,
996
- // then build the full published checkpoints. The last calldata checkpoint may be promotable from a
997
- // local proposed block (checkpointToPromote), in which case it carries no blob to fetch. A missing or
998
- // undecodable blob throws and propagates, rolling back the L1 sync point so the fetch is retried.
999
- const toFetchBlobs = checkpointToPromote
1000
- ? checkpointsToIngest.filter(c => c.checkpointNumber !== checkpointToPromote.checkpoint.number)
1001
- : checkpointsToIngest;
1002
- const blobFetched = await asyncPool(10, toFetchBlobs, async checkpoint =>
1003
- retrievedToPublishedCheckpoint({
1004
- ...checkpoint,
1005
- checkpointBlobData: await getCheckpointBlobDataFromBlobs(
1006
- this.blobClient,
1007
- checkpoint.l1.blockHash,
1008
- checkpoint.blobHashes,
1009
- checkpoint.checkpointNumber,
1010
- this.log,
1011
- !initialSyncComplete,
1012
- checkpoint.parentBeaconBlockRoot,
1013
- checkpoint.l1.timestamp,
1014
- ),
1015
- }),
1016
- );
1070
+ // Split valid checkpoints: the promoted one (if any) is persisted via the proposed-promotion path,
1071
+ // the rest via addCheckpoints. Both paths run within the same store transaction for atomicity.
1072
+ const [[maybeValidCheckpointToPromote], checkpointsToAdd] = partition(
1073
+ validCheckpoints,
1074
+ c => c.checkpoint.number === checkpointToPromote?.checkpoint.number,
1075
+ );
1017
1076
 
1018
- // Index the built checkpoints by number so we can ingest them in calldata order, slotting in the
1019
- // promoted checkpoint (built from a local proposed block rather than blobs).
1020
- const publishedByNumber = new Map(
1021
- blobFetched.map(publishedCheckpoint => [publishedCheckpoint.checkpoint.number, publishedCheckpoint]),
1022
- );
1023
- if (checkpointToPromote) {
1024
- publishedByNumber.set(checkpointToPromote.checkpoint.number, checkpointToPromote);
1025
- }
1077
+ const [processDuration, result] = await elapsed(() =>
1078
+ execInSpan(this.tracer, 'Archiver.addCheckpoints', async () => {
1079
+ const addResult = await this.updater.addCheckpoints(
1080
+ checkpointsToAdd,
1081
+ updatedValidationResult,
1082
+ maybeValidCheckpointToPromote && {
1083
+ l1: lastCalldataCheckpoint.l1,
1084
+ attestations: lastCalldataCheckpoint.attestations,
1085
+ verbatimAttestations: lastCalldataCheckpoint.verbatimAttestations,
1086
+ checkpoint: maybeValidCheckpointToPromote,
1087
+ },
1088
+ evictProposedFrom,
1089
+ );
1090
+ // Set before the span wrappers finish, so a throw while closing the span doesn't count as unpersisted.
1091
+ batchPersisted = true;
1092
+ return addResult;
1093
+ }),
1094
+ );
1026
1095
 
1027
- const validCheckpoints: PublishedCheckpoint[] = [];
1028
- for (const calldataCheckpoint of checkpointsToIngest) {
1029
- const published = publishedByNumber.get(calldataCheckpoint.checkpointNumber)!;
1096
+ if (validCheckpoints.length > 0) {
1097
+ this.instrumentation.processNewCheckpointedBlocks(
1098
+ processDuration / validCheckpoints.length,
1099
+ validCheckpoints.flatMap(c => c.checkpoint.blocks),
1100
+ );
1101
+ }
1030
1102
 
1031
- validCheckpoints.push(published);
1032
- this.log.debug(
1033
- `Ingesting new checkpoint ${published.checkpoint.number} with ${published.checkpoint.blocks.length} blocks`,
1034
- {
1035
- checkpointHash: published.checkpoint.hash(),
1036
- l1BlockNumber: published.l1.blockNumber,
1037
- ...published.checkpoint.header.toInspect(),
1038
- blocks: published.checkpoint.blocks.map(b => b.getStats()),
1039
- },
1040
- );
1041
- }
1103
+ // Record blocks newly fetched from L1 checkpoint payloads as added. The promoted checkpoint (if any) is
1104
+ // excluded: its blocks were already in local archiver storage (added via the proposed-block path) so the
1105
+ // block stream does not need them re-downloaded.
1106
+ blocksAdded.push(...checkpointsToAdd.flatMap(c => c.checkpoint.blocks));
1042
1107
 
1043
- for (const published of validCheckpoints) {
1044
- this.instrumentation.processCheckpointL1Timing({
1045
- slotNumber: published.checkpoint.header.slotNumber,
1046
- l1Timestamp: published.l1.timestamp,
1047
- l1Constants: this.l1Constants,
1048
- });
1049
- }
1108
+ // If blocks were pruned due to conflict with L1 checkpoints, emit event
1109
+ if (result.prunedBlocks && result.prunedBlocks.length > 0) {
1110
+ const prunedCheckpointNumber = result.prunedBlocks[0].checkpointNumber;
1111
+ const prunedSlotNumber = result.prunedBlocks[0].header.globalVariables.slotNumber;
1050
1112
 
1051
- try {
1052
- const updatedValidationResult =
1053
- rollupStatus.validationResult === initialValidationResult ? undefined : rollupStatus.validationResult;
1054
-
1055
- // Split valid checkpoints: the promoted one (if any) is persisted via the proposed-promotion path,
1056
- // the rest via addCheckpoints. Both paths run within the same store transaction for atomicity.
1057
- const [[maybeValidCheckpointToPromote], checkpointsToAdd] = partition(
1058
- validCheckpoints,
1059
- c => c.checkpoint.number === checkpointToPromote?.checkpoint.number,
1060
- );
1113
+ this.log.info(
1114
+ `Pruned ${result.prunedBlocks.length} mismatching blocks for checkpoint ${prunedCheckpointNumber}`,
1115
+ { prunedBlocks: result.prunedBlocks.map(b => b.toBlockInfo()), prunedSlotNumber, prunedCheckpointNumber },
1116
+ );
1061
1117
 
1062
- const [processDuration, result] = await elapsed(() =>
1063
- execInSpan(this.tracer, 'Archiver.addCheckpoints', () =>
1064
- this.updater.addCheckpoints(
1065
- checkpointsToAdd,
1066
- updatedValidationResult,
1067
- maybeValidCheckpointToPromote && {
1068
- l1: lastCalldataCheckpoint.l1,
1069
- attestations: lastCalldataCheckpoint.attestations,
1070
- verbatimAttestations: lastCalldataCheckpoint.verbatimAttestations,
1071
- checkpoint: maybeValidCheckpointToPromote,
1072
- },
1073
- evictProposedFrom,
1074
- ),
1075
- ),
1076
- );
1118
+ this.instrumentation.recordPrune('l1_conflict');
1077
1119
 
1078
- if (validCheckpoints.length > 0) {
1079
- this.instrumentation.processNewCheckpointedBlocks(
1080
- processDuration / validCheckpoints.length,
1081
- validCheckpoints.flatMap(c => c.checkpoint.blocks),
1082
- );
1120
+ // Emit event for listening services to react to the prune.
1121
+ // Note: slotNumber comes from the first pruned block. If pruned blocks theoretically spanned multiple slots,
1122
+ // only one slot number would be reported (though in practice all blocks in a checkpoint span a single slot).
1123
+ this.events.emit(L2BlockSourceEvents.L2PruneUncheckpointed, {
1124
+ type: L2BlockSourceEvents.L2PruneUncheckpointed,
1125
+ slotNumber: prunedSlotNumber,
1126
+ blocks: result.prunedBlocks,
1127
+ });
1128
+ }
1129
+ } catch (err) {
1130
+ if (err instanceof InitialCheckpointNumberNotSequentialError) {
1131
+ const { previousCheckpointNumber, newCheckpointNumber } = err;
1132
+ const previousCheckpoint = previousCheckpointNumber
1133
+ ? await this.stores.blocks.getCheckpointData(CheckpointNumber(previousCheckpointNumber))
1134
+ : undefined;
1135
+ const lastFinalizedCheckpoint = await this.stores.blocks.getCheckpointData(
1136
+ await this.stores.blocks.getFinalizedCheckpointNumber(),
1137
+ );
1138
+ const updatedL1SyncPoint =
1139
+ previousCheckpoint?.l1.blockNumber ??
1140
+ lastFinalizedCheckpoint?.l1.blockNumber ??
1141
+ this.l1Constants.l1StartBlock;
1142
+ await this.stores.blocks.setSynchedL1BlockNumber(updatedL1SyncPoint);
1143
+ this.log.warn(
1144
+ `Attempting to insert checkpoint ${newCheckpointNumber} with previous block ${previousCheckpointNumber}. Rolling back L1 sync point to ${updatedL1SyncPoint} to try and fetch the missing blocks.`,
1145
+ {
1146
+ previousCheckpointNumber,
1147
+ previousCheckpoint: previousCheckpoint?.header.toInspect(),
1148
+ lastFinalizedCheckpoint: lastFinalizedCheckpoint?.header.toInspect(),
1149
+ l1StartBlock: this.l1Constants.l1StartBlock,
1150
+ newCheckpointNumber,
1151
+ updatedL1SyncPoint,
1152
+ },
1153
+ );
1154
+ }
1155
+ throw err;
1083
1156
  }
1084
1157
 
1085
- // Record blocks newly fetched from L1 checkpoint payloads as added. The promoted checkpoint (if any) is
1086
- // excluded: its blocks were already in local archiver storage (added via the proposed-block path) so the
1087
- // block stream does not need them re-downloaded.
1088
- blocksAdded.push(...checkpointsToAdd.flatMap(c => c.checkpoint.blocks));
1089
-
1090
- // If blocks were pruned due to conflict with L1 checkpoints, emit event
1091
- if (result.prunedBlocks && result.prunedBlocks.length > 0) {
1092
- const prunedCheckpointNumber = result.prunedBlocks[0].checkpointNumber;
1093
- const prunedSlotNumber = result.prunedBlocks[0].header.globalVariables.slotNumber;
1094
-
1095
- this.log.info(
1096
- `Pruned ${result.prunedBlocks.length} mismatching blocks for checkpoint ${prunedCheckpointNumber}`,
1097
- { prunedBlocks: result.prunedBlocks.map(b => b.toBlockInfo()), prunedSlotNumber, prunedCheckpointNumber },
1098
- );
1099
-
1100
- this.instrumentation.recordPrune('l1_conflict');
1101
-
1102
- // Emit event for listening services to react to the prune.
1103
- // Note: slotNumber comes from the first pruned block. If pruned blocks theoretically spanned multiple slots,
1104
- // only one slot number would be reported (though in practice all blocks in a checkpoint span a single slot).
1105
- this.events.emit(L2BlockSourceEvents.L2PruneUncheckpointed, {
1106
- type: L2BlockSourceEvents.L2PruneUncheckpointed,
1107
- slotNumber: prunedSlotNumber,
1108
- blocks: result.prunedBlocks,
1158
+ for (const checkpoint of validCheckpoints) {
1159
+ this.log.info(`Downloaded checkpoint ${checkpoint.checkpoint.number}`, {
1160
+ checkpointHash: checkpoint.checkpoint.hash(),
1161
+ checkpointNumber: checkpoint.checkpoint.number,
1162
+ blockCount: checkpoint.checkpoint.blocks.length,
1163
+ txCount: checkpoint.checkpoint.blocks.reduce((acc, b) => acc + b.body.txEffects.length, 0),
1164
+ header: checkpoint.checkpoint.header.toInspect(),
1165
+ archiveRoot: checkpoint.checkpoint.archive.root.toString(),
1166
+ archiveNextLeafIndex: checkpoint.checkpoint.archive.nextAvailableLeafIndex,
1109
1167
  });
1110
1168
  }
1169
+ lastRetrievedCheckpoint = validCheckpoints.at(-1) ?? lastRetrievedCheckpoint;
1170
+ // The last checkpoint seen on L1 this batch (valid or rejected), tracked from calldata since
1171
+ // rejected checkpoints are no longer built into PublishedCheckpoints.
1172
+ lastSeenCheckpoint = lastCalldataCheckpoint;
1111
1173
  } catch (err) {
1112
- if (err instanceof InitialCheckpointNumberNotSequentialError) {
1113
- const { previousCheckpointNumber, newCheckpointNumber } = err;
1114
- const previousCheckpoint = previousCheckpointNumber
1115
- ? await this.stores.blocks.getCheckpointData(CheckpointNumber(previousCheckpointNumber))
1116
- : undefined;
1117
- const lastFinalizedCheckpoint = await this.stores.blocks.getCheckpointData(
1118
- await this.stores.blocks.getFinalizedCheckpointNumber(),
1119
- );
1120
- const updatedL1SyncPoint =
1121
- previousCheckpoint?.l1.blockNumber ??
1122
- lastFinalizedCheckpoint?.l1.blockNumber ??
1123
- this.l1Constants.l1StartBlock;
1124
- await this.stores.blocks.setSynchedL1BlockNumber(updatedL1SyncPoint);
1125
- this.log.warn(
1126
- `Attempting to insert checkpoint ${newCheckpointNumber} with previous block ${previousCheckpointNumber}. Rolling back L1 sync point to ${updatedL1SyncPoint} to try and fetch the missing blocks.`,
1127
- {
1128
- previousCheckpointNumber,
1129
- previousCheckpoint: previousCheckpoint?.header.toInspect(),
1130
- lastFinalizedCheckpoint: lastFinalizedCheckpoint?.header.toInspect(),
1131
- l1StartBlock: this.l1Constants.l1StartBlock,
1132
- newCheckpointNumber,
1133
- updatedL1SyncPoint,
1134
- },
1135
- );
1174
+ const restoreTo = syncPointBeforeBatch ?? this.l1Constants.l1StartBlock;
1175
+ const currentSyncPoint = await this.stores.blocks.getSynchedL1BlockNumber();
1176
+ // Only ever lower it, so a deeper rewind made while handling the error (e.g. on a checkpoint gap) is kept.
1177
+ if (!batchPersisted && currentSyncPoint !== undefined && currentSyncPoint > restoreTo) {
1178
+ this.log.warn(`Restoring L1 sync point to ${restoreTo} after failing to process checkpoints`, {
1179
+ currentSyncPoint,
1180
+ restoreTo,
1181
+ searchStartBlock,
1182
+ searchEndBlock,
1183
+ });
1184
+ await this.stores.blocks.setSynchedL1BlockNumber(restoreTo);
1136
1185
  }
1137
1186
  throw err;
1138
1187
  }
1139
-
1140
- for (const checkpoint of validCheckpoints) {
1141
- this.log.info(`Downloaded checkpoint ${checkpoint.checkpoint.number}`, {
1142
- checkpointHash: checkpoint.checkpoint.hash(),
1143
- checkpointNumber: checkpoint.checkpoint.number,
1144
- blockCount: checkpoint.checkpoint.blocks.length,
1145
- txCount: checkpoint.checkpoint.blocks.reduce((acc, b) => acc + b.body.txEffects.length, 0),
1146
- header: checkpoint.checkpoint.header.toInspect(),
1147
- archiveRoot: checkpoint.checkpoint.archive.root.toString(),
1148
- archiveNextLeafIndex: checkpoint.checkpoint.archive.nextAvailableLeafIndex,
1149
- });
1150
- }
1151
- lastRetrievedCheckpoint = validCheckpoints.at(-1) ?? lastRetrievedCheckpoint;
1152
- // The last checkpoint seen on L1 this batch (valid or rejected), tracked from calldata since
1153
- // rejected checkpoints are no longer built into PublishedCheckpoints.
1154
- lastSeenCheckpoint = lastCalldataCheckpoint;
1155
1188
  } while (searchEndBlock < currentL1BlockNumber);
1156
1189
 
1157
1190
  // Important that we update AFTER inserting the blocks.
@@ -1169,7 +1202,7 @@ export class ArchiverL1Synchronizer implements Traceable {
1169
1202
  * (those entries chain off the now-invalid local state) within the same addCheckpoints transaction.
1170
1203
  */
1171
1204
  private async tryBuildPublishedCheckpointFromProposed(
1172
- calldataCheckpoint: RetrievedCheckpointFromCalldata | undefined,
1205
+ calldataCheckpoint: ResolvedCheckpointFromCalldata | undefined,
1173
1206
  ): Promise<PublishedCheckpoint | { diverged: true; fromCheckpointNumber: CheckpointNumber } | undefined> {
1174
1207
  if (this.config.skipPromoteProposedCheckpointDuringL1Sync || !calldataCheckpoint) {
1175
1208
  return undefined;