@aztec/archiver 0.0.1-commit.9d2bcf6d → 0.0.1-commit.9ebd450e8
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 +12 -6
- package/dest/archiver.d.ts +13 -8
- package/dest/archiver.d.ts.map +1 -1
- package/dest/archiver.js +90 -114
- package/dest/config.d.ts +5 -3
- package/dest/config.d.ts.map +1 -1
- package/dest/config.js +15 -3
- package/dest/errors.d.ts +50 -10
- package/dest/errors.d.ts.map +1 -1
- package/dest/errors.js +67 -16
- package/dest/factory.d.ts +4 -5
- package/dest/factory.d.ts.map +1 -1
- package/dest/factory.js +33 -27
- package/dest/index.d.ts +4 -2
- package/dest/index.d.ts.map +1 -1
- package/dest/index.js +3 -1
- package/dest/l1/bin/retrieve-calldata.js +36 -33
- package/dest/l1/calldata_retriever.d.ts +73 -50
- package/dest/l1/calldata_retriever.d.ts.map +1 -1
- package/dest/l1/calldata_retriever.js +191 -259
- package/dest/l1/data_retrieval.d.ts +26 -17
- package/dest/l1/data_retrieval.d.ts.map +1 -1
- package/dest/l1/data_retrieval.js +42 -47
- package/dest/l1/spire_proposer.d.ts +5 -5
- package/dest/l1/spire_proposer.d.ts.map +1 -1
- package/dest/l1/spire_proposer.js +9 -17
- 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_source_base.d.ts +14 -7
- package/dest/modules/data_source_base.d.ts.map +1 -1
- package/dest/modules/data_source_base.js +39 -77
- package/dest/modules/data_store_updater.d.ts +35 -15
- package/dest/modules/data_store_updater.d.ts.map +1 -1
- package/dest/modules/data_store_updater.js +137 -96
- package/dest/modules/instrumentation.d.ts +21 -3
- package/dest/modules/instrumentation.d.ts.map +1 -1
- package/dest/modules/instrumentation.js +41 -8
- package/dest/modules/l1_synchronizer.d.ts +10 -9
- package/dest/modules/l1_synchronizer.d.ts.map +1 -1
- package/dest/modules/l1_synchronizer.js +279 -150
- package/dest/modules/validation.d.ts +1 -1
- package/dest/modules/validation.d.ts.map +1 -1
- package/dest/modules/validation.js +2 -2
- package/dest/store/block_store.d.ts +73 -28
- package/dest/store/block_store.d.ts.map +1 -1
- package/dest/store/block_store.js +395 -136
- package/dest/store/contract_class_store.d.ts +2 -3
- package/dest/store/contract_class_store.d.ts.map +1 -1
- package/dest/store/contract_class_store.js +7 -67
- 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 +6 -2
- package/dest/store/kv_archiver_store.d.ts +67 -24
- package/dest/store/kv_archiver_store.d.ts.map +1 -1
- package/dest/store/kv_archiver_store.js +82 -27
- package/dest/store/l2_tips_cache.d.ts +20 -0
- package/dest/store/l2_tips_cache.d.ts.map +1 -0
- package/dest/store/l2_tips_cache.js +109 -0
- package/dest/store/log_store.d.ts +6 -3
- package/dest/store/log_store.d.ts.map +1 -1
- package/dest/store/log_store.js +150 -55
- package/dest/store/message_store.d.ts +5 -1
- package/dest/store/message_store.d.ts.map +1 -1
- package/dest/store/message_store.js +21 -9
- package/dest/test/fake_l1_state.d.ts +24 -1
- package/dest/test/fake_l1_state.d.ts.map +1 -1
- package/dest/test/fake_l1_state.js +145 -28
- package/dest/test/mock_archiver.d.ts +1 -1
- package/dest/test/mock_archiver.d.ts.map +1 -1
- package/dest/test/mock_archiver.js +3 -2
- 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 +2 -1
- package/dest/test/mock_l2_block_source.d.ts +26 -5
- package/dest/test/mock_l2_block_source.d.ts.map +1 -1
- package/dest/test/mock_l2_block_source.js +160 -89
- package/dest/test/mock_structs.d.ts +4 -1
- package/dest/test/mock_structs.d.ts.map +1 -1
- package/dest/test/mock_structs.js +13 -1
- package/dest/test/noop_l1_archiver.d.ts +4 -1
- package/dest/test/noop_l1_archiver.d.ts.map +1 -1
- package/dest/test/noop_l1_archiver.js +9 -3
- package/package.json +13 -13
- package/src/archiver.ts +120 -137
- package/src/config.ts +22 -2
- package/src/errors.ts +104 -26
- package/src/factory.ts +47 -24
- package/src/index.ts +3 -1
- package/src/l1/README.md +25 -68
- package/src/l1/bin/retrieve-calldata.ts +46 -39
- package/src/l1/calldata_retriever.ts +250 -379
- package/src/l1/data_retrieval.ts +58 -69
- package/src/l1/spire_proposer.ts +7 -15
- package/src/l1/validate_historical_logs.ts +140 -0
- package/src/modules/data_source_base.ts +78 -98
- package/src/modules/data_store_updater.ts +164 -126
- package/src/modules/instrumentation.ts +56 -9
- package/src/modules/l1_synchronizer.ts +357 -188
- package/src/modules/validation.ts +2 -2
- package/src/store/block_store.ts +503 -172
- package/src/store/contract_class_store.ts +8 -106
- package/src/store/contract_instance_store.ts +8 -5
- package/src/store/kv_archiver_store.ts +130 -41
- package/src/store/l2_tips_cache.ts +134 -0
- package/src/store/log_store.ts +221 -63
- package/src/store/message_store.ts +27 -10
- package/src/structs/inbox_message.ts +1 -1
- package/src/test/fake_l1_state.ts +193 -32
- package/src/test/mock_archiver.ts +3 -2
- package/src/test/mock_l1_to_l2_message_source.ts +1 -0
- package/src/test/mock_l2_block_source.ts +209 -82
- package/src/test/mock_structs.ts +20 -6
- package/src/test/noop_l1_archiver.ts +10 -2
package/src/store/block_store.ts
CHANGED
|
@@ -9,6 +9,7 @@ import { isDefined } from '@aztec/foundation/types';
|
|
|
9
9
|
import type { AztecAsyncKVStore, AztecAsyncMap, AztecAsyncSingleton, Range } from '@aztec/kv-store';
|
|
10
10
|
import type { AztecAddress } from '@aztec/stdlib/aztec-address';
|
|
11
11
|
import {
|
|
12
|
+
type BlockData,
|
|
12
13
|
BlockHash,
|
|
13
14
|
Body,
|
|
14
15
|
CheckpointedL2Block,
|
|
@@ -18,8 +19,16 @@ import {
|
|
|
18
19
|
deserializeValidateCheckpointResult,
|
|
19
20
|
serializeValidateCheckpointResult,
|
|
20
21
|
} from '@aztec/stdlib/block';
|
|
21
|
-
import {
|
|
22
|
-
|
|
22
|
+
import {
|
|
23
|
+
Checkpoint,
|
|
24
|
+
type CheckpointData,
|
|
25
|
+
type CommonCheckpointData,
|
|
26
|
+
L1PublishedData,
|
|
27
|
+
type ProposedCheckpointData,
|
|
28
|
+
type ProposedCheckpointInput,
|
|
29
|
+
PublishedCheckpoint,
|
|
30
|
+
} from '@aztec/stdlib/checkpoint';
|
|
31
|
+
import { type L1RollupConstants, getEpochAtSlot } from '@aztec/stdlib/epoch-helpers';
|
|
23
32
|
import { CheckpointHeader } from '@aztec/stdlib/rollup';
|
|
24
33
|
import { AppendOnlyTreeSnapshot } from '@aztec/stdlib/trees';
|
|
25
34
|
import {
|
|
@@ -34,16 +43,20 @@ import {
|
|
|
34
43
|
} from '@aztec/stdlib/tx';
|
|
35
44
|
|
|
36
45
|
import {
|
|
46
|
+
BlockAlreadyCheckpointedError,
|
|
37
47
|
BlockArchiveNotConsistentError,
|
|
38
48
|
BlockIndexNotSequentialError,
|
|
39
49
|
BlockNotFoundError,
|
|
40
50
|
BlockNumberNotSequentialError,
|
|
41
51
|
CannotOverwriteCheckpointedBlockError,
|
|
42
52
|
CheckpointNotFoundError,
|
|
43
|
-
CheckpointNumberNotConsistentError,
|
|
44
53
|
CheckpointNumberNotSequentialError,
|
|
45
|
-
InitialBlockNumberNotSequentialError,
|
|
46
54
|
InitialCheckpointNumberNotSequentialError,
|
|
55
|
+
NoProposedCheckpointToPromoteError,
|
|
56
|
+
ProposedCheckpointArchiveRootMismatchError,
|
|
57
|
+
ProposedCheckpointNotSequentialError,
|
|
58
|
+
ProposedCheckpointPromotionNotSequentialError,
|
|
59
|
+
ProposedCheckpointStaleError,
|
|
47
60
|
} from '../errors.js';
|
|
48
61
|
|
|
49
62
|
export { TxReceipt, type TxEffect, type TxHash } from '@aztec/stdlib/tx';
|
|
@@ -58,24 +71,25 @@ type BlockStorage = {
|
|
|
58
71
|
indexWithinCheckpoint: number;
|
|
59
72
|
};
|
|
60
73
|
|
|
61
|
-
|
|
74
|
+
/** Checkpoint Storage shared between Checkpoints + Proposed Checkpoints */
|
|
75
|
+
type CommonCheckpointStorage = {
|
|
62
76
|
header: Buffer;
|
|
63
77
|
archive: Buffer;
|
|
78
|
+
checkpointOutHash: Buffer;
|
|
64
79
|
checkpointNumber: number;
|
|
65
80
|
startBlock: number;
|
|
66
|
-
|
|
81
|
+
blockCount: number;
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
type CheckpointStorage = CommonCheckpointStorage & {
|
|
67
85
|
l1: Buffer;
|
|
68
86
|
attestations: Buffer[];
|
|
69
87
|
};
|
|
70
88
|
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
startBlock: number;
|
|
76
|
-
numBlocks: number;
|
|
77
|
-
l1: L1PublishedData;
|
|
78
|
-
attestations: Buffer[];
|
|
89
|
+
/** Storage format for a proposed checkpoint (attested but not yet L1-confirmed). */
|
|
90
|
+
type ProposedCheckpointStorage = CommonCheckpointStorage & {
|
|
91
|
+
totalManaUsed: string;
|
|
92
|
+
feeAssetPriceModifier: string;
|
|
79
93
|
};
|
|
80
94
|
|
|
81
95
|
export type RemoveCheckpointsResult = { blocksRemoved: L2Block[] | undefined };
|
|
@@ -90,6 +104,9 @@ export class BlockStore {
|
|
|
90
104
|
/** Map checkpoint number to checkpoint data */
|
|
91
105
|
#checkpoints: AztecAsyncMap<number, CheckpointStorage>;
|
|
92
106
|
|
|
107
|
+
/** Map slot number to checkpoint number, for looking up checkpoints by slot range. */
|
|
108
|
+
#slotToCheckpoint: AztecAsyncMap<number, number>;
|
|
109
|
+
|
|
93
110
|
/** Map block hash to list of tx hashes */
|
|
94
111
|
#blockTxs: AztecAsyncMap<string, Buffer>;
|
|
95
112
|
|
|
@@ -102,6 +119,9 @@ export class BlockStore {
|
|
|
102
119
|
/** Stores last proven checkpoint */
|
|
103
120
|
#lastProvenCheckpoint: AztecAsyncSingleton<number>;
|
|
104
121
|
|
|
122
|
+
/** Stores last finalized checkpoint (proven at or before the finalized L1 block) */
|
|
123
|
+
#lastFinalizedCheckpoint: AztecAsyncSingleton<number>;
|
|
124
|
+
|
|
105
125
|
/** Stores the pending chain validation status */
|
|
106
126
|
#pendingChainValidationStatus: AztecAsyncSingleton<Buffer>;
|
|
107
127
|
|
|
@@ -114,12 +134,12 @@ export class BlockStore {
|
|
|
114
134
|
/** Index mapping block archive to block number */
|
|
115
135
|
#blockArchiveIndex: AztecAsyncMap<string, number>;
|
|
116
136
|
|
|
137
|
+
/** Singleton: assumes max 1-deep pipeline. For deeper pipelining, replace with a map keyed by checkpoint number. */
|
|
138
|
+
#proposedCheckpoint: AztecAsyncSingleton<ProposedCheckpointStorage>;
|
|
139
|
+
|
|
117
140
|
#log = createLogger('archiver:block_store');
|
|
118
141
|
|
|
119
|
-
constructor(
|
|
120
|
-
private db: AztecAsyncKVStore,
|
|
121
|
-
private l1Constants: Pick<L1RollupConstants, 'epochDuration'>,
|
|
122
|
-
) {
|
|
142
|
+
constructor(private db: AztecAsyncKVStore) {
|
|
123
143
|
this.#blocks = db.openMap('archiver_blocks');
|
|
124
144
|
this.#blockTxs = db.openMap('archiver_block_txs');
|
|
125
145
|
this.#txEffects = db.openMap('archiver_tx_effects');
|
|
@@ -128,119 +148,114 @@ export class BlockStore {
|
|
|
128
148
|
this.#blockArchiveIndex = db.openMap('archiver_block_archive_index');
|
|
129
149
|
this.#lastSynchedL1Block = db.openSingleton('archiver_last_synched_l1_block');
|
|
130
150
|
this.#lastProvenCheckpoint = db.openSingleton('archiver_last_proven_l2_checkpoint');
|
|
151
|
+
this.#lastFinalizedCheckpoint = db.openSingleton('archiver_last_finalized_l2_checkpoint');
|
|
131
152
|
this.#pendingChainValidationStatus = db.openSingleton('archiver_pending_chain_validation_status');
|
|
132
153
|
this.#checkpoints = db.openMap('archiver_checkpoints');
|
|
154
|
+
this.#slotToCheckpoint = db.openMap('archiver_slot_to_checkpoint');
|
|
155
|
+
this.#proposedCheckpoint = db.openSingleton('proposed_checkpoint_data');
|
|
133
156
|
}
|
|
134
157
|
|
|
135
158
|
/**
|
|
136
|
-
*
|
|
137
|
-
*
|
|
138
|
-
* TODO(#13569): Compute proper finalized block number based on L1 finalized block.
|
|
139
|
-
* TODO(palla/mbps): Even the provisional computation is wrong, since it should subtract checkpoints, not blocks
|
|
159
|
+
* Returns the finalized L2 block number. An L2 block is finalized when it was proven
|
|
160
|
+
* in an L1 block that has itself been finalized on Ethereum.
|
|
140
161
|
* @returns The finalized block number.
|
|
141
162
|
*/
|
|
142
163
|
async getFinalizedL2BlockNumber(): Promise<BlockNumber> {
|
|
143
|
-
const
|
|
144
|
-
|
|
164
|
+
const finalizedCheckpointNumber = await this.getFinalizedCheckpointNumber();
|
|
165
|
+
if (finalizedCheckpointNumber === INITIAL_CHECKPOINT_NUMBER - 1) {
|
|
166
|
+
return BlockNumber(INITIAL_L2_BLOCK_NUM - 1);
|
|
167
|
+
}
|
|
168
|
+
const checkpointStorage = await this.#checkpoints.getAsync(finalizedCheckpointNumber);
|
|
169
|
+
if (!checkpointStorage) {
|
|
170
|
+
throw new CheckpointNotFoundError(finalizedCheckpointNumber);
|
|
171
|
+
}
|
|
172
|
+
return BlockNumber(checkpointStorage.startBlock + checkpointStorage.blockCount - 1);
|
|
145
173
|
}
|
|
146
174
|
|
|
147
175
|
/**
|
|
148
|
-
* Append new proposed
|
|
149
|
-
*
|
|
176
|
+
* Append a new proposed block to the store.
|
|
177
|
+
* This is an uncheckpointed block that has been proposed by the sequencer but not yet included in a checkpoint on L1.
|
|
150
178
|
* For checkpointed blocks (already published to L1), use addCheckpoints() instead.
|
|
151
|
-
* @param
|
|
179
|
+
* @param block - The proposed L2 block to be added to the store.
|
|
152
180
|
* @returns True if the operation is successful.
|
|
153
181
|
*/
|
|
154
|
-
async
|
|
155
|
-
if (blocks.length === 0) {
|
|
156
|
-
return true;
|
|
157
|
-
}
|
|
158
|
-
|
|
182
|
+
async addProposedBlock(block: L2Block, opts: { force?: boolean } = {}): Promise<boolean> {
|
|
159
183
|
return await this.db.transactionAsync(async () => {
|
|
160
|
-
|
|
161
|
-
const
|
|
162
|
-
const
|
|
163
|
-
const
|
|
164
|
-
const firstBlockLastArchive = blocks[0].header.lastArchive.root;
|
|
184
|
+
const blockNumber = block.number;
|
|
185
|
+
const blockCheckpointNumber = block.checkpointNumber;
|
|
186
|
+
const blockIndex = block.indexWithinCheckpoint;
|
|
187
|
+
const blockLastArchive = block.header.lastArchive.root;
|
|
165
188
|
|
|
166
189
|
// Extract the latest block and checkpoint numbers
|
|
167
|
-
const previousBlockNumber = await this.
|
|
190
|
+
const previousBlockNumber = await this.getLatestL2BlockNumber();
|
|
191
|
+
const proposedCheckpointNumber = await this.getProposedCheckpointNumber();
|
|
168
192
|
const previousCheckpointNumber = await this.getLatestCheckpointNumber();
|
|
169
193
|
|
|
170
194
|
// Verify we're not overwriting checkpointed blocks
|
|
171
195
|
const lastCheckpointedBlockNumber = await this.getCheckpointedL2BlockNumber();
|
|
172
|
-
if (!opts.force &&
|
|
173
|
-
|
|
196
|
+
if (!opts.force && blockNumber <= lastCheckpointedBlockNumber) {
|
|
197
|
+
// Check if the proposed block matches the already-checkpointed one
|
|
198
|
+
const existingBlock = await this.getBlock(BlockNumber(blockNumber));
|
|
199
|
+
if (existingBlock && existingBlock.archive.root.equals(block.archive.root)) {
|
|
200
|
+
throw new BlockAlreadyCheckpointedError(blockNumber);
|
|
201
|
+
}
|
|
202
|
+
throw new CannotOverwriteCheckpointedBlockError(blockNumber, lastCheckpointedBlockNumber);
|
|
174
203
|
}
|
|
175
204
|
|
|
176
|
-
// Check that the
|
|
177
|
-
if (!opts.force && previousBlockNumber !==
|
|
178
|
-
throw new
|
|
205
|
+
// Check that the block number is the expected one
|
|
206
|
+
if (!opts.force && previousBlockNumber !== blockNumber - 1) {
|
|
207
|
+
throw new BlockNumberNotSequentialError(blockNumber, previousBlockNumber);
|
|
179
208
|
}
|
|
180
209
|
|
|
181
|
-
// The same check as above but for checkpoints
|
|
182
|
-
|
|
183
|
-
|
|
210
|
+
// The same check as above but for checkpoints. Accept the block if either the confirmed
|
|
211
|
+
// checkpoint or the pending (locally validated but not yet confirmed) checkpoint matches.
|
|
212
|
+
const expectedCheckpointNumber = blockCheckpointNumber - 1;
|
|
213
|
+
if (
|
|
214
|
+
!opts.force &&
|
|
215
|
+
previousCheckpointNumber !== expectedCheckpointNumber &&
|
|
216
|
+
proposedCheckpointNumber !== expectedCheckpointNumber
|
|
217
|
+
) {
|
|
218
|
+
const [reported, source]: [CheckpointNumber, 'confirmed' | 'proposed'] =
|
|
219
|
+
proposedCheckpointNumber > previousCheckpointNumber
|
|
220
|
+
? [proposedCheckpointNumber, 'proposed']
|
|
221
|
+
: [previousCheckpointNumber, 'confirmed'];
|
|
222
|
+
throw new CheckpointNumberNotSequentialError(blockCheckpointNumber, reported, source);
|
|
184
223
|
}
|
|
185
224
|
|
|
186
225
|
// Extract the previous block if there is one and see if it is for the same checkpoint or not
|
|
187
226
|
const previousBlockResult = await this.getBlock(previousBlockNumber);
|
|
188
227
|
|
|
189
|
-
let
|
|
228
|
+
let expectedBlockIndex = 0;
|
|
190
229
|
let previousBlockIndex: number | undefined = undefined;
|
|
191
230
|
if (previousBlockResult !== undefined) {
|
|
192
|
-
if (previousBlockResult.checkpointNumber ===
|
|
231
|
+
if (previousBlockResult.checkpointNumber === blockCheckpointNumber) {
|
|
193
232
|
// The previous block is for the same checkpoint, therefore our index should follow it
|
|
194
233
|
previousBlockIndex = previousBlockResult.indexWithinCheckpoint;
|
|
195
|
-
|
|
234
|
+
expectedBlockIndex = previousBlockIndex + 1;
|
|
196
235
|
}
|
|
197
|
-
if (!previousBlockResult.archive.root.equals(
|
|
236
|
+
if (!previousBlockResult.archive.root.equals(blockLastArchive)) {
|
|
198
237
|
throw new BlockArchiveNotConsistentError(
|
|
199
|
-
|
|
238
|
+
blockNumber,
|
|
200
239
|
previousBlockResult.number,
|
|
201
|
-
|
|
240
|
+
blockLastArchive,
|
|
202
241
|
previousBlockResult.archive.root,
|
|
203
242
|
);
|
|
204
243
|
}
|
|
205
244
|
}
|
|
206
245
|
|
|
207
|
-
// Now check that the
|
|
208
|
-
if (!opts.force &&
|
|
209
|
-
throw new BlockIndexNotSequentialError(
|
|
246
|
+
// Now check that the block has the expected index value
|
|
247
|
+
if (!opts.force && expectedBlockIndex !== blockIndex) {
|
|
248
|
+
throw new BlockIndexNotSequentialError(blockIndex, previousBlockIndex);
|
|
210
249
|
}
|
|
211
250
|
|
|
212
|
-
|
|
213
|
-
let previousBlock: L2Block | undefined = undefined;
|
|
214
|
-
for (const block of blocks) {
|
|
215
|
-
if (!opts.force && previousBlock) {
|
|
216
|
-
if (previousBlock.number + 1 !== block.number) {
|
|
217
|
-
throw new BlockNumberNotSequentialError(block.number, previousBlock.number);
|
|
218
|
-
}
|
|
219
|
-
if (previousBlock.indexWithinCheckpoint + 1 !== block.indexWithinCheckpoint) {
|
|
220
|
-
throw new BlockIndexNotSequentialError(block.indexWithinCheckpoint, previousBlock.indexWithinCheckpoint);
|
|
221
|
-
}
|
|
222
|
-
if (!previousBlock.archive.root.equals(block.header.lastArchive.root)) {
|
|
223
|
-
throw new BlockArchiveNotConsistentError(
|
|
224
|
-
block.number,
|
|
225
|
-
previousBlock.number,
|
|
226
|
-
block.header.lastArchive.root,
|
|
227
|
-
previousBlock.archive.root,
|
|
228
|
-
);
|
|
229
|
-
}
|
|
230
|
-
}
|
|
231
|
-
if (!opts.force && firstBlockCheckpointNumber !== block.checkpointNumber) {
|
|
232
|
-
throw new CheckpointNumberNotConsistentError(block.checkpointNumber, firstBlockCheckpointNumber);
|
|
233
|
-
}
|
|
234
|
-
previousBlock = block;
|
|
235
|
-
await this.addBlockToDatabase(block, block.checkpointNumber, block.indexWithinCheckpoint);
|
|
236
|
-
}
|
|
251
|
+
await this.addBlockToDatabase(block, block.checkpointNumber, block.indexWithinCheckpoint);
|
|
237
252
|
|
|
238
253
|
return true;
|
|
239
254
|
});
|
|
240
255
|
}
|
|
241
256
|
|
|
242
257
|
/**
|
|
243
|
-
* Append new
|
|
258
|
+
* Append new checkpoints to the store's list.
|
|
244
259
|
* @param checkpoints - The L2 checkpoints to be added to the store.
|
|
245
260
|
* @returns True if the operation is successful.
|
|
246
261
|
*/
|
|
@@ -250,37 +265,29 @@ export class BlockStore {
|
|
|
250
265
|
}
|
|
251
266
|
|
|
252
267
|
return await this.db.transactionAsync(async () => {
|
|
253
|
-
// Check that the checkpoint immediately before the first block to be added is present in the store.
|
|
254
268
|
const firstCheckpointNumber = checkpoints[0].checkpoint.number;
|
|
255
269
|
const previousCheckpointNumber = await this.getLatestCheckpointNumber();
|
|
256
270
|
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
// There should be a previous checkpoint
|
|
265
|
-
previousCheckpointData = await this.getCheckpointData(previousCheckpointNumber);
|
|
266
|
-
if (previousCheckpointData === undefined) {
|
|
267
|
-
throw new CheckpointNotFoundError(previousCheckpointNumber);
|
|
271
|
+
// Handle already-stored checkpoints at the start of the batch.
|
|
272
|
+
// This can happen after an L1 reorg re-includes a checkpoint in a different L1 block.
|
|
273
|
+
// We accept them if archives match (same content) and update their L1 metadata.
|
|
274
|
+
if (!opts.force && firstCheckpointNumber <= previousCheckpointNumber) {
|
|
275
|
+
checkpoints = await this.skipOrUpdateAlreadyStoredCheckpoints(checkpoints, previousCheckpointNumber);
|
|
276
|
+
if (checkpoints.length === 0) {
|
|
277
|
+
return true;
|
|
268
278
|
}
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
// If we have a previous checkpoint then we need to get the previous block number
|
|
275
|
-
if (previousCheckpointData !== undefined) {
|
|
276
|
-
previousBlockNumber = BlockNumber(previousCheckpointData.startBlock + previousCheckpointData.numBlocks - 1);
|
|
277
|
-
previousBlock = await this.getBlock(previousBlockNumber);
|
|
278
|
-
if (previousBlock === undefined) {
|
|
279
|
-
// We should be able to get the required previous block
|
|
280
|
-
throw new BlockNotFoundError(previousBlockNumber);
|
|
279
|
+
// Re-check sequentiality after skipping
|
|
280
|
+
const newFirstNumber = checkpoints[0].checkpoint.number;
|
|
281
|
+
if (previousCheckpointNumber !== newFirstNumber - 1) {
|
|
282
|
+
throw new InitialCheckpointNumberNotSequentialError(newFirstNumber, previousCheckpointNumber);
|
|
281
283
|
}
|
|
284
|
+
} else if (previousCheckpointNumber !== firstCheckpointNumber - 1 && !opts.force) {
|
|
285
|
+
throw new InitialCheckpointNumberNotSequentialError(firstCheckpointNumber, previousCheckpointNumber);
|
|
282
286
|
}
|
|
283
287
|
|
|
288
|
+
// Get the last block of the previous checkpoint for archive chaining
|
|
289
|
+
let previousBlock = await this.getPreviousCheckpointBlock(checkpoints[0].checkpoint.number);
|
|
290
|
+
|
|
284
291
|
// Iterate over checkpoints array and insert them, checking that the block numbers are sequential.
|
|
285
292
|
let previousCheckpoint: PublishedCheckpoint | undefined = undefined;
|
|
286
293
|
for (const checkpoint of checkpoints) {
|
|
@@ -296,60 +303,146 @@ export class BlockStore {
|
|
|
296
303
|
}
|
|
297
304
|
previousCheckpoint = checkpoint;
|
|
298
305
|
|
|
299
|
-
//
|
|
300
|
-
|
|
301
|
-
const block = checkpoint.checkpoint.blocks[i];
|
|
302
|
-
if (previousBlock) {
|
|
303
|
-
// The blocks should have a sequential block number
|
|
304
|
-
if (previousBlock.number !== block.number - 1) {
|
|
305
|
-
throw new BlockNumberNotSequentialError(block.number, previousBlock.number);
|
|
306
|
-
}
|
|
307
|
-
// If the blocks are for the same checkpoint then they should have sequential indexes
|
|
308
|
-
if (
|
|
309
|
-
previousBlock.checkpointNumber === block.checkpointNumber &&
|
|
310
|
-
previousBlock.indexWithinCheckpoint !== block.indexWithinCheckpoint - 1
|
|
311
|
-
) {
|
|
312
|
-
throw new BlockIndexNotSequentialError(block.indexWithinCheckpoint, previousBlock.indexWithinCheckpoint);
|
|
313
|
-
}
|
|
314
|
-
if (!previousBlock.archive.root.equals(block.header.lastArchive.root)) {
|
|
315
|
-
throw new BlockArchiveNotConsistentError(
|
|
316
|
-
block.number,
|
|
317
|
-
previousBlock.number,
|
|
318
|
-
block.header.lastArchive.root,
|
|
319
|
-
previousBlock.archive.root,
|
|
320
|
-
);
|
|
321
|
-
}
|
|
322
|
-
} else {
|
|
323
|
-
// No previous block, must be block 1 at checkpoint index 0
|
|
324
|
-
if (block.indexWithinCheckpoint !== 0) {
|
|
325
|
-
throw new BlockIndexNotSequentialError(block.indexWithinCheckpoint, undefined);
|
|
326
|
-
}
|
|
327
|
-
if (block.number !== INITIAL_L2_BLOCK_NUM) {
|
|
328
|
-
throw new BlockNumberNotSequentialError(block.number, undefined);
|
|
329
|
-
}
|
|
330
|
-
}
|
|
306
|
+
// Validate block sequencing, indexes, and archive chaining
|
|
307
|
+
this.validateCheckpointBlocks(checkpoint.checkpoint.blocks, previousBlock);
|
|
331
308
|
|
|
332
|
-
|
|
333
|
-
|
|
309
|
+
// Store every block in the database (may already exist, but L1 data is authoritative)
|
|
310
|
+
for (let i = 0; i < checkpoint.checkpoint.blocks.length; i++) {
|
|
311
|
+
await this.addBlockToDatabase(checkpoint.checkpoint.blocks[i], checkpoint.checkpoint.number, i);
|
|
334
312
|
}
|
|
313
|
+
previousBlock = checkpoint.checkpoint.blocks.at(-1);
|
|
335
314
|
|
|
336
315
|
// Store the checkpoint in the database
|
|
337
316
|
await this.#checkpoints.set(checkpoint.checkpoint.number, {
|
|
338
317
|
header: checkpoint.checkpoint.header.toBuffer(),
|
|
339
318
|
archive: checkpoint.checkpoint.archive.toBuffer(),
|
|
319
|
+
checkpointOutHash: checkpoint.checkpoint.getCheckpointOutHash().toBuffer(),
|
|
340
320
|
l1: checkpoint.l1.toBuffer(),
|
|
341
321
|
attestations: checkpoint.attestations.map(attestation => attestation.toBuffer()),
|
|
342
322
|
checkpointNumber: checkpoint.checkpoint.number,
|
|
343
323
|
startBlock: checkpoint.checkpoint.blocks[0].number,
|
|
344
|
-
|
|
324
|
+
blockCount: checkpoint.checkpoint.blocks.length,
|
|
345
325
|
});
|
|
326
|
+
|
|
327
|
+
// Update slot-to-checkpoint index
|
|
328
|
+
await this.#slotToCheckpoint.set(checkpoint.checkpoint.header.slotNumber, checkpoint.checkpoint.number);
|
|
346
329
|
}
|
|
347
330
|
|
|
331
|
+
// Clear the proposed checkpoint if any of the confirmed checkpoints match or supersede it
|
|
332
|
+
const lastConfirmedCheckpointNumber = checkpoints[checkpoints.length - 1].checkpoint.number;
|
|
333
|
+
await this.clearProposedCheckpointIfSuperseded(lastConfirmedCheckpointNumber);
|
|
334
|
+
|
|
348
335
|
await this.#lastSynchedL1Block.set(checkpoints[checkpoints.length - 1].l1.blockNumber);
|
|
349
336
|
return true;
|
|
350
337
|
});
|
|
351
338
|
}
|
|
352
339
|
|
|
340
|
+
/**
|
|
341
|
+
* Handles checkpoints at the start of a batch that are already stored (e.g. due to L1 reorg).
|
|
342
|
+
* Verifies the archive root matches, updates L1 metadata, and returns only the new checkpoints.
|
|
343
|
+
*/
|
|
344
|
+
private async skipOrUpdateAlreadyStoredCheckpoints(
|
|
345
|
+
checkpoints: PublishedCheckpoint[],
|
|
346
|
+
latestStored: CheckpointNumber,
|
|
347
|
+
): Promise<PublishedCheckpoint[]> {
|
|
348
|
+
let i = 0;
|
|
349
|
+
for (; i < checkpoints.length && checkpoints[i].checkpoint.number <= latestStored; i++) {
|
|
350
|
+
const incoming = checkpoints[i];
|
|
351
|
+
const stored = await this.getCheckpointData(incoming.checkpoint.number);
|
|
352
|
+
if (!stored) {
|
|
353
|
+
// Should not happen if latestStored is correct, but be safe
|
|
354
|
+
break;
|
|
355
|
+
}
|
|
356
|
+
// Verify the checkpoint content matches (archive root)
|
|
357
|
+
if (!stored.archive.root.equals(incoming.checkpoint.archive.root)) {
|
|
358
|
+
throw new Error(
|
|
359
|
+
`Checkpoint ${incoming.checkpoint.number} already exists in store but with a different archive root. ` +
|
|
360
|
+
`Stored: ${stored.archive.root}, incoming: ${incoming.checkpoint.archive.root}`,
|
|
361
|
+
);
|
|
362
|
+
}
|
|
363
|
+
// Update L1 metadata and attestations for the already-stored checkpoint
|
|
364
|
+
this.#log.warn(
|
|
365
|
+
`Checkpoint ${incoming.checkpoint.number} already stored, updating L1 info ` +
|
|
366
|
+
`(L1 block ${stored.l1.blockNumber} -> ${incoming.l1.blockNumber})`,
|
|
367
|
+
);
|
|
368
|
+
await this.#checkpoints.set(incoming.checkpoint.number, {
|
|
369
|
+
header: incoming.checkpoint.header.toBuffer(),
|
|
370
|
+
archive: incoming.checkpoint.archive.toBuffer(),
|
|
371
|
+
checkpointOutHash: incoming.checkpoint.getCheckpointOutHash().toBuffer(),
|
|
372
|
+
l1: incoming.l1.toBuffer(),
|
|
373
|
+
attestations: incoming.attestations.map(a => a.toBuffer()),
|
|
374
|
+
checkpointNumber: incoming.checkpoint.number,
|
|
375
|
+
startBlock: incoming.checkpoint.blocks[0].number,
|
|
376
|
+
blockCount: incoming.checkpoint.blocks.length,
|
|
377
|
+
});
|
|
378
|
+
// Update the sync point to reflect the new L1 block
|
|
379
|
+
await this.#lastSynchedL1Block.set(incoming.l1.blockNumber);
|
|
380
|
+
}
|
|
381
|
+
return checkpoints.slice(i);
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
/**
|
|
385
|
+
* Gets the last block of the checkpoint before the given one.
|
|
386
|
+
* Returns undefined if there is no previous checkpoint (i.e. genesis).
|
|
387
|
+
*/
|
|
388
|
+
private async getPreviousCheckpointBlock(checkpointNumber: CheckpointNumber): Promise<L2Block | undefined> {
|
|
389
|
+
const previousCheckpointNumber = CheckpointNumber(checkpointNumber - 1);
|
|
390
|
+
if (previousCheckpointNumber === INITIAL_CHECKPOINT_NUMBER - 1) {
|
|
391
|
+
return undefined;
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
const previousCheckpointData = await this.getCheckpointData(previousCheckpointNumber);
|
|
395
|
+
if (previousCheckpointData === undefined) {
|
|
396
|
+
throw new CheckpointNotFoundError(previousCheckpointNumber);
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
const previousBlockNumber = BlockNumber(previousCheckpointData.startBlock + previousCheckpointData.blockCount - 1);
|
|
400
|
+
const previousBlock = await this.getBlock(previousBlockNumber);
|
|
401
|
+
if (previousBlock === undefined) {
|
|
402
|
+
throw new BlockNotFoundError(previousBlockNumber);
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
return previousBlock;
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
/**
|
|
409
|
+
* Validates that blocks are sequential, have correct indexes, and chain via archive roots.
|
|
410
|
+
* This is the same validation used for both confirmed checkpoints (addCheckpoints) and
|
|
411
|
+
* proposed checkpoints (setProposedCheckpoint).
|
|
412
|
+
*/
|
|
413
|
+
private validateCheckpointBlocks(blocks: L2Block[], previousBlock: L2Block | undefined): void {
|
|
414
|
+
for (const block of blocks) {
|
|
415
|
+
if (previousBlock) {
|
|
416
|
+
if (previousBlock.number !== block.number - 1) {
|
|
417
|
+
throw new BlockNumberNotSequentialError(block.number, previousBlock.number);
|
|
418
|
+
}
|
|
419
|
+
if (previousBlock.checkpointNumber === block.checkpointNumber) {
|
|
420
|
+
if (previousBlock.indexWithinCheckpoint !== block.indexWithinCheckpoint - 1) {
|
|
421
|
+
throw new BlockIndexNotSequentialError(block.indexWithinCheckpoint, previousBlock.indexWithinCheckpoint);
|
|
422
|
+
}
|
|
423
|
+
} else if (block.indexWithinCheckpoint !== 0) {
|
|
424
|
+
throw new BlockIndexNotSequentialError(block.indexWithinCheckpoint, previousBlock.indexWithinCheckpoint);
|
|
425
|
+
}
|
|
426
|
+
if (!previousBlock.archive.root.equals(block.header.lastArchive.root)) {
|
|
427
|
+
throw new BlockArchiveNotConsistentError(
|
|
428
|
+
block.number,
|
|
429
|
+
previousBlock.number,
|
|
430
|
+
block.header.lastArchive.root,
|
|
431
|
+
previousBlock.archive.root,
|
|
432
|
+
);
|
|
433
|
+
}
|
|
434
|
+
} else {
|
|
435
|
+
if (block.indexWithinCheckpoint !== 0) {
|
|
436
|
+
throw new BlockIndexNotSequentialError(block.indexWithinCheckpoint, undefined);
|
|
437
|
+
}
|
|
438
|
+
if (block.number !== INITIAL_L2_BLOCK_NUM) {
|
|
439
|
+
throw new BlockNumberNotSequentialError(block.number, undefined);
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
previousBlock = block;
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
|
|
353
446
|
private async addBlockToDatabase(block: L2Block, checkpointNumber: number, indexWithinCheckpoint: number) {
|
|
354
447
|
const blockHash = await block.hash();
|
|
355
448
|
|
|
@@ -425,7 +518,7 @@ export class BlockStore {
|
|
|
425
518
|
if (!targetCheckpoint) {
|
|
426
519
|
throw new Error(`Target checkpoint ${checkpointNumber} not found in store`);
|
|
427
520
|
}
|
|
428
|
-
lastBlockToKeep = BlockNumber(targetCheckpoint.startBlock + targetCheckpoint.
|
|
521
|
+
lastBlockToKeep = BlockNumber(targetCheckpoint.startBlock + targetCheckpoint.blockCount - 1);
|
|
429
522
|
}
|
|
430
523
|
|
|
431
524
|
// Remove all blocks after lastBlockToKeep (both checkpointed and uncheckpointed)
|
|
@@ -433,10 +526,21 @@ export class BlockStore {
|
|
|
433
526
|
|
|
434
527
|
// Remove all checkpoints after the target
|
|
435
528
|
for (let c = latestCheckpointNumber; c > checkpointNumber; c = CheckpointNumber(c - 1)) {
|
|
529
|
+
const checkpointStorage = await this.#checkpoints.getAsync(c);
|
|
530
|
+
if (checkpointStorage) {
|
|
531
|
+
const slotNumber = CheckpointHeader.fromBuffer(checkpointStorage.header).slotNumber;
|
|
532
|
+
await this.#slotToCheckpoint.delete(slotNumber);
|
|
533
|
+
}
|
|
436
534
|
await this.#checkpoints.delete(c);
|
|
437
535
|
this.#log.debug(`Removed checkpoint ${c}`);
|
|
438
536
|
}
|
|
439
537
|
|
|
538
|
+
// Clear any proposed checkpoint that was orphaned by the removal (its base chain no longer exists)
|
|
539
|
+
const proposedCheckpointNumber = await this.getProposedCheckpointNumber();
|
|
540
|
+
if (proposedCheckpointNumber > checkpointNumber) {
|
|
541
|
+
await this.#proposedCheckpoint.delete();
|
|
542
|
+
}
|
|
543
|
+
|
|
440
544
|
return { blocksRemoved };
|
|
441
545
|
});
|
|
442
546
|
}
|
|
@@ -461,17 +565,32 @@ export class BlockStore {
|
|
|
461
565
|
return checkpoints;
|
|
462
566
|
}
|
|
463
567
|
|
|
464
|
-
|
|
465
|
-
|
|
568
|
+
/** Returns checkpoint data for all checkpoints whose slot falls within the given range (inclusive). */
|
|
569
|
+
async getCheckpointDataForSlotRange(startSlot: SlotNumber, endSlot: SlotNumber): Promise<CheckpointData[]> {
|
|
570
|
+
const result: CheckpointData[] = [];
|
|
571
|
+
for await (const [, checkpointNumber] of this.#slotToCheckpoint.entriesAsync({
|
|
572
|
+
start: startSlot,
|
|
573
|
+
end: endSlot + 1,
|
|
574
|
+
})) {
|
|
575
|
+
const checkpointStorage = await this.#checkpoints.getAsync(checkpointNumber);
|
|
576
|
+
if (checkpointStorage) {
|
|
577
|
+
result.push(this.checkpointDataFromCheckpointStorage(checkpointStorage));
|
|
578
|
+
}
|
|
579
|
+
}
|
|
580
|
+
return result;
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
private checkpointDataFromCheckpointStorage(checkpointStorage: CheckpointStorage): CheckpointData {
|
|
584
|
+
return {
|
|
466
585
|
header: CheckpointHeader.fromBuffer(checkpointStorage.header),
|
|
467
586
|
archive: AppendOnlyTreeSnapshot.fromBuffer(checkpointStorage.archive),
|
|
587
|
+
checkpointOutHash: Fr.fromBuffer(checkpointStorage.checkpointOutHash),
|
|
468
588
|
checkpointNumber: CheckpointNumber(checkpointStorage.checkpointNumber),
|
|
469
|
-
startBlock: checkpointStorage.startBlock,
|
|
470
|
-
|
|
589
|
+
startBlock: BlockNumber(checkpointStorage.startBlock),
|
|
590
|
+
blockCount: checkpointStorage.blockCount,
|
|
471
591
|
l1: L1PublishedData.fromBuffer(checkpointStorage.l1),
|
|
472
|
-
attestations: checkpointStorage.attestations,
|
|
592
|
+
attestations: checkpointStorage.attestations.map(buf => CommitteeAttestation.fromBuffer(buf)),
|
|
473
593
|
};
|
|
474
|
-
return data;
|
|
475
594
|
}
|
|
476
595
|
|
|
477
596
|
async getBlocksForCheckpoint(checkpointNumber: CheckpointNumber): Promise<L2Block[] | undefined> {
|
|
@@ -483,7 +602,7 @@ export class BlockStore {
|
|
|
483
602
|
const blocksForCheckpoint = await toArray(
|
|
484
603
|
this.#blocks.entriesAsync({
|
|
485
604
|
start: checkpoint.startBlock,
|
|
486
|
-
end: checkpoint.startBlock + checkpoint.
|
|
605
|
+
end: checkpoint.startBlock + checkpoint.blockCount,
|
|
487
606
|
}),
|
|
488
607
|
);
|
|
489
608
|
|
|
@@ -527,7 +646,7 @@ export class BlockStore {
|
|
|
527
646
|
const removedBlocks: L2Block[] = [];
|
|
528
647
|
|
|
529
648
|
// Get the latest block number to determine the range
|
|
530
|
-
const latestBlockNumber = await this.
|
|
649
|
+
const latestBlockNumber = await this.getLatestL2BlockNumber();
|
|
531
650
|
|
|
532
651
|
// Iterate from blockNumber + 1 to latestBlockNumber
|
|
533
652
|
for (let bn = blockNumber + 1; bn <= latestBlockNumber; bn++) {
|
|
@@ -556,17 +675,10 @@ export class BlockStore {
|
|
|
556
675
|
if (!checkpointStorage) {
|
|
557
676
|
throw new CheckpointNotFoundError(provenCheckpointNumber);
|
|
558
677
|
} else {
|
|
559
|
-
return BlockNumber(checkpointStorage.startBlock + checkpointStorage.
|
|
678
|
+
return BlockNumber(checkpointStorage.startBlock + checkpointStorage.blockCount - 1);
|
|
560
679
|
}
|
|
561
680
|
}
|
|
562
681
|
|
|
563
|
-
async getLatestBlockNumber(): Promise<BlockNumber> {
|
|
564
|
-
const [latestBlocknumber] = await toArray(this.#blocks.keysAsync({ reverse: true, limit: 1 }));
|
|
565
|
-
return typeof latestBlocknumber === 'number'
|
|
566
|
-
? BlockNumber(latestBlocknumber)
|
|
567
|
-
: BlockNumber(INITIAL_L2_BLOCK_NUM - 1);
|
|
568
|
-
}
|
|
569
|
-
|
|
570
682
|
async getLatestCheckpointNumber(): Promise<CheckpointNumber> {
|
|
571
683
|
const [latestCheckpointNumber] = await toArray(this.#checkpoints.keysAsync({ reverse: true, limit: 1 }));
|
|
572
684
|
if (latestCheckpointNumber === undefined) {
|
|
@@ -575,6 +687,133 @@ export class BlockStore {
|
|
|
575
687
|
return CheckpointNumber(latestCheckpointNumber);
|
|
576
688
|
}
|
|
577
689
|
|
|
690
|
+
async hasProposedCheckpoint(): Promise<boolean> {
|
|
691
|
+
const proposed = await this.#proposedCheckpoint.getAsync();
|
|
692
|
+
return proposed !== undefined;
|
|
693
|
+
}
|
|
694
|
+
|
|
695
|
+
/** Deletes the proposed checkpoint from storage. */
|
|
696
|
+
async deleteProposedCheckpoint(): Promise<void> {
|
|
697
|
+
await this.#proposedCheckpoint.delete();
|
|
698
|
+
}
|
|
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
|
+
|
|
749
|
+
/** Clears the proposed checkpoint if the given confirmed checkpoint number supersedes it. */
|
|
750
|
+
async clearProposedCheckpointIfSuperseded(confirmedCheckpointNumber: CheckpointNumber): Promise<void> {
|
|
751
|
+
const proposedCheckpointNumber = await this.getProposedCheckpointNumber();
|
|
752
|
+
if (proposedCheckpointNumber <= confirmedCheckpointNumber) {
|
|
753
|
+
await this.#proposedCheckpoint.delete();
|
|
754
|
+
}
|
|
755
|
+
}
|
|
756
|
+
|
|
757
|
+
/** Returns the proposed checkpoint data, or undefined if no proposed checkpoint exists. No fallback to confirmed. */
|
|
758
|
+
async getProposedCheckpointOnly(): Promise<ProposedCheckpointData | undefined> {
|
|
759
|
+
const stored = await this.#proposedCheckpoint.getAsync();
|
|
760
|
+
if (!stored) {
|
|
761
|
+
return undefined;
|
|
762
|
+
}
|
|
763
|
+
return this.convertToProposedCheckpointData(stored);
|
|
764
|
+
}
|
|
765
|
+
|
|
766
|
+
/**
|
|
767
|
+
* Gets the checkpoint at the proposed tip
|
|
768
|
+
* - pending checkpoint if it exists
|
|
769
|
+
* - fallsback to latest confirmed checkpoint otherwise
|
|
770
|
+
* @returns CommonCheckpointData
|
|
771
|
+
*/
|
|
772
|
+
async getProposedCheckpoint(): Promise<CommonCheckpointData | undefined> {
|
|
773
|
+
const stored = await this.#proposedCheckpoint.getAsync();
|
|
774
|
+
if (!stored) {
|
|
775
|
+
return this.getCheckpointData(await this.getLatestCheckpointNumber());
|
|
776
|
+
}
|
|
777
|
+
return this.convertToProposedCheckpointData(stored);
|
|
778
|
+
}
|
|
779
|
+
|
|
780
|
+
private convertToProposedCheckpointData(stored: ProposedCheckpointStorage): ProposedCheckpointData {
|
|
781
|
+
return {
|
|
782
|
+
checkpointNumber: CheckpointNumber(stored.checkpointNumber),
|
|
783
|
+
header: CheckpointHeader.fromBuffer(stored.header),
|
|
784
|
+
archive: AppendOnlyTreeSnapshot.fromBuffer(stored.archive),
|
|
785
|
+
checkpointOutHash: Fr.fromBuffer(stored.checkpointOutHash),
|
|
786
|
+
startBlock: BlockNumber(stored.startBlock),
|
|
787
|
+
blockCount: stored.blockCount,
|
|
788
|
+
totalManaUsed: BigInt(stored.totalManaUsed),
|
|
789
|
+
feeAssetPriceModifier: BigInt(stored.feeAssetPriceModifier),
|
|
790
|
+
};
|
|
791
|
+
}
|
|
792
|
+
|
|
793
|
+
/**
|
|
794
|
+
* Attempts to get the proposedCheckpoint's number, if there is not one, then fallback to the latest confirmed checkpoint number.
|
|
795
|
+
* @returns CheckpointNumber
|
|
796
|
+
*/
|
|
797
|
+
async getProposedCheckpointNumber(): Promise<CheckpointNumber> {
|
|
798
|
+
const proposed = await this.getProposedCheckpoint();
|
|
799
|
+
if (!proposed) {
|
|
800
|
+
return await this.getLatestCheckpointNumber();
|
|
801
|
+
}
|
|
802
|
+
return CheckpointNumber(proposed.checkpointNumber);
|
|
803
|
+
}
|
|
804
|
+
|
|
805
|
+
/**
|
|
806
|
+
* Attempts to get the proposedCheckpoint's block number, if there is not one, then fallback to the checkpointed block number
|
|
807
|
+
* @returns BlockNumber
|
|
808
|
+
*/
|
|
809
|
+
async getProposedCheckpointL2BlockNumber(): Promise<BlockNumber> {
|
|
810
|
+
const proposed = await this.getProposedCheckpoint();
|
|
811
|
+
if (!proposed) {
|
|
812
|
+
return await this.getCheckpointedL2BlockNumber();
|
|
813
|
+
}
|
|
814
|
+
return BlockNumber(proposed.startBlock + proposed.blockCount - 1);
|
|
815
|
+
}
|
|
816
|
+
|
|
578
817
|
async getCheckpointedBlock(number: BlockNumber): Promise<CheckpointedL2Block | undefined> {
|
|
579
818
|
const blockStorage = await this.#blocks.getAsync(number);
|
|
580
819
|
if (!blockStorage) {
|
|
@@ -655,6 +894,32 @@ export class BlockStore {
|
|
|
655
894
|
}
|
|
656
895
|
}
|
|
657
896
|
|
|
897
|
+
/**
|
|
898
|
+
* Gets block metadata (without tx data) by block number.
|
|
899
|
+
* @param blockNumber - The number of the block to return.
|
|
900
|
+
* @returns The requested block data.
|
|
901
|
+
*/
|
|
902
|
+
async getBlockData(blockNumber: BlockNumber): Promise<BlockData | undefined> {
|
|
903
|
+
const blockStorage = await this.#blocks.getAsync(blockNumber);
|
|
904
|
+
if (!blockStorage || !blockStorage.header) {
|
|
905
|
+
return undefined;
|
|
906
|
+
}
|
|
907
|
+
return this.getBlockDataFromBlockStorage(blockStorage);
|
|
908
|
+
}
|
|
909
|
+
|
|
910
|
+
/**
|
|
911
|
+
* Gets block metadata (without tx data) by archive root.
|
|
912
|
+
* @param archive - The archive root of the block to return.
|
|
913
|
+
* @returns The requested block data.
|
|
914
|
+
*/
|
|
915
|
+
async getBlockDataByArchive(archive: Fr): Promise<BlockData | undefined> {
|
|
916
|
+
const blockNumber = await this.#blockArchiveIndex.getAsync(archive.toString());
|
|
917
|
+
if (blockNumber === undefined) {
|
|
918
|
+
return undefined;
|
|
919
|
+
}
|
|
920
|
+
return this.getBlockData(BlockNumber(blockNumber));
|
|
921
|
+
}
|
|
922
|
+
|
|
658
923
|
/**
|
|
659
924
|
* Gets an L2 block.
|
|
660
925
|
* @param blockNumber - The number of the block to return.
|
|
@@ -759,15 +1024,24 @@ export class BlockStore {
|
|
|
759
1024
|
}
|
|
760
1025
|
}
|
|
761
1026
|
|
|
1027
|
+
private getBlockDataFromBlockStorage(blockStorage: BlockStorage): BlockData {
|
|
1028
|
+
return {
|
|
1029
|
+
header: BlockHeader.fromBuffer(blockStorage.header),
|
|
1030
|
+
archive: AppendOnlyTreeSnapshot.fromBuffer(blockStorage.archive),
|
|
1031
|
+
blockHash: BlockHash.fromBuffer(blockStorage.blockHash),
|
|
1032
|
+
checkpointNumber: CheckpointNumber(blockStorage.checkpointNumber),
|
|
1033
|
+
indexWithinCheckpoint: IndexWithinCheckpoint(blockStorage.indexWithinCheckpoint),
|
|
1034
|
+
};
|
|
1035
|
+
}
|
|
1036
|
+
|
|
762
1037
|
private async getBlockFromBlockStorage(
|
|
763
1038
|
blockNumber: number,
|
|
764
1039
|
blockStorage: BlockStorage,
|
|
765
1040
|
): Promise<L2Block | undefined> {
|
|
766
|
-
const header =
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
const blockHashString = bufferToHex(blockHash);
|
|
1041
|
+
const { header, archive, blockHash, checkpointNumber, indexWithinCheckpoint } =
|
|
1042
|
+
this.getBlockDataFromBlockStorage(blockStorage);
|
|
1043
|
+
header.setHash(blockHash);
|
|
1044
|
+
const blockHashString = bufferToHex(blockStorage.blockHash);
|
|
771
1045
|
const blockTxsBuffer = await this.#blockTxs.getAsync(blockHashString);
|
|
772
1046
|
if (blockTxsBuffer === undefined) {
|
|
773
1047
|
this.#log.warn(`Could not find body for block ${header.globalVariables.blockNumber} ${blockHash}`);
|
|
@@ -786,13 +1060,7 @@ export class BlockStore {
|
|
|
786
1060
|
txEffects.push(deserializeIndexedTxEffect(txEffect).data);
|
|
787
1061
|
}
|
|
788
1062
|
const body = new Body(txEffects);
|
|
789
|
-
const block = new L2Block(
|
|
790
|
-
archive,
|
|
791
|
-
header,
|
|
792
|
-
body,
|
|
793
|
-
CheckpointNumber(blockStorage.checkpointNumber!),
|
|
794
|
-
IndexWithinCheckpoint(blockStorage.indexWithinCheckpoint),
|
|
795
|
-
);
|
|
1063
|
+
const block = new L2Block(archive, header, body, checkpointNumber, indexWithinCheckpoint);
|
|
796
1064
|
|
|
797
1065
|
if (block.number !== blockNumber) {
|
|
798
1066
|
throw new Error(
|
|
@@ -822,7 +1090,10 @@ export class BlockStore {
|
|
|
822
1090
|
* @param txHash - The hash of a tx we try to get the receipt for.
|
|
823
1091
|
* @returns The requested tx receipt (or undefined if not found).
|
|
824
1092
|
*/
|
|
825
|
-
async getSettledTxReceipt(
|
|
1093
|
+
async getSettledTxReceipt(
|
|
1094
|
+
txHash: TxHash,
|
|
1095
|
+
l1Constants?: Pick<L1RollupConstants, 'epochDuration'>,
|
|
1096
|
+
): Promise<TxReceipt | undefined> {
|
|
826
1097
|
const txEffect = await this.getTxEffect(txHash);
|
|
827
1098
|
if (!txEffect) {
|
|
828
1099
|
return undefined;
|
|
@@ -831,10 +1102,11 @@ export class BlockStore {
|
|
|
831
1102
|
const blockNumber = BlockNumber(txEffect.l2BlockNumber);
|
|
832
1103
|
|
|
833
1104
|
// Use existing archiver methods to determine finalization level
|
|
834
|
-
const [provenBlockNumber, checkpointedBlockNumber, finalizedBlockNumber] = await Promise.all([
|
|
1105
|
+
const [provenBlockNumber, checkpointedBlockNumber, finalizedBlockNumber, blockData] = await Promise.all([
|
|
835
1106
|
this.getProvenBlockNumber(),
|
|
836
1107
|
this.getCheckpointedL2BlockNumber(),
|
|
837
1108
|
this.getFinalizedL2BlockNumber(),
|
|
1109
|
+
this.getBlockData(blockNumber),
|
|
838
1110
|
]);
|
|
839
1111
|
|
|
840
1112
|
let status: TxStatus;
|
|
@@ -848,6 +1120,9 @@ export class BlockStore {
|
|
|
848
1120
|
status = TxStatus.PROPOSED;
|
|
849
1121
|
}
|
|
850
1122
|
|
|
1123
|
+
const epochNumber =
|
|
1124
|
+
blockData && l1Constants ? getEpochAtSlot(blockData.header.globalVariables.slotNumber, l1Constants) : undefined;
|
|
1125
|
+
|
|
851
1126
|
return new TxReceipt(
|
|
852
1127
|
txHash,
|
|
853
1128
|
status,
|
|
@@ -856,6 +1131,7 @@ export class BlockStore {
|
|
|
856
1131
|
txEffect.data.transactionFee.toBigInt(),
|
|
857
1132
|
txEffect.l2BlockHash,
|
|
858
1133
|
blockNumber,
|
|
1134
|
+
epochNumber,
|
|
859
1135
|
);
|
|
860
1136
|
}
|
|
861
1137
|
|
|
@@ -892,7 +1168,7 @@ export class BlockStore {
|
|
|
892
1168
|
if (!checkpoint) {
|
|
893
1169
|
return BlockNumber(INITIAL_L2_BLOCK_NUM - 1);
|
|
894
1170
|
}
|
|
895
|
-
return BlockNumber(checkpoint.startBlock + checkpoint.
|
|
1171
|
+
return BlockNumber(checkpoint.startBlock + checkpoint.blockCount - 1);
|
|
896
1172
|
}
|
|
897
1173
|
|
|
898
1174
|
async getLatestL2BlockNumber(): Promise<BlockNumber> {
|
|
@@ -912,6 +1188,47 @@ export class BlockStore {
|
|
|
912
1188
|
return this.#lastSynchedL1Block.set(l1BlockNumber);
|
|
913
1189
|
}
|
|
914
1190
|
|
|
1191
|
+
/** Sets the proposed checkpoint (not yet L1-confirmed). Only accepts confirmed + 1.
|
|
1192
|
+
* Computes archive and checkpointOutHash from the stored blocks. */
|
|
1193
|
+
async setProposedCheckpoint(proposed: ProposedCheckpointInput) {
|
|
1194
|
+
return await this.db.transactionAsync(async () => {
|
|
1195
|
+
const current = await this.getProposedCheckpointNumber();
|
|
1196
|
+
if (proposed.checkpointNumber <= current) {
|
|
1197
|
+
throw new ProposedCheckpointStaleError(proposed.checkpointNumber, current);
|
|
1198
|
+
}
|
|
1199
|
+
const confirmed = await this.getLatestCheckpointNumber();
|
|
1200
|
+
if (proposed.checkpointNumber !== confirmed + 1) {
|
|
1201
|
+
throw new ProposedCheckpointNotSequentialError(proposed.checkpointNumber, confirmed);
|
|
1202
|
+
}
|
|
1203
|
+
|
|
1204
|
+
// Ensure the previous checkpoint + blocks exist
|
|
1205
|
+
const previousBlock = await this.getPreviousCheckpointBlock(proposed.checkpointNumber);
|
|
1206
|
+
const blocks: L2Block[] = [];
|
|
1207
|
+
for (let i = 0; i < proposed.blockCount; i++) {
|
|
1208
|
+
const block = await this.getBlock(BlockNumber(proposed.startBlock + i));
|
|
1209
|
+
if (!block) {
|
|
1210
|
+
throw new BlockNotFoundError(proposed.startBlock + i);
|
|
1211
|
+
}
|
|
1212
|
+
blocks.push(block);
|
|
1213
|
+
}
|
|
1214
|
+
this.validateCheckpointBlocks(blocks, previousBlock);
|
|
1215
|
+
|
|
1216
|
+
const archive = blocks[blocks.length - 1].archive;
|
|
1217
|
+
const checkpointOutHash = Checkpoint.getCheckpointOutHash(blocks);
|
|
1218
|
+
|
|
1219
|
+
await this.#proposedCheckpoint.set({
|
|
1220
|
+
header: proposed.header.toBuffer(),
|
|
1221
|
+
archive: archive.toBuffer(),
|
|
1222
|
+
checkpointOutHash: checkpointOutHash.toBuffer(),
|
|
1223
|
+
checkpointNumber: proposed.checkpointNumber,
|
|
1224
|
+
startBlock: proposed.startBlock,
|
|
1225
|
+
blockCount: proposed.blockCount,
|
|
1226
|
+
totalManaUsed: proposed.totalManaUsed.toString(),
|
|
1227
|
+
feeAssetPriceModifier: proposed.feeAssetPriceModifier.toString(),
|
|
1228
|
+
});
|
|
1229
|
+
});
|
|
1230
|
+
}
|
|
1231
|
+
|
|
915
1232
|
async getProvenCheckpointNumber(): Promise<CheckpointNumber> {
|
|
916
1233
|
const [latestCheckpointNumber, provenCheckpointNumber] = await Promise.all([
|
|
917
1234
|
this.getLatestCheckpointNumber(),
|
|
@@ -927,6 +1244,20 @@ export class BlockStore {
|
|
|
927
1244
|
return result;
|
|
928
1245
|
}
|
|
929
1246
|
|
|
1247
|
+
async getFinalizedCheckpointNumber(): Promise<CheckpointNumber> {
|
|
1248
|
+
const [latestCheckpointNumber, finalizedCheckpointNumber] = await Promise.all([
|
|
1249
|
+
this.getLatestCheckpointNumber(),
|
|
1250
|
+
this.#lastFinalizedCheckpoint.getAsync(),
|
|
1251
|
+
]);
|
|
1252
|
+
return (finalizedCheckpointNumber ?? 0) > latestCheckpointNumber
|
|
1253
|
+
? latestCheckpointNumber
|
|
1254
|
+
: CheckpointNumber(finalizedCheckpointNumber ?? 0);
|
|
1255
|
+
}
|
|
1256
|
+
|
|
1257
|
+
setFinalizedCheckpointNumber(checkpointNumber: CheckpointNumber) {
|
|
1258
|
+
return this.#lastFinalizedCheckpoint.set(checkpointNumber);
|
|
1259
|
+
}
|
|
1260
|
+
|
|
930
1261
|
#computeBlockRange(start: BlockNumber, limit: number): Required<Pick<Range<number>, 'start' | 'limit'>> {
|
|
931
1262
|
if (limit < 1) {
|
|
932
1263
|
throw new Error(`Invalid limit: ${limit}`);
|