@aztec/validator-client 0.0.1-commit.1bb068fb5 → 0.0.1-commit.1de2a32
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 +62 -57
- package/dest/checkpoint_builder.d.ts +13 -3
- package/dest/checkpoint_builder.d.ts.map +1 -1
- package/dest/checkpoint_builder.js +71 -18
- 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 +1 -1
- package/dest/duties/validation_service.d.ts.map +1 -1
- package/dest/duties/validation_service.js +2 -8
- 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 +12 -4
- package/dest/validator.d.ts.map +1 -1
- package/dest/validator.js +70 -18
- package/package.json +19 -19
- package/src/block_proposal_handler.ts +74 -76
- package/src/checkpoint_builder.ts +83 -14
- package/src/config.ts +22 -1
- package/src/duties/validation_service.ts +2 -8
- package/src/factory.ts +1 -0
- package/src/index.ts +0 -1
- package/src/metrics.ts +18 -0
- package/src/validator.ts +68 -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
|
@@ -9,11 +9,12 @@ import { sleep } from '@aztec/foundation/sleep';
|
|
|
9
9
|
import { DateProvider } from '@aztec/foundation/timer';
|
|
10
10
|
import { AuthRequest, AuthResponse, BlockProposalValidator, ReqRespSubProtocol } from '@aztec/p2p';
|
|
11
11
|
import { OffenseType, WANT_TO_SLASH_EVENT } from '@aztec/slasher';
|
|
12
|
+
import { validateCheckpoint } from '@aztec/stdlib/checkpoint';
|
|
12
13
|
import { getEpochAtSlot, getTimestampForSlot } from '@aztec/stdlib/epoch-helpers';
|
|
13
14
|
import { accumulateCheckpointOutHashes } from '@aztec/stdlib/messaging';
|
|
14
15
|
import { AttestationTimeoutError } from '@aztec/stdlib/validators';
|
|
15
16
|
import { getTelemetryClient } from '@aztec/telemetry-client';
|
|
16
|
-
import { createHASigner } from '@aztec/validator-ha-signer/factory';
|
|
17
|
+
import { createHASigner, createLocalSignerWithProtection } from '@aztec/validator-ha-signer/factory';
|
|
17
18
|
import { DutyType } from '@aztec/validator-ha-signer/types';
|
|
18
19
|
import { EventEmitter } from 'events';
|
|
19
20
|
import { BlockProposalHandler } from './block_proposal_handler.js';
|
|
@@ -42,6 +43,7 @@ const SLASHABLE_BLOCK_PROPOSAL_VALIDATION_RESULT = [
|
|
|
42
43
|
l1ToL2MessageSource;
|
|
43
44
|
config;
|
|
44
45
|
blobClient;
|
|
46
|
+
slashingProtectionSigner;
|
|
45
47
|
dateProvider;
|
|
46
48
|
tracer;
|
|
47
49
|
validationService;
|
|
@@ -53,10 +55,11 @@ const SLASHABLE_BLOCK_PROPOSAL_VALIDATION_RESULT = [
|
|
|
53
55
|
/** Tracks the last checkpoint proposal we created. */ lastProposedCheckpoint;
|
|
54
56
|
lastEpochForCommitteeUpdateLoop;
|
|
55
57
|
epochCacheUpdateLoop;
|
|
58
|
+
/** Tracks the last epoch in which each attester successfully submitted at least one attestation. */ lastAttestedEpochByAttester;
|
|
56
59
|
proposersOfInvalidBlocks;
|
|
57
60
|
/** Tracks the last checkpoint proposal we attested to, to prevent equivocation. */ lastAttestedProposal;
|
|
58
|
-
constructor(keyStore, epochCache, p2pClient, blockProposalHandler, blockSource, checkpointsBuilder, worldState, l1ToL2MessageSource, config, blobClient, dateProvider = new DateProvider(), telemetry = getTelemetryClient(), log = createLogger('validator')){
|
|
59
|
-
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();
|
|
60
63
|
// Create child logger with fisherman prefix if in fisherman mode
|
|
61
64
|
this.log = config.fishermanMode ? log.createChild('[FISHERMAN]') : log;
|
|
62
65
|
this.tracer = telemetry.getTracer('Validator');
|
|
@@ -95,6 +98,7 @@ const SLASHABLE_BLOCK_PROPOSAL_VALIDATION_RESULT = [
|
|
|
95
98
|
this.log.trace(`No committee found for slot`);
|
|
96
99
|
return;
|
|
97
100
|
}
|
|
101
|
+
this.metrics.setCurrentEpoch(epoch);
|
|
98
102
|
if (epoch !== this.lastEpochForCommitteeUpdateLoop) {
|
|
99
103
|
const me = this.getValidatorAddresses();
|
|
100
104
|
const committeeSet = new Set(committee.map((v)=>v.toString()));
|
|
@@ -113,20 +117,33 @@ const SLASHABLE_BLOCK_PROPOSAL_VALIDATION_RESULT = [
|
|
|
113
117
|
static async new(config, checkpointsBuilder, worldState, epochCache, p2pClient, blockSource, l1ToL2MessageSource, txProvider, keyStoreManager, blobClient, dateProvider = new DateProvider(), telemetry = getTelemetryClient()) {
|
|
114
118
|
const metrics = new ValidatorMetrics(telemetry);
|
|
115
119
|
const blockProposalValidator = new BlockProposalValidator(epochCache, {
|
|
116
|
-
txsPermitted: !config.disableTransactions
|
|
120
|
+
txsPermitted: !config.disableTransactions,
|
|
121
|
+
maxTxsPerBlock: config.validateMaxTxsPerBlock
|
|
117
122
|
});
|
|
118
123
|
const blockProposalHandler = new BlockProposalHandler(checkpointsBuilder, worldState, blockSource, l1ToL2MessageSource, txProvider, blockProposalValidator, epochCache, config, metrics, dateProvider, telemetry);
|
|
119
|
-
|
|
124
|
+
const nodeKeystoreAdapter = NodeKeystoreAdapter.fromKeyStoreManager(keyStoreManager);
|
|
125
|
+
let slashingProtectionSigner;
|
|
120
126
|
if (config.haSigningEnabled) {
|
|
127
|
+
// Multi-node HA mode: use PostgreSQL-backed distributed locking.
|
|
121
128
|
// If maxStuckDutiesAgeMs is not explicitly set, compute it from Aztec slot duration
|
|
122
129
|
const haConfig = {
|
|
123
130
|
...config,
|
|
124
131
|
maxStuckDutiesAgeMs: config.maxStuckDutiesAgeMs ?? epochCache.getL1Constants().slotDuration * 2 * 1000
|
|
125
132
|
};
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
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);
|
|
130
147
|
return validator;
|
|
131
148
|
}
|
|
132
149
|
getValidatorAddresses() {
|
|
@@ -153,6 +170,11 @@ const SLASHABLE_BLOCK_PROPOSAL_VALIDATION_RESULT = [
|
|
|
153
170
|
...config
|
|
154
171
|
};
|
|
155
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
|
+
}
|
|
156
178
|
async start() {
|
|
157
179
|
if (this.epochCacheUpdateLoop.isRunning()) {
|
|
158
180
|
this.log.warn(`Validator client already started`);
|
|
@@ -311,12 +333,10 @@ const SLASHABLE_BLOCK_PROPOSAL_VALIDATION_RESULT = [
|
|
|
311
333
|
const proposalInfo = {
|
|
312
334
|
slotNumber,
|
|
313
335
|
archive: proposal.archive.toString(),
|
|
314
|
-
proposer: proposer.toString()
|
|
315
|
-
txCount: proposal.txHashes.length
|
|
336
|
+
proposer: proposer.toString()
|
|
316
337
|
};
|
|
317
338
|
this.log.info(`Received checkpoint proposal for slot ${slotNumber}`, {
|
|
318
339
|
...proposalInfo,
|
|
319
|
-
txHashes: proposal.txHashes.map((t)=>t.toString()),
|
|
320
340
|
fishermanMode: this.config.fishermanMode || false
|
|
321
341
|
});
|
|
322
342
|
// Validate the checkpoint proposal before attesting (unless skipCheckpointProposalValidation is set)
|
|
@@ -346,6 +366,16 @@ const SLASHABLE_BLOCK_PROPOSAL_VALIDATION_RESULT = [
|
|
|
346
366
|
fishermanMode: this.config.fishermanMode || false
|
|
347
367
|
});
|
|
348
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
|
+
}
|
|
349
379
|
// Determine which validators should attest
|
|
350
380
|
let attestors;
|
|
351
381
|
if (partOfCommittee) {
|
|
@@ -442,6 +472,14 @@ const SLASHABLE_BLOCK_PROPOSAL_VALIDATION_RESULT = [
|
|
|
442
472
|
reason: 'no_blocks_for_slot'
|
|
443
473
|
};
|
|
444
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
|
+
}
|
|
445
483
|
this.log.debug(`Found ${blocks.length} blocks for slot ${slot}`, {
|
|
446
484
|
...proposalInfo,
|
|
447
485
|
blockNumbers: blocks.map((b)=>b.number)
|
|
@@ -452,12 +490,9 @@ const SLASHABLE_BLOCK_PROPOSAL_VALIDATION_RESULT = [
|
|
|
452
490
|
const checkpointNumber = firstBlock.checkpointNumber;
|
|
453
491
|
// Get L1-to-L2 messages for this checkpoint
|
|
454
492
|
const l1ToL2Messages = await this.l1ToL2MessageSource.getL1ToL2Messages(checkpointNumber);
|
|
455
|
-
//
|
|
456
|
-
// TODO: There can be a more efficient way to get the previous checkpoint out hashes without having to fetch the
|
|
457
|
-
// actual checkpoints and the blocks/txs in them.
|
|
493
|
+
// Collect the out hashes of all the checkpoints before this one in the same epoch
|
|
458
494
|
const epoch = getEpochAtSlot(slot, this.epochCache.getL1Constants());
|
|
459
|
-
const
|
|
460
|
-
const previousCheckpointOutHashes = previousCheckpoints.map((c)=>c.getCheckpointOutHash());
|
|
495
|
+
const previousCheckpointOutHashes = (await this.blockSource.getCheckpointsDataForEpoch(epoch)).filter((c)=>c.checkpointNumber < checkpointNumber).map((c)=>c.checkpointOutHash);
|
|
461
496
|
// Fork world state at the block before the first block
|
|
462
497
|
const parentBlockNumber = BlockNumber(firstBlock.number - 1);
|
|
463
498
|
const fork = await this.worldState.fork(parentBlockNumber);
|
|
@@ -511,6 +546,22 @@ const SLASHABLE_BLOCK_PROPOSAL_VALIDATION_RESULT = [
|
|
|
511
546
|
reason: 'out_hash_mismatch'
|
|
512
547
|
};
|
|
513
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
|
+
}
|
|
514
565
|
this.log.verbose(`Checkpoint proposal validation successful for slot ${slot}`, proposalInfo);
|
|
515
566
|
return {
|
|
516
567
|
isValid: true
|
|
@@ -527,6 +578,7 @@ const SLASHABLE_BLOCK_PROPOSAL_VALIDATION_RESULT = [
|
|
|
527
578
|
chainId: gv.chainId,
|
|
528
579
|
version: gv.version,
|
|
529
580
|
slotNumber: gv.slotNumber,
|
|
581
|
+
timestamp: gv.timestamp,
|
|
530
582
|
coinbase: gv.coinbase,
|
|
531
583
|
feeRecipient: gv.feeRecipient,
|
|
532
584
|
gasFees: gv.gasFees
|
|
@@ -547,7 +599,7 @@ const SLASHABLE_BLOCK_PROPOSAL_VALIDATION_RESULT = [
|
|
|
547
599
|
return;
|
|
548
600
|
}
|
|
549
601
|
const blobFields = blocks.flatMap((b)=>b.toBlobFields());
|
|
550
|
-
const blobs = getBlobsPerL1Block(blobFields);
|
|
602
|
+
const blobs = await getBlobsPerL1Block(blobFields);
|
|
551
603
|
await this.blobClient.sendBlobsToFilestore(blobs);
|
|
552
604
|
this.log.debug(`Uploaded ${blobs.length} blobs to filestore for checkpoint at slot ${proposal.slotNumber}`, {
|
|
553
605
|
...proposalInfo,
|
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.1de2a32",
|
|
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.1de2a32",
|
|
68
|
+
"@aztec/blob-lib": "0.0.1-commit.1de2a32",
|
|
69
|
+
"@aztec/constants": "0.0.1-commit.1de2a32",
|
|
70
|
+
"@aztec/epoch-cache": "0.0.1-commit.1de2a32",
|
|
71
|
+
"@aztec/ethereum": "0.0.1-commit.1de2a32",
|
|
72
|
+
"@aztec/foundation": "0.0.1-commit.1de2a32",
|
|
73
|
+
"@aztec/node-keystore": "0.0.1-commit.1de2a32",
|
|
74
|
+
"@aztec/noir-protocol-circuits-types": "0.0.1-commit.1de2a32",
|
|
75
|
+
"@aztec/p2p": "0.0.1-commit.1de2a32",
|
|
76
|
+
"@aztec/protocol-contracts": "0.0.1-commit.1de2a32",
|
|
77
|
+
"@aztec/prover-client": "0.0.1-commit.1de2a32",
|
|
78
|
+
"@aztec/simulator": "0.0.1-commit.1de2a32",
|
|
79
|
+
"@aztec/slasher": "0.0.1-commit.1de2a32",
|
|
80
|
+
"@aztec/stdlib": "0.0.1-commit.1de2a32",
|
|
81
|
+
"@aztec/telemetry-client": "0.0.1-commit.1de2a32",
|
|
82
|
+
"@aztec/validator-ha-signer": "0.0.1-commit.1de2a32",
|
|
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.1de2a32",
|
|
90
|
+
"@aztec/world-state": "0.0.1-commit.1de2a32",
|
|
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,
|
|
@@ -501,18 +492,25 @@ export class BlockProposalHandler {
|
|
|
501
492
|
|
|
502
493
|
// Build the new block
|
|
503
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;
|
|
504
499
|
const result = await checkpointBuilder.buildBlock(txs, blockNumber, blockHeader.globalVariables.timestamp, {
|
|
505
500
|
deadline,
|
|
506
501
|
expectedEndState: blockHeader.state,
|
|
502
|
+
maxTransactions: this.config.validateMaxTxsPerBlock,
|
|
503
|
+
maxBlockGas,
|
|
507
504
|
});
|
|
508
505
|
|
|
509
506
|
const { block, failedTxs } = result;
|
|
510
507
|
const numFailedTxs = failedTxs.length;
|
|
511
508
|
|
|
512
|
-
this.log.verbose(`
|
|
509
|
+
this.log.verbose(`Block proposal ${blockNumber} at slot ${slot} transaction re-execution complete`, {
|
|
513
510
|
numFailedTxs,
|
|
514
511
|
numProposalTxs: txHashes.length,
|
|
515
512
|
numProcessedTxs: block.body.txEffects.length,
|
|
513
|
+
blockNumber,
|
|
516
514
|
slot,
|
|
517
515
|
});
|
|
518
516
|
|