@aztec/archiver 0.0.1-commit.b655e406 → 0.0.1-commit.fce3e4f
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/archiver.d.ts +30 -20
- package/dest/archiver/archiver.d.ts.map +1 -1
- package/dest/archiver/archiver.js +294 -208
- package/dest/archiver/archiver_store.d.ts +1 -1
- package/dest/archiver/archiver_store_test_suite.d.ts +1 -1
- package/dest/archiver/archiver_store_test_suite.d.ts.map +1 -1
- package/dest/archiver/archiver_store_test_suite.js +5 -4
- package/dest/archiver/config.d.ts +1 -1
- package/dest/archiver/config.d.ts.map +1 -1
- package/dest/archiver/config.js +5 -0
- package/dest/archiver/data_retrieval.d.ts +17 -17
- package/dest/archiver/data_retrieval.d.ts.map +1 -1
- package/dest/archiver/data_retrieval.js +110 -86
- package/dest/archiver/errors.d.ts +1 -1
- package/dest/archiver/errors.d.ts.map +1 -1
- package/dest/archiver/index.d.ts +1 -1
- package/dest/archiver/instrumentation.d.ts +3 -3
- package/dest/archiver/instrumentation.d.ts.map +1 -1
- package/dest/archiver/kv_archiver_store/block_store.d.ts +1 -1
- package/dest/archiver/kv_archiver_store/block_store.d.ts.map +1 -1
- package/dest/archiver/kv_archiver_store/contract_class_store.d.ts +1 -1
- package/dest/archiver/kv_archiver_store/contract_class_store.d.ts.map +1 -1
- package/dest/archiver/kv_archiver_store/contract_instance_store.d.ts +1 -1
- package/dest/archiver/kv_archiver_store/contract_instance_store.d.ts.map +1 -1
- package/dest/archiver/kv_archiver_store/kv_archiver_store.d.ts +2 -2
- package/dest/archiver/kv_archiver_store/kv_archiver_store.d.ts.map +1 -1
- package/dest/archiver/kv_archiver_store/log_store.d.ts +1 -1
- package/dest/archiver/kv_archiver_store/log_store.d.ts.map +1 -1
- package/dest/archiver/kv_archiver_store/message_store.d.ts +1 -1
- package/dest/archiver/kv_archiver_store/message_store.d.ts.map +1 -1
- package/dest/archiver/structs/data_retrieval.d.ts +1 -1
- package/dest/archiver/structs/inbox_message.d.ts +1 -1
- package/dest/archiver/structs/published.d.ts +3 -2
- package/dest/archiver/structs/published.d.ts.map +1 -1
- package/dest/archiver/validation.d.ts +10 -4
- package/dest/archiver/validation.d.ts.map +1 -1
- package/dest/archiver/validation.js +29 -21
- package/dest/factory.d.ts +1 -1
- package/dest/index.d.ts +2 -2
- package/dest/index.d.ts.map +1 -1
- package/dest/index.js +1 -1
- package/dest/rpc/index.d.ts +2 -2
- package/dest/test/index.d.ts +1 -1
- package/dest/test/mock_archiver.d.ts +1 -1
- package/dest/test/mock_archiver.d.ts.map +1 -1
- 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_l2_block_source.d.ts +7 -6
- package/dest/test/mock_l2_block_source.d.ts.map +1 -1
- package/dest/test/mock_l2_block_source.js +1 -1
- package/dest/test/mock_structs.d.ts +1 -1
- package/package.json +17 -17
- package/src/archiver/archiver.ts +380 -244
- package/src/archiver/archiver_store_test_suite.ts +5 -4
- package/src/archiver/config.ts +5 -0
- package/src/archiver/data_retrieval.ts +156 -125
- package/src/archiver/instrumentation.ts +2 -2
- package/src/archiver/structs/published.ts +2 -1
- package/src/archiver/validation.ts +52 -27
- package/src/index.ts +1 -1
- package/src/test/mock_l2_block_source.ts +7 -6
package/src/archiver/archiver.ts
CHANGED
|
@@ -9,6 +9,7 @@ import {
|
|
|
9
9
|
createEthereumChain,
|
|
10
10
|
} from '@aztec/ethereum';
|
|
11
11
|
import { maxBigint } from '@aztec/foundation/bigint';
|
|
12
|
+
import { EpochNumber, SlotNumber } from '@aztec/foundation/branded-types';
|
|
12
13
|
import { Buffer16, Buffer32 } from '@aztec/foundation/buffer';
|
|
13
14
|
import { merge, pick } from '@aztec/foundation/collection';
|
|
14
15
|
import type { EthAddress } from '@aztec/foundation/eth-address';
|
|
@@ -16,7 +17,6 @@ import { Fr } from '@aztec/foundation/fields';
|
|
|
16
17
|
import { type Logger, createLogger } from '@aztec/foundation/log';
|
|
17
18
|
import { type PromiseWithResolvers, promiseWithResolvers } from '@aztec/foundation/promise';
|
|
18
19
|
import { RunningPromise, makeLoggingErrorHandler } from '@aztec/foundation/running-promise';
|
|
19
|
-
import { sleep } from '@aztec/foundation/sleep';
|
|
20
20
|
import { count } from '@aztec/foundation/string';
|
|
21
21
|
import { DateProvider, Timer, elapsed } from '@aztec/foundation/timer';
|
|
22
22
|
import type { CustomRange } from '@aztec/kv-store';
|
|
@@ -34,12 +34,14 @@ import type { FunctionSelector } from '@aztec/stdlib/abi';
|
|
|
34
34
|
import type { AztecAddress } from '@aztec/stdlib/aztec-address';
|
|
35
35
|
import {
|
|
36
36
|
type ArchiverEmitter,
|
|
37
|
-
|
|
37
|
+
L2Block,
|
|
38
38
|
type L2BlockId,
|
|
39
39
|
type L2BlockSource,
|
|
40
40
|
L2BlockSourceEvents,
|
|
41
41
|
type L2Tips,
|
|
42
|
+
PublishedL2Block,
|
|
42
43
|
} from '@aztec/stdlib/block';
|
|
44
|
+
import type { Checkpoint, PublishedCheckpoint } from '@aztec/stdlib/checkpoint';
|
|
43
45
|
import {
|
|
44
46
|
type ContractClassPublic,
|
|
45
47
|
type ContractDataSource,
|
|
@@ -62,10 +64,10 @@ import type { GetContractClassLogsResponse, GetPublicLogsResponse } from '@aztec
|
|
|
62
64
|
import type { L2LogsSource } from '@aztec/stdlib/interfaces/server';
|
|
63
65
|
import { ContractClassLog, type LogFilter, type PrivateLog, type PublicLog, TxScopedL2Log } from '@aztec/stdlib/logs';
|
|
64
66
|
import type { L1ToL2MessageSource } from '@aztec/stdlib/messaging';
|
|
67
|
+
import type { CheckpointHeader } from '@aztec/stdlib/rollup';
|
|
65
68
|
import { type BlockHeader, type IndexedTxEffect, TxHash, TxReceipt } from '@aztec/stdlib/tx';
|
|
66
69
|
import type { UInt64 } from '@aztec/stdlib/types';
|
|
67
70
|
import {
|
|
68
|
-
Attributes,
|
|
69
71
|
type TelemetryClient,
|
|
70
72
|
type Traceable,
|
|
71
73
|
type Tracer,
|
|
@@ -75,21 +77,20 @@ import {
|
|
|
75
77
|
|
|
76
78
|
import { EventEmitter } from 'events';
|
|
77
79
|
import groupBy from 'lodash.groupby';
|
|
78
|
-
import { type GetContractReturnType, createPublicClient, fallback, http } from 'viem';
|
|
80
|
+
import { type GetContractReturnType, type Hex, createPublicClient, fallback, http } from 'viem';
|
|
79
81
|
|
|
80
82
|
import type { ArchiverDataStore, ArchiverL1SynchPoint } from './archiver_store.js';
|
|
81
83
|
import type { ArchiverConfig } from './config.js';
|
|
82
84
|
import {
|
|
83
|
-
|
|
85
|
+
retrieveCheckpointsFromRollup,
|
|
84
86
|
retrieveL1ToL2Message,
|
|
85
87
|
retrieveL1ToL2Messages,
|
|
86
|
-
|
|
88
|
+
retrievedToPublishedCheckpoint,
|
|
87
89
|
} from './data_retrieval.js';
|
|
88
90
|
import { InitialBlockNumberNotSequentialError, NoBlobBodiesFoundError } from './errors.js';
|
|
89
91
|
import { ArchiverInstrumentation } from './instrumentation.js';
|
|
90
92
|
import type { InboxMessage } from './structs/inbox_message.js';
|
|
91
|
-
import type
|
|
92
|
-
import { type ValidateBlockResult, validateBlockAttestations } from './validation.js';
|
|
93
|
+
import { type ValidateBlockResult, validateCheckpointAttestations } from './validation.js';
|
|
93
94
|
|
|
94
95
|
/**
|
|
95
96
|
* Helper interface to combine all sources this archiver implementation provides.
|
|
@@ -108,17 +109,28 @@ function mapArchiverConfig(config: Partial<ArchiverConfig>) {
|
|
|
108
109
|
pollingIntervalMs: config.archiverPollingIntervalMS,
|
|
109
110
|
batchSize: config.archiverBatchSize,
|
|
110
111
|
skipValidateBlockAttestations: config.skipValidateBlockAttestations,
|
|
112
|
+
maxAllowedEthClientDriftSeconds: config.maxAllowedEthClientDriftSeconds,
|
|
111
113
|
};
|
|
112
114
|
}
|
|
113
115
|
|
|
116
|
+
type RollupStatus = {
|
|
117
|
+
provenCheckpointNumber: number;
|
|
118
|
+
provenArchive: Hex;
|
|
119
|
+
pendingCheckpointNumber: number;
|
|
120
|
+
pendingArchive: Hex;
|
|
121
|
+
validationResult: ValidateBlockResult | undefined;
|
|
122
|
+
lastRetrievedCheckpoint?: PublishedCheckpoint;
|
|
123
|
+
lastL1BlockWithCheckpoint?: bigint;
|
|
124
|
+
};
|
|
125
|
+
|
|
114
126
|
/**
|
|
115
|
-
* Pulls
|
|
127
|
+
* Pulls checkpoints in a non-blocking manner and provides interface for their retrieval.
|
|
116
128
|
* Responsible for handling robust L1 polling so that other components do not need to
|
|
117
129
|
* concern themselves with it.
|
|
118
130
|
*/
|
|
119
131
|
export class Archiver extends (EventEmitter as new () => ArchiverEmitter) implements ArchiveSource, Traceable {
|
|
120
|
-
/** A loop in which we will be continually fetching new
|
|
121
|
-
private runningPromise
|
|
132
|
+
/** A loop in which we will be continually fetching new checkpoints. */
|
|
133
|
+
private runningPromise: RunningPromise;
|
|
122
134
|
|
|
123
135
|
private rollup: RollupContract;
|
|
124
136
|
private inbox: InboxContract;
|
|
@@ -146,9 +158,15 @@ export class Archiver extends (EventEmitter as new () => ArchiverEmitter) implem
|
|
|
146
158
|
private readonly publicClient: ViemPublicClient,
|
|
147
159
|
private readonly l1Addresses: { rollupAddress: EthAddress; inboxAddress: EthAddress; registryAddress: EthAddress },
|
|
148
160
|
readonly dataStore: ArchiverDataStore,
|
|
149
|
-
private config: {
|
|
161
|
+
private config: {
|
|
162
|
+
pollingIntervalMs: number;
|
|
163
|
+
batchSize: number;
|
|
164
|
+
skipValidateBlockAttestations?: boolean;
|
|
165
|
+
maxAllowedEthClientDriftSeconds: number;
|
|
166
|
+
},
|
|
150
167
|
private readonly blobSinkClient: BlobSinkClientInterface,
|
|
151
168
|
private readonly epochCache: EpochCache,
|
|
169
|
+
private readonly dateProvider: DateProvider,
|
|
152
170
|
private readonly instrumentation: ArchiverInstrumentation,
|
|
153
171
|
private readonly l1constants: L1RollupConstants & { l1StartBlockHash: Buffer32; genesisArchiveRoot: Fr },
|
|
154
172
|
private readonly log: Logger = createLogger('archiver'),
|
|
@@ -161,6 +179,15 @@ export class Archiver extends (EventEmitter as new () => ArchiverEmitter) implem
|
|
|
161
179
|
this.rollup = new RollupContract(publicClient, l1Addresses.rollupAddress);
|
|
162
180
|
this.inbox = new InboxContract(publicClient, l1Addresses.inboxAddress);
|
|
163
181
|
this.initialSyncPromise = promiseWithResolvers();
|
|
182
|
+
|
|
183
|
+
// Running promise starts with a small interval inbetween runs, so all iterations needed for the initial sync
|
|
184
|
+
// are done as fast as possible. This then gets updated once the initial sync completes.
|
|
185
|
+
this.runningPromise = new RunningPromise(
|
|
186
|
+
() => this.sync(),
|
|
187
|
+
this.log,
|
|
188
|
+
this.config.pollingIntervalMs / 10,
|
|
189
|
+
makeLoggingErrorHandler(this.log, NoBlobBodiesFoundError, BlockTagTooOldError),
|
|
190
|
+
);
|
|
164
191
|
}
|
|
165
192
|
|
|
166
193
|
/**
|
|
@@ -209,7 +236,10 @@ export class Archiver extends (EventEmitter as new () => ArchiverEmitter) implem
|
|
|
209
236
|
genesisArchiveRoot: Fr.fromHexString(genesisArchiveRoot),
|
|
210
237
|
};
|
|
211
238
|
|
|
212
|
-
const opts = merge(
|
|
239
|
+
const opts = merge(
|
|
240
|
+
{ pollingIntervalMs: 10_000, batchSize: 100, maxAllowedEthClientDriftSeconds: 300 },
|
|
241
|
+
mapArchiverConfig(config),
|
|
242
|
+
);
|
|
213
243
|
|
|
214
244
|
const epochCache = deps.epochCache ?? (await EpochCache.create(config.l1Contracts.rollupAddress, config, deps));
|
|
215
245
|
const telemetry = deps.telemetry ?? getTelemetryClient();
|
|
@@ -221,6 +251,7 @@ export class Archiver extends (EventEmitter as new () => ArchiverEmitter) implem
|
|
|
221
251
|
opts,
|
|
222
252
|
deps.blobSinkClient,
|
|
223
253
|
epochCache,
|
|
254
|
+
deps.dateProvider ?? new DateProvider(),
|
|
224
255
|
await ArchiverInstrumentation.new(telemetry, () => archiverStore.estimateSize()),
|
|
225
256
|
l1Constants,
|
|
226
257
|
);
|
|
@@ -238,38 +269,30 @@ export class Archiver extends (EventEmitter as new () => ArchiverEmitter) implem
|
|
|
238
269
|
* @param blockUntilSynced - If true, blocks until the archiver has fully synced.
|
|
239
270
|
*/
|
|
240
271
|
public async start(blockUntilSynced: boolean): Promise<void> {
|
|
241
|
-
if (this.runningPromise) {
|
|
272
|
+
if (this.runningPromise.isRunning()) {
|
|
242
273
|
throw new Error('Archiver is already running');
|
|
243
274
|
}
|
|
244
275
|
|
|
245
276
|
await this.blobSinkClient.testSources();
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
() => this.sync(false),
|
|
256
|
-
this.log,
|
|
257
|
-
this.config.pollingIntervalMs,
|
|
258
|
-
makeLoggingErrorHandler(
|
|
259
|
-
this.log,
|
|
260
|
-
// Ignored errors will not log to the console
|
|
261
|
-
// We ignore NoBlobBodiesFound as the message may not have been passed to the blob sink yet
|
|
262
|
-
NoBlobBodiesFoundError,
|
|
263
|
-
),
|
|
277
|
+
await this.testEthereumNodeSynced();
|
|
278
|
+
|
|
279
|
+
// Log initial state for the archiver
|
|
280
|
+
const { l1StartBlock } = this.l1constants;
|
|
281
|
+
const { blocksSynchedTo = l1StartBlock, messagesSynchedTo = l1StartBlock } = await this.store.getSynchPoint();
|
|
282
|
+
const currentL2Block = await this.getBlockNumber();
|
|
283
|
+
this.log.info(
|
|
284
|
+
`Starting archiver sync to rollup contract ${this.l1Addresses.rollupAddress.toString()} from L1 block ${blocksSynchedTo} and L2 block ${currentL2Block}`,
|
|
285
|
+
{ blocksSynchedTo, messagesSynchedTo, currentL2Block },
|
|
264
286
|
);
|
|
265
287
|
|
|
288
|
+
// Start sync loop, and return the wait for initial sync if we are asked to block until synced
|
|
266
289
|
this.runningPromise.start();
|
|
290
|
+
if (blockUntilSynced) {
|
|
291
|
+
return this.waitForInitialSync();
|
|
292
|
+
}
|
|
267
293
|
}
|
|
268
294
|
|
|
269
295
|
public syncImmediate() {
|
|
270
|
-
if (!this.runningPromise) {
|
|
271
|
-
throw new Error('Archiver is not running');
|
|
272
|
-
}
|
|
273
296
|
return this.runningPromise.trigger();
|
|
274
297
|
}
|
|
275
298
|
|
|
@@ -277,27 +300,26 @@ export class Archiver extends (EventEmitter as new () => ArchiverEmitter) implem
|
|
|
277
300
|
return this.initialSyncPromise.promise;
|
|
278
301
|
}
|
|
279
302
|
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
return false;
|
|
303
|
+
/** Checks that the ethereum node we are connected to has a latest timestamp no more than the allowed drift. Throw if not. */
|
|
304
|
+
private async testEthereumNodeSynced() {
|
|
305
|
+
const maxAllowedDelay = this.config.maxAllowedEthClientDriftSeconds;
|
|
306
|
+
if (maxAllowedDelay === 0) {
|
|
307
|
+
return;
|
|
308
|
+
}
|
|
309
|
+
const { number, timestamp: l1Timestamp } = await this.publicClient.getBlock({ includeTransactions: false });
|
|
310
|
+
const currentTime = BigInt(this.dateProvider.nowInSeconds());
|
|
311
|
+
if (currentTime - l1Timestamp > BigInt(maxAllowedDelay)) {
|
|
312
|
+
throw new Error(
|
|
313
|
+
`Ethereum node is out of sync (last block synced ${number} at ${l1Timestamp} vs current time ${currentTime})`,
|
|
314
|
+
);
|
|
293
315
|
}
|
|
294
316
|
}
|
|
295
317
|
|
|
296
318
|
/**
|
|
297
319
|
* Fetches logs from L1 contracts and processes them.
|
|
298
320
|
*/
|
|
299
|
-
@trackSpan('Archiver.sync'
|
|
300
|
-
private async sync(
|
|
321
|
+
@trackSpan('Archiver.sync')
|
|
322
|
+
private async sync() {
|
|
301
323
|
/**
|
|
302
324
|
* We keep track of three "pointers" to L1 blocks:
|
|
303
325
|
* 1. the last L1 block that published an L2 block
|
|
@@ -307,8 +329,6 @@ export class Archiver extends (EventEmitter as new () => ArchiverEmitter) implem
|
|
|
307
329
|
* We do this to deal with L1 data providers that are eventually consistent (e.g. Infura).
|
|
308
330
|
* We guard against seeing block X with no data at one point, and later, the provider processes the block and it has data.
|
|
309
331
|
* The archiver will stay back, until there's data on L1 that will move the pointers forward.
|
|
310
|
-
*
|
|
311
|
-
* This code does not handle reorgs.
|
|
312
332
|
*/
|
|
313
333
|
const { l1StartBlock, l1StartBlockHash } = this.l1constants;
|
|
314
334
|
const {
|
|
@@ -320,13 +340,12 @@ export class Archiver extends (EventEmitter as new () => ArchiverEmitter) implem
|
|
|
320
340
|
const currentL1BlockNumber = currentL1Block.number;
|
|
321
341
|
const currentL1BlockHash = Buffer32.fromString(currentL1Block.hash);
|
|
322
342
|
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
}
|
|
343
|
+
this.log.trace(`Starting new archiver sync iteration`, {
|
|
344
|
+
blocksSynchedTo,
|
|
345
|
+
messagesSynchedTo,
|
|
346
|
+
currentL1BlockNumber,
|
|
347
|
+
currentL1BlockHash,
|
|
348
|
+
});
|
|
330
349
|
|
|
331
350
|
// ********** Ensuring Consistency of data pulled from L1 **********
|
|
332
351
|
|
|
@@ -356,28 +375,45 @@ export class Archiver extends (EventEmitter as new () => ArchiverEmitter) implem
|
|
|
356
375
|
? (await this.publicClient.getBlock({ blockNumber: currentL1BlockNumber })).timestamp
|
|
357
376
|
: this.l1Timestamp;
|
|
358
377
|
|
|
359
|
-
//
|
|
378
|
+
// Warn if the latest L1 block timestamp is too old
|
|
379
|
+
const maxAllowedDelay = this.config.maxAllowedEthClientDriftSeconds;
|
|
380
|
+
const now = this.dateProvider.nowInSeconds();
|
|
381
|
+
if (maxAllowedDelay > 0 && Number(currentL1Timestamp) <= now - maxAllowedDelay) {
|
|
382
|
+
this.log.warn(
|
|
383
|
+
`Latest L1 block ${currentL1BlockNumber} timestamp ${currentL1Timestamp} is too old. Make sure your Ethereum node is synced.`,
|
|
384
|
+
{ currentL1BlockNumber, currentL1Timestamp, now, maxAllowedDelay },
|
|
385
|
+
);
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
// ********** Events that are processed per checkpoint **********
|
|
360
389
|
if (currentL1BlockNumber > blocksSynchedTo) {
|
|
361
|
-
// First we retrieve new L2 blocks and store them in the DB. This will also update the
|
|
362
|
-
// pending chain validation status, proven
|
|
363
|
-
const rollupStatus = await this.
|
|
390
|
+
// First we retrieve new checkpoints and L2 blocks and store them in the DB. This will also update the
|
|
391
|
+
// pending chain validation status, proven checkpoint number, and synched L1 block number.
|
|
392
|
+
const rollupStatus = await this.handleCheckpoints(blocksSynchedTo, currentL1BlockNumber);
|
|
364
393
|
// Then we prune the current epoch if it'd reorg on next submission.
|
|
365
|
-
// Note that we don't do this before retrieving
|
|
366
|
-
//
|
|
394
|
+
// Note that we don't do this before retrieving checkpoints because we may need to retrieve
|
|
395
|
+
// checkpoints from more than 2 epochs ago, so we want to make sure we have the latest view of
|
|
367
396
|
// the chain locally before we start unwinding stuff. This can be optimized by figuring out
|
|
368
|
-
// up to which point we're pruning, and then requesting
|
|
397
|
+
// up to which point we're pruning, and then requesting checkpoints up to that point only.
|
|
369
398
|
const { rollupCanPrune } = await this.handleEpochPrune(
|
|
370
|
-
rollupStatus.
|
|
399
|
+
rollupStatus.provenCheckpointNumber,
|
|
371
400
|
currentL1BlockNumber,
|
|
372
401
|
currentL1Timestamp,
|
|
373
402
|
);
|
|
374
403
|
|
|
375
|
-
//
|
|
404
|
+
// If the last checkpoint we processed had an invalid attestation, we manually advance the L1 syncpoint
|
|
405
|
+
// past it, since otherwise we'll keep downloading it and reprocessing it on every iteration until
|
|
406
|
+
// we get a valid checkpoint to advance the syncpoint.
|
|
407
|
+
if (!rollupStatus.validationResult?.valid && rollupStatus.lastL1BlockWithCheckpoint !== undefined) {
|
|
408
|
+
await this.store.setBlockSynchedL1BlockNumber(rollupStatus.lastL1BlockWithCheckpoint);
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
// And lastly we check if we are missing any checkpoints behind us due to a possible L1 reorg.
|
|
376
412
|
// We only do this if rollup cant prune on the next submission. Otherwise we will end up
|
|
377
|
-
// re-syncing the
|
|
413
|
+
// re-syncing the checkpoints we have just unwound above. We also dont do this if the last checkpoint is invalid,
|
|
378
414
|
// since the archiver will rightfully refuse to sync up to it.
|
|
379
415
|
if (!rollupCanPrune && rollupStatus.validationResult?.valid) {
|
|
380
|
-
await this.
|
|
416
|
+
await this.checkForNewCheckpointsBeforeL1SyncPoint(rollupStatus, blocksSynchedTo, currentL1BlockNumber);
|
|
381
417
|
}
|
|
382
418
|
|
|
383
419
|
this.instrumentation.updateL1BlockHeight(currentL1BlockNumber);
|
|
@@ -388,15 +424,18 @@ export class Archiver extends (EventEmitter as new () => ArchiverEmitter) implem
|
|
|
388
424
|
// but the corresponding blocks have not been processed (see #12631).
|
|
389
425
|
this.l1Timestamp = currentL1Timestamp;
|
|
390
426
|
this.l1BlockNumber = currentL1BlockNumber;
|
|
391
|
-
this.initialSyncComplete = true;
|
|
392
|
-
this.initialSyncPromise.resolve();
|
|
393
427
|
|
|
394
|
-
|
|
395
|
-
|
|
428
|
+
// We resolve the initial sync only once we've caught up with the latest L1 block number (with 1 block grace)
|
|
429
|
+
// so if the initial sync took too long, we still go for another iteration.
|
|
430
|
+
if (!this.initialSyncComplete && currentL1BlockNumber + 1n >= (await this.publicClient.getBlockNumber())) {
|
|
431
|
+
this.log.info(`Initial archiver sync to L1 block ${currentL1BlockNumber} complete`, {
|
|
396
432
|
l1BlockNumber: currentL1BlockNumber,
|
|
397
433
|
syncPoint: await this.store.getSynchPoint(),
|
|
398
434
|
...(await this.getL2Tips()),
|
|
399
435
|
});
|
|
436
|
+
this.runningPromise.setPollingIntervalMS(this.config.pollingIntervalMs);
|
|
437
|
+
this.initialSyncComplete = true;
|
|
438
|
+
this.initialSyncPromise.resolve();
|
|
400
439
|
}
|
|
401
440
|
}
|
|
402
441
|
|
|
@@ -414,43 +453,47 @@ export class Archiver extends (EventEmitter as new () => ArchiverEmitter) implem
|
|
|
414
453
|
return result;
|
|
415
454
|
}
|
|
416
455
|
|
|
417
|
-
/** Checks if there'd be a reorg for the next
|
|
418
|
-
private async handleEpochPrune(
|
|
456
|
+
/** Checks if there'd be a reorg for the next checkpoint submission and start pruning now. */
|
|
457
|
+
private async handleEpochPrune(
|
|
458
|
+
provenCheckpointNumber: number,
|
|
459
|
+
currentL1BlockNumber: bigint,
|
|
460
|
+
currentL1Timestamp: bigint,
|
|
461
|
+
) {
|
|
419
462
|
const rollupCanPrune = await this.canPrune(currentL1BlockNumber, currentL1Timestamp);
|
|
420
|
-
const
|
|
421
|
-
const canPrune =
|
|
463
|
+
const localPendingCheckpointNumber = await this.getSynchedCheckpointNumber();
|
|
464
|
+
const canPrune = localPendingCheckpointNumber > provenCheckpointNumber && rollupCanPrune;
|
|
422
465
|
|
|
423
466
|
if (canPrune) {
|
|
424
467
|
const timer = new Timer();
|
|
425
|
-
const pruneFrom =
|
|
468
|
+
const pruneFrom = provenCheckpointNumber + 1;
|
|
426
469
|
|
|
427
|
-
const header = await this.
|
|
470
|
+
const header = await this.getCheckpointHeader(Number(pruneFrom));
|
|
428
471
|
if (header === undefined) {
|
|
429
|
-
throw new Error(`Missing
|
|
472
|
+
throw new Error(`Missing checkpoint header ${pruneFrom}`);
|
|
430
473
|
}
|
|
431
474
|
|
|
432
|
-
const pruneFromSlotNumber = header.
|
|
433
|
-
const pruneFromEpochNumber = getEpochAtSlot(pruneFromSlotNumber, this.l1constants);
|
|
475
|
+
const pruneFromSlotNumber = header.slotNumber;
|
|
476
|
+
const pruneFromEpochNumber: EpochNumber = getEpochAtSlot(pruneFromSlotNumber, this.l1constants);
|
|
434
477
|
|
|
435
|
-
const
|
|
478
|
+
const checkpointsToUnwind = localPendingCheckpointNumber - provenCheckpointNumber;
|
|
436
479
|
|
|
437
|
-
const
|
|
480
|
+
const checkpoints = await this.getCheckpoints(Number(provenCheckpointNumber) + 1, Number(checkpointsToUnwind));
|
|
438
481
|
|
|
439
482
|
// Emit an event for listening services to react to the chain prune
|
|
440
483
|
this.emit(L2BlockSourceEvents.L2PruneDetected, {
|
|
441
484
|
type: L2BlockSourceEvents.L2PruneDetected,
|
|
442
485
|
epochNumber: pruneFromEpochNumber,
|
|
443
|
-
blocks,
|
|
486
|
+
blocks: checkpoints.flatMap(c => L2Block.fromCheckpoint(c)),
|
|
444
487
|
});
|
|
445
488
|
|
|
446
489
|
this.log.debug(
|
|
447
|
-
`L2 prune from ${
|
|
490
|
+
`L2 prune from ${provenCheckpointNumber + 1} to ${localPendingCheckpointNumber} will occur on next checkpoint submission.`,
|
|
448
491
|
);
|
|
449
|
-
await this.
|
|
492
|
+
await this.unwindCheckpoints(localPendingCheckpointNumber, checkpointsToUnwind);
|
|
450
493
|
this.log.warn(
|
|
451
|
-
`Unwound ${count(
|
|
452
|
-
`to ${
|
|
453
|
-
`Updated
|
|
494
|
+
`Unwound ${count(checkpointsToUnwind, 'checkpoint')} from checkpoint ${localPendingCheckpointNumber} ` +
|
|
495
|
+
`to ${provenCheckpointNumber} due to predicted reorg at L1 block ${currentL1BlockNumber}. ` +
|
|
496
|
+
`Updated latest checkpoint is ${await this.getSynchedCheckpointNumber()}.`,
|
|
454
497
|
);
|
|
455
498
|
this.instrumentation.processPrune(timer.ms());
|
|
456
499
|
// TODO(palla/reorg): Do we need to set the block synched L1 block number here?
|
|
@@ -497,7 +540,7 @@ export class Archiver extends (EventEmitter as new () => ArchiverEmitter) implem
|
|
|
497
540
|
remoteMessagesState.totalMessagesInserted === localMessagesInserted &&
|
|
498
541
|
remoteMessagesState.messagesRollingHash.equals(localLastMessage?.rollingHash ?? Buffer16.ZERO)
|
|
499
542
|
) {
|
|
500
|
-
this.log.
|
|
543
|
+
this.log.trace(
|
|
501
544
|
`No L1 to L2 messages to query between L1 blocks ${messagesSyncPoint.l1BlockNumber} and ${currentL1BlockNumber}.`,
|
|
502
545
|
);
|
|
503
546
|
return;
|
|
@@ -629,167 +672,185 @@ export class Archiver extends (EventEmitter as new () => ArchiverEmitter) implem
|
|
|
629
672
|
return Buffer32.fromString(block.hash);
|
|
630
673
|
}
|
|
631
674
|
|
|
632
|
-
private async
|
|
633
|
-
const
|
|
675
|
+
private async handleCheckpoints(blocksSynchedTo: bigint, currentL1BlockNumber: bigint): Promise<RollupStatus> {
|
|
676
|
+
const localPendingCheckpointNumber = await this.getSynchedCheckpointNumber();
|
|
634
677
|
const initialValidationResult: ValidateBlockResult | undefined = await this.store.getPendingChainValidationStatus();
|
|
635
|
-
const [
|
|
636
|
-
|
|
678
|
+
const [
|
|
679
|
+
rollupProvenCheckpointNumber,
|
|
680
|
+
provenArchive,
|
|
681
|
+
rollupPendingCheckpointNumber,
|
|
682
|
+
pendingArchive,
|
|
683
|
+
archiveForLocalPendingCheckpointNumber,
|
|
684
|
+
] = await this.rollup.status(BigInt(localPendingCheckpointNumber), { blockNumber: currentL1BlockNumber });
|
|
685
|
+
const provenCheckpointNumber = Number(rollupProvenCheckpointNumber);
|
|
686
|
+
const pendingCheckpointNumber = Number(rollupPendingCheckpointNumber);
|
|
637
687
|
const rollupStatus = {
|
|
638
|
-
|
|
688
|
+
provenCheckpointNumber,
|
|
639
689
|
provenArchive,
|
|
640
|
-
|
|
690
|
+
pendingCheckpointNumber,
|
|
641
691
|
pendingArchive,
|
|
642
692
|
validationResult: initialValidationResult,
|
|
643
693
|
};
|
|
644
694
|
this.log.trace(`Retrieved rollup status at current L1 block ${currentL1BlockNumber}.`, {
|
|
645
|
-
|
|
695
|
+
localPendingCheckpointNumber,
|
|
646
696
|
blocksSynchedTo,
|
|
647
697
|
currentL1BlockNumber,
|
|
648
|
-
|
|
698
|
+
archiveForLocalPendingCheckpointNumber,
|
|
649
699
|
...rollupStatus,
|
|
650
700
|
});
|
|
651
701
|
|
|
652
|
-
const
|
|
653
|
-
// Annoying edge case: if proven
|
|
654
|
-
// we need to set it to zero. This is an edge case because we dont have a
|
|
655
|
-
// so
|
|
656
|
-
if (
|
|
657
|
-
const
|
|
658
|
-
if (
|
|
659
|
-
await this.
|
|
660
|
-
this.log.info(`Rolled back proven chain to
|
|
702
|
+
const updateProvenCheckpoint = async () => {
|
|
703
|
+
// Annoying edge case: if proven checkpoint is moved back to 0 due to a reorg at the beginning of the chain,
|
|
704
|
+
// we need to set it to zero. This is an edge case because we dont have a checkpoint zero (initial checkpoint is one),
|
|
705
|
+
// so localCheckpointForDestinationProvenCheckpointNumber would not be found below.
|
|
706
|
+
if (provenCheckpointNumber === 0) {
|
|
707
|
+
const localProvenCheckpointNumber = await this.getProvenCheckpointNumber();
|
|
708
|
+
if (localProvenCheckpointNumber !== provenCheckpointNumber) {
|
|
709
|
+
await this.setProvenCheckpointNumber(provenCheckpointNumber);
|
|
710
|
+
this.log.info(`Rolled back proven chain to checkpoint ${provenCheckpointNumber}`, { provenCheckpointNumber });
|
|
661
711
|
}
|
|
662
712
|
}
|
|
663
713
|
|
|
664
|
-
const
|
|
714
|
+
const localCheckpointForDestinationProvenCheckpointNumber = await this.getCheckpoint(provenCheckpointNumber);
|
|
665
715
|
|
|
666
|
-
// Sanity check. I've hit what seems to be a state where the proven
|
|
667
|
-
// synched
|
|
668
|
-
const synched = await this.
|
|
669
|
-
if (
|
|
716
|
+
// Sanity check. I've hit what seems to be a state where the proven checkpoint is set to a value greater than the latest
|
|
717
|
+
// synched checkpoint when requesting L2Tips from the archiver. This is the only place where the proven checkpoint is set.
|
|
718
|
+
const synched = await this.getSynchedCheckpointNumber();
|
|
719
|
+
if (
|
|
720
|
+
localCheckpointForDestinationProvenCheckpointNumber &&
|
|
721
|
+
synched < localCheckpointForDestinationProvenCheckpointNumber.number
|
|
722
|
+
) {
|
|
670
723
|
this.log.error(
|
|
671
|
-
`Hit local
|
|
724
|
+
`Hit local checkpoint greater than last synched checkpoint: ${localCheckpointForDestinationProvenCheckpointNumber.number} > ${synched}`,
|
|
672
725
|
);
|
|
673
726
|
}
|
|
674
727
|
|
|
675
728
|
this.log.trace(
|
|
676
|
-
`Local
|
|
677
|
-
|
|
729
|
+
`Local checkpoint for remote proven checkpoint ${provenCheckpointNumber} is ${
|
|
730
|
+
localCheckpointForDestinationProvenCheckpointNumber?.archive.root.toString() ?? 'undefined'
|
|
678
731
|
}`,
|
|
679
732
|
);
|
|
680
733
|
|
|
734
|
+
const lastProvenBlockNumber = await this.getLastBlockNumberInCheckpoint(provenCheckpointNumber);
|
|
681
735
|
if (
|
|
682
|
-
|
|
683
|
-
provenArchive ===
|
|
736
|
+
localCheckpointForDestinationProvenCheckpointNumber &&
|
|
737
|
+
provenArchive === localCheckpointForDestinationProvenCheckpointNumber.archive.root.toString()
|
|
684
738
|
) {
|
|
685
|
-
const
|
|
686
|
-
if (
|
|
687
|
-
await this.
|
|
688
|
-
this.log.info(`Updated proven chain to
|
|
689
|
-
|
|
739
|
+
const localProvenCheckpointNumber = await this.getProvenCheckpointNumber();
|
|
740
|
+
if (localProvenCheckpointNumber !== provenCheckpointNumber) {
|
|
741
|
+
await this.setProvenCheckpointNumber(provenCheckpointNumber);
|
|
742
|
+
this.log.info(`Updated proven chain to checkpoint ${provenCheckpointNumber}`, {
|
|
743
|
+
provenCheckpointNumber,
|
|
690
744
|
});
|
|
691
|
-
const provenSlotNumber =
|
|
692
|
-
|
|
693
|
-
|
|
745
|
+
const provenSlotNumber = localCheckpointForDestinationProvenCheckpointNumber.header.slotNumber;
|
|
746
|
+
const provenEpochNumber: EpochNumber = getEpochAtSlot(provenSlotNumber, this.l1constants);
|
|
747
|
+
|
|
694
748
|
this.emit(L2BlockSourceEvents.L2BlockProven, {
|
|
695
749
|
type: L2BlockSourceEvents.L2BlockProven,
|
|
696
|
-
blockNumber:
|
|
750
|
+
blockNumber: BigInt(lastProvenBlockNumber),
|
|
697
751
|
slotNumber: provenSlotNumber,
|
|
698
752
|
epochNumber: provenEpochNumber,
|
|
699
753
|
});
|
|
700
754
|
} else {
|
|
701
|
-
this.log.trace(`Proven
|
|
755
|
+
this.log.trace(`Proven checkpoint ${provenCheckpointNumber} already stored.`);
|
|
702
756
|
}
|
|
703
757
|
}
|
|
704
|
-
this.instrumentation.updateLastProvenBlock(
|
|
758
|
+
this.instrumentation.updateLastProvenBlock(lastProvenBlockNumber);
|
|
705
759
|
};
|
|
706
760
|
|
|
707
|
-
// This is an edge case that we only hit if there are no proposed
|
|
708
|
-
// If we have 0
|
|
709
|
-
const
|
|
710
|
-
if (
|
|
761
|
+
// This is an edge case that we only hit if there are no proposed checkpoints.
|
|
762
|
+
// If we have 0 checkpoints locally and there are no checkpoints onchain there is nothing to do.
|
|
763
|
+
const noCheckpoints = localPendingCheckpointNumber === 0 && pendingCheckpointNumber === 0;
|
|
764
|
+
if (noCheckpoints) {
|
|
711
765
|
await this.store.setBlockSynchedL1BlockNumber(currentL1BlockNumber);
|
|
712
766
|
this.log.debug(
|
|
713
|
-
`No
|
|
767
|
+
`No checkpoints to retrieve from ${blocksSynchedTo + 1n} to ${currentL1BlockNumber}, no checkpoints on chain`,
|
|
714
768
|
);
|
|
715
769
|
return rollupStatus;
|
|
716
770
|
}
|
|
717
771
|
|
|
718
|
-
await
|
|
772
|
+
await updateProvenCheckpoint();
|
|
719
773
|
|
|
720
774
|
// Related to the L2 reorgs of the pending chain. We are only interested in actually addressing a reorg if there
|
|
721
|
-
// are any state that could be impacted by it. If we have no
|
|
722
|
-
if (
|
|
723
|
-
const
|
|
724
|
-
if (
|
|
725
|
-
throw new Error(`Missing
|
|
775
|
+
// are any state that could be impacted by it. If we have no checkpoints, there is no impact.
|
|
776
|
+
if (localPendingCheckpointNumber > 0) {
|
|
777
|
+
const localPendingCheckpoint = await this.getCheckpoint(localPendingCheckpointNumber);
|
|
778
|
+
if (localPendingCheckpoint === undefined) {
|
|
779
|
+
throw new Error(`Missing checkpoint ${localPendingCheckpointNumber}`);
|
|
726
780
|
}
|
|
727
781
|
|
|
728
|
-
const localPendingArchiveRoot =
|
|
729
|
-
const
|
|
730
|
-
if (
|
|
782
|
+
const localPendingArchiveRoot = localPendingCheckpoint.archive.root.toString();
|
|
783
|
+
const noCheckpointSinceLast = localPendingCheckpoint && pendingArchive === localPendingArchiveRoot;
|
|
784
|
+
if (noCheckpointSinceLast) {
|
|
731
785
|
// We believe the following line causes a problem when we encounter L1 re-orgs.
|
|
732
786
|
// Basically, by setting the synched L1 block number here, we are saying that we have
|
|
733
|
-
// processed all
|
|
787
|
+
// processed all checkpoints up to the current L1 block number and we will not attempt to retrieve logs from
|
|
734
788
|
// this block again (or any blocks before).
|
|
735
|
-
// However, in the re-org scenario, our L1 node is temporarily lying to us and we end up potentially missing
|
|
789
|
+
// However, in the re-org scenario, our L1 node is temporarily lying to us and we end up potentially missing checkpoints.
|
|
736
790
|
// We must only set this block number based on actually retrieved logs.
|
|
737
791
|
// TODO(#8621): Tackle this properly when we handle L1 Re-orgs.
|
|
738
792
|
// await this.store.setBlockSynchedL1BlockNumber(currentL1BlockNumber);
|
|
739
|
-
this.log.debug(`No
|
|
793
|
+
this.log.debug(`No checkpoints to retrieve from ${blocksSynchedTo + 1n} to ${currentL1BlockNumber}`);
|
|
740
794
|
return rollupStatus;
|
|
741
795
|
}
|
|
742
796
|
|
|
743
|
-
const
|
|
744
|
-
if (!
|
|
745
|
-
// If our local pending
|
|
797
|
+
const localPendingCheckpointInChain = archiveForLocalPendingCheckpointNumber === localPendingArchiveRoot;
|
|
798
|
+
if (!localPendingCheckpointInChain) {
|
|
799
|
+
// If our local pending checkpoint tip is not in the chain on L1 a "prune" must have happened
|
|
746
800
|
// or the L1 have reorged.
|
|
747
801
|
// In any case, we have to figure out how far into the past the action will take us.
|
|
748
|
-
// For simplicity here, we will simply rewind until we end in a
|
|
802
|
+
// For simplicity here, we will simply rewind until we end in a checkpoint that is also on the chain on L1.
|
|
749
803
|
this.log.debug(
|
|
750
|
-
`L2 prune has been detected due to local pending
|
|
751
|
-
{
|
|
804
|
+
`L2 prune has been detected due to local pending checkpoint ${localPendingCheckpointNumber} not in chain`,
|
|
805
|
+
{ localPendingCheckpointNumber, localPendingArchiveRoot, archiveForLocalPendingCheckpointNumber },
|
|
752
806
|
);
|
|
753
807
|
|
|
754
|
-
let tipAfterUnwind =
|
|
808
|
+
let tipAfterUnwind = localPendingCheckpointNumber;
|
|
755
809
|
while (true) {
|
|
756
|
-
const
|
|
757
|
-
if (
|
|
810
|
+
const candidateCheckpoint = await this.getCheckpoint(tipAfterUnwind);
|
|
811
|
+
if (candidateCheckpoint === undefined) {
|
|
758
812
|
break;
|
|
759
813
|
}
|
|
760
814
|
|
|
761
|
-
const archiveAtContract = await this.rollup.archiveAt(BigInt(
|
|
762
|
-
|
|
763
|
-
|
|
815
|
+
const archiveAtContract = await this.rollup.archiveAt(BigInt(candidateCheckpoint.number));
|
|
816
|
+
this.log.trace(
|
|
817
|
+
`Checking local checkpoint ${candidateCheckpoint.number} with archive ${candidateCheckpoint.archive.root}`,
|
|
818
|
+
{
|
|
819
|
+
archiveAtContract,
|
|
820
|
+
archiveLocal: candidateCheckpoint.archive.root.toString(),
|
|
821
|
+
},
|
|
822
|
+
);
|
|
823
|
+
if (archiveAtContract === candidateCheckpoint.archive.root.toString()) {
|
|
764
824
|
break;
|
|
765
825
|
}
|
|
766
826
|
tipAfterUnwind--;
|
|
767
827
|
}
|
|
768
828
|
|
|
769
|
-
const
|
|
770
|
-
await this.
|
|
829
|
+
const checkpointsToUnwind = localPendingCheckpointNumber - tipAfterUnwind;
|
|
830
|
+
await this.unwindCheckpoints(localPendingCheckpointNumber, checkpointsToUnwind);
|
|
771
831
|
|
|
772
832
|
this.log.warn(
|
|
773
|
-
`Unwound ${count(
|
|
774
|
-
`due to mismatched
|
|
775
|
-
`Updated L2 latest
|
|
833
|
+
`Unwound ${count(checkpointsToUnwind, 'checkpoint')} from checkpoint ${localPendingCheckpointNumber} ` +
|
|
834
|
+
`due to mismatched checkpoint hashes at L1 block ${currentL1BlockNumber}. ` +
|
|
835
|
+
`Updated L2 latest checkpoint is ${await this.getSynchedCheckpointNumber()}.`,
|
|
776
836
|
);
|
|
777
837
|
}
|
|
778
838
|
}
|
|
779
839
|
|
|
780
|
-
// Retrieve
|
|
840
|
+
// Retrieve checkpoints in batches. Each batch is estimated to accommodate up to 'blockBatchSize' L1 blocks,
|
|
781
841
|
// computed using the L2 block time vs the L1 block time.
|
|
782
842
|
let searchStartBlock: bigint = blocksSynchedTo;
|
|
783
843
|
let searchEndBlock: bigint = blocksSynchedTo;
|
|
784
|
-
let
|
|
844
|
+
let lastRetrievedCheckpoint: PublishedCheckpoint | undefined;
|
|
845
|
+
let lastL1BlockWithCheckpoint: bigint | undefined = undefined;
|
|
785
846
|
|
|
786
847
|
do {
|
|
787
848
|
[searchStartBlock, searchEndBlock] = this.nextRange(searchEndBlock, currentL1BlockNumber);
|
|
788
849
|
|
|
789
|
-
this.log.trace(`Retrieving
|
|
850
|
+
this.log.trace(`Retrieving checkpoints from L1 block ${searchStartBlock} to ${searchEndBlock}`);
|
|
790
851
|
|
|
791
852
|
// TODO(md): Retrieve from blob sink then from consensus client, then from peers
|
|
792
|
-
const
|
|
853
|
+
const retrievedCheckpoints = await retrieveCheckpointsFromRollup(
|
|
793
854
|
this.rollup.getContract() as GetContractReturnType<typeof RollupAbi, ViemPublicClient>,
|
|
794
855
|
this.publicClient,
|
|
795
856
|
this.blobSinkClient,
|
|
@@ -798,31 +859,35 @@ export class Archiver extends (EventEmitter as new () => ArchiverEmitter) implem
|
|
|
798
859
|
this.log,
|
|
799
860
|
);
|
|
800
861
|
|
|
801
|
-
if (
|
|
862
|
+
if (retrievedCheckpoints.length === 0) {
|
|
802
863
|
// We are not calling `setBlockSynchedL1BlockNumber` because it may cause sync issues if based off infura.
|
|
803
864
|
// See further details in earlier comments.
|
|
804
|
-
this.log.trace(`Retrieved no new
|
|
865
|
+
this.log.trace(`Retrieved no new checkpoints from L1 block ${searchStartBlock} to ${searchEndBlock}`);
|
|
805
866
|
continue;
|
|
806
867
|
}
|
|
807
868
|
|
|
808
|
-
const lastProcessedL1BlockNumber = retrievedBlocks[retrievedBlocks.length - 1].l1.blockNumber;
|
|
809
869
|
this.log.debug(
|
|
810
|
-
`Retrieved ${
|
|
870
|
+
`Retrieved ${retrievedCheckpoints.length} new checkpoints between L1 blocks ${searchStartBlock} and ${searchEndBlock}`,
|
|
871
|
+
{
|
|
872
|
+
lastProcessedCheckpoint: retrievedCheckpoints[retrievedCheckpoints.length - 1].l1,
|
|
873
|
+
searchStartBlock,
|
|
874
|
+
searchEndBlock,
|
|
875
|
+
},
|
|
811
876
|
);
|
|
812
877
|
|
|
813
|
-
const
|
|
814
|
-
const
|
|
878
|
+
const publishedCheckpoints = await Promise.all(retrievedCheckpoints.map(b => retrievedToPublishedCheckpoint(b)));
|
|
879
|
+
const validCheckpoints: PublishedCheckpoint[] = [];
|
|
815
880
|
|
|
816
|
-
for (const
|
|
881
|
+
for (const published of publishedCheckpoints) {
|
|
817
882
|
const validationResult = this.config.skipValidateBlockAttestations
|
|
818
883
|
? { valid: true as const }
|
|
819
|
-
: await
|
|
884
|
+
: await validateCheckpointAttestations(published, this.epochCache, this.l1constants, this.log);
|
|
820
885
|
|
|
821
|
-
// Only update the validation result if it has changed, so we can keep track of the first invalid
|
|
822
|
-
// in case there is a sequence of more than one invalid
|
|
823
|
-
// There is an exception though: if
|
|
886
|
+
// Only update the validation result if it has changed, so we can keep track of the first invalid checkpoint
|
|
887
|
+
// in case there is a sequence of more than one invalid checkpoint, as we need to invalidate the first one.
|
|
888
|
+
// There is an exception though: if a checkpoint is invalidated and replaced with another invalid checkpoint,
|
|
824
889
|
// we need to update the validation result, since we need to be able to invalidate the new one.
|
|
825
|
-
// See test 'chain progresses if an invalid
|
|
890
|
+
// See test 'chain progresses if an invalid checkpoint is invalidated with an invalid one' for more info.
|
|
826
891
|
if (
|
|
827
892
|
rollupStatus.validationResult?.valid !== validationResult.valid ||
|
|
828
893
|
(!rollupStatus.validationResult.valid &&
|
|
@@ -833,9 +898,9 @@ export class Archiver extends (EventEmitter as new () => ArchiverEmitter) implem
|
|
|
833
898
|
}
|
|
834
899
|
|
|
835
900
|
if (!validationResult.valid) {
|
|
836
|
-
this.log.warn(`Skipping
|
|
837
|
-
|
|
838
|
-
l1BlockNumber:
|
|
901
|
+
this.log.warn(`Skipping checkpoint ${published.checkpoint.number} due to invalid attestations`, {
|
|
902
|
+
checkpointHash: published.checkpoint.hash(),
|
|
903
|
+
l1BlockNumber: published.l1.blockNumber,
|
|
839
904
|
...pick(validationResult, 'reason'),
|
|
840
905
|
});
|
|
841
906
|
|
|
@@ -845,28 +910,31 @@ export class Archiver extends (EventEmitter as new () => ArchiverEmitter) implem
|
|
|
845
910
|
validationResult,
|
|
846
911
|
});
|
|
847
912
|
|
|
848
|
-
// We keep consuming
|
|
849
|
-
// We just pretend the invalid ones are not there and keep consuming the next
|
|
850
|
-
// Note that this breaks if the committee ever attests to a descendant of an invalid
|
|
913
|
+
// We keep consuming checkpoints if we find an invalid one, since we do not listen for CheckpointInvalidated events
|
|
914
|
+
// We just pretend the invalid ones are not there and keep consuming the next checkpoints
|
|
915
|
+
// Note that this breaks if the committee ever attests to a descendant of an invalid checkpoint
|
|
851
916
|
continue;
|
|
852
917
|
}
|
|
853
918
|
|
|
854
|
-
|
|
855
|
-
this.log.debug(
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
919
|
+
validCheckpoints.push(published);
|
|
920
|
+
this.log.debug(
|
|
921
|
+
`Ingesting new checkpoint ${published.checkpoint.number} with ${published.checkpoint.blocks.length} blocks`,
|
|
922
|
+
{
|
|
923
|
+
checkpointHash: published.checkpoint.hash(),
|
|
924
|
+
l1BlockNumber: published.l1.blockNumber,
|
|
925
|
+
...published.checkpoint.header.toInspect(),
|
|
926
|
+
blocks: published.checkpoint.blocks.map(b => b.getStats()),
|
|
927
|
+
},
|
|
928
|
+
);
|
|
861
929
|
}
|
|
862
930
|
|
|
863
931
|
try {
|
|
864
932
|
const updatedValidationResult =
|
|
865
933
|
rollupStatus.validationResult === initialValidationResult ? undefined : rollupStatus.validationResult;
|
|
866
|
-
const [processDuration] = await elapsed(() => this.
|
|
934
|
+
const [processDuration] = await elapsed(() => this.addCheckpoints(validCheckpoints, updatedValidationResult));
|
|
867
935
|
this.instrumentation.processNewBlocks(
|
|
868
|
-
processDuration /
|
|
869
|
-
|
|
936
|
+
processDuration / validCheckpoints.length,
|
|
937
|
+
validCheckpoints.flatMap(c => c.checkpoint.blocks),
|
|
870
938
|
);
|
|
871
939
|
} catch (err) {
|
|
872
940
|
if (err instanceof InitialBlockNumberNotSequentialError) {
|
|
@@ -889,56 +957,56 @@ export class Archiver extends (EventEmitter as new () => ArchiverEmitter) implem
|
|
|
889
957
|
throw err;
|
|
890
958
|
}
|
|
891
959
|
|
|
892
|
-
for (const
|
|
893
|
-
this.log.info(`Downloaded
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
960
|
+
for (const checkpoint of validCheckpoints) {
|
|
961
|
+
this.log.info(`Downloaded checkpoint ${checkpoint.checkpoint.number}`, {
|
|
962
|
+
checkpointHash: checkpoint.checkpoint.hash(),
|
|
963
|
+
checkpointNumber: checkpoint.checkpoint.number,
|
|
964
|
+
blockCount: checkpoint.checkpoint.blocks.length,
|
|
965
|
+
txCount: checkpoint.checkpoint.blocks.reduce((acc, b) => acc + b.body.txEffects.length, 0),
|
|
966
|
+
header: checkpoint.checkpoint.header.toInspect(),
|
|
967
|
+
archiveRoot: checkpoint.checkpoint.archive.root.toString(),
|
|
968
|
+
archiveNextLeafIndex: checkpoint.checkpoint.archive.nextAvailableLeafIndex,
|
|
900
969
|
});
|
|
901
970
|
}
|
|
902
|
-
|
|
971
|
+
lastRetrievedCheckpoint = validCheckpoints.at(-1) ?? lastRetrievedCheckpoint;
|
|
972
|
+
lastL1BlockWithCheckpoint = publishedCheckpoints.at(-1)?.l1.blockNumber ?? lastL1BlockWithCheckpoint;
|
|
903
973
|
} while (searchEndBlock < currentL1BlockNumber);
|
|
904
974
|
|
|
905
975
|
// Important that we update AFTER inserting the blocks.
|
|
906
|
-
await
|
|
976
|
+
await updateProvenCheckpoint();
|
|
907
977
|
|
|
908
|
-
return { ...rollupStatus,
|
|
978
|
+
return { ...rollupStatus, lastRetrievedCheckpoint, lastL1BlockWithCheckpoint };
|
|
909
979
|
}
|
|
910
980
|
|
|
911
|
-
private async
|
|
912
|
-
status:
|
|
913
|
-
lastRetrievedBlock?: PublishedL2Block;
|
|
914
|
-
pendingBlockNumber: number;
|
|
915
|
-
},
|
|
981
|
+
private async checkForNewCheckpointsBeforeL1SyncPoint(
|
|
982
|
+
status: RollupStatus,
|
|
916
983
|
blocksSynchedTo: bigint,
|
|
917
984
|
currentL1BlockNumber: bigint,
|
|
918
985
|
) {
|
|
919
|
-
const {
|
|
920
|
-
// Compare the last
|
|
986
|
+
const { lastRetrievedCheckpoint, pendingCheckpointNumber } = status;
|
|
987
|
+
// Compare the last checkpoint we have (either retrieved in this round or loaded from store) with what the
|
|
921
988
|
// rollup contract told us was the latest one (pinned at the currentL1BlockNumber).
|
|
922
|
-
const
|
|
923
|
-
|
|
989
|
+
const latestLocalCheckpointNumber =
|
|
990
|
+
lastRetrievedCheckpoint?.checkpoint.number ?? (await this.getSynchedCheckpointNumber());
|
|
991
|
+
if (latestLocalCheckpointNumber < pendingCheckpointNumber) {
|
|
924
992
|
// Here we have consumed all logs until the `currentL1Block` we pinned at the beginning of the archiver loop,
|
|
925
|
-
// but still
|
|
926
|
-
// We suspect an L1 reorg that added
|
|
927
|
-
// last
|
|
928
|
-
// don't have one, we go back 2 L1 epochs, which is the deepest possible reorg (assuming Casper is working).
|
|
929
|
-
const
|
|
930
|
-
|
|
931
|
-
(
|
|
932
|
-
? await this.
|
|
993
|
+
// but still haven't reached the pending checkpoint according to the call to the rollup contract.
|
|
994
|
+
// We suspect an L1 reorg that added checkpoints *behind* us. If that is the case, it must have happened between
|
|
995
|
+
// the last checkpoint we saw and the current one, so we reset the last synched L1 block number. In the edge case
|
|
996
|
+
// we don't have one, we go back 2 L1 epochs, which is the deepest possible reorg (assuming Casper is working).
|
|
997
|
+
const latestLocalCheckpoint =
|
|
998
|
+
lastRetrievedCheckpoint ??
|
|
999
|
+
(latestLocalCheckpointNumber > 0
|
|
1000
|
+
? await this.getPublishedCheckpoints(latestLocalCheckpointNumber, 1).then(([c]) => c)
|
|
933
1001
|
: undefined);
|
|
934
|
-
const targetL1BlockNumber =
|
|
935
|
-
const
|
|
1002
|
+
const targetL1BlockNumber = latestLocalCheckpoint?.l1.blockNumber ?? maxBigint(currentL1BlockNumber - 64n, 0n);
|
|
1003
|
+
const latestLocalCheckpointArchive = latestLocalCheckpoint?.checkpoint.archive.root.toString();
|
|
936
1004
|
this.log.warn(
|
|
937
|
-
`Failed to reach
|
|
1005
|
+
`Failed to reach checkpoint ${pendingCheckpointNumber} at ${currentL1BlockNumber} (latest is ${latestLocalCheckpointNumber}). ` +
|
|
938
1006
|
`Rolling back last synched L1 block number to ${targetL1BlockNumber}.`,
|
|
939
1007
|
{
|
|
940
|
-
|
|
941
|
-
|
|
1008
|
+
latestLocalCheckpointNumber,
|
|
1009
|
+
latestLocalCheckpointArchive,
|
|
942
1010
|
blocksSynchedTo,
|
|
943
1011
|
currentL1BlockNumber,
|
|
944
1012
|
...status,
|
|
@@ -946,18 +1014,15 @@ export class Archiver extends (EventEmitter as new () => ArchiverEmitter) implem
|
|
|
946
1014
|
);
|
|
947
1015
|
await this.store.setBlockSynchedL1BlockNumber(targetL1BlockNumber);
|
|
948
1016
|
} else {
|
|
949
|
-
this.log.trace(`No new
|
|
950
|
-
|
|
951
|
-
|
|
1017
|
+
this.log.trace(`No new checkpoints behind L1 sync point to retrieve.`, {
|
|
1018
|
+
latestLocalCheckpointNumber,
|
|
1019
|
+
pendingCheckpointNumber,
|
|
952
1020
|
});
|
|
953
1021
|
}
|
|
954
1022
|
}
|
|
955
1023
|
|
|
956
1024
|
/** Resumes the archiver after a stop. */
|
|
957
1025
|
public resume() {
|
|
958
|
-
if (!this.runningPromise) {
|
|
959
|
-
throw new Error(`Archiver was never started`);
|
|
960
|
-
}
|
|
961
1026
|
if (this.runningPromise.isRunning()) {
|
|
962
1027
|
this.log.warn(`Archiver already running`);
|
|
963
1028
|
}
|
|
@@ -971,7 +1036,7 @@ export class Archiver extends (EventEmitter as new () => ArchiverEmitter) implem
|
|
|
971
1036
|
*/
|
|
972
1037
|
public async stop(): Promise<void> {
|
|
973
1038
|
this.log.debug('Stopping...');
|
|
974
|
-
await this.runningPromise
|
|
1039
|
+
await this.runningPromise.stop();
|
|
975
1040
|
|
|
976
1041
|
this.log.info('Stopped.');
|
|
977
1042
|
return Promise.resolve();
|
|
@@ -1005,26 +1070,26 @@ export class Archiver extends (EventEmitter as new () => ArchiverEmitter) implem
|
|
|
1005
1070
|
return Promise.resolve(this.l1Timestamp);
|
|
1006
1071
|
}
|
|
1007
1072
|
|
|
1008
|
-
public getL2SlotNumber(): Promise<
|
|
1073
|
+
public getL2SlotNumber(): Promise<SlotNumber | undefined> {
|
|
1009
1074
|
return Promise.resolve(
|
|
1010
1075
|
this.l1Timestamp === undefined ? undefined : getSlotAtTimestamp(this.l1Timestamp, this.l1constants),
|
|
1011
1076
|
);
|
|
1012
1077
|
}
|
|
1013
1078
|
|
|
1014
|
-
public getL2EpochNumber(): Promise<
|
|
1079
|
+
public getL2EpochNumber(): Promise<EpochNumber | undefined> {
|
|
1015
1080
|
return Promise.resolve(
|
|
1016
1081
|
this.l1Timestamp === undefined ? undefined : getEpochNumberAtTimestamp(this.l1Timestamp, this.l1constants),
|
|
1017
1082
|
);
|
|
1018
1083
|
}
|
|
1019
1084
|
|
|
1020
|
-
public async getBlocksForEpoch(epochNumber:
|
|
1085
|
+
public async getBlocksForEpoch(epochNumber: EpochNumber): Promise<L2Block[]> {
|
|
1021
1086
|
const [start, end] = getSlotRangeForEpoch(epochNumber, this.l1constants);
|
|
1022
1087
|
const blocks: L2Block[] = [];
|
|
1023
1088
|
|
|
1024
1089
|
// Walk the list of blocks backwards and filter by slots matching the requested epoch.
|
|
1025
1090
|
// We'll typically ask for blocks for a very recent epoch, so we shouldn't need an index here.
|
|
1026
1091
|
let block = await this.getBlock(await this.store.getSynchedL2BlockNumber());
|
|
1027
|
-
const slot = (b: L2Block) => b.header.globalVariables.slotNumber
|
|
1092
|
+
const slot = (b: L2Block) => b.header.globalVariables.slotNumber;
|
|
1028
1093
|
while (block && slot(block) >= start) {
|
|
1029
1094
|
if (slot(block) <= end) {
|
|
1030
1095
|
blocks.push(block);
|
|
@@ -1035,7 +1100,7 @@ export class Archiver extends (EventEmitter as new () => ArchiverEmitter) implem
|
|
|
1035
1100
|
return blocks.reverse();
|
|
1036
1101
|
}
|
|
1037
1102
|
|
|
1038
|
-
public async getBlockHeadersForEpoch(epochNumber:
|
|
1103
|
+
public async getBlockHeadersForEpoch(epochNumber: EpochNumber): Promise<BlockHeader[]> {
|
|
1039
1104
|
const [start, end] = getSlotRangeForEpoch(epochNumber, this.l1constants);
|
|
1040
1105
|
const blocks: BlockHeader[] = [];
|
|
1041
1106
|
|
|
@@ -1043,7 +1108,7 @@ export class Archiver extends (EventEmitter as new () => ArchiverEmitter) implem
|
|
|
1043
1108
|
// We'll typically ask for blocks for a very recent epoch, so we shouldn't need an index here.
|
|
1044
1109
|
let number = await this.store.getSynchedL2BlockNumber();
|
|
1045
1110
|
let header = await this.getBlockHeader(number);
|
|
1046
|
-
const slot = (b: BlockHeader) => b.globalVariables.slotNumber
|
|
1111
|
+
const slot = (b: BlockHeader) => b.globalVariables.slotNumber;
|
|
1047
1112
|
while (header && slot(header) >= start) {
|
|
1048
1113
|
if (slot(header) <= end) {
|
|
1049
1114
|
blocks.push(header);
|
|
@@ -1053,10 +1118,10 @@ export class Archiver extends (EventEmitter as new () => ArchiverEmitter) implem
|
|
|
1053
1118
|
return blocks.reverse();
|
|
1054
1119
|
}
|
|
1055
1120
|
|
|
1056
|
-
public async isEpochComplete(epochNumber:
|
|
1121
|
+
public async isEpochComplete(epochNumber: EpochNumber): Promise<boolean> {
|
|
1057
1122
|
// The epoch is complete if the current L2 block is the last one in the epoch (or later)
|
|
1058
1123
|
const header = await this.getBlockHeader('latest');
|
|
1059
|
-
const slot = header
|
|
1124
|
+
const slot = header ? header.globalVariables.slotNumber : undefined;
|
|
1060
1125
|
const [_startSlot, endSlot] = getSlotRangeForEpoch(epochNumber, this.l1constants);
|
|
1061
1126
|
if (slot && slot >= endSlot) {
|
|
1062
1127
|
return true;
|
|
@@ -1086,6 +1151,77 @@ export class Archiver extends (EventEmitter as new () => ArchiverEmitter) implem
|
|
|
1086
1151
|
return this.initialSyncComplete;
|
|
1087
1152
|
}
|
|
1088
1153
|
|
|
1154
|
+
public async getPublishedCheckpoints(from: number, limit: number, proven?: boolean): Promise<PublishedCheckpoint[]> {
|
|
1155
|
+
const blocks = await this.getPublishedBlocks(from, limit, proven);
|
|
1156
|
+
return blocks.map(b => b.toPublishedCheckpoint());
|
|
1157
|
+
}
|
|
1158
|
+
|
|
1159
|
+
public async getCheckpoints(from: number, limit: number, proven?: boolean): Promise<Checkpoint[]> {
|
|
1160
|
+
const published = await this.getPublishedCheckpoints(from, limit, proven);
|
|
1161
|
+
return published.map(p => p.checkpoint);
|
|
1162
|
+
}
|
|
1163
|
+
|
|
1164
|
+
public async getCheckpoint(number: number): Promise<Checkpoint | undefined> {
|
|
1165
|
+
if (number < 0) {
|
|
1166
|
+
number = await this.getSynchedCheckpointNumber();
|
|
1167
|
+
}
|
|
1168
|
+
if (number === 0) {
|
|
1169
|
+
return undefined;
|
|
1170
|
+
}
|
|
1171
|
+
const published = await this.getPublishedCheckpoints(number, 1);
|
|
1172
|
+
return published[0]?.checkpoint;
|
|
1173
|
+
}
|
|
1174
|
+
|
|
1175
|
+
public async getCheckpointHeader(number: number | 'latest'): Promise<CheckpointHeader | undefined> {
|
|
1176
|
+
if (number === 'latest') {
|
|
1177
|
+
number = await this.getSynchedCheckpointNumber();
|
|
1178
|
+
}
|
|
1179
|
+
if (number === 0) {
|
|
1180
|
+
return undefined;
|
|
1181
|
+
}
|
|
1182
|
+
const checkpoint = await this.getCheckpoint(number);
|
|
1183
|
+
return checkpoint?.header;
|
|
1184
|
+
}
|
|
1185
|
+
|
|
1186
|
+
public getCheckpointNumber(): Promise<number> {
|
|
1187
|
+
return this.getSynchedCheckpointNumber();
|
|
1188
|
+
}
|
|
1189
|
+
|
|
1190
|
+
public getSynchedCheckpointNumber(): Promise<number> {
|
|
1191
|
+
// TODO: Checkpoint number will no longer be the same as the block number once we support multiple blocks per checkpoint.
|
|
1192
|
+
return this.store.getSynchedL2BlockNumber();
|
|
1193
|
+
}
|
|
1194
|
+
|
|
1195
|
+
public getProvenCheckpointNumber(): Promise<number> {
|
|
1196
|
+
// TODO: Proven checkpoint number will no longer be the same as the proven block number once we support multiple blocks per checkpoint.
|
|
1197
|
+
return this.store.getProvenL2BlockNumber();
|
|
1198
|
+
}
|
|
1199
|
+
|
|
1200
|
+
public setProvenCheckpointNumber(checkpointNumber: number): Promise<void> {
|
|
1201
|
+
// TODO: Proven checkpoint number will no longer be the same as the proven block number once we support multiple blocks per checkpoint.
|
|
1202
|
+
return this.store.setProvenL2BlockNumber(checkpointNumber);
|
|
1203
|
+
}
|
|
1204
|
+
|
|
1205
|
+
public unwindCheckpoints(from: number, checkpointsToUnwind: number): Promise<boolean> {
|
|
1206
|
+
// TODO: This only works if we have one block per checkpoint.
|
|
1207
|
+
return this.store.unwindBlocks(from, checkpointsToUnwind);
|
|
1208
|
+
}
|
|
1209
|
+
|
|
1210
|
+
public getLastBlockNumberInCheckpoint(checkpointNumber: number): Promise<number> {
|
|
1211
|
+
// TODO: Checkpoint number will no longer be the same as the block number once we support multiple blocks per checkpoint.
|
|
1212
|
+
return Promise.resolve(checkpointNumber);
|
|
1213
|
+
}
|
|
1214
|
+
|
|
1215
|
+
public addCheckpoints(
|
|
1216
|
+
checkpoints: PublishedCheckpoint[],
|
|
1217
|
+
pendingChainValidationStatus?: ValidateBlockResult,
|
|
1218
|
+
): Promise<boolean> {
|
|
1219
|
+
return this.store.addBlocks(
|
|
1220
|
+
checkpoints.map(p => PublishedL2Block.fromPublishedCheckpoint(p)),
|
|
1221
|
+
pendingChainValidationStatus,
|
|
1222
|
+
);
|
|
1223
|
+
}
|
|
1224
|
+
|
|
1089
1225
|
/**
|
|
1090
1226
|
* Gets up to `limit` amount of L2 blocks starting from `from`.
|
|
1091
1227
|
* @param from - Number of the first block to return (inclusive).
|