@aztec/validator-client 0.0.1-commit.9d2bcf6d → 0.0.1-commit.9ef841308
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 +60 -18
- package/dest/checkpoint_builder.d.ts +21 -8
- package/dest/checkpoint_builder.d.ts.map +1 -1
- package/dest/checkpoint_builder.js +124 -46
- package/dest/config.d.ts +1 -1
- package/dest/config.d.ts.map +1 -1
- package/dest/config.js +26 -6
- package/dest/duties/validation_service.d.ts +2 -2
- package/dest/duties/validation_service.d.ts.map +1 -1
- package/dest/duties/validation_service.js +6 -12
- package/dest/factory.d.ts +7 -4
- package/dest/factory.d.ts.map +1 -1
- package/dest/factory.js +6 -5
- package/dest/index.d.ts +2 -3
- package/dest/index.d.ts.map +1 -1
- package/dest/index.js +1 -2
- package/dest/key_store/ha_key_store.js +1 -1
- package/dest/metrics.d.ts +10 -2
- package/dest/metrics.d.ts.map +1 -1
- package/dest/metrics.js +12 -0
- package/dest/proposal_handler.d.ts +94 -0
- package/dest/proposal_handler.d.ts.map +1 -0
- package/dest/{block_proposal_handler.js → proposal_handler.js} +377 -67
- package/dest/validator.d.ts +35 -21
- package/dest/validator.d.ts.map +1 -1
- package/dest/validator.js +177 -218
- package/package.json +19 -19
- package/src/checkpoint_builder.ts +142 -39
- package/src/config.ts +26 -6
- package/src/duties/validation_service.ts +12 -11
- package/src/factory.ts +9 -3
- package/src/index.ts +1 -2
- package/src/key_store/ha_key_store.ts +1 -1
- package/src/metrics.ts +19 -1
- package/src/proposal_handler.ts +907 -0
- package/src/validator.ts +240 -248
- package/dest/block_proposal_handler.d.ts +0 -63
- package/dest/block_proposal_handler.d.ts.map +0 -1
- package/dest/tx_validator/index.d.ts +0 -3
- package/dest/tx_validator/index.d.ts.map +0 -1
- package/dest/tx_validator/index.js +0 -2
- package/dest/tx_validator/nullifier_cache.d.ts +0 -14
- package/dest/tx_validator/nullifier_cache.d.ts.map +0 -1
- package/dest/tx_validator/nullifier_cache.js +0 -24
- package/dest/tx_validator/tx_validator_factory.d.ts +0 -19
- package/dest/tx_validator/tx_validator_factory.d.ts.map +0 -1
- package/dest/tx_validator/tx_validator_factory.js +0 -54
- package/src/block_proposal_handler.ts +0 -555
- package/src/tx_validator/index.ts +0 -2
- package/src/tx_validator/nullifier_cache.ts +0 -30
- package/src/tx_validator/tx_validator_factory.ts +0 -154
|
@@ -63,19 +63,24 @@ function _ts_dispose_resources(env) {
|
|
|
63
63
|
return next();
|
|
64
64
|
})(env);
|
|
65
65
|
}
|
|
66
|
+
import { encodeCheckpointBlobDataFromBlocks, getBlobsPerL1Block } from '@aztec/blob-lib';
|
|
66
67
|
import { INITIAL_L2_BLOCK_NUM } from '@aztec/constants';
|
|
68
|
+
import { validateFeeAssetPriceModifier } from '@aztec/ethereum/contracts';
|
|
67
69
|
import { BlockNumber, CheckpointNumber, SlotNumber } from '@aztec/foundation/branded-types';
|
|
68
|
-
import {
|
|
70
|
+
import { pick } from '@aztec/foundation/collection';
|
|
69
71
|
import { Fr } from '@aztec/foundation/curves/bn254';
|
|
70
72
|
import { TimeoutError } from '@aztec/foundation/error';
|
|
71
73
|
import { createLogger } from '@aztec/foundation/log';
|
|
72
74
|
import { retryUntil } from '@aztec/foundation/retry';
|
|
73
75
|
import { DateProvider, Timer } from '@aztec/foundation/timer';
|
|
76
|
+
import { validateCheckpoint } from '@aztec/stdlib/checkpoint';
|
|
74
77
|
import { getEpochAtSlot, getTimestampForSlot } from '@aztec/stdlib/epoch-helpers';
|
|
75
|
-
import {
|
|
76
|
-
import {
|
|
78
|
+
import { Gas } from '@aztec/stdlib/gas';
|
|
79
|
+
import { accumulateCheckpointOutHashes, computeInHashFromL1ToL2Messages } from '@aztec/stdlib/messaging';
|
|
80
|
+
import { MerkleTreeId } from '@aztec/stdlib/trees';
|
|
81
|
+
import { ReExFailedTxsError, ReExInitialStateMismatchError, ReExStateMismatchError, ReExTimeoutError, TransactionsNotAvailableError } from '@aztec/stdlib/validators';
|
|
77
82
|
import { getTelemetryClient } from '@aztec/telemetry-client';
|
|
78
|
-
export class
|
|
83
|
+
/** Handles block and checkpoint proposals for both validator and non-validator nodes. */ export class ProposalHandler {
|
|
79
84
|
checkpointsBuilder;
|
|
80
85
|
worldState;
|
|
81
86
|
blockSource;
|
|
@@ -84,11 +89,12 @@ export class BlockProposalHandler {
|
|
|
84
89
|
blockProposalValidator;
|
|
85
90
|
epochCache;
|
|
86
91
|
config;
|
|
92
|
+
blobClient;
|
|
87
93
|
metrics;
|
|
88
94
|
dateProvider;
|
|
89
95
|
log;
|
|
90
96
|
tracer;
|
|
91
|
-
constructor(checkpointsBuilder, worldState, blockSource, l1ToL2MessageSource, txProvider, blockProposalValidator, epochCache, config, metrics, dateProvider = new DateProvider(), telemetry = getTelemetryClient(), log = createLogger('validator:
|
|
97
|
+
constructor(checkpointsBuilder, worldState, blockSource, l1ToL2MessageSource, txProvider, blockProposalValidator, epochCache, config, blobClient, metrics, dateProvider = new DateProvider(), telemetry = getTelemetryClient(), log = createLogger('validator:proposal-handler')){
|
|
92
98
|
this.checkpointsBuilder = checkpointsBuilder;
|
|
93
99
|
this.worldState = worldState;
|
|
94
100
|
this.blockSource = blockSource;
|
|
@@ -97,31 +103,39 @@ export class BlockProposalHandler {
|
|
|
97
103
|
this.blockProposalValidator = blockProposalValidator;
|
|
98
104
|
this.epochCache = epochCache;
|
|
99
105
|
this.config = config;
|
|
106
|
+
this.blobClient = blobClient;
|
|
100
107
|
this.metrics = metrics;
|
|
101
108
|
this.dateProvider = dateProvider;
|
|
102
109
|
this.log = log;
|
|
103
110
|
if (config.fishermanMode) {
|
|
104
111
|
this.log = this.log.createChild('[FISHERMAN]');
|
|
105
112
|
}
|
|
106
|
-
this.tracer = telemetry.getTracer('
|
|
113
|
+
this.tracer = telemetry.getTracer('ProposalHandler');
|
|
107
114
|
}
|
|
108
|
-
|
|
109
|
-
|
|
115
|
+
/**
|
|
116
|
+
* Registers non-validator handlers for block and checkpoint proposals on the p2p client.
|
|
117
|
+
* Block proposals are always registered. Checkpoint proposals are registered if the blob client can upload.
|
|
118
|
+
*/ register(p2pClient, shouldReexecute) {
|
|
119
|
+
// Non-validator handler that processes or re-executes for monitoring but does not attest.
|
|
110
120
|
// Returns boolean indicating whether the proposal was valid.
|
|
111
|
-
const
|
|
121
|
+
const blockHandler = async (proposal, proposalSender)=>{
|
|
112
122
|
try {
|
|
113
|
-
const
|
|
123
|
+
const { slotNumber, blockNumber } = proposal;
|
|
124
|
+
const result = await this.handleBlockProposal(proposal, proposalSender, shouldReexecute);
|
|
114
125
|
if (result.isValid) {
|
|
115
|
-
this.log.info(`Non-validator
|
|
126
|
+
this.log.info(`Non-validator block proposal ${blockNumber} at slot ${slotNumber} handled`, {
|
|
116
127
|
blockNumber: result.blockNumber,
|
|
128
|
+
slotNumber,
|
|
117
129
|
reexecutionTimeMs: result.reexecutionResult?.reexecutionTimeMs,
|
|
118
130
|
totalManaUsed: result.reexecutionResult?.totalManaUsed,
|
|
119
|
-
numTxs: result.reexecutionResult?.block?.body?.txEffects?.length ?? 0
|
|
131
|
+
numTxs: result.reexecutionResult?.block?.body?.txEffects?.length ?? 0,
|
|
132
|
+
reexecuted: shouldReexecute
|
|
120
133
|
});
|
|
121
134
|
return true;
|
|
122
135
|
} else {
|
|
123
|
-
this.log.warn(`Non-validator
|
|
136
|
+
this.log.warn(`Non-validator block proposal ${blockNumber} at slot ${slotNumber} failed processing with ${result.reason}`, {
|
|
124
137
|
blockNumber: result.blockNumber,
|
|
138
|
+
slotNumber,
|
|
125
139
|
reason: result.reason
|
|
126
140
|
});
|
|
127
141
|
return false;
|
|
@@ -131,7 +145,30 @@ export class BlockProposalHandler {
|
|
|
131
145
|
return false;
|
|
132
146
|
}
|
|
133
147
|
};
|
|
134
|
-
p2pClient.registerBlockProposalHandler(
|
|
148
|
+
p2pClient.registerBlockProposalHandler(blockHandler);
|
|
149
|
+
// Register checkpoint proposal handler if blob uploads are enabled and we are reexecuting
|
|
150
|
+
if (this.blobClient.canUpload() && shouldReexecute) {
|
|
151
|
+
const checkpointHandler = async (checkpoint, _sender)=>{
|
|
152
|
+
try {
|
|
153
|
+
const proposalInfo = {
|
|
154
|
+
proposalSlotNumber: checkpoint.slotNumber,
|
|
155
|
+
archive: checkpoint.archive.toString(),
|
|
156
|
+
proposer: checkpoint.getSender()?.toString()
|
|
157
|
+
};
|
|
158
|
+
const result = await this.handleCheckpointProposal(checkpoint, proposalInfo);
|
|
159
|
+
if (result.isValid) {
|
|
160
|
+
this.log.info(`Non-validator checkpoint proposal at slot ${checkpoint.slotNumber} handled`, proposalInfo);
|
|
161
|
+
} else {
|
|
162
|
+
this.log.warn(`Non-validator checkpoint proposal at slot ${checkpoint.slotNumber} failed: ${result.reason}`, proposalInfo);
|
|
163
|
+
}
|
|
164
|
+
} catch (error) {
|
|
165
|
+
this.log.error('Error processing checkpoint proposal in non-validator handler', error);
|
|
166
|
+
}
|
|
167
|
+
// Non-validators don't attest
|
|
168
|
+
return undefined;
|
|
169
|
+
};
|
|
170
|
+
p2pClient.registerCheckpointProposalHandler(checkpointHandler);
|
|
171
|
+
}
|
|
135
172
|
return this;
|
|
136
173
|
}
|
|
137
174
|
async handleBlockProposal(proposal, proposalSender, shouldReexecute) {
|
|
@@ -148,7 +185,9 @@ export class BlockProposalHandler {
|
|
|
148
185
|
}
|
|
149
186
|
const proposalInfo = {
|
|
150
187
|
...proposal.toBlockInfo(),
|
|
151
|
-
proposer: proposer.toString()
|
|
188
|
+
proposer: proposer.toString(),
|
|
189
|
+
blockNumber: undefined,
|
|
190
|
+
checkpointNumber: undefined
|
|
152
191
|
};
|
|
153
192
|
this.log.info(`Processing proposal for slot ${slotNumber}`, {
|
|
154
193
|
...proposalInfo,
|
|
@@ -164,9 +203,28 @@ export class BlockProposalHandler {
|
|
|
164
203
|
reason: 'invalid_proposal'
|
|
165
204
|
};
|
|
166
205
|
}
|
|
167
|
-
//
|
|
168
|
-
|
|
169
|
-
|
|
206
|
+
// Ensure the block source is synced before checking for existing blocks,
|
|
207
|
+
// since a pending checkpoint prune may remove blocks we'd otherwise find.
|
|
208
|
+
// This affects mostly the block_number_already_exists check, since a pending
|
|
209
|
+
// checkpoint prune could remove a block that would conflict with this proposal.
|
|
210
|
+
// When pipelining is enabled, the proposer builds ahead of L1 submission, so the
|
|
211
|
+
// block source won't have synced to the proposed slot yet. Skip the sync wait to
|
|
212
|
+
// avoid eating into the attestation window.
|
|
213
|
+
if (!this.epochCache.isProposerPipeliningEnabled()) {
|
|
214
|
+
const blockSourceSync = await this.waitForBlockSourceSync(slotNumber);
|
|
215
|
+
if (!blockSourceSync) {
|
|
216
|
+
this.log.warn(`Block source is not synced, skipping processing`, proposalInfo);
|
|
217
|
+
return {
|
|
218
|
+
isValid: false,
|
|
219
|
+
reason: 'block_source_not_synced'
|
|
220
|
+
};
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
// Check that the parent proposal is a block we know, otherwise reexecution would fail.
|
|
224
|
+
// If we don't find it immediately, we keep retrying for a while; it may be we still
|
|
225
|
+
// need to process other block proposals to get to it.
|
|
226
|
+
const parentBlock = await this.getParentBlock(proposal);
|
|
227
|
+
if (parentBlock === undefined) {
|
|
170
228
|
this.log.warn(`Parent block for proposal not found, skipping processing`, proposalInfo);
|
|
171
229
|
return {
|
|
172
230
|
isValid: false,
|
|
@@ -174,9 +232,9 @@ export class BlockProposalHandler {
|
|
|
174
232
|
};
|
|
175
233
|
}
|
|
176
234
|
// Check that the parent block's slot is not greater than the proposal's slot.
|
|
177
|
-
if (
|
|
235
|
+
if (parentBlock !== 'genesis' && parentBlock.header.getSlot() > slotNumber) {
|
|
178
236
|
this.log.warn(`Parent block slot is greater than proposal slot, skipping processing`, {
|
|
179
|
-
parentBlockSlot:
|
|
237
|
+
parentBlockSlot: parentBlock.header.getSlot().toString(),
|
|
180
238
|
proposalSlot: slotNumber.toString(),
|
|
181
239
|
...proposalInfo
|
|
182
240
|
});
|
|
@@ -186,7 +244,8 @@ export class BlockProposalHandler {
|
|
|
186
244
|
};
|
|
187
245
|
}
|
|
188
246
|
// Compute the block number based on the parent block
|
|
189
|
-
const blockNumber =
|
|
247
|
+
const blockNumber = parentBlock === 'genesis' ? BlockNumber(INITIAL_L2_BLOCK_NUM) : BlockNumber(parentBlock.header.getBlockNumber() + 1);
|
|
248
|
+
proposalInfo.blockNumber = blockNumber;
|
|
190
249
|
// Check that this block number does not exist already
|
|
191
250
|
const existingBlock = await this.blockSource.getBlockHeader(blockNumber);
|
|
192
251
|
if (existingBlock) {
|
|
@@ -203,8 +262,16 @@ export class BlockProposalHandler {
|
|
|
203
262
|
pinnedPeer: proposalSender,
|
|
204
263
|
deadline: this.getReexecutionDeadline(slotNumber, config)
|
|
205
264
|
});
|
|
265
|
+
// If reexecution is disabled, bail. We were just interested in triggering tx collection.
|
|
266
|
+
if (!shouldReexecute) {
|
|
267
|
+
this.log.info(`Received valid block ${blockNumber} proposal at index ${proposal.indexWithinCheckpoint} on slot ${slotNumber}`, proposalInfo);
|
|
268
|
+
return {
|
|
269
|
+
isValid: true,
|
|
270
|
+
blockNumber
|
|
271
|
+
};
|
|
272
|
+
}
|
|
206
273
|
// Compute the checkpoint number for this block and validate checkpoint consistency
|
|
207
|
-
const checkpointResult =
|
|
274
|
+
const checkpointResult = this.computeCheckpointNumber(proposal, parentBlock, proposalInfo);
|
|
208
275
|
if (checkpointResult.reason) {
|
|
209
276
|
return {
|
|
210
277
|
isValid: false,
|
|
@@ -213,6 +280,7 @@ export class BlockProposalHandler {
|
|
|
213
280
|
};
|
|
214
281
|
}
|
|
215
282
|
const checkpointNumber = checkpointResult.checkpointNumber;
|
|
283
|
+
proposalInfo.checkpointNumber = checkpointNumber;
|
|
216
284
|
// Check that I have the same set of l1ToL2Messages as the proposal
|
|
217
285
|
const l1ToL2Messages = await this.l1ToL2MessageSource.getL1ToL2Messages(checkpointNumber);
|
|
218
286
|
const computedInHash = computeInHashFromL1ToL2Messages(l1ToL2Messages);
|
|
@@ -241,35 +309,32 @@ export class BlockProposalHandler {
|
|
|
241
309
|
reason: 'txs_not_available'
|
|
242
310
|
};
|
|
243
311
|
}
|
|
312
|
+
// Collect the out hashes of all the checkpoints before this one in the same epoch
|
|
313
|
+
const epoch = getEpochAtSlot(slotNumber, this.epochCache.getL1Constants());
|
|
314
|
+
const previousCheckpointOutHashes = (await this.blockSource.getCheckpointsDataForEpoch(epoch)).filter((c)=>c.checkpointNumber < checkpointNumber).map((c)=>c.checkpointOutHash);
|
|
244
315
|
// Try re-executing the transactions in the proposal if needed
|
|
245
316
|
let reexecutionResult;
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
const
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
reexecutionResult
|
|
257
|
-
}
|
|
258
|
-
this.log.error(`Error reexecuting txs while processing block proposal`, error, proposalInfo);
|
|
259
|
-
const reason = this.getReexecuteFailureReason(error);
|
|
260
|
-
return {
|
|
261
|
-
isValid: false,
|
|
262
|
-
blockNumber,
|
|
263
|
-
reason,
|
|
264
|
-
reexecutionResult
|
|
265
|
-
};
|
|
266
|
-
}
|
|
317
|
+
try {
|
|
318
|
+
this.log.verbose(`Re-executing transactions in the proposal`, proposalInfo);
|
|
319
|
+
reexecutionResult = await this.reexecuteTransactions(proposal, blockNumber, checkpointNumber, txs, l1ToL2Messages, previousCheckpointOutHashes);
|
|
320
|
+
} catch (error) {
|
|
321
|
+
this.log.error(`Error reexecuting txs while processing block proposal`, error, proposalInfo);
|
|
322
|
+
const reason = this.getReexecuteFailureReason(error);
|
|
323
|
+
return {
|
|
324
|
+
isValid: false,
|
|
325
|
+
blockNumber,
|
|
326
|
+
reason,
|
|
327
|
+
reexecutionResult
|
|
328
|
+
};
|
|
267
329
|
}
|
|
268
330
|
// If we succeeded, push this block into the archiver (unless disabled)
|
|
269
331
|
if (reexecutionResult?.block && this.config.skipPushProposedBlocksToArchiver === false) {
|
|
270
332
|
await this.blockSource.addBlock(reexecutionResult?.block);
|
|
271
333
|
}
|
|
272
|
-
this.log.info(`Successfully
|
|
334
|
+
this.log.info(`Successfully re-executed block ${blockNumber} proposal at index ${proposal.indexWithinCheckpoint} on slot ${slotNumber}`, {
|
|
335
|
+
...proposalInfo,
|
|
336
|
+
...pick(reexecutionResult, 'reexecutionTimeMs', 'totalManaUsed')
|
|
337
|
+
});
|
|
273
338
|
return {
|
|
274
339
|
isValid: true,
|
|
275
340
|
blockNumber,
|
|
@@ -288,7 +353,7 @@ export class BlockProposalHandler {
|
|
|
288
353
|
const currentTime = this.dateProvider.now();
|
|
289
354
|
const timeoutDurationMs = deadline.getTime() - currentTime;
|
|
290
355
|
try {
|
|
291
|
-
return await this.blockSource.
|
|
356
|
+
return await this.blockSource.getBlockDataByArchive(parentArchive) ?? (timeoutDurationMs <= 0 ? undefined : await retryUntil(()=>this.blockSource.syncImmediate().then(()=>this.blockSource.getBlockDataByArchive(parentArchive)), 'force archiver sync', timeoutDurationMs / 1000, 0.5));
|
|
292
357
|
} catch (err) {
|
|
293
358
|
if (err instanceof TimeoutError) {
|
|
294
359
|
this.log.debug(`Timed out getting parent block by archive root`, {
|
|
@@ -302,8 +367,8 @@ export class BlockProposalHandler {
|
|
|
302
367
|
return undefined;
|
|
303
368
|
}
|
|
304
369
|
}
|
|
305
|
-
|
|
306
|
-
if (
|
|
370
|
+
computeCheckpointNumber(proposal, parentBlock, proposalInfo) {
|
|
371
|
+
if (parentBlock === 'genesis') {
|
|
307
372
|
// First block is in checkpoint 1
|
|
308
373
|
if (proposal.indexWithinCheckpoint !== 0) {
|
|
309
374
|
this.log.warn(`First block proposal has non-zero indexWithinCheckpoint`, proposalInfo);
|
|
@@ -315,20 +380,9 @@ export class BlockProposalHandler {
|
|
|
315
380
|
checkpointNumber: CheckpointNumber.INITIAL
|
|
316
381
|
};
|
|
317
382
|
}
|
|
318
|
-
// Get the parent block to find its checkpoint number
|
|
319
|
-
// TODO(palla/mbps): The block header should include the checkpoint number to avoid this lookup,
|
|
320
|
-
// or at least the L2BlockSource should return a different struct that includes it.
|
|
321
|
-
const parentBlockNumber = parentBlockHeader.getBlockNumber();
|
|
322
|
-
const parentBlock = await this.blockSource.getL2Block(parentBlockNumber);
|
|
323
|
-
if (!parentBlock) {
|
|
324
|
-
this.log.warn(`Parent block ${parentBlockNumber} not found in archiver`, proposalInfo);
|
|
325
|
-
return {
|
|
326
|
-
reason: 'invalid_proposal'
|
|
327
|
-
};
|
|
328
|
-
}
|
|
329
383
|
if (proposal.indexWithinCheckpoint === 0) {
|
|
330
384
|
// If this is the first block in a new checkpoint, increment the checkpoint number
|
|
331
|
-
if (!(proposal.blockHeader.getSlot() >
|
|
385
|
+
if (!(proposal.blockHeader.getSlot() > parentBlock.header.getSlot())) {
|
|
332
386
|
this.log.warn(`Slot should be greater than parent block slot for first block in checkpoint`, proposalInfo);
|
|
333
387
|
return {
|
|
334
388
|
reason: 'invalid_proposal'
|
|
@@ -345,7 +399,7 @@ export class BlockProposalHandler {
|
|
|
345
399
|
reason: 'invalid_proposal'
|
|
346
400
|
};
|
|
347
401
|
}
|
|
348
|
-
if (proposal.blockHeader.getSlot() !==
|
|
402
|
+
if (proposal.blockHeader.getSlot() !== parentBlock.header.getSlot()) {
|
|
349
403
|
this.log.warn(`Slot should be equal to parent block slot for non-first block in checkpoint`, proposalInfo);
|
|
350
404
|
return {
|
|
351
405
|
reason: 'invalid_proposal'
|
|
@@ -445,8 +499,39 @@ export class BlockProposalHandler {
|
|
|
445
499
|
const nextSlotTimestampSeconds = Number(getTimestampForSlot(SlotNumber(slot + 1), config));
|
|
446
500
|
return new Date(nextSlotTimestampSeconds * 1000);
|
|
447
501
|
}
|
|
502
|
+
/** Waits for the block source to sync L1 data up to at least the slot before the given one. */ async waitForBlockSourceSync(slot) {
|
|
503
|
+
const deadline = this.getReexecutionDeadline(slot, this.checkpointsBuilder.getConfig());
|
|
504
|
+
const timeoutMs = deadline.getTime() - this.dateProvider.now();
|
|
505
|
+
if (slot === 0) {
|
|
506
|
+
return true;
|
|
507
|
+
}
|
|
508
|
+
// Make a quick check before triggering an archiver sync
|
|
509
|
+
const syncedSlot = await this.blockSource.getSyncedL2SlotNumber();
|
|
510
|
+
if (syncedSlot !== undefined && syncedSlot + 1 >= slot) {
|
|
511
|
+
return true;
|
|
512
|
+
}
|
|
513
|
+
try {
|
|
514
|
+
// Trigger an immediate sync of the block source, and wait until it reports being synced to the required slot
|
|
515
|
+
return await retryUntil(async ()=>{
|
|
516
|
+
await this.blockSource.syncImmediate();
|
|
517
|
+
const syncedSlot = await this.blockSource.getSyncedL2SlotNumber();
|
|
518
|
+
return syncedSlot !== undefined && syncedSlot + 1 >= slot;
|
|
519
|
+
}, 'wait for block source sync', timeoutMs / 1000, 0.5);
|
|
520
|
+
} catch (err) {
|
|
521
|
+
if (err instanceof TimeoutError) {
|
|
522
|
+
this.log.warn(`Timed out waiting for block source to sync to slot ${slot}`);
|
|
523
|
+
return false;
|
|
524
|
+
} else {
|
|
525
|
+
throw err;
|
|
526
|
+
}
|
|
527
|
+
}
|
|
528
|
+
}
|
|
448
529
|
getReexecuteFailureReason(err) {
|
|
449
|
-
if (err instanceof
|
|
530
|
+
if (err instanceof TransactionsNotAvailableError) {
|
|
531
|
+
return 'txs_not_available';
|
|
532
|
+
} else if (err instanceof ReExInitialStateMismatchError) {
|
|
533
|
+
return 'initial_state_mismatch';
|
|
534
|
+
} else if (err instanceof ReExStateMismatchError) {
|
|
450
535
|
return 'state_mismatch';
|
|
451
536
|
} else if (err instanceof ReExFailedTxsError) {
|
|
452
537
|
return 'failed_txs';
|
|
@@ -479,30 +564,43 @@ export class BlockProposalHandler {
|
|
|
479
564
|
// Fork before the block to be built
|
|
480
565
|
const parentBlockNumber = BlockNumber(blockNumber - 1);
|
|
481
566
|
await this.worldState.syncImmediate(parentBlockNumber);
|
|
482
|
-
const fork = _ts_add_disposable_resource(env, await this.worldState.fork(parentBlockNumber),
|
|
483
|
-
//
|
|
567
|
+
const fork = _ts_add_disposable_resource(env, await this.worldState.fork(parentBlockNumber), true);
|
|
568
|
+
// Verify the fork's archive root matches the proposal's expected last archive.
|
|
569
|
+
// If they don't match, our world state synced to a different chain and reexecution would fail.
|
|
570
|
+
const forkArchiveRoot = new Fr((await fork.getTreeInfo(MerkleTreeId.ARCHIVE)).root);
|
|
571
|
+
if (!forkArchiveRoot.equals(proposal.blockHeader.lastArchive.root)) {
|
|
572
|
+
throw new ReExInitialStateMismatchError(proposal.blockHeader.lastArchive.root, forkArchiveRoot);
|
|
573
|
+
}
|
|
574
|
+
// Build checkpoint constants from proposal (excludes blockNumber which is per-block)
|
|
484
575
|
const constants = {
|
|
485
576
|
chainId: new Fr(config.l1ChainId),
|
|
486
577
|
version: new Fr(config.rollupVersion),
|
|
487
578
|
slotNumber: slot,
|
|
579
|
+
timestamp: blockHeader.globalVariables.timestamp,
|
|
488
580
|
coinbase: blockHeader.globalVariables.coinbase,
|
|
489
581
|
feeRecipient: blockHeader.globalVariables.feeRecipient,
|
|
490
582
|
gasFees: blockHeader.globalVariables.gasFees
|
|
491
583
|
};
|
|
492
584
|
// Create checkpoint builder with prior blocks
|
|
493
|
-
const checkpointBuilder = await this.checkpointsBuilder.openCheckpoint(checkpointNumber, constants, l1ToL2Messages, previousCheckpointOutHashes, fork, priorBlocks, this.log.getBindings());
|
|
585
|
+
const checkpointBuilder = await this.checkpointsBuilder.openCheckpoint(checkpointNumber, constants, 0n, l1ToL2Messages, previousCheckpointOutHashes, fork, priorBlocks, this.log.getBindings());
|
|
494
586
|
// Build the new block
|
|
495
587
|
const deadline = this.getReexecutionDeadline(slot, config);
|
|
588
|
+
const maxBlockGas = this.config.validateMaxL2BlockGas !== undefined || this.config.validateMaxDABlockGas !== undefined ? new Gas(this.config.validateMaxDABlockGas ?? Infinity, this.config.validateMaxL2BlockGas ?? Infinity) : undefined;
|
|
496
589
|
const result = await checkpointBuilder.buildBlock(txs, blockNumber, blockHeader.globalVariables.timestamp, {
|
|
590
|
+
isBuildingProposal: false,
|
|
591
|
+
minValidTxs: 0,
|
|
497
592
|
deadline,
|
|
498
|
-
expectedEndState: blockHeader.state
|
|
593
|
+
expectedEndState: blockHeader.state,
|
|
594
|
+
maxTransactions: this.config.validateMaxTxsPerBlock,
|
|
595
|
+
maxBlockGas
|
|
499
596
|
});
|
|
500
597
|
const { block, failedTxs } = result;
|
|
501
598
|
const numFailedTxs = failedTxs.length;
|
|
502
|
-
this.log.verbose(`
|
|
599
|
+
this.log.verbose(`Block proposal ${blockNumber} at slot ${slot} transaction re-execution complete`, {
|
|
503
600
|
numFailedTxs,
|
|
504
601
|
numProposalTxs: txHashes.length,
|
|
505
602
|
numProcessedTxs: block.body.txEffects.length,
|
|
603
|
+
blockNumber,
|
|
506
604
|
slot
|
|
507
605
|
});
|
|
508
606
|
if (numFailedTxs > 0) {
|
|
@@ -540,7 +638,219 @@ export class BlockProposalHandler {
|
|
|
540
638
|
env.error = e;
|
|
541
639
|
env.hasError = true;
|
|
542
640
|
} finally{
|
|
543
|
-
_ts_dispose_resources(env);
|
|
641
|
+
const result = _ts_dispose_resources(env);
|
|
642
|
+
if (result) await result;
|
|
643
|
+
}
|
|
644
|
+
}
|
|
645
|
+
/**
|
|
646
|
+
* Validates a checkpoint proposal and uploads blobs if configured.
|
|
647
|
+
* Used by both non-validator nodes (via register) and the validator client (via delegation).
|
|
648
|
+
*/ async handleCheckpointProposal(proposal, proposalInfo) {
|
|
649
|
+
const proposer = proposal.getSender();
|
|
650
|
+
if (!proposer) {
|
|
651
|
+
this.log.warn(`Received checkpoint proposal with invalid signature for slot ${proposal.slotNumber}`);
|
|
652
|
+
return {
|
|
653
|
+
isValid: false,
|
|
654
|
+
reason: 'invalid_signature'
|
|
655
|
+
};
|
|
656
|
+
}
|
|
657
|
+
if (!validateFeeAssetPriceModifier(proposal.feeAssetPriceModifier)) {
|
|
658
|
+
this.log.warn(`Received checkpoint proposal with invalid feeAssetPriceModifier ${proposal.feeAssetPriceModifier} for slot ${proposal.slotNumber}`);
|
|
659
|
+
return {
|
|
660
|
+
isValid: false,
|
|
661
|
+
reason: 'invalid_fee_asset_price_modifier'
|
|
662
|
+
};
|
|
663
|
+
}
|
|
664
|
+
const result = await this.validateCheckpointProposal(proposal, proposalInfo);
|
|
665
|
+
// Upload blobs to filestore if validation passed (fire and forget)
|
|
666
|
+
if (result.isValid) {
|
|
667
|
+
this.tryUploadBlobsForCheckpoint(proposal, proposalInfo);
|
|
668
|
+
}
|
|
669
|
+
return result;
|
|
670
|
+
}
|
|
671
|
+
/**
|
|
672
|
+
* Validates a checkpoint proposal by building the full checkpoint and comparing it with the proposal.
|
|
673
|
+
* @returns Validation result with isValid flag and reason if invalid.
|
|
674
|
+
*/ async validateCheckpointProposal(proposal, proposalInfo) {
|
|
675
|
+
const slot = proposal.slotNumber;
|
|
676
|
+
// Timeout block syncing at the start of the next slot
|
|
677
|
+
const config = this.checkpointsBuilder.getConfig();
|
|
678
|
+
const nextSlotTimestampSeconds = Number(getTimestampForSlot(SlotNumber(slot + 1), config));
|
|
679
|
+
const timeoutSeconds = Math.max(1, nextSlotTimestampSeconds - Math.floor(this.dateProvider.now() / 1000));
|
|
680
|
+
// Wait for last block to sync by archive
|
|
681
|
+
let lastBlockHeader;
|
|
682
|
+
try {
|
|
683
|
+
lastBlockHeader = await retryUntil(async ()=>{
|
|
684
|
+
await this.blockSource.syncImmediate();
|
|
685
|
+
return this.blockSource.getBlockHeaderByArchive(proposal.archive);
|
|
686
|
+
}, `waiting for block with archive ${proposal.archive.toString()} for slot ${slot}`, timeoutSeconds, 0.5);
|
|
687
|
+
} catch (err) {
|
|
688
|
+
if (err instanceof TimeoutError) {
|
|
689
|
+
this.log.warn(`Timed out waiting for block with archive matching checkpoint proposal`, proposalInfo);
|
|
690
|
+
return {
|
|
691
|
+
isValid: false,
|
|
692
|
+
reason: 'last_block_not_found'
|
|
693
|
+
};
|
|
694
|
+
}
|
|
695
|
+
this.log.error(`Error fetching last block for checkpoint proposal`, err, proposalInfo);
|
|
696
|
+
return {
|
|
697
|
+
isValid: false,
|
|
698
|
+
reason: 'block_fetch_error'
|
|
699
|
+
};
|
|
700
|
+
}
|
|
701
|
+
if (!lastBlockHeader) {
|
|
702
|
+
this.log.warn(`Last block not found for checkpoint proposal`, proposalInfo);
|
|
703
|
+
return {
|
|
704
|
+
isValid: false,
|
|
705
|
+
reason: 'last_block_not_found'
|
|
706
|
+
};
|
|
707
|
+
}
|
|
708
|
+
// Get all full blocks for the slot and checkpoint
|
|
709
|
+
const blocks = await this.blockSource.getBlocksForSlot(slot);
|
|
710
|
+
if (blocks.length === 0) {
|
|
711
|
+
this.log.warn(`No blocks found for slot ${slot}`, proposalInfo);
|
|
712
|
+
return {
|
|
713
|
+
isValid: false,
|
|
714
|
+
reason: 'no_blocks_for_slot'
|
|
715
|
+
};
|
|
716
|
+
}
|
|
717
|
+
// Ensure the last block for this slot matches the archive in the checkpoint proposal
|
|
718
|
+
if (!blocks.at(-1)?.archive.root.equals(proposal.archive)) {
|
|
719
|
+
this.log.warn(`Last block archive mismatch for checkpoint proposal`, proposalInfo);
|
|
720
|
+
return {
|
|
721
|
+
isValid: false,
|
|
722
|
+
reason: 'last_block_archive_mismatch'
|
|
723
|
+
};
|
|
724
|
+
}
|
|
725
|
+
this.log.debug(`Found ${blocks.length} blocks for slot ${slot}`, {
|
|
726
|
+
...proposalInfo,
|
|
727
|
+
blockNumbers: blocks.map((b)=>b.number)
|
|
728
|
+
});
|
|
729
|
+
// Get checkpoint constants from first block
|
|
730
|
+
const firstBlock = blocks[0];
|
|
731
|
+
const constants = this.extractCheckpointConstants(firstBlock);
|
|
732
|
+
const checkpointNumber = firstBlock.checkpointNumber;
|
|
733
|
+
// Get L1-to-L2 messages for this checkpoint
|
|
734
|
+
const l1ToL2Messages = await this.l1ToL2MessageSource.getL1ToL2Messages(checkpointNumber);
|
|
735
|
+
// Collect the out hashes of all the checkpoints before this one in the same epoch
|
|
736
|
+
const epoch = getEpochAtSlot(slot, this.epochCache.getL1Constants());
|
|
737
|
+
const previousCheckpointOutHashes = (await this.blockSource.getCheckpointsDataForEpoch(epoch)).filter((c)=>c.checkpointNumber < checkpointNumber).map((c)=>c.checkpointOutHash);
|
|
738
|
+
// Fork world state at the block before the first block
|
|
739
|
+
const parentBlockNumber = BlockNumber(firstBlock.number - 1);
|
|
740
|
+
const fork = await this.worldState.fork(parentBlockNumber);
|
|
741
|
+
try {
|
|
742
|
+
// Create checkpoint builder with all existing blocks
|
|
743
|
+
const checkpointBuilder = await this.checkpointsBuilder.openCheckpoint(checkpointNumber, constants, proposal.feeAssetPriceModifier, l1ToL2Messages, previousCheckpointOutHashes, fork, blocks, this.log.getBindings());
|
|
744
|
+
// Complete the checkpoint to get computed values
|
|
745
|
+
const computedCheckpoint = await checkpointBuilder.completeCheckpoint();
|
|
746
|
+
// Compare checkpoint header with proposal
|
|
747
|
+
if (!computedCheckpoint.header.equals(proposal.checkpointHeader)) {
|
|
748
|
+
this.log.warn(`Checkpoint header mismatch`, {
|
|
749
|
+
...proposalInfo,
|
|
750
|
+
computed: computedCheckpoint.header.toInspect(),
|
|
751
|
+
proposal: proposal.checkpointHeader.toInspect()
|
|
752
|
+
});
|
|
753
|
+
return {
|
|
754
|
+
isValid: false,
|
|
755
|
+
reason: 'checkpoint_header_mismatch'
|
|
756
|
+
};
|
|
757
|
+
}
|
|
758
|
+
// Compare archive root with proposal
|
|
759
|
+
if (!computedCheckpoint.archive.root.equals(proposal.archive)) {
|
|
760
|
+
this.log.warn(`Archive root mismatch`, {
|
|
761
|
+
...proposalInfo,
|
|
762
|
+
computed: computedCheckpoint.archive.root.toString(),
|
|
763
|
+
proposal: proposal.archive.toString()
|
|
764
|
+
});
|
|
765
|
+
return {
|
|
766
|
+
isValid: false,
|
|
767
|
+
reason: 'archive_mismatch'
|
|
768
|
+
};
|
|
769
|
+
}
|
|
770
|
+
// Check that the accumulated epoch out hash matches the value in the proposal.
|
|
771
|
+
// The epoch out hash is the accumulated hash of all checkpoint out hashes in the epoch.
|
|
772
|
+
const checkpointOutHash = computedCheckpoint.getCheckpointOutHash();
|
|
773
|
+
const computedEpochOutHash = accumulateCheckpointOutHashes([
|
|
774
|
+
...previousCheckpointOutHashes,
|
|
775
|
+
checkpointOutHash
|
|
776
|
+
]);
|
|
777
|
+
const proposalEpochOutHash = proposal.checkpointHeader.epochOutHash;
|
|
778
|
+
if (!computedEpochOutHash.equals(proposalEpochOutHash)) {
|
|
779
|
+
this.log.warn(`Epoch out hash mismatch`, {
|
|
780
|
+
proposalEpochOutHash: proposalEpochOutHash.toString(),
|
|
781
|
+
computedEpochOutHash: computedEpochOutHash.toString(),
|
|
782
|
+
checkpointOutHash: checkpointOutHash.toString(),
|
|
783
|
+
previousCheckpointOutHashes: previousCheckpointOutHashes.map((h)=>h.toString()),
|
|
784
|
+
...proposalInfo
|
|
785
|
+
});
|
|
786
|
+
return {
|
|
787
|
+
isValid: false,
|
|
788
|
+
reason: 'out_hash_mismatch'
|
|
789
|
+
};
|
|
790
|
+
}
|
|
791
|
+
// Final round of validations on the checkpoint, just in case.
|
|
792
|
+
try {
|
|
793
|
+
validateCheckpoint(computedCheckpoint, {
|
|
794
|
+
rollupManaLimit: this.checkpointsBuilder.getConfig().rollupManaLimit,
|
|
795
|
+
maxDABlockGas: this.config.validateMaxDABlockGas,
|
|
796
|
+
maxL2BlockGas: this.config.validateMaxL2BlockGas,
|
|
797
|
+
maxTxsPerBlock: this.config.validateMaxTxsPerBlock,
|
|
798
|
+
maxTxsPerCheckpoint: this.config.validateMaxTxsPerCheckpoint
|
|
799
|
+
});
|
|
800
|
+
} catch (err) {
|
|
801
|
+
this.log.warn(`Checkpoint validation failed: ${err}`, proposalInfo);
|
|
802
|
+
return {
|
|
803
|
+
isValid: false,
|
|
804
|
+
reason: 'checkpoint_validation_failed'
|
|
805
|
+
};
|
|
806
|
+
}
|
|
807
|
+
this.log.verbose(`Checkpoint proposal validation successful for slot ${slot}`, proposalInfo);
|
|
808
|
+
return {
|
|
809
|
+
isValid: true
|
|
810
|
+
};
|
|
811
|
+
} finally{
|
|
812
|
+
await fork.close();
|
|
813
|
+
}
|
|
814
|
+
}
|
|
815
|
+
/** Extracts checkpoint global variables from a block. */ extractCheckpointConstants(block) {
|
|
816
|
+
const gv = block.header.globalVariables;
|
|
817
|
+
return {
|
|
818
|
+
chainId: gv.chainId,
|
|
819
|
+
version: gv.version,
|
|
820
|
+
slotNumber: gv.slotNumber,
|
|
821
|
+
timestamp: gv.timestamp,
|
|
822
|
+
coinbase: gv.coinbase,
|
|
823
|
+
feeRecipient: gv.feeRecipient,
|
|
824
|
+
gasFees: gv.gasFees
|
|
825
|
+
};
|
|
826
|
+
}
|
|
827
|
+
/** Triggers blob upload for a checkpoint if the blob client can upload (fire and forget). */ tryUploadBlobsForCheckpoint(proposal, proposalInfo) {
|
|
828
|
+
if (this.blobClient.canUpload()) {
|
|
829
|
+
void this.uploadBlobsForCheckpoint(proposal, proposalInfo);
|
|
830
|
+
}
|
|
831
|
+
}
|
|
832
|
+
/** Uploads blobs for a checkpoint to the filestore. */ async uploadBlobsForCheckpoint(proposal, proposalInfo) {
|
|
833
|
+
try {
|
|
834
|
+
const lastBlockHeader = await this.blockSource.getBlockHeaderByArchive(proposal.archive);
|
|
835
|
+
if (!lastBlockHeader) {
|
|
836
|
+
this.log.warn(`Failed to get last block header for blob upload`, proposalInfo);
|
|
837
|
+
return;
|
|
838
|
+
}
|
|
839
|
+
const blocks = await this.blockSource.getBlocksForSlot(proposal.slotNumber);
|
|
840
|
+
if (blocks.length === 0) {
|
|
841
|
+
this.log.warn(`No blocks found for blob upload`, proposalInfo);
|
|
842
|
+
return;
|
|
843
|
+
}
|
|
844
|
+
const blockBlobData = blocks.map((b)=>b.toBlockBlobData());
|
|
845
|
+
const blobFields = encodeCheckpointBlobDataFromBlocks(blockBlobData);
|
|
846
|
+
const blobs = await getBlobsPerL1Block(blobFields);
|
|
847
|
+
await this.blobClient.sendBlobsToFilestore(blobs);
|
|
848
|
+
this.log.debug(`Uploaded ${blobs.length} blobs to filestore for checkpoint at slot ${proposal.slotNumber}`, {
|
|
849
|
+
...proposalInfo,
|
|
850
|
+
numBlobs: blobs.length
|
|
851
|
+
});
|
|
852
|
+
} catch (err) {
|
|
853
|
+
this.log.warn(`Failed to upload blobs for checkpoint: ${err}`, proposalInfo);
|
|
544
854
|
}
|
|
545
855
|
}
|
|
546
856
|
}
|