@aztec/validator-client 0.0.1-commit.fffb133c → 0.0.1-dev
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 +62 -18
- package/dest/checkpoint_builder.d.ts +26 -14
- package/dest/checkpoint_builder.d.ts.map +1 -1
- package/dest/checkpoint_builder.js +136 -45
- package/dest/config.d.ts +1 -1
- package/dest/config.d.ts.map +1 -1
- package/dest/config.js +26 -3
- package/dest/duties/validation_service.d.ts +3 -4
- package/dest/duties/validation_service.d.ts.map +1 -1
- package/dest/duties/validation_service.js +11 -22
- 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.d.ts +1 -1
- package/dest/key_store/ha_key_store.d.ts.map +1 -1
- package/dest/key_store/ha_key_store.js +3 -3
- package/dest/metrics.d.ts +12 -3
- package/dest/metrics.d.ts.map +1 -1
- package/dest/metrics.js +46 -5
- 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} +376 -69
- package/dest/validator.d.ts +37 -24
- package/dest/validator.d.ts.map +1 -1
- package/dest/validator.js +167 -215
- package/package.json +19 -17
- package/src/checkpoint_builder.ts +183 -50
- package/src/config.ts +26 -3
- package/src/duties/validation_service.ts +18 -24
- package/src/factory.ts +9 -3
- package/src/index.ts +1 -2
- package/src/key_store/ha_key_store.ts +3 -3
- package/src/metrics.ts +63 -6
- package/src/proposal_handler.ts +903 -0
- package/src/validator.ts +228 -248
- package/dest/block_proposal_handler.d.ts +0 -64
- 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 -18
- 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 -554
- 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 -135
|
@@ -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,19 +203,34 @@ 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
|
+
// TODO(@Maddiaa0): This may break staggered slots.
|
|
211
|
+
const blockSourceSync = await this.waitForBlockSourceSync(slotNumber);
|
|
212
|
+
if (!blockSourceSync) {
|
|
213
|
+
this.log.warn(`Block source is not synced, skipping processing`, proposalInfo);
|
|
214
|
+
return {
|
|
215
|
+
isValid: false,
|
|
216
|
+
reason: 'block_source_not_synced'
|
|
217
|
+
};
|
|
218
|
+
}
|
|
219
|
+
// Check that the parent proposal is a block we know, otherwise reexecution would fail.
|
|
220
|
+
// If we don't find it immediately, we keep retrying for a while; it may be we still
|
|
221
|
+
// need to process other block proposals to get to it.
|
|
222
|
+
const parentBlock = await this.getParentBlock(proposal);
|
|
223
|
+
if (parentBlock === undefined) {
|
|
170
224
|
this.log.warn(`Parent block for proposal not found, skipping processing`, proposalInfo);
|
|
171
225
|
return {
|
|
172
226
|
isValid: false,
|
|
173
227
|
reason: 'parent_block_not_found'
|
|
174
228
|
};
|
|
175
229
|
}
|
|
176
|
-
// Check that the parent block's slot is
|
|
177
|
-
if (
|
|
178
|
-
this.log.warn(`Parent block slot is greater than
|
|
179
|
-
parentBlockSlot:
|
|
230
|
+
// Check that the parent block's slot is not greater than the proposal's slot.
|
|
231
|
+
if (parentBlock !== 'genesis' && parentBlock.header.getSlot() > slotNumber) {
|
|
232
|
+
this.log.warn(`Parent block slot is greater than proposal slot, skipping processing`, {
|
|
233
|
+
parentBlockSlot: parentBlock.header.getSlot().toString(),
|
|
180
234
|
proposalSlot: slotNumber.toString(),
|
|
181
235
|
...proposalInfo
|
|
182
236
|
});
|
|
@@ -186,7 +240,8 @@ export class BlockProposalHandler {
|
|
|
186
240
|
};
|
|
187
241
|
}
|
|
188
242
|
// Compute the block number based on the parent block
|
|
189
|
-
const blockNumber =
|
|
243
|
+
const blockNumber = parentBlock === 'genesis' ? BlockNumber(INITIAL_L2_BLOCK_NUM) : BlockNumber(parentBlock.header.getBlockNumber() + 1);
|
|
244
|
+
proposalInfo.blockNumber = blockNumber;
|
|
190
245
|
// Check that this block number does not exist already
|
|
191
246
|
const existingBlock = await this.blockSource.getBlockHeader(blockNumber);
|
|
192
247
|
if (existingBlock) {
|
|
@@ -203,8 +258,16 @@ export class BlockProposalHandler {
|
|
|
203
258
|
pinnedPeer: proposalSender,
|
|
204
259
|
deadline: this.getReexecutionDeadline(slotNumber, config)
|
|
205
260
|
});
|
|
261
|
+
// If reexecution is disabled, bail. We were just interested in triggering tx collection.
|
|
262
|
+
if (!shouldReexecute) {
|
|
263
|
+
this.log.info(`Received valid block ${blockNumber} proposal at index ${proposal.indexWithinCheckpoint} on slot ${slotNumber}`, proposalInfo);
|
|
264
|
+
return {
|
|
265
|
+
isValid: true,
|
|
266
|
+
blockNumber
|
|
267
|
+
};
|
|
268
|
+
}
|
|
206
269
|
// Compute the checkpoint number for this block and validate checkpoint consistency
|
|
207
|
-
const checkpointResult =
|
|
270
|
+
const checkpointResult = this.computeCheckpointNumber(proposal, parentBlock, proposalInfo);
|
|
208
271
|
if (checkpointResult.reason) {
|
|
209
272
|
return {
|
|
210
273
|
isValid: false,
|
|
@@ -213,6 +276,7 @@ export class BlockProposalHandler {
|
|
|
213
276
|
};
|
|
214
277
|
}
|
|
215
278
|
const checkpointNumber = checkpointResult.checkpointNumber;
|
|
279
|
+
proposalInfo.checkpointNumber = checkpointNumber;
|
|
216
280
|
// Check that I have the same set of l1ToL2Messages as the proposal
|
|
217
281
|
const l1ToL2Messages = await this.l1ToL2MessageSource.getL1ToL2Messages(checkpointNumber);
|
|
218
282
|
const computedInHash = computeInHashFromL1ToL2Messages(l1ToL2Messages);
|
|
@@ -241,35 +305,32 @@ export class BlockProposalHandler {
|
|
|
241
305
|
reason: 'txs_not_available'
|
|
242
306
|
};
|
|
243
307
|
}
|
|
308
|
+
// Collect the out hashes of all the checkpoints before this one in the same epoch
|
|
309
|
+
const epoch = getEpochAtSlot(slotNumber, this.epochCache.getL1Constants());
|
|
310
|
+
const previousCheckpointOutHashes = (await this.blockSource.getCheckpointsDataForEpoch(epoch)).filter((c)=>c.checkpointNumber < checkpointNumber).map((c)=>c.checkpointOutHash);
|
|
244
311
|
// Try re-executing the transactions in the proposal if needed
|
|
245
312
|
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
|
-
}
|
|
313
|
+
try {
|
|
314
|
+
this.log.verbose(`Re-executing transactions in the proposal`, proposalInfo);
|
|
315
|
+
reexecutionResult = await this.reexecuteTransactions(proposal, blockNumber, checkpointNumber, txs, l1ToL2Messages, previousCheckpointOutHashes);
|
|
316
|
+
} catch (error) {
|
|
317
|
+
this.log.error(`Error reexecuting txs while processing block proposal`, error, proposalInfo);
|
|
318
|
+
const reason = this.getReexecuteFailureReason(error);
|
|
319
|
+
return {
|
|
320
|
+
isValid: false,
|
|
321
|
+
blockNumber,
|
|
322
|
+
reason,
|
|
323
|
+
reexecutionResult
|
|
324
|
+
};
|
|
267
325
|
}
|
|
268
326
|
// If we succeeded, push this block into the archiver (unless disabled)
|
|
269
327
|
if (reexecutionResult?.block && this.config.skipPushProposedBlocksToArchiver === false) {
|
|
270
328
|
await this.blockSource.addBlock(reexecutionResult?.block);
|
|
271
329
|
}
|
|
272
|
-
this.log.info(`Successfully
|
|
330
|
+
this.log.info(`Successfully re-executed block ${blockNumber} proposal at index ${proposal.indexWithinCheckpoint} on slot ${slotNumber}`, {
|
|
331
|
+
...proposalInfo,
|
|
332
|
+
...pick(reexecutionResult, 'reexecutionTimeMs', 'totalManaUsed')
|
|
333
|
+
});
|
|
273
334
|
return {
|
|
274
335
|
isValid: true,
|
|
275
336
|
blockNumber,
|
|
@@ -288,7 +349,7 @@ export class BlockProposalHandler {
|
|
|
288
349
|
const currentTime = this.dateProvider.now();
|
|
289
350
|
const timeoutDurationMs = deadline.getTime() - currentTime;
|
|
290
351
|
try {
|
|
291
|
-
return await this.blockSource.
|
|
352
|
+
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
353
|
} catch (err) {
|
|
293
354
|
if (err instanceof TimeoutError) {
|
|
294
355
|
this.log.debug(`Timed out getting parent block by archive root`, {
|
|
@@ -302,8 +363,8 @@ export class BlockProposalHandler {
|
|
|
302
363
|
return undefined;
|
|
303
364
|
}
|
|
304
365
|
}
|
|
305
|
-
|
|
306
|
-
if (
|
|
366
|
+
computeCheckpointNumber(proposal, parentBlock, proposalInfo) {
|
|
367
|
+
if (parentBlock === 'genesis') {
|
|
307
368
|
// First block is in checkpoint 1
|
|
308
369
|
if (proposal.indexWithinCheckpoint !== 0) {
|
|
309
370
|
this.log.warn(`First block proposal has non-zero indexWithinCheckpoint`, proposalInfo);
|
|
@@ -315,20 +376,9 @@ export class BlockProposalHandler {
|
|
|
315
376
|
checkpointNumber: CheckpointNumber.INITIAL
|
|
316
377
|
};
|
|
317
378
|
}
|
|
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
379
|
if (proposal.indexWithinCheckpoint === 0) {
|
|
330
380
|
// If this is the first block in a new checkpoint, increment the checkpoint number
|
|
331
|
-
if (!(proposal.blockHeader.getSlot() >
|
|
381
|
+
if (!(proposal.blockHeader.getSlot() > parentBlock.header.getSlot())) {
|
|
332
382
|
this.log.warn(`Slot should be greater than parent block slot for first block in checkpoint`, proposalInfo);
|
|
333
383
|
return {
|
|
334
384
|
reason: 'invalid_proposal'
|
|
@@ -345,7 +395,7 @@ export class BlockProposalHandler {
|
|
|
345
395
|
reason: 'invalid_proposal'
|
|
346
396
|
};
|
|
347
397
|
}
|
|
348
|
-
if (proposal.blockHeader.getSlot() !==
|
|
398
|
+
if (proposal.blockHeader.getSlot() !== parentBlock.header.getSlot()) {
|
|
349
399
|
this.log.warn(`Slot should be equal to parent block slot for non-first block in checkpoint`, proposalInfo);
|
|
350
400
|
return {
|
|
351
401
|
reason: 'invalid_proposal'
|
|
@@ -445,8 +495,39 @@ export class BlockProposalHandler {
|
|
|
445
495
|
const nextSlotTimestampSeconds = Number(getTimestampForSlot(SlotNumber(slot + 1), config));
|
|
446
496
|
return new Date(nextSlotTimestampSeconds * 1000);
|
|
447
497
|
}
|
|
498
|
+
/** Waits for the block source to sync L1 data up to at least the slot before the given one. */ async waitForBlockSourceSync(slot) {
|
|
499
|
+
const deadline = this.getReexecutionDeadline(slot, this.checkpointsBuilder.getConfig());
|
|
500
|
+
const timeoutMs = deadline.getTime() - this.dateProvider.now();
|
|
501
|
+
if (slot === 0) {
|
|
502
|
+
return true;
|
|
503
|
+
}
|
|
504
|
+
// Make a quick check before triggering an archiver sync
|
|
505
|
+
const syncedSlot = await this.blockSource.getSyncedL2SlotNumber();
|
|
506
|
+
if (syncedSlot !== undefined && syncedSlot + 1 >= slot) {
|
|
507
|
+
return true;
|
|
508
|
+
}
|
|
509
|
+
try {
|
|
510
|
+
// Trigger an immediate sync of the block source, and wait until it reports being synced to the required slot
|
|
511
|
+
return await retryUntil(async ()=>{
|
|
512
|
+
await this.blockSource.syncImmediate();
|
|
513
|
+
const syncedSlot = await this.blockSource.getSyncedL2SlotNumber();
|
|
514
|
+
return syncedSlot !== undefined && syncedSlot + 1 >= slot;
|
|
515
|
+
}, 'wait for block source sync', timeoutMs / 1000, 0.5);
|
|
516
|
+
} catch (err) {
|
|
517
|
+
if (err instanceof TimeoutError) {
|
|
518
|
+
this.log.warn(`Timed out waiting for block source to sync to slot ${slot}`);
|
|
519
|
+
return false;
|
|
520
|
+
} else {
|
|
521
|
+
throw err;
|
|
522
|
+
}
|
|
523
|
+
}
|
|
524
|
+
}
|
|
448
525
|
getReexecuteFailureReason(err) {
|
|
449
|
-
if (err instanceof
|
|
526
|
+
if (err instanceof TransactionsNotAvailableError) {
|
|
527
|
+
return 'txs_not_available';
|
|
528
|
+
} else if (err instanceof ReExInitialStateMismatchError) {
|
|
529
|
+
return 'initial_state_mismatch';
|
|
530
|
+
} else if (err instanceof ReExStateMismatchError) {
|
|
450
531
|
return 'state_mismatch';
|
|
451
532
|
} else if (err instanceof ReExFailedTxsError) {
|
|
452
533
|
return 'failed_txs';
|
|
@@ -478,30 +559,44 @@ export class BlockProposalHandler {
|
|
|
478
559
|
const priorBlocks = allBlocksInSlot.filter((b)=>b.number < blockNumber && b.header.getSlot() === slot);
|
|
479
560
|
// Fork before the block to be built
|
|
480
561
|
const parentBlockNumber = BlockNumber(blockNumber - 1);
|
|
481
|
-
|
|
482
|
-
|
|
562
|
+
await this.worldState.syncImmediate(parentBlockNumber);
|
|
563
|
+
const fork = _ts_add_disposable_resource(env, await this.worldState.fork(parentBlockNumber), true);
|
|
564
|
+
// Verify the fork's archive root matches the proposal's expected last archive.
|
|
565
|
+
// If they don't match, our world state synced to a different chain and reexecution would fail.
|
|
566
|
+
const forkArchiveRoot = new Fr((await fork.getTreeInfo(MerkleTreeId.ARCHIVE)).root);
|
|
567
|
+
if (!forkArchiveRoot.equals(proposal.blockHeader.lastArchive.root)) {
|
|
568
|
+
throw new ReExInitialStateMismatchError(proposal.blockHeader.lastArchive.root, forkArchiveRoot);
|
|
569
|
+
}
|
|
570
|
+
// Build checkpoint constants from proposal (excludes blockNumber which is per-block)
|
|
483
571
|
const constants = {
|
|
484
572
|
chainId: new Fr(config.l1ChainId),
|
|
485
573
|
version: new Fr(config.rollupVersion),
|
|
486
574
|
slotNumber: slot,
|
|
575
|
+
timestamp: blockHeader.globalVariables.timestamp,
|
|
487
576
|
coinbase: blockHeader.globalVariables.coinbase,
|
|
488
577
|
feeRecipient: blockHeader.globalVariables.feeRecipient,
|
|
489
578
|
gasFees: blockHeader.globalVariables.gasFees
|
|
490
579
|
};
|
|
491
580
|
// Create checkpoint builder with prior blocks
|
|
492
|
-
const checkpointBuilder = await this.checkpointsBuilder.openCheckpoint(checkpointNumber, constants, l1ToL2Messages, previousCheckpointOutHashes, fork, priorBlocks);
|
|
581
|
+
const checkpointBuilder = await this.checkpointsBuilder.openCheckpoint(checkpointNumber, constants, 0n, l1ToL2Messages, previousCheckpointOutHashes, fork, priorBlocks, this.log.getBindings());
|
|
493
582
|
// Build the new block
|
|
494
583
|
const deadline = this.getReexecutionDeadline(slot, config);
|
|
584
|
+
const maxBlockGas = this.config.validateMaxL2BlockGas !== undefined || this.config.validateMaxDABlockGas !== undefined ? new Gas(this.config.validateMaxDABlockGas ?? Infinity, this.config.validateMaxL2BlockGas ?? Infinity) : undefined;
|
|
495
585
|
const result = await checkpointBuilder.buildBlock(txs, blockNumber, blockHeader.globalVariables.timestamp, {
|
|
586
|
+
isBuildingProposal: false,
|
|
587
|
+
minValidTxs: 0,
|
|
496
588
|
deadline,
|
|
497
|
-
expectedEndState: blockHeader.state
|
|
589
|
+
expectedEndState: blockHeader.state,
|
|
590
|
+
maxTransactions: this.config.validateMaxTxsPerBlock,
|
|
591
|
+
maxBlockGas
|
|
498
592
|
});
|
|
499
593
|
const { block, failedTxs } = result;
|
|
500
594
|
const numFailedTxs = failedTxs.length;
|
|
501
|
-
this.log.verbose(`
|
|
595
|
+
this.log.verbose(`Block proposal ${blockNumber} at slot ${slot} transaction re-execution complete`, {
|
|
502
596
|
numFailedTxs,
|
|
503
597
|
numProposalTxs: txHashes.length,
|
|
504
598
|
numProcessedTxs: block.body.txEffects.length,
|
|
599
|
+
blockNumber,
|
|
505
600
|
slot
|
|
506
601
|
});
|
|
507
602
|
if (numFailedTxs > 0) {
|
|
@@ -539,7 +634,219 @@ export class BlockProposalHandler {
|
|
|
539
634
|
env.error = e;
|
|
540
635
|
env.hasError = true;
|
|
541
636
|
} finally{
|
|
542
|
-
_ts_dispose_resources(env);
|
|
637
|
+
const result = _ts_dispose_resources(env);
|
|
638
|
+
if (result) await result;
|
|
639
|
+
}
|
|
640
|
+
}
|
|
641
|
+
/**
|
|
642
|
+
* Validates a checkpoint proposal and uploads blobs if configured.
|
|
643
|
+
* Used by both non-validator nodes (via register) and the validator client (via delegation).
|
|
644
|
+
*/ async handleCheckpointProposal(proposal, proposalInfo) {
|
|
645
|
+
const proposer = proposal.getSender();
|
|
646
|
+
if (!proposer) {
|
|
647
|
+
this.log.warn(`Received checkpoint proposal with invalid signature for slot ${proposal.slotNumber}`);
|
|
648
|
+
return {
|
|
649
|
+
isValid: false,
|
|
650
|
+
reason: 'invalid_signature'
|
|
651
|
+
};
|
|
652
|
+
}
|
|
653
|
+
if (!validateFeeAssetPriceModifier(proposal.feeAssetPriceModifier)) {
|
|
654
|
+
this.log.warn(`Received checkpoint proposal with invalid feeAssetPriceModifier ${proposal.feeAssetPriceModifier} for slot ${proposal.slotNumber}`);
|
|
655
|
+
return {
|
|
656
|
+
isValid: false,
|
|
657
|
+
reason: 'invalid_fee_asset_price_modifier'
|
|
658
|
+
};
|
|
659
|
+
}
|
|
660
|
+
const result = await this.validateCheckpointProposal(proposal, proposalInfo);
|
|
661
|
+
// Upload blobs to filestore if validation passed (fire and forget)
|
|
662
|
+
if (result.isValid) {
|
|
663
|
+
this.tryUploadBlobsForCheckpoint(proposal, proposalInfo);
|
|
664
|
+
}
|
|
665
|
+
return result;
|
|
666
|
+
}
|
|
667
|
+
/**
|
|
668
|
+
* Validates a checkpoint proposal by building the full checkpoint and comparing it with the proposal.
|
|
669
|
+
* @returns Validation result with isValid flag and reason if invalid.
|
|
670
|
+
*/ async validateCheckpointProposal(proposal, proposalInfo) {
|
|
671
|
+
const slot = proposal.slotNumber;
|
|
672
|
+
// Timeout block syncing at the start of the next slot
|
|
673
|
+
const config = this.checkpointsBuilder.getConfig();
|
|
674
|
+
const nextSlotTimestampSeconds = Number(getTimestampForSlot(SlotNumber(slot + 1), config));
|
|
675
|
+
const timeoutSeconds = Math.max(1, nextSlotTimestampSeconds - Math.floor(this.dateProvider.now() / 1000));
|
|
676
|
+
// Wait for last block to sync by archive
|
|
677
|
+
let lastBlockHeader;
|
|
678
|
+
try {
|
|
679
|
+
lastBlockHeader = await retryUntil(async ()=>{
|
|
680
|
+
await this.blockSource.syncImmediate();
|
|
681
|
+
return this.blockSource.getBlockHeaderByArchive(proposal.archive);
|
|
682
|
+
}, `waiting for block with archive ${proposal.archive.toString()} for slot ${slot}`, timeoutSeconds, 0.5);
|
|
683
|
+
} catch (err) {
|
|
684
|
+
if (err instanceof TimeoutError) {
|
|
685
|
+
this.log.warn(`Timed out waiting for block with archive matching checkpoint proposal`, proposalInfo);
|
|
686
|
+
return {
|
|
687
|
+
isValid: false,
|
|
688
|
+
reason: 'last_block_not_found'
|
|
689
|
+
};
|
|
690
|
+
}
|
|
691
|
+
this.log.error(`Error fetching last block for checkpoint proposal`, err, proposalInfo);
|
|
692
|
+
return {
|
|
693
|
+
isValid: false,
|
|
694
|
+
reason: 'block_fetch_error'
|
|
695
|
+
};
|
|
696
|
+
}
|
|
697
|
+
if (!lastBlockHeader) {
|
|
698
|
+
this.log.warn(`Last block not found for checkpoint proposal`, proposalInfo);
|
|
699
|
+
return {
|
|
700
|
+
isValid: false,
|
|
701
|
+
reason: 'last_block_not_found'
|
|
702
|
+
};
|
|
703
|
+
}
|
|
704
|
+
// Get all full blocks for the slot and checkpoint
|
|
705
|
+
const blocks = await this.blockSource.getBlocksForSlot(slot);
|
|
706
|
+
if (blocks.length === 0) {
|
|
707
|
+
this.log.warn(`No blocks found for slot ${slot}`, proposalInfo);
|
|
708
|
+
return {
|
|
709
|
+
isValid: false,
|
|
710
|
+
reason: 'no_blocks_for_slot'
|
|
711
|
+
};
|
|
712
|
+
}
|
|
713
|
+
// Ensure the last block for this slot matches the archive in the checkpoint proposal
|
|
714
|
+
if (!blocks.at(-1)?.archive.root.equals(proposal.archive)) {
|
|
715
|
+
this.log.warn(`Last block archive mismatch for checkpoint proposal`, proposalInfo);
|
|
716
|
+
return {
|
|
717
|
+
isValid: false,
|
|
718
|
+
reason: 'last_block_archive_mismatch'
|
|
719
|
+
};
|
|
720
|
+
}
|
|
721
|
+
this.log.debug(`Found ${blocks.length} blocks for slot ${slot}`, {
|
|
722
|
+
...proposalInfo,
|
|
723
|
+
blockNumbers: blocks.map((b)=>b.number)
|
|
724
|
+
});
|
|
725
|
+
// Get checkpoint constants from first block
|
|
726
|
+
const firstBlock = blocks[0];
|
|
727
|
+
const constants = this.extractCheckpointConstants(firstBlock);
|
|
728
|
+
const checkpointNumber = firstBlock.checkpointNumber;
|
|
729
|
+
// Get L1-to-L2 messages for this checkpoint
|
|
730
|
+
const l1ToL2Messages = await this.l1ToL2MessageSource.getL1ToL2Messages(checkpointNumber);
|
|
731
|
+
// Collect the out hashes of all the checkpoints before this one in the same epoch
|
|
732
|
+
const epoch = getEpochAtSlot(slot, this.epochCache.getL1Constants());
|
|
733
|
+
const previousCheckpointOutHashes = (await this.blockSource.getCheckpointsDataForEpoch(epoch)).filter((c)=>c.checkpointNumber < checkpointNumber).map((c)=>c.checkpointOutHash);
|
|
734
|
+
// Fork world state at the block before the first block
|
|
735
|
+
const parentBlockNumber = BlockNumber(firstBlock.number - 1);
|
|
736
|
+
const fork = await this.worldState.fork(parentBlockNumber);
|
|
737
|
+
try {
|
|
738
|
+
// Create checkpoint builder with all existing blocks
|
|
739
|
+
const checkpointBuilder = await this.checkpointsBuilder.openCheckpoint(checkpointNumber, constants, proposal.feeAssetPriceModifier, l1ToL2Messages, previousCheckpointOutHashes, fork, blocks, this.log.getBindings());
|
|
740
|
+
// Complete the checkpoint to get computed values
|
|
741
|
+
const computedCheckpoint = await checkpointBuilder.completeCheckpoint();
|
|
742
|
+
// Compare checkpoint header with proposal
|
|
743
|
+
if (!computedCheckpoint.header.equals(proposal.checkpointHeader)) {
|
|
744
|
+
this.log.warn(`Checkpoint header mismatch`, {
|
|
745
|
+
...proposalInfo,
|
|
746
|
+
computed: computedCheckpoint.header.toInspect(),
|
|
747
|
+
proposal: proposal.checkpointHeader.toInspect()
|
|
748
|
+
});
|
|
749
|
+
return {
|
|
750
|
+
isValid: false,
|
|
751
|
+
reason: 'checkpoint_header_mismatch'
|
|
752
|
+
};
|
|
753
|
+
}
|
|
754
|
+
// Compare archive root with proposal
|
|
755
|
+
if (!computedCheckpoint.archive.root.equals(proposal.archive)) {
|
|
756
|
+
this.log.warn(`Archive root mismatch`, {
|
|
757
|
+
...proposalInfo,
|
|
758
|
+
computed: computedCheckpoint.archive.root.toString(),
|
|
759
|
+
proposal: proposal.archive.toString()
|
|
760
|
+
});
|
|
761
|
+
return {
|
|
762
|
+
isValid: false,
|
|
763
|
+
reason: 'archive_mismatch'
|
|
764
|
+
};
|
|
765
|
+
}
|
|
766
|
+
// Check that the accumulated epoch out hash matches the value in the proposal.
|
|
767
|
+
// The epoch out hash is the accumulated hash of all checkpoint out hashes in the epoch.
|
|
768
|
+
const checkpointOutHash = computedCheckpoint.getCheckpointOutHash();
|
|
769
|
+
const computedEpochOutHash = accumulateCheckpointOutHashes([
|
|
770
|
+
...previousCheckpointOutHashes,
|
|
771
|
+
checkpointOutHash
|
|
772
|
+
]);
|
|
773
|
+
const proposalEpochOutHash = proposal.checkpointHeader.epochOutHash;
|
|
774
|
+
if (!computedEpochOutHash.equals(proposalEpochOutHash)) {
|
|
775
|
+
this.log.warn(`Epoch out hash mismatch`, {
|
|
776
|
+
proposalEpochOutHash: proposalEpochOutHash.toString(),
|
|
777
|
+
computedEpochOutHash: computedEpochOutHash.toString(),
|
|
778
|
+
checkpointOutHash: checkpointOutHash.toString(),
|
|
779
|
+
previousCheckpointOutHashes: previousCheckpointOutHashes.map((h)=>h.toString()),
|
|
780
|
+
...proposalInfo
|
|
781
|
+
});
|
|
782
|
+
return {
|
|
783
|
+
isValid: false,
|
|
784
|
+
reason: 'out_hash_mismatch'
|
|
785
|
+
};
|
|
786
|
+
}
|
|
787
|
+
// Final round of validations on the checkpoint, just in case.
|
|
788
|
+
try {
|
|
789
|
+
validateCheckpoint(computedCheckpoint, {
|
|
790
|
+
rollupManaLimit: this.checkpointsBuilder.getConfig().rollupManaLimit,
|
|
791
|
+
maxDABlockGas: this.config.validateMaxDABlockGas,
|
|
792
|
+
maxL2BlockGas: this.config.validateMaxL2BlockGas,
|
|
793
|
+
maxTxsPerBlock: this.config.validateMaxTxsPerBlock,
|
|
794
|
+
maxTxsPerCheckpoint: this.config.validateMaxTxsPerCheckpoint
|
|
795
|
+
});
|
|
796
|
+
} catch (err) {
|
|
797
|
+
this.log.warn(`Checkpoint validation failed: ${err}`, proposalInfo);
|
|
798
|
+
return {
|
|
799
|
+
isValid: false,
|
|
800
|
+
reason: 'checkpoint_validation_failed'
|
|
801
|
+
};
|
|
802
|
+
}
|
|
803
|
+
this.log.verbose(`Checkpoint proposal validation successful for slot ${slot}`, proposalInfo);
|
|
804
|
+
return {
|
|
805
|
+
isValid: true
|
|
806
|
+
};
|
|
807
|
+
} finally{
|
|
808
|
+
await fork.close();
|
|
809
|
+
}
|
|
810
|
+
}
|
|
811
|
+
/** Extracts checkpoint global variables from a block. */ extractCheckpointConstants(block) {
|
|
812
|
+
const gv = block.header.globalVariables;
|
|
813
|
+
return {
|
|
814
|
+
chainId: gv.chainId,
|
|
815
|
+
version: gv.version,
|
|
816
|
+
slotNumber: gv.slotNumber,
|
|
817
|
+
timestamp: gv.timestamp,
|
|
818
|
+
coinbase: gv.coinbase,
|
|
819
|
+
feeRecipient: gv.feeRecipient,
|
|
820
|
+
gasFees: gv.gasFees
|
|
821
|
+
};
|
|
822
|
+
}
|
|
823
|
+
/** Triggers blob upload for a checkpoint if the blob client can upload (fire and forget). */ tryUploadBlobsForCheckpoint(proposal, proposalInfo) {
|
|
824
|
+
if (this.blobClient.canUpload()) {
|
|
825
|
+
void this.uploadBlobsForCheckpoint(proposal, proposalInfo);
|
|
826
|
+
}
|
|
827
|
+
}
|
|
828
|
+
/** Uploads blobs for a checkpoint to the filestore. */ async uploadBlobsForCheckpoint(proposal, proposalInfo) {
|
|
829
|
+
try {
|
|
830
|
+
const lastBlockHeader = await this.blockSource.getBlockHeaderByArchive(proposal.archive);
|
|
831
|
+
if (!lastBlockHeader) {
|
|
832
|
+
this.log.warn(`Failed to get last block header for blob upload`, proposalInfo);
|
|
833
|
+
return;
|
|
834
|
+
}
|
|
835
|
+
const blocks = await this.blockSource.getBlocksForSlot(proposal.slotNumber);
|
|
836
|
+
if (blocks.length === 0) {
|
|
837
|
+
this.log.warn(`No blocks found for blob upload`, proposalInfo);
|
|
838
|
+
return;
|
|
839
|
+
}
|
|
840
|
+
const blockBlobData = blocks.map((b)=>b.toBlockBlobData());
|
|
841
|
+
const blobFields = encodeCheckpointBlobDataFromBlocks(blockBlobData);
|
|
842
|
+
const blobs = await getBlobsPerL1Block(blobFields);
|
|
843
|
+
await this.blobClient.sendBlobsToFilestore(blobs);
|
|
844
|
+
this.log.debug(`Uploaded ${blobs.length} blobs to filestore for checkpoint at slot ${proposal.slotNumber}`, {
|
|
845
|
+
...proposalInfo,
|
|
846
|
+
numBlobs: blobs.length
|
|
847
|
+
});
|
|
848
|
+
} catch (err) {
|
|
849
|
+
this.log.warn(`Failed to upload blobs for checkpoint: ${err}`, proposalInfo);
|
|
543
850
|
}
|
|
544
851
|
}
|
|
545
852
|
}
|