@aztec/validator-client 4.0.0-devnet.2-patch.3 → 4.0.0-devnet.3-patch.0
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 +41 -0
- package/dest/checkpoint_builder.d.ts +19 -6
- package/dest/checkpoint_builder.d.ts.map +1 -1
- package/dest/checkpoint_builder.js +115 -39
- package/dest/config.d.ts +1 -1
- package/dest/config.d.ts.map +1 -1
- package/dest/config.js +20 -0
- package/dest/duties/validation_service.d.ts +1 -1
- package/dest/duties/validation_service.d.ts.map +1 -1
- package/dest/duties/validation_service.js +3 -9
- 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} +356 -36
- package/dest/validator.d.ts +11 -22
- package/dest/validator.d.ts.map +1 -1
- package/dest/validator.js +41 -217
- package/package.json +19 -19
- package/src/checkpoint_builder.ts +135 -39
- package/src/config.ts +20 -0
- package/src/duties/validation_service.ts +3 -9
- 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/{block_proposal_handler.ts → proposal_handler.ts} +412 -44
- package/src/validator.ts +48 -240
- 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/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,18 +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';
|
|
70
|
+
import { pick } from '@aztec/foundation/collection';
|
|
68
71
|
import { Fr } from '@aztec/foundation/curves/bn254';
|
|
69
72
|
import { TimeoutError } from '@aztec/foundation/error';
|
|
70
73
|
import { createLogger } from '@aztec/foundation/log';
|
|
71
74
|
import { retryUntil } from '@aztec/foundation/retry';
|
|
72
75
|
import { DateProvider, Timer } from '@aztec/foundation/timer';
|
|
76
|
+
import { validateCheckpoint } from '@aztec/stdlib/checkpoint';
|
|
73
77
|
import { getEpochAtSlot, getTimestampForSlot } from '@aztec/stdlib/epoch-helpers';
|
|
74
|
-
import {
|
|
75
|
-
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';
|
|
76
82
|
import { getTelemetryClient } from '@aztec/telemetry-client';
|
|
77
|
-
export class
|
|
83
|
+
/** Handles block and checkpoint proposals for both validator and non-validator nodes. */ export class ProposalHandler {
|
|
78
84
|
checkpointsBuilder;
|
|
79
85
|
worldState;
|
|
80
86
|
blockSource;
|
|
@@ -83,11 +89,12 @@ export class BlockProposalHandler {
|
|
|
83
89
|
blockProposalValidator;
|
|
84
90
|
epochCache;
|
|
85
91
|
config;
|
|
92
|
+
blobClient;
|
|
86
93
|
metrics;
|
|
87
94
|
dateProvider;
|
|
88
95
|
log;
|
|
89
96
|
tracer;
|
|
90
|
-
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')){
|
|
91
98
|
this.checkpointsBuilder = checkpointsBuilder;
|
|
92
99
|
this.worldState = worldState;
|
|
93
100
|
this.blockSource = blockSource;
|
|
@@ -96,31 +103,39 @@ export class BlockProposalHandler {
|
|
|
96
103
|
this.blockProposalValidator = blockProposalValidator;
|
|
97
104
|
this.epochCache = epochCache;
|
|
98
105
|
this.config = config;
|
|
106
|
+
this.blobClient = blobClient;
|
|
99
107
|
this.metrics = metrics;
|
|
100
108
|
this.dateProvider = dateProvider;
|
|
101
109
|
this.log = log;
|
|
102
110
|
if (config.fishermanMode) {
|
|
103
111
|
this.log = this.log.createChild('[FISHERMAN]');
|
|
104
112
|
}
|
|
105
|
-
this.tracer = telemetry.getTracer('
|
|
113
|
+
this.tracer = telemetry.getTracer('ProposalHandler');
|
|
106
114
|
}
|
|
107
|
-
|
|
108
|
-
|
|
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.
|
|
109
120
|
// Returns boolean indicating whether the proposal was valid.
|
|
110
|
-
const
|
|
121
|
+
const blockHandler = async (proposal, proposalSender)=>{
|
|
111
122
|
try {
|
|
112
|
-
const
|
|
123
|
+
const { slotNumber, blockNumber } = proposal;
|
|
124
|
+
const result = await this.handleBlockProposal(proposal, proposalSender, shouldReexecute);
|
|
113
125
|
if (result.isValid) {
|
|
114
|
-
this.log.info(`Non-validator
|
|
126
|
+
this.log.info(`Non-validator block proposal ${blockNumber} at slot ${slotNumber} handled`, {
|
|
115
127
|
blockNumber: result.blockNumber,
|
|
128
|
+
slotNumber,
|
|
116
129
|
reexecutionTimeMs: result.reexecutionResult?.reexecutionTimeMs,
|
|
117
130
|
totalManaUsed: result.reexecutionResult?.totalManaUsed,
|
|
118
|
-
numTxs: result.reexecutionResult?.block?.body?.txEffects?.length ?? 0
|
|
131
|
+
numTxs: result.reexecutionResult?.block?.body?.txEffects?.length ?? 0,
|
|
132
|
+
reexecuted: shouldReexecute
|
|
119
133
|
});
|
|
120
134
|
return true;
|
|
121
135
|
} else {
|
|
122
|
-
this.log.warn(`Non-validator
|
|
136
|
+
this.log.warn(`Non-validator block proposal ${blockNumber} at slot ${slotNumber} failed processing with ${result.reason}`, {
|
|
123
137
|
blockNumber: result.blockNumber,
|
|
138
|
+
slotNumber,
|
|
124
139
|
reason: result.reason
|
|
125
140
|
});
|
|
126
141
|
return false;
|
|
@@ -130,7 +145,30 @@ export class BlockProposalHandler {
|
|
|
130
145
|
return false;
|
|
131
146
|
}
|
|
132
147
|
};
|
|
133
|
-
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
|
+
}
|
|
134
172
|
return this;
|
|
135
173
|
}
|
|
136
174
|
async handleBlockProposal(proposal, proposalSender, shouldReexecute) {
|
|
@@ -147,7 +185,9 @@ export class BlockProposalHandler {
|
|
|
147
185
|
}
|
|
148
186
|
const proposalInfo = {
|
|
149
187
|
...proposal.toBlockInfo(),
|
|
150
|
-
proposer: proposer.toString()
|
|
188
|
+
proposer: proposer.toString(),
|
|
189
|
+
blockNumber: undefined,
|
|
190
|
+
checkpointNumber: undefined
|
|
151
191
|
};
|
|
152
192
|
this.log.info(`Processing proposal for slot ${slotNumber}`, {
|
|
153
193
|
...proposalInfo,
|
|
@@ -163,7 +203,22 @@ export class BlockProposalHandler {
|
|
|
163
203
|
reason: 'invalid_proposal'
|
|
164
204
|
};
|
|
165
205
|
}
|
|
166
|
-
//
|
|
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.
|
|
167
222
|
const parentBlock = await this.getParentBlock(proposal);
|
|
168
223
|
if (parentBlock === undefined) {
|
|
169
224
|
this.log.warn(`Parent block for proposal not found, skipping processing`, proposalInfo);
|
|
@@ -186,6 +241,7 @@ export class BlockProposalHandler {
|
|
|
186
241
|
}
|
|
187
242
|
// Compute the block number based on the parent block
|
|
188
243
|
const blockNumber = parentBlock === 'genesis' ? BlockNumber(INITIAL_L2_BLOCK_NUM) : BlockNumber(parentBlock.header.getBlockNumber() + 1);
|
|
244
|
+
proposalInfo.blockNumber = blockNumber;
|
|
189
245
|
// Check that this block number does not exist already
|
|
190
246
|
const existingBlock = await this.blockSource.getBlockHeader(blockNumber);
|
|
191
247
|
if (existingBlock) {
|
|
@@ -202,6 +258,14 @@ export class BlockProposalHandler {
|
|
|
202
258
|
pinnedPeer: proposalSender,
|
|
203
259
|
deadline: this.getReexecutionDeadline(slotNumber, config)
|
|
204
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
|
+
}
|
|
205
269
|
// Compute the checkpoint number for this block and validate checkpoint consistency
|
|
206
270
|
const checkpointResult = this.computeCheckpointNumber(proposal, parentBlock, proposalInfo);
|
|
207
271
|
if (checkpointResult.reason) {
|
|
@@ -212,6 +276,7 @@ export class BlockProposalHandler {
|
|
|
212
276
|
};
|
|
213
277
|
}
|
|
214
278
|
const checkpointNumber = checkpointResult.checkpointNumber;
|
|
279
|
+
proposalInfo.checkpointNumber = checkpointNumber;
|
|
215
280
|
// Check that I have the same set of l1ToL2Messages as the proposal
|
|
216
281
|
const l1ToL2Messages = await this.l1ToL2MessageSource.getL1ToL2Messages(checkpointNumber);
|
|
217
282
|
const computedInHash = computeInHashFromL1ToL2Messages(l1ToL2Messages);
|
|
@@ -240,31 +305,32 @@ export class BlockProposalHandler {
|
|
|
240
305
|
reason: 'txs_not_available'
|
|
241
306
|
};
|
|
242
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);
|
|
243
311
|
// Try re-executing the transactions in the proposal if needed
|
|
244
312
|
let reexecutionResult;
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
blockNumber,
|
|
258
|
-
reason,
|
|
259
|
-
reexecutionResult
|
|
260
|
-
};
|
|
261
|
-
}
|
|
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
|
+
};
|
|
262
325
|
}
|
|
263
326
|
// If we succeeded, push this block into the archiver (unless disabled)
|
|
264
327
|
if (reexecutionResult?.block && this.config.skipPushProposedBlocksToArchiver === false) {
|
|
265
328
|
await this.blockSource.addBlock(reexecutionResult?.block);
|
|
266
329
|
}
|
|
267
|
-
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
|
+
});
|
|
268
334
|
return {
|
|
269
335
|
isValid: true,
|
|
270
336
|
blockNumber,
|
|
@@ -429,8 +495,39 @@ export class BlockProposalHandler {
|
|
|
429
495
|
const nextSlotTimestampSeconds = Number(getTimestampForSlot(SlotNumber(slot + 1), config));
|
|
430
496
|
return new Date(nextSlotTimestampSeconds * 1000);
|
|
431
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
|
+
}
|
|
432
525
|
getReexecuteFailureReason(err) {
|
|
433
|
-
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) {
|
|
434
531
|
return 'state_mismatch';
|
|
435
532
|
} else if (err instanceof ReExFailedTxsError) {
|
|
436
533
|
return 'failed_txs';
|
|
@@ -464,6 +561,12 @@ export class BlockProposalHandler {
|
|
|
464
561
|
const parentBlockNumber = BlockNumber(blockNumber - 1);
|
|
465
562
|
await this.worldState.syncImmediate(parentBlockNumber);
|
|
466
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
|
+
}
|
|
467
570
|
// Build checkpoint constants from proposal (excludes blockNumber which is per-block)
|
|
468
571
|
const constants = {
|
|
469
572
|
chainId: new Fr(config.l1ChainId),
|
|
@@ -478,16 +581,22 @@ export class BlockProposalHandler {
|
|
|
478
581
|
const checkpointBuilder = await this.checkpointsBuilder.openCheckpoint(checkpointNumber, constants, 0n, l1ToL2Messages, previousCheckpointOutHashes, fork, priorBlocks, this.log.getBindings());
|
|
479
582
|
// Build the new block
|
|
480
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;
|
|
481
585
|
const result = await checkpointBuilder.buildBlock(txs, blockNumber, blockHeader.globalVariables.timestamp, {
|
|
586
|
+
isBuildingProposal: false,
|
|
587
|
+
minValidTxs: 0,
|
|
482
588
|
deadline,
|
|
483
|
-
expectedEndState: blockHeader.state
|
|
589
|
+
expectedEndState: blockHeader.state,
|
|
590
|
+
maxTransactions: this.config.validateMaxTxsPerBlock,
|
|
591
|
+
maxBlockGas
|
|
484
592
|
});
|
|
485
593
|
const { block, failedTxs } = result;
|
|
486
594
|
const numFailedTxs = failedTxs.length;
|
|
487
|
-
this.log.verbose(`
|
|
595
|
+
this.log.verbose(`Block proposal ${blockNumber} at slot ${slot} transaction re-execution complete`, {
|
|
488
596
|
numFailedTxs,
|
|
489
597
|
numProposalTxs: txHashes.length,
|
|
490
598
|
numProcessedTxs: block.body.txEffects.length,
|
|
599
|
+
blockNumber,
|
|
491
600
|
slot
|
|
492
601
|
});
|
|
493
602
|
if (numFailedTxs > 0) {
|
|
@@ -529,4 +638,215 @@ export class BlockProposalHandler {
|
|
|
529
638
|
if (result) await result;
|
|
530
639
|
}
|
|
531
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);
|
|
850
|
+
}
|
|
851
|
+
}
|
|
532
852
|
}
|
package/dest/validator.d.ts
CHANGED
|
@@ -4,7 +4,7 @@ import { BlockNumber, CheckpointNumber, IndexWithinCheckpoint, SlotNumber } from
|
|
|
4
4
|
import { Fr } from '@aztec/foundation/curves/bn254';
|
|
5
5
|
import type { EthAddress } from '@aztec/foundation/eth-address';
|
|
6
6
|
import type { Signature } from '@aztec/foundation/eth-signature';
|
|
7
|
-
import { type
|
|
7
|
+
import { type Logger } from '@aztec/foundation/log';
|
|
8
8
|
import { DateProvider } from '@aztec/foundation/timer';
|
|
9
9
|
import type { KeystoreManager } from '@aztec/node-keystore';
|
|
10
10
|
import type { P2P, PeerId } from '@aztec/p2p';
|
|
@@ -12,17 +12,17 @@ import { type Watcher, type WatcherEmitter } from '@aztec/slasher';
|
|
|
12
12
|
import type { AztecAddress } from '@aztec/stdlib/aztec-address';
|
|
13
13
|
import type { CommitteeAttestationsAndSigners, L2BlockSink, L2BlockSource } from '@aztec/stdlib/block';
|
|
14
14
|
import type { CreateCheckpointProposalLastBlockData, ITxProvider, Validator, ValidatorClientFullConfig, WorldStateSynchronizer } from '@aztec/stdlib/interfaces/server';
|
|
15
|
-
import {
|
|
15
|
+
import type { L1ToL2MessageSource } from '@aztec/stdlib/messaging';
|
|
16
16
|
import { type BlockProposal, type BlockProposalOptions, type CheckpointAttestation, CheckpointProposal, type CheckpointProposalCore, type CheckpointProposalOptions } from '@aztec/stdlib/p2p';
|
|
17
17
|
import type { CheckpointHeader } from '@aztec/stdlib/rollup';
|
|
18
18
|
import type { BlockHeader, Tx } from '@aztec/stdlib/tx';
|
|
19
19
|
import { type TelemetryClient, type Tracer } from '@aztec/telemetry-client';
|
|
20
|
-
import { type SigningContext } from '@aztec/validator-ha-signer/types';
|
|
20
|
+
import { type SigningContext, type SlashingProtectionDatabase } from '@aztec/validator-ha-signer/types';
|
|
21
21
|
import type { ValidatorHASigner } from '@aztec/validator-ha-signer/validator-ha-signer';
|
|
22
22
|
import type { TypedDataDefinition } from 'viem';
|
|
23
|
-
import { BlockProposalHandler } from './block_proposal_handler.js';
|
|
24
23
|
import type { FullNodeCheckpointsBuilder } from './checkpoint_builder.js';
|
|
25
24
|
import type { ExtendedValidatorKeyStore } from './key_store/interface.js';
|
|
25
|
+
import { ProposalHandler } from './proposal_handler.js';
|
|
26
26
|
declare const ValidatorClient_base: new () => WatcherEmitter;
|
|
27
27
|
/**
|
|
28
28
|
* Validator Client
|
|
@@ -31,11 +31,7 @@ export declare class ValidatorClient extends ValidatorClient_base implements Val
|
|
|
31
31
|
private keyStore;
|
|
32
32
|
private epochCache;
|
|
33
33
|
private p2pClient;
|
|
34
|
-
private
|
|
35
|
-
private blockSource;
|
|
36
|
-
private checkpointsBuilder;
|
|
37
|
-
private worldState;
|
|
38
|
-
private l1ToL2MessageSource;
|
|
34
|
+
private proposalHandler;
|
|
39
35
|
private config;
|
|
40
36
|
private blobClient;
|
|
41
37
|
private haSigner;
|
|
@@ -51,15 +47,17 @@ export declare class ValidatorClient extends ValidatorClient_base implements Val
|
|
|
51
47
|
private lastProposedCheckpoint?;
|
|
52
48
|
private lastEpochForCommitteeUpdateLoop;
|
|
53
49
|
private epochCacheUpdateLoop;
|
|
50
|
+
/** Tracks the last epoch in which each attester successfully submitted at least one attestation. */
|
|
51
|
+
private lastAttestedEpochByAttester;
|
|
54
52
|
private proposersOfInvalidBlocks;
|
|
55
53
|
/** Tracks the last checkpoint proposal we attested to, to prevent equivocation. */
|
|
56
54
|
private lastAttestedProposal?;
|
|
57
|
-
protected constructor(keyStore: ExtendedValidatorKeyStore, epochCache: EpochCache, p2pClient: P2P,
|
|
55
|
+
protected constructor(keyStore: ExtendedValidatorKeyStore, epochCache: EpochCache, p2pClient: P2P, proposalHandler: ProposalHandler, config: ValidatorClientFullConfig, blobClient: BlobClientInterface, haSigner: ValidatorHASigner | undefined, dateProvider?: DateProvider, telemetry?: TelemetryClient, log?: Logger);
|
|
58
56
|
static validateKeyStoreConfiguration(keyStoreManager: KeystoreManager, logger?: Logger): void;
|
|
59
57
|
private handleEpochCommitteeUpdate;
|
|
60
|
-
static new(config: ValidatorClientFullConfig, checkpointsBuilder: FullNodeCheckpointsBuilder, worldState: WorldStateSynchronizer, epochCache: EpochCache, p2pClient: P2P, blockSource: L2BlockSource & L2BlockSink, l1ToL2MessageSource: L1ToL2MessageSource, txProvider: ITxProvider, keyStoreManager: KeystoreManager, blobClient: BlobClientInterface, dateProvider?: DateProvider, telemetry?: TelemetryClient): Promise<ValidatorClient>;
|
|
58
|
+
static new(config: ValidatorClientFullConfig, checkpointsBuilder: FullNodeCheckpointsBuilder, worldState: WorldStateSynchronizer, epochCache: EpochCache, p2pClient: P2P, blockSource: L2BlockSource & L2BlockSink, l1ToL2MessageSource: L1ToL2MessageSource, txProvider: ITxProvider, keyStoreManager: KeystoreManager, blobClient: BlobClientInterface, dateProvider?: DateProvider, telemetry?: TelemetryClient, slashingProtectionDb?: SlashingProtectionDatabase): Promise<ValidatorClient>;
|
|
61
59
|
getValidatorAddresses(): EthAddress[];
|
|
62
|
-
|
|
60
|
+
getProposalHandler(): ProposalHandler;
|
|
63
61
|
signWithAddress(addr: EthAddress, msg: TypedDataDefinition, context: SigningContext): Promise<Signature>;
|
|
64
62
|
getCoinbaseForAttestor(attestor: EthAddress): EthAddress;
|
|
65
63
|
getFeeRecipientForAttestor(attestor: EthAddress): AztecAddress;
|
|
@@ -89,15 +87,6 @@ export declare class ValidatorClient extends ValidatorClient_base implements Val
|
|
|
89
87
|
*/
|
|
90
88
|
private shouldAttestToSlot;
|
|
91
89
|
private createCheckpointAttestationsFromProposal;
|
|
92
|
-
private validateCheckpointProposal;
|
|
93
|
-
/**
|
|
94
|
-
* Extract checkpoint global variables from a block.
|
|
95
|
-
*/
|
|
96
|
-
private extractCheckpointConstants;
|
|
97
|
-
/**
|
|
98
|
-
* Uploads blobs for a checkpoint to the filestore (fire and forget).
|
|
99
|
-
*/
|
|
100
|
-
protected uploadBlobsForCheckpoint(proposal: CheckpointProposalCore, proposalInfo: LogData): Promise<void>;
|
|
101
90
|
private slashInvalidBlock;
|
|
102
91
|
/**
|
|
103
92
|
* Handle detection of a duplicate proposal (equivocation).
|
|
@@ -118,4 +107,4 @@ export declare class ValidatorClient extends ValidatorClient_base implements Val
|
|
|
118
107
|
private handleAuthRequest;
|
|
119
108
|
}
|
|
120
109
|
export {};
|
|
121
|
-
//# sourceMappingURL=data:application/json;base64,
|
|
110
|
+
//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoidmFsaWRhdG9yLmQudHMiLCJzb3VyY2VSb290IjoiIiwic291cmNlcyI6WyIuLi9zcmMvdmFsaWRhdG9yLnRzIl0sIm5hbWVzIjpbXSwibWFwcGluZ3MiOiJBQUFBLE9BQU8sS0FBSyxFQUFFLG1CQUFtQixFQUFFLE1BQU0sMkJBQTJCLENBQUM7QUFDckUsT0FBTyxLQUFLLEVBQUUsVUFBVSxFQUFFLE1BQU0sb0JBQW9CLENBQUM7QUFDckQsT0FBTyxFQUNMLFdBQVcsRUFDWCxnQkFBZ0IsRUFFaEIscUJBQXFCLEVBQ3JCLFVBQVUsRUFDWCxNQUFNLGlDQUFpQyxDQUFDO0FBQ3pDLE9BQU8sRUFBRSxFQUFFLEVBQUUsTUFBTSxnQ0FBZ0MsQ0FBQztBQUNwRCxPQUFPLEtBQUssRUFBRSxVQUFVLEVBQUUsTUFBTSwrQkFBK0IsQ0FBQztBQUNoRSxPQUFPLEtBQUssRUFBRSxTQUFTLEVBQUUsTUFBTSxpQ0FBaUMsQ0FBQztBQUNqRSxPQUFPLEVBQUUsS0FBSyxNQUFNLEVBQWdCLE1BQU0sdUJBQXVCLENBQUM7QUFHbEUsT0FBTyxFQUFFLFlBQVksRUFBRSxNQUFNLHlCQUF5QixDQUFDO0FBQ3ZELE9BQU8sS0FBSyxFQUFFLGVBQWUsRUFBRSxNQUFNLHNCQUFzQixDQUFDO0FBQzVELE9BQU8sS0FBSyxFQUFtRCxHQUFHLEVBQUUsTUFBTSxFQUFFLE1BQU0sWUFBWSxDQUFDO0FBRS9GLE9BQU8sRUFBb0MsS0FBSyxPQUFPLEVBQUUsS0FBSyxjQUFjLEVBQUUsTUFBTSxnQkFBZ0IsQ0FBQztBQUNyRyxPQUFPLEtBQUssRUFBRSxZQUFZLEVBQUUsTUFBTSw2QkFBNkIsQ0FBQztBQUNoRSxPQUFPLEtBQUssRUFBRSwrQkFBK0IsRUFBRSxXQUFXLEVBQUUsYUFBYSxFQUFFLE1BQU0scUJBQXFCLENBQUM7QUFFdkcsT0FBTyxLQUFLLEVBQ1YscUNBQXFDLEVBQ3JDLFdBQVcsRUFDWCxTQUFTLEVBQ1QseUJBQXlCLEVBQ3pCLHNCQUFzQixFQUN2QixNQUFNLGlDQUFpQyxDQUFD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
|