@aztec/validator-client 0.0.1-commit.7cf39cb55 → 0.0.1-commit.7ffbba4
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 +42 -0
- package/dest/block_proposal_handler.d.ts +3 -3
- package/dest/block_proposal_handler.d.ts.map +1 -1
- package/dest/block_proposal_handler.js +63 -58
- package/dest/checkpoint_builder.d.ts +15 -5
- package/dest/checkpoint_builder.d.ts.map +1 -1
- package/dest/checkpoint_builder.js +80 -25
- package/dest/config.d.ts +1 -1
- package/dest/config.d.ts.map +1 -1
- package/dest/config.js +22 -1
- 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 +5 -11
- package/dest/factory.d.ts +1 -1
- package/dest/factory.d.ts.map +1 -1
- package/dest/factory.js +2 -1
- package/dest/index.d.ts +1 -2
- package/dest/index.d.ts.map +1 -1
- package/dest/index.js +0 -1
- package/dest/metrics.d.ts +9 -1
- package/dest/metrics.d.ts.map +1 -1
- package/dest/metrics.js +12 -0
- package/dest/validator.d.ts +13 -5
- package/dest/validator.d.ts.map +1 -1
- package/dest/validator.js +79 -21
- package/package.json +19 -19
- package/src/block_proposal_handler.ts +75 -76
- package/src/checkpoint_builder.ts +90 -14
- package/src/config.ts +22 -1
- package/src/duties/validation_service.ts +11 -10
- package/src/factory.ts +1 -0
- package/src/index.ts +0 -1
- package/src/metrics.ts +18 -0
- package/src/validator.ts +80 -16
- 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
package/dest/validator.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { getBlobsPerL1Block } from '@aztec/blob-lib';
|
|
2
|
+
import { validateFeeAssetPriceModifier } from '@aztec/ethereum/contracts';
|
|
2
3
|
import { BlockNumber, SlotNumber } from '@aztec/foundation/branded-types';
|
|
3
4
|
import { TimeoutError } from '@aztec/foundation/error';
|
|
4
5
|
import { createLogger } from '@aztec/foundation/log';
|
|
@@ -8,11 +9,12 @@ import { sleep } from '@aztec/foundation/sleep';
|
|
|
8
9
|
import { DateProvider } from '@aztec/foundation/timer';
|
|
9
10
|
import { AuthRequest, AuthResponse, BlockProposalValidator, ReqRespSubProtocol } from '@aztec/p2p';
|
|
10
11
|
import { OffenseType, WANT_TO_SLASH_EVENT } from '@aztec/slasher';
|
|
12
|
+
import { validateCheckpoint } from '@aztec/stdlib/checkpoint';
|
|
11
13
|
import { getEpochAtSlot, getTimestampForSlot } from '@aztec/stdlib/epoch-helpers';
|
|
12
14
|
import { accumulateCheckpointOutHashes } from '@aztec/stdlib/messaging';
|
|
13
15
|
import { AttestationTimeoutError } from '@aztec/stdlib/validators';
|
|
14
16
|
import { getTelemetryClient } from '@aztec/telemetry-client';
|
|
15
|
-
import { createHASigner } from '@aztec/validator-ha-signer/factory';
|
|
17
|
+
import { createHASigner, createLocalSignerWithProtection } from '@aztec/validator-ha-signer/factory';
|
|
16
18
|
import { DutyType } from '@aztec/validator-ha-signer/types';
|
|
17
19
|
import { EventEmitter } from 'events';
|
|
18
20
|
import { BlockProposalHandler } from './block_proposal_handler.js';
|
|
@@ -41,6 +43,7 @@ const SLASHABLE_BLOCK_PROPOSAL_VALIDATION_RESULT = [
|
|
|
41
43
|
l1ToL2MessageSource;
|
|
42
44
|
config;
|
|
43
45
|
blobClient;
|
|
46
|
+
slashingProtectionSigner;
|
|
44
47
|
dateProvider;
|
|
45
48
|
tracer;
|
|
46
49
|
validationService;
|
|
@@ -52,10 +55,11 @@ const SLASHABLE_BLOCK_PROPOSAL_VALIDATION_RESULT = [
|
|
|
52
55
|
/** Tracks the last checkpoint proposal we created. */ lastProposedCheckpoint;
|
|
53
56
|
lastEpochForCommitteeUpdateLoop;
|
|
54
57
|
epochCacheUpdateLoop;
|
|
58
|
+
/** Tracks the last epoch in which each attester successfully submitted at least one attestation. */ lastAttestedEpochByAttester;
|
|
55
59
|
proposersOfInvalidBlocks;
|
|
56
60
|
/** Tracks the last checkpoint proposal we attested to, to prevent equivocation. */ lastAttestedProposal;
|
|
57
|
-
constructor(keyStore, epochCache, p2pClient, blockProposalHandler, blockSource, checkpointsBuilder, worldState, l1ToL2MessageSource, config, blobClient, dateProvider = new DateProvider(), telemetry = getTelemetryClient(), log = createLogger('validator')){
|
|
58
|
-
super(), this.keyStore = keyStore, this.epochCache = epochCache, this.p2pClient = p2pClient, this.blockProposalHandler = blockProposalHandler, this.blockSource = blockSource, this.checkpointsBuilder = checkpointsBuilder, this.worldState = worldState, this.l1ToL2MessageSource = l1ToL2MessageSource, this.config = config, this.blobClient = blobClient, this.dateProvider = dateProvider, this.hasRegisteredHandlers = false, this.proposersOfInvalidBlocks = new Set();
|
|
61
|
+
constructor(keyStore, epochCache, p2pClient, blockProposalHandler, blockSource, checkpointsBuilder, worldState, l1ToL2MessageSource, config, blobClient, slashingProtectionSigner, dateProvider = new DateProvider(), telemetry = getTelemetryClient(), log = createLogger('validator')){
|
|
62
|
+
super(), this.keyStore = keyStore, this.epochCache = epochCache, this.p2pClient = p2pClient, this.blockProposalHandler = blockProposalHandler, this.blockSource = blockSource, this.checkpointsBuilder = checkpointsBuilder, this.worldState = worldState, this.l1ToL2MessageSource = l1ToL2MessageSource, this.config = config, this.blobClient = blobClient, this.slashingProtectionSigner = slashingProtectionSigner, this.dateProvider = dateProvider, this.hasRegisteredHandlers = false, this.lastAttestedEpochByAttester = new Map(), this.proposersOfInvalidBlocks = new Set();
|
|
59
63
|
// Create child logger with fisherman prefix if in fisherman mode
|
|
60
64
|
this.log = config.fishermanMode ? log.createChild('[FISHERMAN]') : log;
|
|
61
65
|
this.tracer = telemetry.getTracer('Validator');
|
|
@@ -94,6 +98,7 @@ const SLASHABLE_BLOCK_PROPOSAL_VALIDATION_RESULT = [
|
|
|
94
98
|
this.log.trace(`No committee found for slot`);
|
|
95
99
|
return;
|
|
96
100
|
}
|
|
101
|
+
this.metrics.setCurrentEpoch(epoch);
|
|
97
102
|
if (epoch !== this.lastEpochForCommitteeUpdateLoop) {
|
|
98
103
|
const me = this.getValidatorAddresses();
|
|
99
104
|
const committeeSet = new Set(committee.map((v)=>v.toString()));
|
|
@@ -112,20 +117,33 @@ const SLASHABLE_BLOCK_PROPOSAL_VALIDATION_RESULT = [
|
|
|
112
117
|
static async new(config, checkpointsBuilder, worldState, epochCache, p2pClient, blockSource, l1ToL2MessageSource, txProvider, keyStoreManager, blobClient, dateProvider = new DateProvider(), telemetry = getTelemetryClient()) {
|
|
113
118
|
const metrics = new ValidatorMetrics(telemetry);
|
|
114
119
|
const blockProposalValidator = new BlockProposalValidator(epochCache, {
|
|
115
|
-
txsPermitted: !config.disableTransactions
|
|
120
|
+
txsPermitted: !config.disableTransactions,
|
|
121
|
+
maxTxsPerBlock: config.validateMaxTxsPerBlock
|
|
116
122
|
});
|
|
117
123
|
const blockProposalHandler = new BlockProposalHandler(checkpointsBuilder, worldState, blockSource, l1ToL2MessageSource, txProvider, blockProposalValidator, epochCache, config, metrics, dateProvider, telemetry);
|
|
118
|
-
|
|
124
|
+
const nodeKeystoreAdapter = NodeKeystoreAdapter.fromKeyStoreManager(keyStoreManager);
|
|
125
|
+
let slashingProtectionSigner;
|
|
119
126
|
if (config.haSigningEnabled) {
|
|
127
|
+
// Multi-node HA mode: use PostgreSQL-backed distributed locking.
|
|
120
128
|
// If maxStuckDutiesAgeMs is not explicitly set, compute it from Aztec slot duration
|
|
121
129
|
const haConfig = {
|
|
122
130
|
...config,
|
|
123
131
|
maxStuckDutiesAgeMs: config.maxStuckDutiesAgeMs ?? epochCache.getL1Constants().slotDuration * 2 * 1000
|
|
124
132
|
};
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
133
|
+
({ signer: slashingProtectionSigner } = await createHASigner(haConfig, {
|
|
134
|
+
telemetryClient: telemetry,
|
|
135
|
+
dateProvider
|
|
136
|
+
}));
|
|
137
|
+
} else {
|
|
138
|
+
// Single-node mode: use LMDB-backed local signing protection.
|
|
139
|
+
// This prevents double-signing if the node crashes and restarts mid-proposal.
|
|
140
|
+
({ signer: slashingProtectionSigner } = await createLocalSignerWithProtection(config, {
|
|
141
|
+
telemetryClient: telemetry,
|
|
142
|
+
dateProvider
|
|
143
|
+
}));
|
|
144
|
+
}
|
|
145
|
+
const validatorKeyStore = new HAKeyStore(nodeKeystoreAdapter, slashingProtectionSigner);
|
|
146
|
+
const validator = new ValidatorClient(validatorKeyStore, epochCache, p2pClient, blockProposalHandler, blockSource, checkpointsBuilder, worldState, l1ToL2MessageSource, config, blobClient, slashingProtectionSigner, dateProvider, telemetry);
|
|
129
147
|
return validator;
|
|
130
148
|
}
|
|
131
149
|
getValidatorAddresses() {
|
|
@@ -152,6 +170,11 @@ const SLASHABLE_BLOCK_PROPOSAL_VALIDATION_RESULT = [
|
|
|
152
170
|
...config
|
|
153
171
|
};
|
|
154
172
|
}
|
|
173
|
+
reloadKeystore(newManager) {
|
|
174
|
+
const newAdapter = NodeKeystoreAdapter.fromKeyStoreManager(newManager);
|
|
175
|
+
this.keyStore = new HAKeyStore(newAdapter, this.slashingProtectionSigner);
|
|
176
|
+
this.validationService = new ValidationService(this.keyStore, this.log.createChild('validation-service'));
|
|
177
|
+
}
|
|
155
178
|
async start() {
|
|
156
179
|
if (this.epochCacheUpdateLoop.isRunning()) {
|
|
157
180
|
this.log.warn(`Validator client already started`);
|
|
@@ -299,18 +322,21 @@ const SLASHABLE_BLOCK_PROPOSAL_VALIDATION_RESULT = [
|
|
|
299
322
|
});
|
|
300
323
|
return undefined;
|
|
301
324
|
}
|
|
325
|
+
// Validate fee asset price modifier is within allowed range
|
|
326
|
+
if (!validateFeeAssetPriceModifier(proposal.feeAssetPriceModifier)) {
|
|
327
|
+
this.log.warn(`Received checkpoint proposal with invalid feeAssetPriceModifier ${proposal.feeAssetPriceModifier} for slot ${slotNumber}`);
|
|
328
|
+
return undefined;
|
|
329
|
+
}
|
|
302
330
|
// Check that I have any address in current committee before attesting
|
|
303
331
|
const inCommittee = await this.epochCache.filterInCommittee(slotNumber, this.getValidatorAddresses());
|
|
304
332
|
const partOfCommittee = inCommittee.length > 0;
|
|
305
333
|
const proposalInfo = {
|
|
306
334
|
slotNumber,
|
|
307
335
|
archive: proposal.archive.toString(),
|
|
308
|
-
proposer: proposer.toString()
|
|
309
|
-
txCount: proposal.txHashes.length
|
|
336
|
+
proposer: proposer.toString()
|
|
310
337
|
};
|
|
311
338
|
this.log.info(`Received checkpoint proposal for slot ${slotNumber}`, {
|
|
312
339
|
...proposalInfo,
|
|
313
|
-
txHashes: proposal.txHashes.map((t)=>t.toString()),
|
|
314
340
|
fishermanMode: this.config.fishermanMode || false
|
|
315
341
|
});
|
|
316
342
|
// Validate the checkpoint proposal before attesting (unless skipCheckpointProposalValidation is set)
|
|
@@ -340,6 +366,16 @@ const SLASHABLE_BLOCK_PROPOSAL_VALIDATION_RESULT = [
|
|
|
340
366
|
fishermanMode: this.config.fishermanMode || false
|
|
341
367
|
});
|
|
342
368
|
this.metrics.incSuccessfulAttestations(inCommittee.length);
|
|
369
|
+
// Track epoch participation per attester: count each (attester, epoch) pair at most once
|
|
370
|
+
const proposalEpoch = getEpochAtSlot(slotNumber, this.epochCache.getL1Constants());
|
|
371
|
+
for (const attester of inCommittee){
|
|
372
|
+
const key = attester.toString();
|
|
373
|
+
const lastEpoch = this.lastAttestedEpochByAttester.get(key);
|
|
374
|
+
if (lastEpoch === undefined || proposalEpoch > lastEpoch) {
|
|
375
|
+
this.lastAttestedEpochByAttester.set(key, proposalEpoch);
|
|
376
|
+
this.metrics.incAttestedEpochCount(attester);
|
|
377
|
+
}
|
|
378
|
+
}
|
|
343
379
|
// Determine which validators should attest
|
|
344
380
|
let attestors;
|
|
345
381
|
if (partOfCommittee) {
|
|
@@ -436,6 +472,14 @@ const SLASHABLE_BLOCK_PROPOSAL_VALIDATION_RESULT = [
|
|
|
436
472
|
reason: 'no_blocks_for_slot'
|
|
437
473
|
};
|
|
438
474
|
}
|
|
475
|
+
// Ensure the last block for this slot matches the archive in the checkpoint proposal
|
|
476
|
+
if (!blocks.at(-1)?.archive.root.equals(proposal.archive)) {
|
|
477
|
+
this.log.warn(`Last block archive mismatch for checkpoint proposal`, proposalInfo);
|
|
478
|
+
return {
|
|
479
|
+
isValid: false,
|
|
480
|
+
reason: 'last_block_archive_mismatch'
|
|
481
|
+
};
|
|
482
|
+
}
|
|
439
483
|
this.log.debug(`Found ${blocks.length} blocks for slot ${slot}`, {
|
|
440
484
|
...proposalInfo,
|
|
441
485
|
blockNumbers: blocks.map((b)=>b.number)
|
|
@@ -446,18 +490,15 @@ const SLASHABLE_BLOCK_PROPOSAL_VALIDATION_RESULT = [
|
|
|
446
490
|
const checkpointNumber = firstBlock.checkpointNumber;
|
|
447
491
|
// Get L1-to-L2 messages for this checkpoint
|
|
448
492
|
const l1ToL2Messages = await this.l1ToL2MessageSource.getL1ToL2Messages(checkpointNumber);
|
|
449
|
-
//
|
|
450
|
-
// TODO: There can be a more efficient way to get the previous checkpoint out hashes without having to fetch the
|
|
451
|
-
// actual checkpoints and the blocks/txs in them.
|
|
493
|
+
// Collect the out hashes of all the checkpoints before this one in the same epoch
|
|
452
494
|
const epoch = getEpochAtSlot(slot, this.epochCache.getL1Constants());
|
|
453
|
-
const
|
|
454
|
-
const previousCheckpointOutHashes = previousCheckpoints.map((c)=>c.getCheckpointOutHash());
|
|
495
|
+
const previousCheckpointOutHashes = (await this.blockSource.getCheckpointsDataForEpoch(epoch)).filter((c)=>c.checkpointNumber < checkpointNumber).map((c)=>c.checkpointOutHash);
|
|
455
496
|
// Fork world state at the block before the first block
|
|
456
497
|
const parentBlockNumber = BlockNumber(firstBlock.number - 1);
|
|
457
498
|
const fork = await this.worldState.fork(parentBlockNumber);
|
|
458
499
|
try {
|
|
459
500
|
// Create checkpoint builder with all existing blocks
|
|
460
|
-
const checkpointBuilder = await this.checkpointsBuilder.openCheckpoint(checkpointNumber, constants, l1ToL2Messages, previousCheckpointOutHashes, fork, blocks, this.log.getBindings());
|
|
501
|
+
const checkpointBuilder = await this.checkpointsBuilder.openCheckpoint(checkpointNumber, constants, proposal.feeAssetPriceModifier, l1ToL2Messages, previousCheckpointOutHashes, fork, blocks, this.log.getBindings());
|
|
461
502
|
// Complete the checkpoint to get computed values
|
|
462
503
|
const computedCheckpoint = await checkpointBuilder.completeCheckpoint();
|
|
463
504
|
// Compare checkpoint header with proposal
|
|
@@ -505,6 +546,22 @@ const SLASHABLE_BLOCK_PROPOSAL_VALIDATION_RESULT = [
|
|
|
505
546
|
reason: 'out_hash_mismatch'
|
|
506
547
|
};
|
|
507
548
|
}
|
|
549
|
+
// Final round of validations on the checkpoint, just in case.
|
|
550
|
+
try {
|
|
551
|
+
validateCheckpoint(computedCheckpoint, {
|
|
552
|
+
rollupManaLimit: this.checkpointsBuilder.getConfig().rollupManaLimit,
|
|
553
|
+
maxDABlockGas: this.config.validateMaxDABlockGas,
|
|
554
|
+
maxL2BlockGas: this.config.validateMaxL2BlockGas,
|
|
555
|
+
maxTxsPerBlock: this.config.validateMaxTxsPerBlock,
|
|
556
|
+
maxTxsPerCheckpoint: this.config.validateMaxTxsPerCheckpoint
|
|
557
|
+
});
|
|
558
|
+
} catch (err) {
|
|
559
|
+
this.log.warn(`Checkpoint validation failed: ${err}`, proposalInfo);
|
|
560
|
+
return {
|
|
561
|
+
isValid: false,
|
|
562
|
+
reason: 'checkpoint_validation_failed'
|
|
563
|
+
};
|
|
564
|
+
}
|
|
508
565
|
this.log.verbose(`Checkpoint proposal validation successful for slot ${slot}`, proposalInfo);
|
|
509
566
|
return {
|
|
510
567
|
isValid: true
|
|
@@ -521,6 +578,7 @@ const SLASHABLE_BLOCK_PROPOSAL_VALIDATION_RESULT = [
|
|
|
521
578
|
chainId: gv.chainId,
|
|
522
579
|
version: gv.version,
|
|
523
580
|
slotNumber: gv.slotNumber,
|
|
581
|
+
timestamp: gv.timestamp,
|
|
524
582
|
coinbase: gv.coinbase,
|
|
525
583
|
feeRecipient: gv.feeRecipient,
|
|
526
584
|
gasFees: gv.gasFees
|
|
@@ -541,7 +599,7 @@ const SLASHABLE_BLOCK_PROPOSAL_VALIDATION_RESULT = [
|
|
|
541
599
|
return;
|
|
542
600
|
}
|
|
543
601
|
const blobFields = blocks.flatMap((b)=>b.toBlobFields());
|
|
544
|
-
const blobs = getBlobsPerL1Block(blobFields);
|
|
602
|
+
const blobs = await getBlobsPerL1Block(blobFields);
|
|
545
603
|
await this.blobClient.sendBlobsToFilestore(blobs);
|
|
546
604
|
this.log.debug(`Uploaded ${blobs.length} blobs to filestore for checkpoint at slot ${proposal.slotNumber}`, {
|
|
547
605
|
...proposalInfo,
|
|
@@ -629,7 +687,7 @@ const SLASHABLE_BLOCK_PROPOSAL_VALIDATION_RESULT = [
|
|
|
629
687
|
this.lastProposedBlock = newProposal;
|
|
630
688
|
return newProposal;
|
|
631
689
|
}
|
|
632
|
-
async createCheckpointProposal(checkpointHeader, archive, lastBlockInfo, proposerAddress, options = {}) {
|
|
690
|
+
async createCheckpointProposal(checkpointHeader, archive, feeAssetPriceModifier, lastBlockInfo, proposerAddress, options = {}) {
|
|
633
691
|
// Validate that we're not creating a proposal for an older or equal slot
|
|
634
692
|
if (this.lastProposedCheckpoint) {
|
|
635
693
|
const lastSlot = this.lastProposedCheckpoint.slotNumber;
|
|
@@ -639,7 +697,7 @@ const SLASHABLE_BLOCK_PROPOSAL_VALIDATION_RESULT = [
|
|
|
639
697
|
}
|
|
640
698
|
}
|
|
641
699
|
this.log.info(`Assembling checkpoint proposal for slot ${checkpointHeader.slotNumber}`);
|
|
642
|
-
const newProposal = await this.validationService.createCheckpointProposal(checkpointHeader, archive, lastBlockInfo, proposerAddress, options);
|
|
700
|
+
const newProposal = await this.validationService.createCheckpointProposal(checkpointHeader, archive, feeAssetPriceModifier, lastBlockInfo, proposerAddress, options);
|
|
643
701
|
this.lastProposedCheckpoint = newProposal;
|
|
644
702
|
return newProposal;
|
|
645
703
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@aztec/validator-client",
|
|
3
|
-
"version": "0.0.1-commit.
|
|
3
|
+
"version": "0.0.1-commit.7ffbba4",
|
|
4
4
|
"main": "dest/index.js",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"exports": {
|
|
@@ -64,30 +64,30 @@
|
|
|
64
64
|
]
|
|
65
65
|
},
|
|
66
66
|
"dependencies": {
|
|
67
|
-
"@aztec/blob-client": "0.0.1-commit.
|
|
68
|
-
"@aztec/blob-lib": "0.0.1-commit.
|
|
69
|
-
"@aztec/constants": "0.0.1-commit.
|
|
70
|
-
"@aztec/epoch-cache": "0.0.1-commit.
|
|
71
|
-
"@aztec/ethereum": "0.0.1-commit.
|
|
72
|
-
"@aztec/foundation": "0.0.1-commit.
|
|
73
|
-
"@aztec/node-keystore": "0.0.1-commit.
|
|
74
|
-
"@aztec/noir-protocol-circuits-types": "0.0.1-commit.
|
|
75
|
-
"@aztec/p2p": "0.0.1-commit.
|
|
76
|
-
"@aztec/protocol-contracts": "0.0.1-commit.
|
|
77
|
-
"@aztec/prover-client": "0.0.1-commit.
|
|
78
|
-
"@aztec/simulator": "0.0.1-commit.
|
|
79
|
-
"@aztec/slasher": "0.0.1-commit.
|
|
80
|
-
"@aztec/stdlib": "0.0.1-commit.
|
|
81
|
-
"@aztec/telemetry-client": "0.0.1-commit.
|
|
82
|
-
"@aztec/validator-ha-signer": "0.0.1-commit.
|
|
67
|
+
"@aztec/blob-client": "0.0.1-commit.7ffbba4",
|
|
68
|
+
"@aztec/blob-lib": "0.0.1-commit.7ffbba4",
|
|
69
|
+
"@aztec/constants": "0.0.1-commit.7ffbba4",
|
|
70
|
+
"@aztec/epoch-cache": "0.0.1-commit.7ffbba4",
|
|
71
|
+
"@aztec/ethereum": "0.0.1-commit.7ffbba4",
|
|
72
|
+
"@aztec/foundation": "0.0.1-commit.7ffbba4",
|
|
73
|
+
"@aztec/node-keystore": "0.0.1-commit.7ffbba4",
|
|
74
|
+
"@aztec/noir-protocol-circuits-types": "0.0.1-commit.7ffbba4",
|
|
75
|
+
"@aztec/p2p": "0.0.1-commit.7ffbba4",
|
|
76
|
+
"@aztec/protocol-contracts": "0.0.1-commit.7ffbba4",
|
|
77
|
+
"@aztec/prover-client": "0.0.1-commit.7ffbba4",
|
|
78
|
+
"@aztec/simulator": "0.0.1-commit.7ffbba4",
|
|
79
|
+
"@aztec/slasher": "0.0.1-commit.7ffbba4",
|
|
80
|
+
"@aztec/stdlib": "0.0.1-commit.7ffbba4",
|
|
81
|
+
"@aztec/telemetry-client": "0.0.1-commit.7ffbba4",
|
|
82
|
+
"@aztec/validator-ha-signer": "0.0.1-commit.7ffbba4",
|
|
83
83
|
"koa": "^2.16.1",
|
|
84
84
|
"koa-router": "^13.1.1",
|
|
85
85
|
"tslib": "^2.4.0",
|
|
86
86
|
"viem": "npm:@aztec/viem@2.38.2"
|
|
87
87
|
},
|
|
88
88
|
"devDependencies": {
|
|
89
|
-
"@aztec/archiver": "0.0.1-commit.
|
|
90
|
-
"@aztec/world-state": "0.0.1-commit.
|
|
89
|
+
"@aztec/archiver": "0.0.1-commit.7ffbba4",
|
|
90
|
+
"@aztec/world-state": "0.0.1-commit.7ffbba4",
|
|
91
91
|
"@electric-sql/pglite": "^0.3.14",
|
|
92
92
|
"@jest/globals": "^30.0.0",
|
|
93
93
|
"@types/jest": "^30.0.0",
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { INITIAL_L2_BLOCK_NUM } from '@aztec/constants';
|
|
2
2
|
import type { EpochCache } from '@aztec/epoch-cache';
|
|
3
3
|
import { BlockNumber, CheckpointNumber, SlotNumber } from '@aztec/foundation/branded-types';
|
|
4
|
-
import {
|
|
4
|
+
import { pick } from '@aztec/foundation/collection';
|
|
5
5
|
import { Fr } from '@aztec/foundation/curves/bn254';
|
|
6
6
|
import { TimeoutError } from '@aztec/foundation/error';
|
|
7
7
|
import { createLogger } from '@aztec/foundation/log';
|
|
@@ -9,16 +9,13 @@ import { retryUntil } from '@aztec/foundation/retry';
|
|
|
9
9
|
import { DateProvider, Timer } from '@aztec/foundation/timer';
|
|
10
10
|
import type { P2P, PeerId } from '@aztec/p2p';
|
|
11
11
|
import { BlockProposalValidator } from '@aztec/p2p/msg_validators';
|
|
12
|
-
import type { L2Block, L2BlockSink, L2BlockSource } from '@aztec/stdlib/block';
|
|
12
|
+
import type { BlockData, L2Block, L2BlockSink, L2BlockSource } from '@aztec/stdlib/block';
|
|
13
13
|
import { getEpochAtSlot, getTimestampForSlot } from '@aztec/stdlib/epoch-helpers';
|
|
14
|
+
import { Gas } from '@aztec/stdlib/gas';
|
|
14
15
|
import type { ITxProvider, ValidatorClientFullConfig, WorldStateSynchronizer } from '@aztec/stdlib/interfaces/server';
|
|
15
|
-
import {
|
|
16
|
-
type L1ToL2MessageSource,
|
|
17
|
-
computeCheckpointOutHash,
|
|
18
|
-
computeInHashFromL1ToL2Messages,
|
|
19
|
-
} from '@aztec/stdlib/messaging';
|
|
16
|
+
import { type L1ToL2MessageSource, computeInHashFromL1ToL2Messages } from '@aztec/stdlib/messaging';
|
|
20
17
|
import type { BlockProposal } from '@aztec/stdlib/p2p';
|
|
21
|
-
import {
|
|
18
|
+
import type { CheckpointGlobalVariables, FailedTx, Tx } from '@aztec/stdlib/tx';
|
|
22
19
|
import {
|
|
23
20
|
ReExFailedTxsError,
|
|
24
21
|
ReExStateMismatchError,
|
|
@@ -92,25 +89,28 @@ export class BlockProposalHandler {
|
|
|
92
89
|
this.tracer = telemetry.getTracer('BlockProposalHandler');
|
|
93
90
|
}
|
|
94
91
|
|
|
95
|
-
|
|
96
|
-
// Non-validator handler that re-executes for monitoring but does not attest.
|
|
92
|
+
register(p2pClient: P2P, shouldReexecute: boolean): BlockProposalHandler {
|
|
93
|
+
// Non-validator handler that processes or re-executes for monitoring but does not attest.
|
|
97
94
|
// Returns boolean indicating whether the proposal was valid.
|
|
98
95
|
const handler = async (proposal: BlockProposal, proposalSender: PeerId): Promise<boolean> => {
|
|
99
96
|
try {
|
|
100
|
-
const
|
|
97
|
+
const { slotNumber, blockNumber } = proposal;
|
|
98
|
+
const result = await this.handleBlockProposal(proposal, proposalSender, shouldReexecute);
|
|
101
99
|
if (result.isValid) {
|
|
102
|
-
this.log.info(`Non-validator
|
|
100
|
+
this.log.info(`Non-validator block proposal ${blockNumber} at slot ${slotNumber} handled`, {
|
|
103
101
|
blockNumber: result.blockNumber,
|
|
102
|
+
slotNumber,
|
|
104
103
|
reexecutionTimeMs: result.reexecutionResult?.reexecutionTimeMs,
|
|
105
104
|
totalManaUsed: result.reexecutionResult?.totalManaUsed,
|
|
106
105
|
numTxs: result.reexecutionResult?.block?.body?.txEffects?.length ?? 0,
|
|
106
|
+
reexecuted: shouldReexecute,
|
|
107
107
|
});
|
|
108
108
|
return true;
|
|
109
109
|
} else {
|
|
110
|
-
this.log.warn(
|
|
111
|
-
blockNumber
|
|
112
|
-
reason: result.reason,
|
|
113
|
-
|
|
110
|
+
this.log.warn(
|
|
111
|
+
`Non-validator block proposal ${blockNumber} at slot ${slotNumber} failed processing with ${result.reason}`,
|
|
112
|
+
{ blockNumber: result.blockNumber, slotNumber, reason: result.reason },
|
|
113
|
+
);
|
|
114
114
|
return false;
|
|
115
115
|
}
|
|
116
116
|
} catch (error) {
|
|
@@ -153,16 +153,16 @@ export class BlockProposalHandler {
|
|
|
153
153
|
}
|
|
154
154
|
|
|
155
155
|
// Check that the parent proposal is a block we know, otherwise reexecution would fail
|
|
156
|
-
const
|
|
157
|
-
if (
|
|
156
|
+
const parentBlock = await this.getParentBlock(proposal);
|
|
157
|
+
if (parentBlock === undefined) {
|
|
158
158
|
this.log.warn(`Parent block for proposal not found, skipping processing`, proposalInfo);
|
|
159
159
|
return { isValid: false, reason: 'parent_block_not_found' };
|
|
160
160
|
}
|
|
161
161
|
|
|
162
162
|
// Check that the parent block's slot is not greater than the proposal's slot.
|
|
163
|
-
if (
|
|
163
|
+
if (parentBlock !== 'genesis' && parentBlock.header.getSlot() > slotNumber) {
|
|
164
164
|
this.log.warn(`Parent block slot is greater than proposal slot, skipping processing`, {
|
|
165
|
-
parentBlockSlot:
|
|
165
|
+
parentBlockSlot: parentBlock.header.getSlot().toString(),
|
|
166
166
|
proposalSlot: slotNumber.toString(),
|
|
167
167
|
...proposalInfo,
|
|
168
168
|
});
|
|
@@ -171,9 +171,9 @@ export class BlockProposalHandler {
|
|
|
171
171
|
|
|
172
172
|
// Compute the block number based on the parent block
|
|
173
173
|
const blockNumber =
|
|
174
|
-
|
|
174
|
+
parentBlock === 'genesis'
|
|
175
175
|
? BlockNumber(INITIAL_L2_BLOCK_NUM)
|
|
176
|
-
: BlockNumber(
|
|
176
|
+
: BlockNumber(parentBlock.header.getBlockNumber() + 1);
|
|
177
177
|
|
|
178
178
|
// Check that this block number does not exist already
|
|
179
179
|
const existingBlock = await this.blockSource.getBlockHeader(blockNumber);
|
|
@@ -189,8 +189,17 @@ export class BlockProposalHandler {
|
|
|
189
189
|
deadline: this.getReexecutionDeadline(slotNumber, config),
|
|
190
190
|
});
|
|
191
191
|
|
|
192
|
+
// If reexecution is disabled, bail. We are just interested in triggering tx collection.
|
|
193
|
+
if (!shouldReexecute) {
|
|
194
|
+
this.log.info(
|
|
195
|
+
`Received valid block ${blockNumber} proposal at index ${proposal.indexWithinCheckpoint} on slot ${slotNumber}`,
|
|
196
|
+
proposalInfo,
|
|
197
|
+
);
|
|
198
|
+
return { isValid: true, blockNumber };
|
|
199
|
+
}
|
|
200
|
+
|
|
192
201
|
// Compute the checkpoint number for this block and validate checkpoint consistency
|
|
193
|
-
const checkpointResult =
|
|
202
|
+
const checkpointResult = this.computeCheckpointNumber(proposal, parentBlock, proposalInfo);
|
|
194
203
|
if (checkpointResult.reason) {
|
|
195
204
|
return { isValid: false, blockNumber, reason: checkpointResult.reason };
|
|
196
205
|
}
|
|
@@ -215,36 +224,28 @@ export class BlockProposalHandler {
|
|
|
215
224
|
return { isValid: false, blockNumber, reason: 'txs_not_available' };
|
|
216
225
|
}
|
|
217
226
|
|
|
227
|
+
// Collect the out hashes of all the checkpoints before this one in the same epoch
|
|
228
|
+
const epoch = getEpochAtSlot(slotNumber, this.epochCache.getL1Constants());
|
|
229
|
+
const previousCheckpointOutHashes = (await this.blockSource.getCheckpointsDataForEpoch(epoch))
|
|
230
|
+
.filter(c => c.checkpointNumber < checkpointNumber)
|
|
231
|
+
.map(c => c.checkpointOutHash);
|
|
232
|
+
|
|
218
233
|
// Try re-executing the transactions in the proposal if needed
|
|
219
234
|
let reexecutionResult;
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
const previousCheckpointOutHashes = blocksByCheckpoint.map(checkpointBlocks =>
|
|
230
|
-
computeCheckpointOutHash(checkpointBlocks.map(b => b.block.body.txEffects.map(tx => tx.l2ToL1Msgs))),
|
|
235
|
+
try {
|
|
236
|
+
this.log.verbose(`Re-executing transactions in the proposal`, proposalInfo);
|
|
237
|
+
reexecutionResult = await this.reexecuteTransactions(
|
|
238
|
+
proposal,
|
|
239
|
+
blockNumber,
|
|
240
|
+
checkpointNumber,
|
|
241
|
+
txs,
|
|
242
|
+
l1ToL2Messages,
|
|
243
|
+
previousCheckpointOutHashes,
|
|
231
244
|
);
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
proposal,
|
|
237
|
-
blockNumber,
|
|
238
|
-
checkpointNumber,
|
|
239
|
-
txs,
|
|
240
|
-
l1ToL2Messages,
|
|
241
|
-
previousCheckpointOutHashes,
|
|
242
|
-
);
|
|
243
|
-
} catch (error) {
|
|
244
|
-
this.log.error(`Error reexecuting txs while processing block proposal`, error, proposalInfo);
|
|
245
|
-
const reason = this.getReexecuteFailureReason(error);
|
|
246
|
-
return { isValid: false, blockNumber, reason, reexecutionResult };
|
|
247
|
-
}
|
|
245
|
+
} catch (error) {
|
|
246
|
+
this.log.error(`Error reexecuting txs while processing block proposal`, error, proposalInfo);
|
|
247
|
+
const reason = this.getReexecuteFailureReason(error);
|
|
248
|
+
return { isValid: false, blockNumber, reason, reexecutionResult };
|
|
248
249
|
}
|
|
249
250
|
|
|
250
251
|
// If we succeeded, push this block into the archiver (unless disabled)
|
|
@@ -253,14 +254,14 @@ export class BlockProposalHandler {
|
|
|
253
254
|
}
|
|
254
255
|
|
|
255
256
|
this.log.info(
|
|
256
|
-
`Successfully
|
|
257
|
-
proposalInfo,
|
|
257
|
+
`Successfully re-executed block ${blockNumber} proposal at index ${proposal.indexWithinCheckpoint} on slot ${slotNumber}`,
|
|
258
|
+
{ ...proposalInfo, ...pick(reexecutionResult, 'reexecutionTimeMs', 'totalManaUsed') },
|
|
258
259
|
);
|
|
259
260
|
|
|
260
261
|
return { isValid: true, blockNumber, reexecutionResult };
|
|
261
262
|
}
|
|
262
263
|
|
|
263
|
-
private async getParentBlock(proposal: BlockProposal): Promise<'genesis' |
|
|
264
|
+
private async getParentBlock(proposal: BlockProposal): Promise<'genesis' | BlockData | undefined> {
|
|
264
265
|
const parentArchive = proposal.blockHeader.lastArchive.root;
|
|
265
266
|
const slot = proposal.slotNumber;
|
|
266
267
|
const config = this.checkpointsBuilder.getConfig();
|
|
@@ -276,12 +277,11 @@ export class BlockProposalHandler {
|
|
|
276
277
|
|
|
277
278
|
try {
|
|
278
279
|
return (
|
|
279
|
-
(await this.blockSource.
|
|
280
|
+
(await this.blockSource.getBlockDataByArchive(parentArchive)) ??
|
|
280
281
|
(timeoutDurationMs <= 0
|
|
281
282
|
? undefined
|
|
282
283
|
: await retryUntil(
|
|
283
|
-
() =>
|
|
284
|
-
this.blockSource.syncImmediate().then(() => this.blockSource.getBlockHeaderByArchive(parentArchive)),
|
|
284
|
+
() => this.blockSource.syncImmediate().then(() => this.blockSource.getBlockDataByArchive(parentArchive)),
|
|
285
285
|
'force archiver sync',
|
|
286
286
|
timeoutDurationMs / 1000,
|
|
287
287
|
0.5,
|
|
@@ -297,12 +297,12 @@ export class BlockProposalHandler {
|
|
|
297
297
|
}
|
|
298
298
|
}
|
|
299
299
|
|
|
300
|
-
private
|
|
300
|
+
private computeCheckpointNumber(
|
|
301
301
|
proposal: BlockProposal,
|
|
302
|
-
|
|
302
|
+
parentBlock: 'genesis' | BlockData,
|
|
303
303
|
proposalInfo: object,
|
|
304
|
-
):
|
|
305
|
-
if (
|
|
304
|
+
): CheckpointComputationResult {
|
|
305
|
+
if (parentBlock === 'genesis') {
|
|
306
306
|
// First block is in checkpoint 1
|
|
307
307
|
if (proposal.indexWithinCheckpoint !== 0) {
|
|
308
308
|
this.log.warn(`First block proposal has non-zero indexWithinCheckpoint`, proposalInfo);
|
|
@@ -311,19 +311,9 @@ export class BlockProposalHandler {
|
|
|
311
311
|
return { checkpointNumber: CheckpointNumber.INITIAL };
|
|
312
312
|
}
|
|
313
313
|
|
|
314
|
-
// Get the parent block to find its checkpoint number
|
|
315
|
-
// TODO(palla/mbps): The block header should include the checkpoint number to avoid this lookup,
|
|
316
|
-
// or at least the L2BlockSource should return a different struct that includes it.
|
|
317
|
-
const parentBlockNumber = parentBlockHeader.getBlockNumber();
|
|
318
|
-
const parentBlock = await this.blockSource.getL2Block(parentBlockNumber);
|
|
319
|
-
if (!parentBlock) {
|
|
320
|
-
this.log.warn(`Parent block ${parentBlockNumber} not found in archiver`, proposalInfo);
|
|
321
|
-
return { reason: 'invalid_proposal' };
|
|
322
|
-
}
|
|
323
|
-
|
|
324
314
|
if (proposal.indexWithinCheckpoint === 0) {
|
|
325
315
|
// If this is the first block in a new checkpoint, increment the checkpoint number
|
|
326
|
-
if (!(proposal.blockHeader.getSlot() >
|
|
316
|
+
if (!(proposal.blockHeader.getSlot() > parentBlock.header.getSlot())) {
|
|
327
317
|
this.log.warn(`Slot should be greater than parent block slot for first block in checkpoint`, proposalInfo);
|
|
328
318
|
return { reason: 'invalid_proposal' };
|
|
329
319
|
}
|
|
@@ -335,7 +325,7 @@ export class BlockProposalHandler {
|
|
|
335
325
|
this.log.warn(`Non-sequential indexWithinCheckpoint`, proposalInfo);
|
|
336
326
|
return { reason: 'invalid_proposal' };
|
|
337
327
|
}
|
|
338
|
-
if (proposal.blockHeader.getSlot() !==
|
|
328
|
+
if (proposal.blockHeader.getSlot() !== parentBlock.header.getSlot()) {
|
|
339
329
|
this.log.warn(`Slot should be equal to parent block slot for non-first block in checkpoint`, proposalInfo);
|
|
340
330
|
return { reason: 'invalid_proposal' };
|
|
341
331
|
}
|
|
@@ -356,7 +346,7 @@ export class BlockProposalHandler {
|
|
|
356
346
|
*/
|
|
357
347
|
private validateNonFirstBlockInCheckpoint(
|
|
358
348
|
proposal: BlockProposal,
|
|
359
|
-
parentBlock:
|
|
349
|
+
parentBlock: BlockData,
|
|
360
350
|
proposalInfo: object,
|
|
361
351
|
): CheckpointComputationResult | undefined {
|
|
362
352
|
const proposalGlobals = proposal.blockHeader.globalVariables;
|
|
@@ -475,13 +465,14 @@ export class BlockProposalHandler {
|
|
|
475
465
|
// Fork before the block to be built
|
|
476
466
|
const parentBlockNumber = BlockNumber(blockNumber - 1);
|
|
477
467
|
await this.worldState.syncImmediate(parentBlockNumber);
|
|
478
|
-
using fork = await this.worldState.fork(parentBlockNumber);
|
|
468
|
+
await using fork = await this.worldState.fork(parentBlockNumber);
|
|
479
469
|
|
|
480
|
-
// Build checkpoint constants from proposal (excludes blockNumber
|
|
470
|
+
// Build checkpoint constants from proposal (excludes blockNumber which is per-block)
|
|
481
471
|
const constants: CheckpointGlobalVariables = {
|
|
482
472
|
chainId: new Fr(config.l1ChainId),
|
|
483
473
|
version: new Fr(config.rollupVersion),
|
|
484
474
|
slotNumber: slot,
|
|
475
|
+
timestamp: blockHeader.globalVariables.timestamp,
|
|
485
476
|
coinbase: blockHeader.globalVariables.coinbase,
|
|
486
477
|
feeRecipient: blockHeader.globalVariables.feeRecipient,
|
|
487
478
|
gasFees: blockHeader.globalVariables.gasFees,
|
|
@@ -491,6 +482,7 @@ export class BlockProposalHandler {
|
|
|
491
482
|
const checkpointBuilder = await this.checkpointsBuilder.openCheckpoint(
|
|
492
483
|
checkpointNumber,
|
|
493
484
|
constants,
|
|
485
|
+
0n, // only takes effect in the following checkpoint.
|
|
494
486
|
l1ToL2Messages,
|
|
495
487
|
previousCheckpointOutHashes,
|
|
496
488
|
fork,
|
|
@@ -500,18 +492,25 @@ export class BlockProposalHandler {
|
|
|
500
492
|
|
|
501
493
|
// Build the new block
|
|
502
494
|
const deadline = this.getReexecutionDeadline(slot, config);
|
|
495
|
+
const maxBlockGas =
|
|
496
|
+
this.config.validateMaxL2BlockGas !== undefined || this.config.validateMaxDABlockGas !== undefined
|
|
497
|
+
? new Gas(this.config.validateMaxDABlockGas ?? Infinity, this.config.validateMaxL2BlockGas ?? Infinity)
|
|
498
|
+
: undefined;
|
|
503
499
|
const result = await checkpointBuilder.buildBlock(txs, blockNumber, blockHeader.globalVariables.timestamp, {
|
|
504
500
|
deadline,
|
|
505
501
|
expectedEndState: blockHeader.state,
|
|
502
|
+
maxTransactions: this.config.validateMaxTxsPerBlock,
|
|
503
|
+
maxBlockGas,
|
|
506
504
|
});
|
|
507
505
|
|
|
508
506
|
const { block, failedTxs } = result;
|
|
509
507
|
const numFailedTxs = failedTxs.length;
|
|
510
508
|
|
|
511
|
-
this.log.verbose(`
|
|
509
|
+
this.log.verbose(`Block proposal ${blockNumber} at slot ${slot} transaction re-execution complete`, {
|
|
512
510
|
numFailedTxs,
|
|
513
511
|
numProposalTxs: txHashes.length,
|
|
514
512
|
numProcessedTxs: block.body.txEffects.length,
|
|
513
|
+
blockNumber,
|
|
515
514
|
slot,
|
|
516
515
|
});
|
|
517
516
|
|