@aztec/archiver 0.0.1-commit.f650c0a5c → 0.0.1-commit.f7ea82942
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/dest/archiver.d.ts +4 -2
- package/dest/archiver.d.ts.map +1 -1
- package/dest/archiver.js +12 -1
- package/dest/config.d.ts +3 -1
- package/dest/config.d.ts.map +1 -1
- package/dest/config.js +13 -2
- package/dest/errors.d.ts +17 -1
- package/dest/errors.d.ts.map +1 -1
- package/dest/errors.js +22 -0
- package/dest/factory.d.ts +1 -1
- package/dest/factory.d.ts.map +1 -1
- package/dest/factory.js +2 -1
- package/dest/index.d.ts +3 -2
- package/dest/index.d.ts.map +1 -1
- package/dest/index.js +2 -1
- package/dest/l1/data_retrieval.d.ts +18 -9
- package/dest/l1/data_retrieval.d.ts.map +1 -1
- package/dest/l1/data_retrieval.js +13 -19
- package/dest/l1/validate_historical_logs.d.ts +23 -0
- package/dest/l1/validate_historical_logs.d.ts.map +1 -0
- package/dest/l1/validate_historical_logs.js +108 -0
- package/dest/modules/data_store_updater.d.ts +12 -5
- package/dest/modules/data_store_updater.d.ts.map +1 -1
- package/dest/modules/data_store_updater.js +13 -3
- package/dest/modules/instrumentation.d.ts +7 -2
- package/dest/modules/instrumentation.d.ts.map +1 -1
- package/dest/modules/instrumentation.js +22 -6
- package/dest/modules/l1_synchronizer.d.ts +4 -1
- package/dest/modules/l1_synchronizer.d.ts.map +1 -1
- package/dest/modules/l1_synchronizer.js +114 -25
- package/dest/store/block_store.d.ts +10 -3
- package/dest/store/block_store.d.ts.map +1 -1
- package/dest/store/block_store.js +41 -3
- package/dest/store/kv_archiver_store.d.ts +9 -3
- package/dest/store/kv_archiver_store.d.ts.map +1 -1
- package/dest/store/kv_archiver_store.js +7 -0
- package/dest/store/l2_tips_cache.d.ts +1 -1
- package/dest/store/l2_tips_cache.d.ts.map +1 -1
- package/dest/store/l2_tips_cache.js +2 -2
- 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 +2 -4
- package/dest/test/fake_l1_state.d.ts +6 -3
- package/dest/test/fake_l1_state.d.ts.map +1 -1
- package/dest/test/fake_l1_state.js +17 -7
- package/dest/test/noop_l1_archiver.d.ts +1 -1
- package/dest/test/noop_l1_archiver.d.ts.map +1 -1
- package/dest/test/noop_l1_archiver.js +4 -1
- package/package.json +13 -13
- package/src/archiver.ts +23 -3
- package/src/config.ts +14 -1
- package/src/errors.ts +34 -0
- package/src/factory.ts +1 -0
- package/src/index.ts +2 -1
- package/src/l1/data_retrieval.ts +30 -35
- package/src/l1/validate_historical_logs.ts +140 -0
- package/src/modules/data_store_updater.ts +27 -3
- package/src/modules/instrumentation.ts +27 -7
- package/src/modules/l1_synchronizer.ts +168 -27
- package/src/store/block_store.ts +53 -1
- package/src/store/kv_archiver_store.ts +15 -0
- package/src/store/l2_tips_cache.ts +8 -2
- package/src/store/log_store.ts +2 -5
- package/src/test/fake_l1_state.ts +23 -10
- package/src/test/noop_l1_archiver.ts +3 -0
|
@@ -2,12 +2,13 @@ import type { BlobClientInterface } from '@aztec/blob-client/client';
|
|
|
2
2
|
import { EpochCache } from '@aztec/epoch-cache';
|
|
3
3
|
import { InboxContract, type InboxContractState, RollupContract } from '@aztec/ethereum/contracts';
|
|
4
4
|
import type { L1BlockId } from '@aztec/ethereum/l1-types';
|
|
5
|
+
import { getFinalizedL1Block } from '@aztec/ethereum/queries';
|
|
5
6
|
import type { ViemPublicClient, ViemPublicDebugClient } from '@aztec/ethereum/types';
|
|
6
7
|
import { asyncPool } from '@aztec/foundation/async-pool';
|
|
7
8
|
import { maxBigint } from '@aztec/foundation/bigint';
|
|
8
9
|
import { BlockNumber, CheckpointNumber, EpochNumber } from '@aztec/foundation/branded-types';
|
|
9
10
|
import { Buffer16, Buffer32 } from '@aztec/foundation/buffer';
|
|
10
|
-
import { pick } from '@aztec/foundation/collection';
|
|
11
|
+
import { partition, pick } from '@aztec/foundation/collection';
|
|
11
12
|
import { Fr } from '@aztec/foundation/curves/bn254';
|
|
12
13
|
import { type Logger, createLogger } from '@aztec/foundation/log';
|
|
13
14
|
import { retryTimes } from '@aztec/foundation/retry';
|
|
@@ -15,14 +16,16 @@ import { count } from '@aztec/foundation/string';
|
|
|
15
16
|
import { DateProvider, Timer, elapsed } from '@aztec/foundation/timer';
|
|
16
17
|
import { isDefined, isErrorClass } from '@aztec/foundation/types';
|
|
17
18
|
import { type ArchiverEmitter, L2BlockSourceEvents, type ValidateCheckpointResult } from '@aztec/stdlib/block';
|
|
18
|
-
import { PublishedCheckpoint } from '@aztec/stdlib/checkpoint';
|
|
19
|
+
import { Checkpoint, PublishedCheckpoint } from '@aztec/stdlib/checkpoint';
|
|
19
20
|
import { type L1RollupConstants, getEpochAtSlot, getSlotAtNextL1Block } from '@aztec/stdlib/epoch-helpers';
|
|
20
21
|
import { computeInHashFromL1ToL2Messages } from '@aztec/stdlib/messaging';
|
|
21
22
|
import { type Traceable, type Tracer, execInSpan, trackSpan } from '@aztec/telemetry-client';
|
|
22
23
|
|
|
23
24
|
import { InitialCheckpointNumberNotSequentialError } from '../errors.js';
|
|
24
25
|
import {
|
|
25
|
-
|
|
26
|
+
type RetrievedCheckpointFromCalldata,
|
|
27
|
+
getCheckpointBlobDataFromBlobs,
|
|
28
|
+
retrieveCheckpointCalldataFromRollup,
|
|
26
29
|
retrieveL1ToL2Message,
|
|
27
30
|
retrieveL1ToL2Messages,
|
|
28
31
|
retrievedToPublishedCheckpoint,
|
|
@@ -66,6 +69,7 @@ export class ArchiverL1Synchronizer implements Traceable {
|
|
|
66
69
|
private config: {
|
|
67
70
|
batchSize: number;
|
|
68
71
|
skipValidateCheckpointAttestations?: boolean;
|
|
72
|
+
skipPromoteProposedCheckpointDuringL1Sync?: boolean;
|
|
69
73
|
maxAllowedEthClientDriftSeconds: number;
|
|
70
74
|
},
|
|
71
75
|
private readonly blobClient: BlobClientInterface,
|
|
@@ -91,6 +95,7 @@ export class ArchiverL1Synchronizer implements Traceable {
|
|
|
91
95
|
public setConfig(newConfig: {
|
|
92
96
|
batchSize: number;
|
|
93
97
|
skipValidateCheckpointAttestations?: boolean;
|
|
98
|
+
skipPromoteProposedCheckpointDuringL1Sync?: boolean;
|
|
94
99
|
maxAllowedEthClientDriftSeconds: number;
|
|
95
100
|
}) {
|
|
96
101
|
this.config = newConfig;
|
|
@@ -215,7 +220,11 @@ export class ArchiverL1Synchronizer implements Traceable {
|
|
|
215
220
|
/** Query L1 for its finalized block and update the finalized checkpoint accordingly. */
|
|
216
221
|
private async updateFinalizedCheckpoint(): Promise<void> {
|
|
217
222
|
try {
|
|
218
|
-
const finalizedL1Block = await this.publicClient
|
|
223
|
+
const finalizedL1Block = await getFinalizedL1Block(this.publicClient);
|
|
224
|
+
if (!finalizedL1Block) {
|
|
225
|
+
this.log.trace(`Skipping finalized checkpoint update: L1 has no finalized block yet.`);
|
|
226
|
+
return;
|
|
227
|
+
}
|
|
219
228
|
const finalizedL1BlockNumber = finalizedL1Block.number;
|
|
220
229
|
const finalizedCheckpointNumber = await this.rollup.getProvenCheckpointNumber({
|
|
221
230
|
blockNumber: finalizedL1BlockNumber,
|
|
@@ -405,7 +414,7 @@ export class ArchiverL1Synchronizer implements Traceable {
|
|
|
405
414
|
`Failed to store L1 to L2 messages retrieved from L1: ${error.message}. Rolling back syncpoint to retry.`,
|
|
406
415
|
{ inboxMessage: error.inboxMessage },
|
|
407
416
|
);
|
|
408
|
-
await this.rollbackL1ToL2Messages(remoteMessagesState
|
|
417
|
+
await this.rollbackL1ToL2Messages(remoteMessagesState);
|
|
409
418
|
return false;
|
|
410
419
|
}
|
|
411
420
|
throw error;
|
|
@@ -420,7 +429,7 @@ export class ArchiverL1Synchronizer implements Traceable {
|
|
|
420
429
|
`Local L1 to L2 messages state does not match remote after sync attempt. Rolling back syncpoint to retry.`,
|
|
421
430
|
{ localLastMessageAfterSync, remoteMessagesState },
|
|
422
431
|
);
|
|
423
|
-
await this.rollbackL1ToL2Messages(remoteMessagesState
|
|
432
|
+
await this.rollbackL1ToL2Messages(remoteMessagesState);
|
|
424
433
|
return false;
|
|
425
434
|
}
|
|
426
435
|
|
|
@@ -475,18 +484,39 @@ export class ArchiverL1Synchronizer implements Traceable {
|
|
|
475
484
|
* Rolls back local L1 to L2 messages to the last common message with L1, and updates the syncpoint to the L1 block of that message.
|
|
476
485
|
* If no common message is found, rolls back all messages and sets the syncpoint to the start block.
|
|
477
486
|
*/
|
|
478
|
-
private async rollbackL1ToL2Messages(
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
//
|
|
487
|
+
private async rollbackL1ToL2Messages(remoteMessagesState: InboxContractState): Promise<L1BlockId> {
|
|
488
|
+
const { treeInProgress: remoteTreeInProgress, messagesRollingHash: remoteRollingHash } = remoteMessagesState;
|
|
489
|
+
|
|
490
|
+
// Slowly go back through our messages until we find the last common message. We could query the logs in
|
|
491
|
+
// batch as an optimization, but the depth of the reorg should not be deep, and this is a very rare case,
|
|
492
|
+
// so it's fine to query one log at a time.
|
|
482
493
|
let commonMsg: undefined | InboxMessage;
|
|
483
494
|
let messagesToDelete = 0;
|
|
484
495
|
this.log.verbose(`Searching most recent common L1 to L2 message`);
|
|
485
496
|
for await (const localMsg of this.store.iterateL1ToL2Messages({ reverse: true })) {
|
|
497
|
+
const logCtx = { remoteMsg: undefined as InboxMessage | undefined, localMsg, remoteMessagesState };
|
|
498
|
+
|
|
499
|
+
// First check if the local message rolling hash matches the current rolling hash of the inbox contract,
|
|
500
|
+
// which means we just need to rollback some local messages and we should be back in sync. This means there
|
|
501
|
+
// was an L1 reorg that removed some of the messages we had, but no new messages were added compared.
|
|
502
|
+
if (localMsg.rollingHash.equals(remoteRollingHash)) {
|
|
503
|
+
this.log.info(
|
|
504
|
+
`Found common L1 to L2 message at index ${localMsg.index} on L1 block ${localMsg.l1BlockNumber} matching current remote state`,
|
|
505
|
+
logCtx,
|
|
506
|
+
);
|
|
507
|
+
commonMsg = localMsg;
|
|
508
|
+
break;
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
// If there's no match with the current remote state, check if the message exists on the inbox contract at all
|
|
512
|
+
// by looking at the inbox events. If the L1 reorg *added* new messages in addition to deleting existing ones,
|
|
513
|
+
// then the current remote state's rolling hash will not match anything we have locally, so we need to check existence
|
|
514
|
+
// of individual messages via logs. Note we use logs and not historical queries so we don't have to depend on
|
|
515
|
+
// an archival rpc node, since the message could be from a long time ago if we're catching up with syncing.
|
|
486
516
|
const remoteMsg = await retrieveL1ToL2Message(this.inbox, localMsg);
|
|
487
|
-
|
|
517
|
+
logCtx.remoteMsg = remoteMsg;
|
|
488
518
|
if (remoteMsg && remoteMsg.rollingHash.equals(localMsg.rollingHash)) {
|
|
489
|
-
this.log.
|
|
519
|
+
this.log.info(
|
|
490
520
|
`Found most recent common L1 to L2 message at index ${localMsg.index} on L1 block ${localMsg.l1BlockNumber}`,
|
|
491
521
|
logCtx,
|
|
492
522
|
);
|
|
@@ -718,22 +748,20 @@ export class ArchiverL1Synchronizer implements Traceable {
|
|
|
718
748
|
|
|
719
749
|
this.log.trace(`Retrieving checkpoints from L1 block ${searchStartBlock} to ${searchEndBlock}`);
|
|
720
750
|
|
|
721
|
-
//
|
|
722
|
-
const
|
|
723
|
-
|
|
751
|
+
// First fetch calldata only, no blobs yet, since we may be able to just get that data out of the proposed chain
|
|
752
|
+
const calldataCheckpoints = await execInSpan(this.tracer, 'Archiver.retrieveCheckpointCalldataFromRollup', () =>
|
|
753
|
+
retrieveCheckpointCalldataFromRollup(
|
|
724
754
|
this.rollup,
|
|
725
755
|
this.publicClient,
|
|
726
756
|
this.debugClient,
|
|
727
|
-
this.blobClient,
|
|
728
757
|
searchStartBlock, // TODO(palla/reorg): If the L2 reorg was due to an L1 reorg, we need to start search earlier
|
|
729
758
|
searchEndBlock,
|
|
730
759
|
this.instrumentation,
|
|
731
760
|
this.log,
|
|
732
|
-
!initialSyncComplete, // isHistoricalSync
|
|
733
761
|
),
|
|
734
762
|
);
|
|
735
763
|
|
|
736
|
-
if (
|
|
764
|
+
if (calldataCheckpoints.length === 0) {
|
|
737
765
|
// We are not calling `setBlockSynchedL1BlockNumber` because it may cause sync issues if based off infura.
|
|
738
766
|
// See further details in earlier comments.
|
|
739
767
|
this.log.trace(`Retrieved no new checkpoints from L1 block ${searchStartBlock} to ${searchEndBlock}`);
|
|
@@ -741,17 +769,43 @@ export class ArchiverL1Synchronizer implements Traceable {
|
|
|
741
769
|
}
|
|
742
770
|
|
|
743
771
|
this.log.debug(
|
|
744
|
-
`Retrieved ${
|
|
772
|
+
`Retrieved ${calldataCheckpoints.length} new checkpoint calldata between L1 blocks ${searchStartBlock} and ${searchEndBlock}`,
|
|
745
773
|
{
|
|
746
|
-
lastProcessedCheckpoint:
|
|
774
|
+
lastProcessedCheckpoint: calldataCheckpoints[calldataCheckpoints.length - 1].l1,
|
|
747
775
|
searchStartBlock,
|
|
748
776
|
searchEndBlock,
|
|
749
777
|
},
|
|
750
778
|
);
|
|
751
779
|
|
|
752
|
-
|
|
780
|
+
// Check if the last checkpoint matches the proposed one (so we can skip blob fetch).
|
|
781
|
+
// We only check the last one because the proposed checkpoint is always the most recent one,
|
|
782
|
+
// and if it's in a multi-checkpoint batch it will always be last (sorted by L1 block number).
|
|
783
|
+
const lastCalldataCheckpoint = calldataCheckpoints[calldataCheckpoints.length - 1];
|
|
784
|
+
const checkpointToPromote = await this.tryBuildPublishedCheckpointFromProposed(lastCalldataCheckpoint);
|
|
785
|
+
|
|
786
|
+
// Then fetch blobs in parallel and build the full published checkpoints
|
|
787
|
+
const toFetchBlobs = checkpointToPromote ? calldataCheckpoints.slice(0, -1) : calldataCheckpoints;
|
|
788
|
+
const blobFetched = await asyncPool(10, toFetchBlobs, async checkpoint =>
|
|
789
|
+
retrievedToPublishedCheckpoint({
|
|
790
|
+
...checkpoint,
|
|
791
|
+
checkpointBlobData: await getCheckpointBlobDataFromBlobs(
|
|
792
|
+
this.blobClient,
|
|
793
|
+
checkpoint.l1.blockHash,
|
|
794
|
+
checkpoint.blobHashes,
|
|
795
|
+
checkpoint.checkpointNumber,
|
|
796
|
+
this.log,
|
|
797
|
+
!initialSyncComplete,
|
|
798
|
+
checkpoint.parentBeaconBlockRoot,
|
|
799
|
+
checkpoint.l1.timestamp,
|
|
800
|
+
),
|
|
801
|
+
}),
|
|
802
|
+
);
|
|
803
|
+
|
|
804
|
+
// And add the promoted checkpoint to the list of all checkpoints
|
|
805
|
+
const publishedCheckpoints = checkpointToPromote ? [...blobFetched, checkpointToPromote] : blobFetched;
|
|
753
806
|
const validCheckpoints: PublishedCheckpoint[] = [];
|
|
754
807
|
|
|
808
|
+
// Now loop through all checkpoints and validate their attestations
|
|
755
809
|
for (const published of publishedCheckpoints) {
|
|
756
810
|
const validationResult = this.config.skipValidateCheckpointAttestations
|
|
757
811
|
? { valid: true as const }
|
|
@@ -832,15 +886,34 @@ export class ArchiverL1Synchronizer implements Traceable {
|
|
|
832
886
|
try {
|
|
833
887
|
const updatedValidationResult =
|
|
834
888
|
rollupStatus.validationResult === initialValidationResult ? undefined : rollupStatus.validationResult;
|
|
889
|
+
|
|
890
|
+
// Split valid checkpoints: the promoted one (if any) is persisted via the proposed-promotion path,
|
|
891
|
+
// the rest via addCheckpoints. Both paths run within the same store transaction for atomicity.
|
|
892
|
+
const [[maybeValidCheckpointToPromote], checkpointsToAdd] = partition(
|
|
893
|
+
validCheckpoints,
|
|
894
|
+
c => c.checkpoint.number === checkpointToPromote?.checkpoint.number,
|
|
895
|
+
);
|
|
896
|
+
|
|
835
897
|
const [processDuration, result] = await elapsed(() =>
|
|
836
898
|
execInSpan(this.tracer, 'Archiver.addCheckpoints', () =>
|
|
837
|
-
this.updater.addCheckpoints(
|
|
899
|
+
this.updater.addCheckpoints(
|
|
900
|
+
checkpointsToAdd,
|
|
901
|
+
updatedValidationResult,
|
|
902
|
+
maybeValidCheckpointToPromote && {
|
|
903
|
+
l1: lastCalldataCheckpoint.l1,
|
|
904
|
+
attestations: lastCalldataCheckpoint.attestations,
|
|
905
|
+
checkpoint: maybeValidCheckpointToPromote,
|
|
906
|
+
},
|
|
907
|
+
),
|
|
838
908
|
),
|
|
839
909
|
);
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
910
|
+
|
|
911
|
+
if (checkpointsToAdd.length > 0) {
|
|
912
|
+
this.instrumentation.processNewCheckpointedBlocks(
|
|
913
|
+
processDuration / checkpointsToAdd.length,
|
|
914
|
+
checkpointsToAdd.flatMap(c => c.checkpoint.blocks),
|
|
915
|
+
);
|
|
916
|
+
}
|
|
844
917
|
|
|
845
918
|
// If blocks were pruned due to conflict with L1 checkpoints, emit event
|
|
846
919
|
if (result.prunedBlocks && result.prunedBlocks.length > 0) {
|
|
@@ -893,7 +966,7 @@ export class ArchiverL1Synchronizer implements Traceable {
|
|
|
893
966
|
});
|
|
894
967
|
}
|
|
895
968
|
lastRetrievedCheckpoint = validCheckpoints.at(-1) ?? lastRetrievedCheckpoint;
|
|
896
|
-
lastL1BlockWithCheckpoint =
|
|
969
|
+
lastL1BlockWithCheckpoint = calldataCheckpoints.at(-1)?.l1.blockNumber ?? lastL1BlockWithCheckpoint;
|
|
897
970
|
} while (searchEndBlock < currentL1BlockNumber);
|
|
898
971
|
|
|
899
972
|
// Important that we update AFTER inserting the blocks.
|
|
@@ -902,6 +975,74 @@ export class ArchiverL1Synchronizer implements Traceable {
|
|
|
902
975
|
return { ...rollupStatus, lastRetrievedCheckpoint, lastL1BlockWithCheckpoint };
|
|
903
976
|
}
|
|
904
977
|
|
|
978
|
+
/** Checks if this checkpoint matches the local proposed one, and if so, loads local data to build a synthetic published checkpoint. */
|
|
979
|
+
private async tryBuildPublishedCheckpointFromProposed(
|
|
980
|
+
calldataCheckpoint: RetrievedCheckpointFromCalldata | undefined,
|
|
981
|
+
): Promise<PublishedCheckpoint | undefined> {
|
|
982
|
+
const proposed = await this.store.getProposedCheckpointOnly();
|
|
983
|
+
if (
|
|
984
|
+
this.config.skipPromoteProposedCheckpointDuringL1Sync ||
|
|
985
|
+
!proposed ||
|
|
986
|
+
!calldataCheckpoint ||
|
|
987
|
+
proposed.checkpointNumber !== calldataCheckpoint.checkpointNumber
|
|
988
|
+
) {
|
|
989
|
+
return undefined;
|
|
990
|
+
}
|
|
991
|
+
|
|
992
|
+
if (
|
|
993
|
+
!proposed.header.equals(calldataCheckpoint.header) ||
|
|
994
|
+
!proposed.archive.root.equals(calldataCheckpoint.archiveRoot)
|
|
995
|
+
) {
|
|
996
|
+
this.log.warn(
|
|
997
|
+
`Local proposed checkpoint ${proposed.checkpointNumber} does not match checkpoint retrieved from L1, overriding with L1 data`,
|
|
998
|
+
{
|
|
999
|
+
proposedCheckpointNumber: proposed.checkpointNumber,
|
|
1000
|
+
proposedHeader: proposed.header.toInspect(),
|
|
1001
|
+
proposedArchiveRoot: proposed.archive.root.toString(),
|
|
1002
|
+
calldataCheckpointNumber: calldataCheckpoint.checkpointNumber,
|
|
1003
|
+
calldataHeader: calldataCheckpoint.header.toInspect(),
|
|
1004
|
+
calldataArchiveRoot: calldataCheckpoint.archiveRoot.toString(),
|
|
1005
|
+
},
|
|
1006
|
+
);
|
|
1007
|
+
return undefined;
|
|
1008
|
+
}
|
|
1009
|
+
|
|
1010
|
+
this.log.debug(
|
|
1011
|
+
`Building published checkpoint from proposed ${calldataCheckpoint.checkpointNumber} (skipping blob fetch)`,
|
|
1012
|
+
{ proposedHeader: proposed.header.toInspect(), proposedArchiveRoot: proposed.archive.root.toString() },
|
|
1013
|
+
);
|
|
1014
|
+
|
|
1015
|
+
const blocks = await this.store.getBlocks(BlockNumber(proposed.startBlock), proposed.blockCount);
|
|
1016
|
+
if (blocks.length !== proposed.blockCount) {
|
|
1017
|
+
this.log.warn(
|
|
1018
|
+
`Local proposed checkpoint ${proposed.checkpointNumber} has wrong block count (expected ${proposed.blockCount} blocks starting at ${proposed.startBlock} but got ${blocks.length})`,
|
|
1019
|
+
{
|
|
1020
|
+
proposedCheckpointNumber: proposed.checkpointNumber,
|
|
1021
|
+
proposedStartBlock: proposed.startBlock,
|
|
1022
|
+
proposedBlockCount: proposed.blockCount,
|
|
1023
|
+
retrievedBlocks: blocks.map(b => b.number),
|
|
1024
|
+
},
|
|
1025
|
+
);
|
|
1026
|
+
return undefined;
|
|
1027
|
+
}
|
|
1028
|
+
|
|
1029
|
+
const checkpoint = Checkpoint.from({
|
|
1030
|
+
archive: proposed.archive,
|
|
1031
|
+
header: proposed.header,
|
|
1032
|
+
blocks,
|
|
1033
|
+
number: proposed.checkpointNumber,
|
|
1034
|
+
feeAssetPriceModifier: proposed.feeAssetPriceModifier,
|
|
1035
|
+
});
|
|
1036
|
+
const promotedCheckpoint = PublishedCheckpoint.from({
|
|
1037
|
+
checkpoint,
|
|
1038
|
+
l1: calldataCheckpoint.l1,
|
|
1039
|
+
attestations: calldataCheckpoint.attestations,
|
|
1040
|
+
});
|
|
1041
|
+
this.instrumentation.processCheckpointPromoted();
|
|
1042
|
+
|
|
1043
|
+
return promotedCheckpoint;
|
|
1044
|
+
}
|
|
1045
|
+
|
|
905
1046
|
private async checkForNewCheckpointsBeforeL1SyncPoint(
|
|
906
1047
|
status: RollupStatus,
|
|
907
1048
|
blocksSynchedTo: bigint,
|
package/src/store/block_store.ts
CHANGED
|
@@ -52,7 +52,10 @@ import {
|
|
|
52
52
|
CheckpointNotFoundError,
|
|
53
53
|
CheckpointNumberNotSequentialError,
|
|
54
54
|
InitialCheckpointNumberNotSequentialError,
|
|
55
|
+
NoProposedCheckpointToPromoteError,
|
|
56
|
+
ProposedCheckpointArchiveRootMismatchError,
|
|
55
57
|
ProposedCheckpointNotSequentialError,
|
|
58
|
+
ProposedCheckpointPromotionNotSequentialError,
|
|
56
59
|
ProposedCheckpointStaleError,
|
|
57
60
|
} from '../errors.js';
|
|
58
61
|
|
|
@@ -694,6 +697,55 @@ export class BlockStore {
|
|
|
694
697
|
await this.#proposedCheckpoint.delete();
|
|
695
698
|
}
|
|
696
699
|
|
|
700
|
+
/**
|
|
701
|
+
* Promotes the proposed checkpoint singleton to a confirmed checkpoint entry.
|
|
702
|
+
* This persists the checkpoint to the store, clears the proposed singleton, and updates the L1 sync point.
|
|
703
|
+
* Should only be called after the checkpoint has been validated.
|
|
704
|
+
* @param expectedArchiveRoot - The archive root to match against the proposed checkpoint, to guard against races.
|
|
705
|
+
*/
|
|
706
|
+
async promoteProposedToCheckpointed(
|
|
707
|
+
l1: L1PublishedData,
|
|
708
|
+
attestations: CommitteeAttestation[],
|
|
709
|
+
expectedArchiveRoot: Fr,
|
|
710
|
+
): Promise<void> {
|
|
711
|
+
return await this.db.transactionAsync(async () => {
|
|
712
|
+
const proposed = await this.getProposedCheckpointOnly();
|
|
713
|
+
if (!proposed) {
|
|
714
|
+
throw new NoProposedCheckpointToPromoteError();
|
|
715
|
+
}
|
|
716
|
+
if (!proposed.archive.root.equals(expectedArchiveRoot)) {
|
|
717
|
+
throw new ProposedCheckpointArchiveRootMismatchError(expectedArchiveRoot, proposed.archive.root);
|
|
718
|
+
}
|
|
719
|
+
|
|
720
|
+
// Verify sequentiality: promoted checkpoint must follow the latest confirmed one
|
|
721
|
+
const latestCheckpointNumber = await this.getLatestCheckpointNumber();
|
|
722
|
+
if (latestCheckpointNumber !== proposed.checkpointNumber - 1) {
|
|
723
|
+
throw new ProposedCheckpointPromotionNotSequentialError(proposed.checkpointNumber, latestCheckpointNumber);
|
|
724
|
+
}
|
|
725
|
+
|
|
726
|
+
// Write the checkpoint entry
|
|
727
|
+
await this.#checkpoints.set(proposed.checkpointNumber, {
|
|
728
|
+
header: proposed.header.toBuffer(),
|
|
729
|
+
archive: proposed.archive.toBuffer(),
|
|
730
|
+
checkpointOutHash: proposed.checkpointOutHash.toBuffer(),
|
|
731
|
+
l1: l1.toBuffer(),
|
|
732
|
+
attestations: attestations.map(attestation => attestation.toBuffer()),
|
|
733
|
+
checkpointNumber: proposed.checkpointNumber,
|
|
734
|
+
startBlock: proposed.startBlock,
|
|
735
|
+
blockCount: proposed.blockCount,
|
|
736
|
+
});
|
|
737
|
+
|
|
738
|
+
// Update the slot-to-checkpoint index
|
|
739
|
+
await this.#slotToCheckpoint.set(proposed.header.slotNumber, proposed.checkpointNumber);
|
|
740
|
+
|
|
741
|
+
// Clear the proposed checkpoint singleton
|
|
742
|
+
await this.#proposedCheckpoint.delete();
|
|
743
|
+
|
|
744
|
+
// Update the last synced L1 block
|
|
745
|
+
await this.#lastSynchedL1Block.set(l1.blockNumber);
|
|
746
|
+
});
|
|
747
|
+
}
|
|
748
|
+
|
|
697
749
|
/** Clears the proposed checkpoint if the given confirmed checkpoint number supersedes it. */
|
|
698
750
|
async clearProposedCheckpointIfSuperseded(confirmedCheckpointNumber: CheckpointNumber): Promise<void> {
|
|
699
751
|
const proposedCheckpointNumber = await this.getProposedCheckpointNumber();
|
|
@@ -976,7 +1028,7 @@ export class BlockStore {
|
|
|
976
1028
|
return {
|
|
977
1029
|
header: BlockHeader.fromBuffer(blockStorage.header),
|
|
978
1030
|
archive: AppendOnlyTreeSnapshot.fromBuffer(blockStorage.archive),
|
|
979
|
-
blockHash:
|
|
1031
|
+
blockHash: BlockHash.fromBuffer(blockStorage.blockHash),
|
|
980
1032
|
checkpointNumber: CheckpointNumber(blockStorage.checkpointNumber),
|
|
981
1033
|
indexWithinCheckpoint: IndexWithinCheckpoint(blockStorage.indexWithinCheckpoint),
|
|
982
1034
|
};
|
|
@@ -10,12 +10,14 @@ import {
|
|
|
10
10
|
type BlockData,
|
|
11
11
|
BlockHash,
|
|
12
12
|
CheckpointedL2Block,
|
|
13
|
+
type CommitteeAttestation,
|
|
13
14
|
L2Block,
|
|
14
15
|
type ValidateCheckpointResult,
|
|
15
16
|
} from '@aztec/stdlib/block';
|
|
16
17
|
import type {
|
|
17
18
|
CheckpointData,
|
|
18
19
|
CommonCheckpointData,
|
|
20
|
+
L1PublishedData,
|
|
19
21
|
ProposedCheckpointData,
|
|
20
22
|
ProposedCheckpointInput,
|
|
21
23
|
PublishedCheckpoint,
|
|
@@ -647,6 +649,19 @@ export class KVArchiverDataStore implements ContractDataSource {
|
|
|
647
649
|
return this.#blockStore.deleteProposedCheckpoint();
|
|
648
650
|
}
|
|
649
651
|
|
|
652
|
+
/**
|
|
653
|
+
* Promotes the proposed checkpoint to a confirmed checkpoint entry.
|
|
654
|
+
* Should only be called after the checkpoint has been validated.
|
|
655
|
+
* @param expectedArchiveRoot - The archive root to match against the proposed checkpoint, to guard against races.
|
|
656
|
+
*/
|
|
657
|
+
public promoteProposedToCheckpointed(
|
|
658
|
+
l1: L1PublishedData,
|
|
659
|
+
attestations: CommitteeAttestation[],
|
|
660
|
+
expectedArchiveRoot: Fr,
|
|
661
|
+
): Promise<void> {
|
|
662
|
+
return this.#blockStore.promoteProposedToCheckpointed(l1, attestations, expectedArchiveRoot);
|
|
663
|
+
}
|
|
664
|
+
|
|
650
665
|
/**
|
|
651
666
|
* Gets the number of the latest L2 block processed.
|
|
652
667
|
* @returns The number of the latest L2 block processed.
|
|
@@ -1,6 +1,12 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { INITIAL_L2_BLOCK_NUM } from '@aztec/constants';
|
|
2
2
|
import { BlockNumber, CheckpointNumber } from '@aztec/foundation/branded-types';
|
|
3
|
-
import {
|
|
3
|
+
import {
|
|
4
|
+
type BlockData,
|
|
5
|
+
type CheckpointId,
|
|
6
|
+
GENESIS_BLOCK_HEADER_HASH,
|
|
7
|
+
GENESIS_CHECKPOINT_HEADER_HASH,
|
|
8
|
+
type L2Tips,
|
|
9
|
+
} from '@aztec/stdlib/block';
|
|
4
10
|
|
|
5
11
|
import type { BlockStore } from './block_store.js';
|
|
6
12
|
|
package/src/store/log_store.ts
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import { INITIAL_L2_BLOCK_NUM } from '@aztec/constants';
|
|
2
2
|
import { BlockNumber } from '@aztec/foundation/branded-types';
|
|
3
3
|
import { compactArray, filterAsync } from '@aztec/foundation/collection';
|
|
4
|
-
import { Fr } from '@aztec/foundation/curves/bn254';
|
|
5
4
|
import { createLogger } from '@aztec/foundation/log';
|
|
6
5
|
import { BufferReader, numToUInt32BE } from '@aztec/foundation/serialize';
|
|
7
6
|
import type { AztecAsyncKVStore, AztecAsyncMap } from '@aztec/kv-store';
|
|
@@ -302,13 +301,11 @@ export class LogStore {
|
|
|
302
301
|
}
|
|
303
302
|
|
|
304
303
|
#unpackBlockHash(reader: BufferReader): BlockHash {
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
if (!blockHash) {
|
|
304
|
+
if (reader.remainingBytes() === 0) {
|
|
308
305
|
throw new Error('Failed to read block hash from log entry buffer');
|
|
309
306
|
}
|
|
310
307
|
|
|
311
|
-
return
|
|
308
|
+
return BlockHash.fromBuffer(reader);
|
|
312
309
|
}
|
|
313
310
|
|
|
314
311
|
deleteLogs(blocks: L2Block[]): Promise<boolean> {
|
|
@@ -151,8 +151,10 @@ export class FakeL1State {
|
|
|
151
151
|
// Computed from checkpoints based on L1 block visibility
|
|
152
152
|
private pendingCheckpointNumber: CheckpointNumber = CheckpointNumber(0);
|
|
153
153
|
|
|
154
|
-
// The L1 block number reported as "finalized" (defaults to the start block)
|
|
155
|
-
|
|
154
|
+
// The L1 block number reported as "finalized" (defaults to the start block).
|
|
155
|
+
// `undefined` simulates the startup window on a fresh devnet where
|
|
156
|
+
// `getBlock({ blockTag: 'finalized' })` fails with "finalized block not found".
|
|
157
|
+
private finalizedL1BlockNumber: bigint | undefined;
|
|
156
158
|
|
|
157
159
|
constructor(private readonly config: FakeL1StateConfig) {
|
|
158
160
|
this.l1BlockNumber = config.l1StartBlock;
|
|
@@ -288,8 +290,11 @@ export class FakeL1State {
|
|
|
288
290
|
this.updatePendingCheckpointNumber();
|
|
289
291
|
}
|
|
290
292
|
|
|
291
|
-
/**
|
|
292
|
-
|
|
293
|
+
/**
|
|
294
|
+
* Sets the L1 block number that will be reported as "finalized". Pass `undefined` to
|
|
295
|
+
* simulate a chain that does not yet have a finalized block (devnet startup).
|
|
296
|
+
*/
|
|
297
|
+
setFinalizedL1BlockNumber(blockNumber: bigint | undefined): void {
|
|
293
298
|
this.finalizedL1BlockNumber = blockNumber;
|
|
294
299
|
}
|
|
295
300
|
|
|
@@ -502,8 +507,8 @@ export class FakeL1State {
|
|
|
502
507
|
Promise.resolve(this.getMessageSentLogs(fromBlock, toBlock)),
|
|
503
508
|
);
|
|
504
509
|
|
|
505
|
-
mockInbox.getMessageSentEventByHash.mockImplementation((msgHash: string,
|
|
506
|
-
Promise.resolve(this.getMessageSentLogByHash(msgHash,
|
|
510
|
+
mockInbox.getMessageSentEventByHash.mockImplementation((msgHash: string, aroundL1BlockNumber: bigint) =>
|
|
511
|
+
Promise.resolve(this.getMessageSentLogByHash(msgHash, aroundL1BlockNumber) as MessageSentLog),
|
|
507
512
|
);
|
|
508
513
|
|
|
509
514
|
return mockInbox;
|
|
@@ -519,6 +524,11 @@ export class FakeL1State {
|
|
|
519
524
|
publicClient.getBlock.mockImplementation((async (args: { blockNumber?: bigint; blockTag?: string } = {}) => {
|
|
520
525
|
let blockNum: bigint;
|
|
521
526
|
if (args.blockTag === 'finalized') {
|
|
527
|
+
if (this.finalizedL1BlockNumber === undefined) {
|
|
528
|
+
throw Object.assign(new Error('finalized block not found'), {
|
|
529
|
+
details: 'finalized block not found',
|
|
530
|
+
});
|
|
531
|
+
}
|
|
522
532
|
blockNum = this.finalizedL1BlockNumber;
|
|
523
533
|
} else {
|
|
524
534
|
blockNum = args.blockNumber ?? (await publicClient.getBlockNumber());
|
|
@@ -544,10 +554,10 @@ export class FakeL1State {
|
|
|
544
554
|
createMockBlobClient(): MockProxy<BlobClientInterface> {
|
|
545
555
|
const blobClient = mock<BlobClientInterface>();
|
|
546
556
|
|
|
547
|
-
// The blockId is the
|
|
557
|
+
// The blockId is the L1 block hash, which we derive from the L1 block number
|
|
548
558
|
blobClient.getBlobSidecar.mockImplementation((blockId: `0x${string}`) =>
|
|
549
559
|
Promise.resolve(
|
|
550
|
-
this.checkpoints.find(cpData => cpData.
|
|
560
|
+
this.checkpoints.find(cpData => Buffer32.fromBigInt(cpData.l1BlockNumber).toString() === blockId)?.blobs ?? [],
|
|
551
561
|
),
|
|
552
562
|
);
|
|
553
563
|
|
|
@@ -623,9 +633,12 @@ export class FakeL1State {
|
|
|
623
633
|
}));
|
|
624
634
|
}
|
|
625
635
|
|
|
626
|
-
private getMessageSentLogByHash(msgHash: string,
|
|
636
|
+
private getMessageSentLogByHash(msgHash: string, aroundL1BlockNumber: bigint): MessageSentLog | undefined {
|
|
627
637
|
const msg = this.messages.find(
|
|
628
|
-
msg =>
|
|
638
|
+
msg =>
|
|
639
|
+
msg.leaf.toString() === msgHash &&
|
|
640
|
+
msg.l1BlockNumber >= aroundL1BlockNumber - 5n &&
|
|
641
|
+
msg.l1BlockNumber <= aroundL1BlockNumber + 5n,
|
|
629
642
|
);
|
|
630
643
|
if (!msg) {
|
|
631
644
|
return undefined;
|
|
@@ -70,7 +70,9 @@ export class NoopL1Archiver extends Archiver {
|
|
|
70
70
|
debugClient,
|
|
71
71
|
rollup,
|
|
72
72
|
{
|
|
73
|
+
rollupAddress: EthAddress.ZERO,
|
|
73
74
|
registryAddress: EthAddress.ZERO,
|
|
75
|
+
inboxAddress: EthAddress.ZERO,
|
|
74
76
|
governanceProposerAddress: EthAddress.ZERO,
|
|
75
77
|
slashingProposerAddress: EthAddress.ZERO,
|
|
76
78
|
},
|
|
@@ -81,6 +83,7 @@ export class NoopL1Archiver extends Archiver {
|
|
|
81
83
|
skipValidateCheckpointAttestations: true,
|
|
82
84
|
maxAllowedEthClientDriftSeconds: 300,
|
|
83
85
|
ethereumAllowNoDebugHosts: true, // Skip trace validation
|
|
86
|
+
skipHistoricalLogsCheck: true, // Skip historical logs validation
|
|
84
87
|
},
|
|
85
88
|
blobClient,
|
|
86
89
|
instrumentation,
|