@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.
- package/README.md +7 -1
- package/dest/errors.d.ts +20 -1
- package/dest/errors.d.ts.map +1 -1
- package/dest/errors.js +22 -0
- package/dest/l1/bin/retrieve-calldata.js +14 -4
- package/dest/l1/calldata_retriever.d.ts +15 -17
- package/dest/l1/calldata_retriever.d.ts.map +1 -1
- package/dest/l1/calldata_retriever.js +2 -9
- package/dest/l1/data_retrieval.d.ts +7 -3
- package/dest/l1/data_retrieval.d.ts.map +1 -1
- package/dest/l1/data_retrieval.js +14 -9
- package/dest/modules/data_store_updater.js +5 -5
- package/dest/modules/l1_synchronizer.d.ts +1 -1
- package/dest/modules/l1_synchronizer.d.ts.map +1 -1
- package/dest/modules/l1_synchronizer.js +228 -191
- package/dest/modules/validation.d.ts +14 -8
- package/dest/modules/validation.d.ts.map +1 -1
- package/dest/modules/validation.js +72 -22
- package/dest/store/block_store.d.ts +2 -1
- package/dest/store/block_store.d.ts.map +1 -1
- package/dest/store/block_store.js +34 -14
- package/dest/store/contract_class_store.d.ts +1 -1
- package/dest/store/contract_class_store.d.ts.map +1 -1
- package/dest/store/contract_class_store.js +4 -2
- package/dest/store/contract_instance_store.d.ts +5 -4
- package/dest/store/contract_instance_store.d.ts.map +1 -1
- package/dest/store/contract_instance_store.js +20 -9
- package/dest/test/fake_l1_state.d.ts +3 -1
- package/dest/test/fake_l1_state.d.ts.map +1 -1
- package/dest/test/fake_l1_state.js +4 -4
- package/package.json +13 -13
- package/src/errors.ts +32 -0
- package/src/l1/bin/retrieve-calldata.ts +15 -4
- package/src/l1/calldata_retriever.ts +14 -20
- package/src/l1/data_retrieval.ts +22 -14
- package/src/modules/data_store_updater.ts +4 -4
- package/src/modules/l1_synchronizer.ts +294 -261
- package/src/modules/validation.ts +87 -28
- package/src/store/block_store.ts +41 -13
- package/src/store/contract_class_store.ts +4 -4
- package/src/store/contract_instance_store.ts +24 -17
- 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
|
|
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 {
|
|
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
|
-
//
|
|
867
|
-
//
|
|
868
|
-
//
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
const
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
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
|
-
//
|
|
897
|
-
//
|
|
898
|
-
//
|
|
899
|
-
|
|
900
|
-
const
|
|
901
|
-
|
|
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
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
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
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
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
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
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
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
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
|
-
|
|
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
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
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
|
-
|
|
963
|
-
l1BlockNumber:
|
|
964
|
-
|
|
965
|
-
|
|
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
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
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
|
-
|
|
993
|
-
|
|
1066
|
+
try {
|
|
1067
|
+
const updatedValidationResult =
|
|
1068
|
+
rollupStatus.validationResult === initialValidationResult ? undefined : rollupStatus.validationResult;
|
|
994
1069
|
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
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
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
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
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
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
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
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
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
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
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
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
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
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
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
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:
|
|
1205
|
+
calldataCheckpoint: ResolvedCheckpointFromCalldata | undefined,
|
|
1173
1206
|
): Promise<PublishedCheckpoint | { diverged: true; fromCheckpointNumber: CheckpointNumber } | undefined> {
|
|
1174
1207
|
if (this.config.skipPromoteProposedCheckpointDuringL1Sync || !calldataCheckpoint) {
|
|
1175
1208
|
return undefined;
|