@aztec/archiver 0.0.1-commit.2f68f620 → 0.0.1-commit.3100065
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 +1 -3
- package/dest/archiver.d.ts +22 -4
- package/dest/archiver.d.ts.map +1 -1
- package/dest/archiver.js +79 -35
- package/dest/config.d.ts +5 -4
- package/dest/config.d.ts.map +1 -1
- package/dest/config.js +27 -5
- package/dest/factory.d.ts +8 -2
- package/dest/factory.d.ts.map +1 -1
- package/dest/factory.js +37 -3
- package/dest/l1/calldata_retriever.d.ts +8 -1
- package/dest/l1/calldata_retriever.d.ts.map +1 -1
- package/dest/l1/calldata_retriever.js +7 -6
- package/dest/l1/data_retrieval.d.ts +7 -2
- package/dest/l1/data_retrieval.d.ts.map +1 -1
- package/dest/modules/contract_data_source_adapter.d.ts +2 -2
- package/dest/modules/contract_data_source_adapter.d.ts.map +1 -1
- package/dest/modules/contract_data_source_adapter.js +1 -9
- package/dest/modules/data_source_base.d.ts +2 -3
- package/dest/modules/data_source_base.d.ts.map +1 -1
- package/dest/modules/data_source_base.js +9 -35
- package/dest/modules/data_store_updater.d.ts +1 -1
- package/dest/modules/data_store_updater.d.ts.map +1 -1
- package/dest/modules/data_store_updater.js +11 -5
- package/dest/modules/instrumentation.d.ts +9 -1
- package/dest/modules/instrumentation.d.ts.map +1 -1
- package/dest/modules/instrumentation.js +23 -2
- package/dest/modules/l1_synchronizer.d.ts +3 -3
- package/dest/modules/l1_synchronizer.d.ts.map +1 -1
- package/dest/modules/l1_synchronizer.js +78 -41
- package/dest/modules/validation.d.ts +29 -7
- package/dest/modules/validation.d.ts.map +1 -1
- package/dest/modules/validation.js +31 -13
- package/dest/store/block_store.d.ts +20 -11
- package/dest/store/block_store.d.ts.map +1 -1
- package/dest/store/block_store.js +43 -25
- 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 +19 -1
- package/dest/store/contract_instance_store.d.ts +1 -1
- package/dest/store/contract_instance_store.d.ts.map +1 -1
- package/dest/store/contract_instance_store.js +18 -1
- package/dest/store/log_store.d.ts +1 -1
- package/dest/store/log_store.d.ts.map +1 -1
- package/dest/store/log_store.js +3 -3
- package/dest/store/log_store_codec.d.ts +9 -1
- package/dest/store/log_store_codec.d.ts.map +1 -1
- package/dest/store/log_store_codec.js +9 -0
- package/dest/test/fake_l1_state.js +3 -3
- package/dest/test/mock_l1_to_l2_message_source.d.ts +1 -1
- package/dest/test/mock_l1_to_l2_message_source.d.ts.map +1 -1
- package/dest/test/mock_l1_to_l2_message_source.js +1 -2
- package/dest/test/mock_l2_block_source.d.ts +1 -4
- package/dest/test/mock_l2_block_source.d.ts.map +1 -1
- package/dest/test/mock_l2_block_source.js +8 -22
- package/dest/test/mock_structs.d.ts +1 -1
- package/dest/test/mock_structs.d.ts.map +1 -1
- package/dest/test/mock_structs.js +2 -2
- package/dest/test/noop_l1_archiver.d.ts +7 -3
- package/dest/test/noop_l1_archiver.d.ts.map +1 -1
- package/dest/test/noop_l1_archiver.js +9 -6
- package/package.json +14 -13
- package/src/archiver.ts +107 -41
- package/src/config.ts +34 -9
- package/src/factory.ts +46 -4
- package/src/l1/calldata_retriever.ts +13 -6
- package/src/l1/data_retrieval.ts +8 -1
- package/src/modules/contract_data_source_adapter.ts +1 -10
- package/src/modules/data_source_base.ts +9 -38
- package/src/modules/data_store_updater.ts +15 -5
- package/src/modules/instrumentation.ts +15 -2
- package/src/modules/l1_synchronizer.ts +120 -57
- package/src/modules/validation.ts +71 -15
- package/src/store/block_store.ts +42 -31
- package/src/store/contract_class_store.ts +19 -1
- package/src/store/contract_instance_store.ts +18 -1
- package/src/store/log_store.ts +3 -2
- package/src/store/log_store_codec.ts +11 -0
- package/src/test/fake_l1_state.ts +3 -3
- package/src/test/mock_l1_to_l2_message_source.ts +0 -1
- package/src/test/mock_l2_block_source.ts +8 -22
- package/src/test/mock_structs.ts +5 -2
- package/src/test/noop_l1_archiver.ts +15 -5
|
@@ -16,8 +16,19 @@ import { retryTimes } from '@aztec/foundation/retry';
|
|
|
16
16
|
import { count } from '@aztec/foundation/string';
|
|
17
17
|
import { DateProvider, Timer, elapsed } from '@aztec/foundation/timer';
|
|
18
18
|
import { isDefined, isErrorClass } from '@aztec/foundation/types';
|
|
19
|
-
import {
|
|
20
|
-
|
|
19
|
+
import {
|
|
20
|
+
type ArchiverEmitter,
|
|
21
|
+
type L2Block,
|
|
22
|
+
L2BlockSourceEvents,
|
|
23
|
+
type ValidateCheckpointResult,
|
|
24
|
+
} from '@aztec/stdlib/block';
|
|
25
|
+
import {
|
|
26
|
+
Checkpoint,
|
|
27
|
+
type CheckpointData,
|
|
28
|
+
type CheckpointInfo,
|
|
29
|
+
type L1PublishedData,
|
|
30
|
+
PublishedCheckpoint,
|
|
31
|
+
} from '@aztec/stdlib/checkpoint';
|
|
21
32
|
import { type L1RollupConstants, getEpochAtSlot, getSlotAtNextL1Block } from '@aztec/stdlib/epoch-helpers';
|
|
22
33
|
import { computeInHashFromL1ToL2Messages } from '@aztec/stdlib/messaging';
|
|
23
34
|
import type { CoordinationSignatureContext } from '@aztec/stdlib/p2p';
|
|
@@ -39,7 +50,7 @@ import { MessageStoreError } from '../store/message_store.js';
|
|
|
39
50
|
import type { InboxMessage } from '../structs/inbox_message.js';
|
|
40
51
|
import { ArchiverDataStoreUpdater } from './data_store_updater.js';
|
|
41
52
|
import type { ArchiverInstrumentation } from './instrumentation.js';
|
|
42
|
-
import {
|
|
53
|
+
import { validateCheckpointAttestationsFromCalldata } from './validation.js';
|
|
43
54
|
|
|
44
55
|
type RollupStatus = {
|
|
45
56
|
provenCheckpointNumber: CheckpointNumber;
|
|
@@ -50,7 +61,9 @@ type RollupStatus = {
|
|
|
50
61
|
/** Last valid checkpoint observed on L1 and synced on this iteration */
|
|
51
62
|
lastRetrievedCheckpoint?: PublishedCheckpoint;
|
|
52
63
|
/** Last checkpoint observed on L1 across both valid and rejected entries on this iteration */
|
|
53
|
-
lastSeenCheckpoint?:
|
|
64
|
+
lastSeenCheckpoint?: { checkpointNumber: CheckpointNumber; l1: L1PublishedData };
|
|
65
|
+
/** Blocks added while handling checkpoints this iteration, for the aggregate block source update event. */
|
|
66
|
+
blocksAdded: L2Block[];
|
|
54
67
|
};
|
|
55
68
|
|
|
56
69
|
/**
|
|
@@ -139,7 +152,9 @@ export class ArchiverL1Synchronizer implements Traceable {
|
|
|
139
152
|
}
|
|
140
153
|
|
|
141
154
|
@trackSpan('Archiver.syncFromL1')
|
|
142
|
-
public async syncFromL1(initialSyncComplete: boolean): Promise<
|
|
155
|
+
public async syncFromL1(initialSyncComplete: boolean): Promise<L2Block[]> {
|
|
156
|
+
const blocksAdded: L2Block[] = [];
|
|
157
|
+
|
|
143
158
|
// In between the various calls to L1, the block number can move meaning some of the following
|
|
144
159
|
// calls will return data for blocks that were not present during earlier calls. To combat this
|
|
145
160
|
// we ensure that all data retrieval methods only retrieve data up to the currentBlockNumber
|
|
@@ -152,7 +167,7 @@ export class ArchiverL1Synchronizer implements Traceable {
|
|
|
152
167
|
|
|
153
168
|
if (this.l1BlockHash && currentL1BlockHash.equals(this.l1BlockHash)) {
|
|
154
169
|
this.log.trace(`No new L1 blocks since last sync at L1 block ${this.l1BlockNumber}`);
|
|
155
|
-
return;
|
|
170
|
+
return blocksAdded;
|
|
156
171
|
}
|
|
157
172
|
|
|
158
173
|
// Log at error if the latest L1 block timestamp is too old
|
|
@@ -190,6 +205,7 @@ export class ArchiverL1Synchronizer implements Traceable {
|
|
|
190
205
|
// First we retrieve new checkpoints and L2 blocks and store them in the DB. This will also update the
|
|
191
206
|
// pending chain validation status, proven checkpoint number, and synched L1 block number.
|
|
192
207
|
const rollupStatus = await this.handleCheckpoints(blocksSynchedTo, currentL1BlockNumber, initialSyncComplete);
|
|
208
|
+
blocksAdded.push(...rollupStatus.blocksAdded);
|
|
193
209
|
|
|
194
210
|
// Then we try pruning uncheckpointed blocks if a new slot was mined without checkpoints
|
|
195
211
|
await this.pruneUncheckpointedBlocks(currentL1Timestamp);
|
|
@@ -231,6 +247,8 @@ export class ArchiverL1Synchronizer implements Traceable {
|
|
|
231
247
|
l1TimestampAtStart: currentL1Timestamp,
|
|
232
248
|
l1BlockNumberAtEnd,
|
|
233
249
|
});
|
|
250
|
+
|
|
251
|
+
return blocksAdded;
|
|
234
252
|
}
|
|
235
253
|
|
|
236
254
|
/** Updates the finalized checkpoint using the pre-fetched finalized L1 block from the current sync iteration. */
|
|
@@ -261,7 +279,7 @@ export class ArchiverL1Synchronizer implements Traceable {
|
|
|
261
279
|
}
|
|
262
280
|
|
|
263
281
|
/** Prune all proposed local blocks that should have been checkpointed by now. */
|
|
264
|
-
private async pruneUncheckpointedBlocks(currentL1Timestamp: bigint) {
|
|
282
|
+
private async pruneUncheckpointedBlocks(currentL1Timestamp: bigint): Promise<void> {
|
|
265
283
|
const [lastCheckpointedBlockNumber, lastProposedBlockNumber] = await Promise.all([
|
|
266
284
|
this.stores.blocks.getCheckpointedL2BlockNumber(),
|
|
267
285
|
this.stores.blocks.getLatestL2BlockNumber(),
|
|
@@ -296,6 +314,7 @@ export class ArchiverL1Synchronizer implements Traceable {
|
|
|
296
314
|
|
|
297
315
|
const prunedBlocks = await this.updater.removeUncheckpointedBlocksAfter(lastCheckpointedBlockNumber);
|
|
298
316
|
if (prunedBlocks.length > 0) {
|
|
317
|
+
this.instrumentation.recordPrune('uncheckpointed');
|
|
299
318
|
this.events.emit(L2BlockSourceEvents.L2PruneUncheckpointed, {
|
|
300
319
|
type: L2BlockSourceEvents.L2PruneUncheckpointed,
|
|
301
320
|
slotNumber: firstUncheckpointedBlockSlot,
|
|
@@ -616,6 +635,7 @@ export class ArchiverL1Synchronizer implements Traceable {
|
|
|
616
635
|
currentL1BlockNumber: bigint,
|
|
617
636
|
initialSyncComplete: boolean,
|
|
618
637
|
): Promise<RollupStatus> {
|
|
638
|
+
const blocksAdded: L2Block[] = [];
|
|
619
639
|
const localPendingCheckpointNumber = await this.stores.blocks.getLatestCheckpointNumber();
|
|
620
640
|
const initialValidationResult: ValidateCheckpointResult | undefined =
|
|
621
641
|
await this.stores.blocks.getPendingChainValidationStatus();
|
|
@@ -634,6 +654,7 @@ export class ArchiverL1Synchronizer implements Traceable {
|
|
|
634
654
|
pendingCheckpointNumber,
|
|
635
655
|
pendingArchive: pendingArchive.toString(),
|
|
636
656
|
validationResult: initialValidationResult,
|
|
657
|
+
blocksAdded,
|
|
637
658
|
};
|
|
638
659
|
this.log.trace(`Retrieved rollup status at current L1 block ${currentL1BlockNumber}.`, {
|
|
639
660
|
localPendingCheckpointNumber,
|
|
@@ -776,6 +797,9 @@ export class ArchiverL1Synchronizer implements Traceable {
|
|
|
776
797
|
|
|
777
798
|
const checkpointsToRemove = localPendingCheckpointNumber - tipAfterUnwind;
|
|
778
799
|
await this.updater.removeCheckpointsAfter(CheckpointNumber(tipAfterUnwind));
|
|
800
|
+
if (checkpointsToRemove > 0) {
|
|
801
|
+
this.instrumentation.recordPrune('l1_mismatch');
|
|
802
|
+
}
|
|
779
803
|
|
|
780
804
|
this.log.warn(
|
|
781
805
|
`Removed ${count(checkpointsToRemove, 'checkpoint')} after checkpoint ${tipAfterUnwind} ` +
|
|
@@ -790,7 +814,7 @@ export class ArchiverL1Synchronizer implements Traceable {
|
|
|
790
814
|
let searchStartBlock: bigint = blocksSynchedTo;
|
|
791
815
|
let searchEndBlock: bigint = blocksSynchedTo;
|
|
792
816
|
let lastRetrievedCheckpoint: PublishedCheckpoint | undefined;
|
|
793
|
-
let lastSeenCheckpoint:
|
|
817
|
+
let lastSeenCheckpoint: { checkpointNumber: CheckpointNumber; l1: L1PublishedData } | undefined;
|
|
794
818
|
|
|
795
819
|
do {
|
|
796
820
|
[searchStartBlock, searchEndBlock] = this.nextRange(searchEndBlock, currentL1BlockNumber);
|
|
@@ -835,37 +859,21 @@ export class ArchiverL1Synchronizer implements Traceable {
|
|
|
835
859
|
const evictProposedFrom =
|
|
836
860
|
promoteResult && 'diverged' in promoteResult ? promoteResult.fromCheckpointNumber : undefined;
|
|
837
861
|
|
|
838
|
-
//
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
this.blobClient,
|
|
845
|
-
checkpoint.l1.blockHash,
|
|
846
|
-
checkpoint.blobHashes,
|
|
847
|
-
checkpoint.checkpointNumber,
|
|
848
|
-
this.log,
|
|
849
|
-
!initialSyncComplete,
|
|
850
|
-
checkpoint.parentBeaconBlockRoot,
|
|
851
|
-
checkpoint.l1.timestamp,
|
|
852
|
-
),
|
|
853
|
-
}),
|
|
854
|
-
);
|
|
855
|
-
|
|
856
|
-
// And add the promoted checkpoint to the list of all checkpoints
|
|
857
|
-
const publishedCheckpoints = checkpointToPromote ? [...blobFetched, checkpointToPromote] : blobFetched;
|
|
858
|
-
const validCheckpoints: PublishedCheckpoint[] = [];
|
|
862
|
+
// Validate attestations from CALLDATA before fetching any blobs. A checkpoint with invalid
|
|
863
|
+
// attestations (or one descending from a rejected ancestor) is rejected here without fetching its
|
|
864
|
+
// blobs, so a malformed blob does not throw during decode before the rejection path runs and
|
|
865
|
+
// stall sync. The signed consensus payload (header, archive root, fee asset price
|
|
866
|
+
// modifier) is fully available from calldata.
|
|
867
|
+
const checkpointsToIngest: RetrievedCheckpointFromCalldata[] = [];
|
|
859
868
|
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
// Check the attestations uploaded by the publisher to L1 are correct
|
|
869
|
+
for (const calldataCheckpoint of calldataCheckpoints) {
|
|
870
|
+
// Check the attestations uploaded by the publisher to L1 are correct.
|
|
863
871
|
// Rollup contract does not validate attestations to save on gas, so this
|
|
864
872
|
// falls on the nodes to verify offchain and skip those checkpoints.
|
|
865
873
|
const validationResult = this.config.skipValidateCheckpointAttestations
|
|
866
874
|
? { valid: true as const }
|
|
867
|
-
: await
|
|
868
|
-
|
|
875
|
+
: await validateCheckpointAttestationsFromCalldata(
|
|
876
|
+
calldataCheckpoint,
|
|
869
877
|
this.epochCache,
|
|
870
878
|
this.l1Constants,
|
|
871
879
|
this.getSignatureContext(),
|
|
@@ -877,7 +885,7 @@ export class ArchiverL1Synchronizer implements Traceable {
|
|
|
877
885
|
// ancestor was skipped earlier (e.g. due to invalid attestations), the catch handler
|
|
878
886
|
// would roll back the L1 sync point, and the next iteration would re-fetch and re-throw.
|
|
879
887
|
const rejectedAncestor = await this.stores.blocks.getRejectedCheckpointByArchiveRoot(
|
|
880
|
-
|
|
888
|
+
calldataCheckpoint.header.lastArchiveRoot,
|
|
881
889
|
);
|
|
882
890
|
|
|
883
891
|
// Update the validation result if it has changed, so we can keep track of the first invalid checkpoint
|
|
@@ -899,9 +907,9 @@ export class ArchiverL1Synchronizer implements Traceable {
|
|
|
899
907
|
}
|
|
900
908
|
|
|
901
909
|
if (!validationResult.valid) {
|
|
902
|
-
this.log.warn(`Skipping checkpoint ${
|
|
903
|
-
|
|
904
|
-
l1BlockNumber:
|
|
910
|
+
this.log.warn(`Skipping checkpoint ${calldataCheckpoint.checkpointNumber} due to invalid attestations`, {
|
|
911
|
+
checkpointNumber: calldataCheckpoint.checkpointNumber,
|
|
912
|
+
l1BlockNumber: calldataCheckpoint.l1.blockNumber,
|
|
905
913
|
...pick(validationResult, 'reason'),
|
|
906
914
|
});
|
|
907
915
|
|
|
@@ -915,11 +923,11 @@ export class ArchiverL1Synchronizer implements Traceable {
|
|
|
915
923
|
// is detected and skipped (rather than tripping the addCheckpoints consecutive-number
|
|
916
924
|
// check and causing the sync point to roll back in a loop).
|
|
917
925
|
await this.stores.blocks.addRejectedCheckpoint({
|
|
918
|
-
checkpointNumber:
|
|
919
|
-
archiveRoot:
|
|
920
|
-
parentArchiveRoot:
|
|
921
|
-
slotNumber:
|
|
922
|
-
l1:
|
|
926
|
+
checkpointNumber: calldataCheckpoint.checkpointNumber,
|
|
927
|
+
archiveRoot: calldataCheckpoint.archiveRoot,
|
|
928
|
+
parentArchiveRoot: calldataCheckpoint.header.lastArchiveRoot,
|
|
929
|
+
slotNumber: calldataCheckpoint.header.slotNumber,
|
|
930
|
+
l1: calldataCheckpoint.l1,
|
|
923
931
|
reason: 'invalid-attestations' as const,
|
|
924
932
|
});
|
|
925
933
|
|
|
@@ -927,15 +935,20 @@ export class ArchiverL1Synchronizer implements Traceable {
|
|
|
927
935
|
}
|
|
928
936
|
|
|
929
937
|
if (rejectedAncestor) {
|
|
930
|
-
const descendantInfo =
|
|
938
|
+
const descendantInfo: CheckpointInfo = {
|
|
939
|
+
archive: calldataCheckpoint.archiveRoot,
|
|
940
|
+
lastArchive: calldataCheckpoint.header.lastArchiveRoot,
|
|
941
|
+
slotNumber: calldataCheckpoint.header.slotNumber,
|
|
942
|
+
checkpointNumber: calldataCheckpoint.checkpointNumber,
|
|
943
|
+
timestamp: calldataCheckpoint.header.timestamp,
|
|
944
|
+
};
|
|
931
945
|
this.log.warn(
|
|
932
|
-
`Skipping checkpoint ${
|
|
946
|
+
`Skipping checkpoint ${calldataCheckpoint.checkpointNumber} as it is a descendant of ` +
|
|
933
947
|
`rejected checkpoint ${rejectedAncestor.checkpointNumber} (${rejectedAncestor.reason})`,
|
|
934
948
|
{
|
|
935
|
-
checkpointNumber:
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
l1BlockHash: published.l1.blockHash,
|
|
949
|
+
checkpointNumber: calldataCheckpoint.checkpointNumber,
|
|
950
|
+
l1BlockNumber: calldataCheckpoint.l1.blockNumber,
|
|
951
|
+
l1BlockHash: calldataCheckpoint.l1.blockHash,
|
|
939
952
|
ancestorCheckpointNumber: rejectedAncestor.checkpointNumber,
|
|
940
953
|
ancestorArchiveRoot: rejectedAncestor.archiveRoot.toString(),
|
|
941
954
|
ancestorReason: rejectedAncestor.reason,
|
|
@@ -952,17 +965,54 @@ export class ArchiverL1Synchronizer implements Traceable {
|
|
|
952
965
|
// Persist this chainpoint as rejected as well, so we can construct a chain of
|
|
953
966
|
// skipped checkpoints starting from the first one with invalid attestations.
|
|
954
967
|
await this.stores.blocks.addRejectedCheckpoint({
|
|
955
|
-
checkpointNumber:
|
|
956
|
-
archiveRoot:
|
|
957
|
-
parentArchiveRoot:
|
|
958
|
-
slotNumber:
|
|
959
|
-
l1:
|
|
968
|
+
checkpointNumber: calldataCheckpoint.checkpointNumber,
|
|
969
|
+
archiveRoot: calldataCheckpoint.archiveRoot,
|
|
970
|
+
parentArchiveRoot: calldataCheckpoint.header.lastArchiveRoot,
|
|
971
|
+
slotNumber: calldataCheckpoint.header.slotNumber,
|
|
972
|
+
l1: calldataCheckpoint.l1,
|
|
960
973
|
reason: 'descends-from-invalid-attestations' as const,
|
|
961
974
|
});
|
|
962
975
|
|
|
963
976
|
continue;
|
|
964
977
|
}
|
|
965
978
|
|
|
979
|
+
checkpointsToIngest.push(calldataCheckpoint);
|
|
980
|
+
}
|
|
981
|
+
|
|
982
|
+
// Fetch blobs in parallel only for the surviving (attestation-valid, non-descendant) checkpoints,
|
|
983
|
+
// then build the full published checkpoints. The last calldata checkpoint may be promotable from a
|
|
984
|
+
// local proposed block (checkpointToPromote), in which case it carries no blob to fetch. A missing or
|
|
985
|
+
// undecodable blob throws and propagates, rolling back the L1 sync point so the fetch is retried.
|
|
986
|
+
const toFetchBlobs = checkpointToPromote
|
|
987
|
+
? checkpointsToIngest.filter(c => c.checkpointNumber !== checkpointToPromote.checkpoint.number)
|
|
988
|
+
: checkpointsToIngest;
|
|
989
|
+
const blobFetched = await asyncPool(10, toFetchBlobs, async checkpoint =>
|
|
990
|
+
retrievedToPublishedCheckpoint({
|
|
991
|
+
...checkpoint,
|
|
992
|
+
checkpointBlobData: await getCheckpointBlobDataFromBlobs(
|
|
993
|
+
this.blobClient,
|
|
994
|
+
checkpoint.l1.blockHash,
|
|
995
|
+
checkpoint.blobHashes,
|
|
996
|
+
checkpoint.checkpointNumber,
|
|
997
|
+
this.log,
|
|
998
|
+
!initialSyncComplete,
|
|
999
|
+
checkpoint.parentBeaconBlockRoot,
|
|
1000
|
+
checkpoint.l1.timestamp,
|
|
1001
|
+
),
|
|
1002
|
+
}),
|
|
1003
|
+
);
|
|
1004
|
+
|
|
1005
|
+
// Index the built checkpoints by number so we can ingest them in calldata order, slotting in the
|
|
1006
|
+
// promoted checkpoint (built from a local proposed block rather than blobs).
|
|
1007
|
+
const publishedByNumber = new Map(blobFetched.map(published => [published.checkpoint.number, published]));
|
|
1008
|
+
if (checkpointToPromote) {
|
|
1009
|
+
publishedByNumber.set(checkpointToPromote.checkpoint.number, checkpointToPromote);
|
|
1010
|
+
}
|
|
1011
|
+
|
|
1012
|
+
const validCheckpoints: PublishedCheckpoint[] = [];
|
|
1013
|
+
for (const calldataCheckpoint of checkpointsToIngest) {
|
|
1014
|
+
const published = publishedByNumber.get(calldataCheckpoint.checkpointNumber)!;
|
|
1015
|
+
|
|
966
1016
|
// Check the inHash of the checkpoint against the l1->l2 messages.
|
|
967
1017
|
// The messages should've been synced up to the currentL1BlockNumber and must be available for the published
|
|
968
1018
|
// checkpoints we just retrieved.
|
|
@@ -1035,6 +1085,11 @@ export class ArchiverL1Synchronizer implements Traceable {
|
|
|
1035
1085
|
);
|
|
1036
1086
|
}
|
|
1037
1087
|
|
|
1088
|
+
// Record blocks newly fetched from L1 checkpoint payloads as added. The promoted checkpoint (if any) is
|
|
1089
|
+
// excluded: its blocks were already in local archiver storage (added via the proposed-block path) so the
|
|
1090
|
+
// block stream does not need them re-downloaded.
|
|
1091
|
+
blocksAdded.push(...checkpointsToAdd.flatMap(c => c.checkpoint.blocks));
|
|
1092
|
+
|
|
1038
1093
|
// If blocks were pruned due to conflict with L1 checkpoints, emit event
|
|
1039
1094
|
if (result.prunedBlocks && result.prunedBlocks.length > 0) {
|
|
1040
1095
|
const prunedCheckpointNumber = result.prunedBlocks[0].checkpointNumber;
|
|
@@ -1045,6 +1100,8 @@ export class ArchiverL1Synchronizer implements Traceable {
|
|
|
1045
1100
|
{ prunedBlocks: result.prunedBlocks.map(b => b.toBlockInfo()), prunedSlotNumber, prunedCheckpointNumber },
|
|
1046
1101
|
);
|
|
1047
1102
|
|
|
1103
|
+
this.instrumentation.recordPrune('l1_conflict');
|
|
1104
|
+
|
|
1048
1105
|
// Emit event for listening services to react to the prune.
|
|
1049
1106
|
// Note: slotNumber comes from the first pruned block. If pruned blocks theoretically spanned multiple slots,
|
|
1050
1107
|
// only one slot number would be reported (though in practice all blocks in a checkpoint span a single slot).
|
|
@@ -1095,7 +1152,9 @@ export class ArchiverL1Synchronizer implements Traceable {
|
|
|
1095
1152
|
});
|
|
1096
1153
|
}
|
|
1097
1154
|
lastRetrievedCheckpoint = validCheckpoints.at(-1) ?? lastRetrievedCheckpoint;
|
|
1098
|
-
|
|
1155
|
+
// The last checkpoint seen on L1 this batch (valid or rejected), tracked from calldata since
|
|
1156
|
+
// rejected checkpoints are no longer built into PublishedCheckpoints.
|
|
1157
|
+
lastSeenCheckpoint = lastCalldataCheckpoint;
|
|
1099
1158
|
} while (searchEndBlock < currentL1BlockNumber);
|
|
1100
1159
|
|
|
1101
1160
|
// Important that we update AFTER inserting the blocks.
|
|
@@ -1205,7 +1264,7 @@ export class ArchiverL1Synchronizer implements Traceable {
|
|
|
1205
1264
|
// Compare the last checkpoint (valid or not) we have (either retrieved in this round or loaded from store)
|
|
1206
1265
|
// with what the rollup contract told us was the latest one (pinned at the currentL1BlockNumber).
|
|
1207
1266
|
const latestLocalCheckpointNumber =
|
|
1208
|
-
lastSeenCheckpoint?.
|
|
1267
|
+
lastSeenCheckpoint?.checkpointNumber ??
|
|
1209
1268
|
CheckpointNumber.max(
|
|
1210
1269
|
await this.stores.blocks.getLatestCheckpointNumber(),
|
|
1211
1270
|
await this.stores.blocks.getLatestRejectedCheckpointNumber(),
|
|
@@ -1218,7 +1277,11 @@ export class ArchiverL1Synchronizer implements Traceable {
|
|
|
1218
1277
|
// We suspect an L1 reorg that added checkpoints *behind* us. If that is the case, it must have happened between
|
|
1219
1278
|
// the last checkpoint we saw and the current one, so we reset the last synched L1 block number. In the edge case
|
|
1220
1279
|
// we don't have one, we go back 2 L1 epochs, which is the deepest possible reorg (assuming Casper is working).
|
|
1221
|
-
const latestLocalCheckpoint:
|
|
1280
|
+
const latestLocalCheckpoint:
|
|
1281
|
+
| { checkpointNumber: CheckpointNumber; l1: L1PublishedData }
|
|
1282
|
+
| CheckpointData
|
|
1283
|
+
| RejectedCheckpoint
|
|
1284
|
+
| undefined =
|
|
1222
1285
|
lastSeenCheckpoint ??
|
|
1223
1286
|
(await this.stores.blocks.getCheckpointData(latestLocalCheckpointNumber)) ??
|
|
1224
1287
|
(await this.stores.blocks.getRejectedCheckpointByNumber(latestLocalCheckpointNumber));
|
|
@@ -1,16 +1,20 @@
|
|
|
1
1
|
import type { EpochCache } from '@aztec/epoch-cache';
|
|
2
|
-
import {
|
|
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
|
|
31
|
-
*
|
|
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
|
|
34
|
-
|
|
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
|
|
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
|
|
43
|
-
const
|
|
44
|
-
const
|
|
45
|
-
const
|
|
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
|
|
103
|
+
const logData = { checkpointNumber, slot, epoch, headerHash, archiveRoot };
|
|
49
104
|
|
|
50
|
-
logger?.debug(`Validating attestations for checkpoint ${
|
|
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:
|
|
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 ${
|
|
182
|
+
`Checkpoint attestations validated successfully for checkpoint ${checkpointNumber} at slot ${slot}`,
|
|
127
183
|
logData,
|
|
128
184
|
);
|
|
129
185
|
return { valid: true };
|
package/src/store/block_store.ts
CHANGED
|
@@ -306,12 +306,18 @@ export class BlockStore {
|
|
|
306
306
|
|
|
307
307
|
/**
|
|
308
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.
|
|
309
312
|
* @param checkpoints - The L2 checkpoints to be added to the store.
|
|
310
|
-
* @returns
|
|
313
|
+
* @returns The checkpoints that were actually inserted (excluding already-stored ones).
|
|
311
314
|
*/
|
|
312
|
-
async addCheckpoints(
|
|
315
|
+
async addCheckpoints(
|
|
316
|
+
checkpoints: PublishedCheckpoint[],
|
|
317
|
+
opts: { force?: boolean } = {},
|
|
318
|
+
): Promise<PublishedCheckpoint[]> {
|
|
313
319
|
if (checkpoints.length === 0) {
|
|
314
|
-
return
|
|
320
|
+
return [];
|
|
315
321
|
}
|
|
316
322
|
|
|
317
323
|
return await this.db.transactionAsync(async () => {
|
|
@@ -324,7 +330,7 @@ export class BlockStore {
|
|
|
324
330
|
if (!opts.force && firstCheckpointNumber <= previousCheckpointNumber) {
|
|
325
331
|
checkpoints = await this.skipOrUpdateAlreadyStoredCheckpoints(checkpoints, previousCheckpointNumber);
|
|
326
332
|
if (checkpoints.length === 0) {
|
|
327
|
-
return
|
|
333
|
+
return [];
|
|
328
334
|
}
|
|
329
335
|
// Re-check sequentiality after skipping
|
|
330
336
|
const newFirstNumber = checkpoints[0].checkpoint.number;
|
|
@@ -387,7 +393,7 @@ export class BlockStore {
|
|
|
387
393
|
}
|
|
388
394
|
|
|
389
395
|
await this.advanceSynchedL1BlockNumber(checkpoints[checkpoints.length - 1].l1.blockNumber);
|
|
390
|
-
return
|
|
396
|
+
return checkpoints;
|
|
391
397
|
});
|
|
392
398
|
}
|
|
393
399
|
|
|
@@ -621,6 +627,26 @@ export class BlockStore {
|
|
|
621
627
|
return checkpoints;
|
|
622
628
|
}
|
|
623
629
|
|
|
630
|
+
/**
|
|
631
|
+
* Returns up to `limit` checkpoints anchored at `fromSlot`, ordered nearest-first, walking the slot index.
|
|
632
|
+
* With `reverse`, takes the checkpoints at or before `fromSlot` (descending by slot); otherwise the
|
|
633
|
+
* checkpoints at or after it (ascending). `limit: 1, reverse: true` yields the latest checkpoint at or
|
|
634
|
+
* before the slot in a single range scan.
|
|
635
|
+
*/
|
|
636
|
+
async getCheckpointsBySlot(fromSlot: SlotNumber, limit: number, reverse: boolean): Promise<CheckpointData[]> {
|
|
637
|
+
// The KV range bounds are direction-dependent: forward is [start, end), reverse is (start, end], so a
|
|
638
|
+
// reverse scan uses `end: fromSlot` (inclusive) with no +1 to include the checkpoint at fromSlot itself.
|
|
639
|
+
const range = reverse ? { end: fromSlot, reverse: true, limit } : { start: fromSlot, limit };
|
|
640
|
+
const result: CheckpointData[] = [];
|
|
641
|
+
for await (const [, checkpointNumber] of this.#slotToCheckpoint.entriesAsync(range)) {
|
|
642
|
+
const checkpointStorage = await this.#checkpoints.getAsync(checkpointNumber);
|
|
643
|
+
if (checkpointStorage) {
|
|
644
|
+
result.push(this.checkpointDataFromCheckpointStorage(checkpointStorage));
|
|
645
|
+
}
|
|
646
|
+
}
|
|
647
|
+
return result;
|
|
648
|
+
}
|
|
649
|
+
|
|
624
650
|
/** Returns checkpoint data for all checkpoints whose slot falls within the given range (inclusive). */
|
|
625
651
|
async getCheckpointDataForSlotRange(startSlot: SlotNumber, endSlot: SlotNumber): Promise<CheckpointData[]> {
|
|
626
652
|
const result: CheckpointData[] = [];
|
|
@@ -1167,14 +1193,13 @@ export class BlockStore {
|
|
|
1167
1193
|
}
|
|
1168
1194
|
|
|
1169
1195
|
/**
|
|
1170
|
-
* Resolves all
|
|
1171
|
-
*
|
|
1172
|
-
*
|
|
1173
|
-
*
|
|
1174
|
-
*
|
|
1175
|
-
*
|
|
1176
|
-
*
|
|
1177
|
-
* back to checkpointed when no pending checkpoint exists).
|
|
1196
|
+
* Resolves all four L2 chain tips (proposed, checkpointed, proven, finalized) in a single
|
|
1197
|
+
* read-only transaction so the snapshot is internally consistent. Each underlying record is
|
|
1198
|
+
* read at most once: latest block and latest confirmed checkpoint are loaded directly (no
|
|
1199
|
+
* separate "find the number, then look up data" hop), the proven/finalized checkpoint
|
|
1200
|
+
* singletons are read once and their storage entries are reused if they coincide with the
|
|
1201
|
+
* latest checkpoint, and per-tip block hashes are deduped when two tips land on the same block
|
|
1202
|
+
* (e.g. finalized == proven).
|
|
1178
1203
|
*
|
|
1179
1204
|
* The result is guaranteed to satisfy `finalized <= proven <= checkpointed <= proposed` (by
|
|
1180
1205
|
* block number). Genesis is represented by `(INITIAL_L2_BLOCK_NUM - 1)` and the supplied
|
|
@@ -1197,9 +1222,6 @@ export class BlockStore {
|
|
|
1197
1222
|
|
|
1198
1223
|
// Load latest block and checkpoint entries
|
|
1199
1224
|
const [latestBlockEntry] = await toArray(this.#blocks.entriesAsync({ reverse: true, limit: 1 }));
|
|
1200
|
-
const [proposedCheckpointEntry] = await toArray(
|
|
1201
|
-
this.#proposedCheckpoints.entriesAsync({ reverse: true, limit: 1 }),
|
|
1202
|
-
);
|
|
1203
1225
|
const [latestCheckpointEntry] = await toArray(this.#checkpoints.entriesAsync({ reverse: true, limit: 1 }));
|
|
1204
1226
|
const latestCheckpointNumber = latestCheckpointEntry
|
|
1205
1227
|
? CheckpointNumber(latestCheckpointEntry[0])
|
|
@@ -1285,14 +1307,6 @@ export class BlockStore {
|
|
|
1285
1307
|
const provenTip = await buildTipFromCheckpoint(provenCheckpoint);
|
|
1286
1308
|
const finalizedTip = await buildTipFromCheckpoint(finalizedCheckpoint);
|
|
1287
1309
|
|
|
1288
|
-
// Proposed checkpoint falls back to the checkpoint tip if it's not set. And if local storage is
|
|
1289
|
-
// inconsistent and the proposed checkpoint is behind the checkpointed tip, we patch that and
|
|
1290
|
-
// report the checkpointed tip as the proposed checkpoint to maintain the invariant.
|
|
1291
|
-
const proposedCheckpointTip =
|
|
1292
|
-
proposedCheckpointEntry === undefined || proposedCheckpointEntry[0] <= latestCheckpointNumber
|
|
1293
|
-
? checkpointedTip
|
|
1294
|
-
: await buildTipFromCheckpoint(proposedCheckpointEntry[1]);
|
|
1295
|
-
|
|
1296
1310
|
// A checkpointed block past the latest stored block would mean a checkpoint
|
|
1297
1311
|
// references blocks that aren't in blocks.
|
|
1298
1312
|
if (proposedBlockId.number < checkpointedTip.block.number) {
|
|
@@ -1304,11 +1318,10 @@ export class BlockStore {
|
|
|
1304
1318
|
// Assert that checkpoint numbers are increasing
|
|
1305
1319
|
if (
|
|
1306
1320
|
finalizedTip.checkpoint.number > provenTip.checkpoint.number ||
|
|
1307
|
-
provenTip.checkpoint.number > checkpointedTip.checkpoint.number
|
|
1308
|
-
checkpointedTip.checkpoint.number > proposedCheckpointTip.checkpoint.number
|
|
1321
|
+
provenTip.checkpoint.number > checkpointedTip.checkpoint.number
|
|
1309
1322
|
) {
|
|
1310
1323
|
throw new Error(
|
|
1311
|
-
`Inconsistent checkpoint numbers in chain tips: finalized=${finalizedTip.checkpoint.number} proven=${provenTip.checkpoint.number} checkpointed=${checkpointedTip.checkpoint.number}
|
|
1324
|
+
`Inconsistent checkpoint numbers in chain tips: finalized=${finalizedTip.checkpoint.number} proven=${provenTip.checkpoint.number} checkpointed=${checkpointedTip.checkpoint.number}`,
|
|
1312
1325
|
);
|
|
1313
1326
|
}
|
|
1314
1327
|
|
|
@@ -1316,17 +1329,15 @@ export class BlockStore {
|
|
|
1316
1329
|
if (
|
|
1317
1330
|
finalizedTip.block.number > provenTip.block.number ||
|
|
1318
1331
|
provenTip.block.number > checkpointedTip.block.number ||
|
|
1319
|
-
checkpointedTip.block.number >
|
|
1320
|
-
proposedCheckpointTip.block.number > proposedBlockId.number
|
|
1332
|
+
checkpointedTip.block.number > proposedBlockId.number
|
|
1321
1333
|
) {
|
|
1322
1334
|
throw new Error(
|
|
1323
|
-
`Inconsistent block numbers in chain tips: finalized=${finalizedTip.block.number} proven=${provenTip.block.number} checkpointed=${checkpointedTip.block.number}
|
|
1335
|
+
`Inconsistent block numbers in chain tips: finalized=${finalizedTip.block.number} proven=${provenTip.block.number} checkpointed=${checkpointedTip.block.number} proposed=${proposedBlockId.number}`,
|
|
1324
1336
|
);
|
|
1325
1337
|
}
|
|
1326
1338
|
|
|
1327
1339
|
return {
|
|
1328
1340
|
proposed: proposedBlockId,
|
|
1329
|
-
proposedCheckpoint: proposedCheckpointTip,
|
|
1330
1341
|
checkpointed: checkpointedTip,
|
|
1331
1342
|
proven: provenTip,
|
|
1332
1343
|
finalized: finalizedTip,
|