@aztec/validator-client 0.0.1-commit.7d4e6cd → 0.0.1-commit.86469d5
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 -15
- package/dest/block_proposal_handler.d.ts +8 -8
- package/dest/block_proposal_handler.d.ts.map +1 -1
- package/dest/block_proposal_handler.js +27 -32
- package/dest/checkpoint_builder.d.ts +21 -25
- package/dest/checkpoint_builder.d.ts.map +1 -1
- package/dest/checkpoint_builder.js +50 -32
- package/dest/config.d.ts +1 -1
- package/dest/config.d.ts.map +1 -1
- package/dest/config.js +8 -14
- package/dest/duties/validation_service.d.ts +19 -6
- package/dest/duties/validation_service.d.ts.map +1 -1
- package/dest/duties/validation_service.js +72 -19
- package/dest/factory.d.ts +2 -2
- package/dest/factory.d.ts.map +1 -1
- package/dest/factory.js +1 -1
- package/dest/key_store/ha_key_store.d.ts +99 -0
- package/dest/key_store/ha_key_store.d.ts.map +1 -0
- package/dest/key_store/ha_key_store.js +208 -0
- package/dest/key_store/index.d.ts +2 -1
- package/dest/key_store/index.d.ts.map +1 -1
- package/dest/key_store/index.js +1 -0
- package/dest/key_store/interface.d.ts +36 -6
- package/dest/key_store/interface.d.ts.map +1 -1
- package/dest/key_store/local_key_store.d.ts +10 -5
- package/dest/key_store/local_key_store.d.ts.map +1 -1
- package/dest/key_store/local_key_store.js +8 -4
- package/dest/key_store/node_keystore_adapter.d.ts +18 -5
- package/dest/key_store/node_keystore_adapter.d.ts.map +1 -1
- package/dest/key_store/node_keystore_adapter.js +18 -4
- package/dest/key_store/web3signer_key_store.d.ts +10 -5
- package/dest/key_store/web3signer_key_store.d.ts.map +1 -1
- package/dest/key_store/web3signer_key_store.js +8 -4
- package/dest/metrics.d.ts +4 -3
- package/dest/metrics.d.ts.map +1 -1
- package/dest/metrics.js +34 -5
- package/dest/tx_validator/tx_validator_factory.d.ts +4 -3
- package/dest/tx_validator/tx_validator_factory.d.ts.map +1 -1
- package/dest/tx_validator/tx_validator_factory.js +17 -16
- package/dest/validator.d.ts +13 -13
- package/dest/validator.d.ts.map +1 -1
- package/dest/validator.js +82 -80
- package/package.json +21 -17
- package/src/block_proposal_handler.ts +41 -42
- package/src/checkpoint_builder.ts +85 -38
- package/src/config.ts +7 -13
- package/src/duties/validation_service.ts +91 -23
- package/src/factory.ts +1 -0
- package/src/key_store/ha_key_store.ts +269 -0
- package/src/key_store/index.ts +1 -0
- package/src/key_store/interface.ts +44 -5
- package/src/key_store/local_key_store.ts +13 -4
- package/src/key_store/node_keystore_adapter.ts +27 -4
- package/src/key_store/web3signer_key_store.ts +17 -4
- package/src/metrics.ts +45 -6
- package/src/tx_validator/tx_validator_factory.ts +52 -31
- package/src/validator.ts +98 -93
package/dest/validator.js
CHANGED
|
@@ -8,11 +8,16 @@ import { sleep } from '@aztec/foundation/sleep';
|
|
|
8
8
|
import { DateProvider } from '@aztec/foundation/timer';
|
|
9
9
|
import { AuthRequest, AuthResponse, BlockProposalValidator, ReqRespSubProtocol } from '@aztec/p2p';
|
|
10
10
|
import { OffenseType, WANT_TO_SLASH_EVENT } from '@aztec/slasher';
|
|
11
|
+
import { getEpochAtSlot } from '@aztec/stdlib/epoch-helpers';
|
|
12
|
+
import { accumulateCheckpointOutHashes } from '@aztec/stdlib/messaging';
|
|
11
13
|
import { AttestationTimeoutError } from '@aztec/stdlib/validators';
|
|
12
14
|
import { getTelemetryClient } from '@aztec/telemetry-client';
|
|
15
|
+
import { createHASigner } from '@aztec/validator-ha-signer/factory';
|
|
16
|
+
import { DutyType } from '@aztec/validator-ha-signer/types';
|
|
13
17
|
import { EventEmitter } from 'events';
|
|
14
18
|
import { BlockProposalHandler } from './block_proposal_handler.js';
|
|
15
19
|
import { ValidationService } from './duties/validation_service.js';
|
|
20
|
+
import { HAKeyStore } from './key_store/ha_key_store.js';
|
|
16
21
|
import { NodeKeystoreAdapter } from './key_store/node_keystore_adapter.js';
|
|
17
22
|
import { ValidatorMetrics } from './metrics.js';
|
|
18
23
|
// We maintain a set of proposers who have proposed invalid blocks.
|
|
@@ -48,12 +53,8 @@ const SLASHABLE_BLOCK_PROPOSAL_VALIDATION_RESULT = [
|
|
|
48
53
|
lastEpochForCommitteeUpdateLoop;
|
|
49
54
|
epochCacheUpdateLoop;
|
|
50
55
|
proposersOfInvalidBlocks;
|
|
51
|
-
// TODO(palla/mbps): Remove this once checkpoint validation is stable and we can validate all blocks properly.
|
|
52
|
-
// Tracks slots for which we have successfully validated a block proposal, so we can attest to checkpoint proposals for those slots.
|
|
53
|
-
// eslint-disable-next-line aztec-custom/no-non-primitive-in-collections
|
|
54
|
-
validatedBlockSlots;
|
|
55
56
|
constructor(keyStore, epochCache, p2pClient, blockProposalHandler, blockSource, checkpointsBuilder, worldState, l1ToL2MessageSource, config, blobClient, dateProvider = new DateProvider(), telemetry = getTelemetryClient(), log = createLogger('validator')){
|
|
56
|
-
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()
|
|
57
|
+
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();
|
|
57
58
|
// Create child logger with fisherman prefix if in fisherman mode
|
|
58
59
|
this.log = config.fishermanMode ? log.createChild('[FISHERMAN]') : log;
|
|
59
60
|
this.tracer = telemetry.getTracer('Validator');
|
|
@@ -107,13 +108,23 @@ const SLASHABLE_BLOCK_PROPOSAL_VALIDATION_RESULT = [
|
|
|
107
108
|
this.log.error(`Error updating epoch committee`, err);
|
|
108
109
|
}
|
|
109
110
|
}
|
|
110
|
-
static new(config, checkpointsBuilder, worldState, epochCache, p2pClient, blockSource, l1ToL2MessageSource, txProvider, keyStoreManager, blobClient, dateProvider = new DateProvider(), telemetry = getTelemetryClient()) {
|
|
111
|
+
static async new(config, checkpointsBuilder, worldState, epochCache, p2pClient, blockSource, l1ToL2MessageSource, txProvider, keyStoreManager, blobClient, dateProvider = new DateProvider(), telemetry = getTelemetryClient()) {
|
|
111
112
|
const metrics = new ValidatorMetrics(telemetry);
|
|
112
113
|
const blockProposalValidator = new BlockProposalValidator(epochCache, {
|
|
113
114
|
txsPermitted: !config.disableTransactions
|
|
114
115
|
});
|
|
115
|
-
const blockProposalHandler = new BlockProposalHandler(checkpointsBuilder, worldState, blockSource, l1ToL2MessageSource, txProvider, blockProposalValidator, config, metrics, dateProvider, telemetry);
|
|
116
|
-
|
|
116
|
+
const blockProposalHandler = new BlockProposalHandler(checkpointsBuilder, worldState, blockSource, l1ToL2MessageSource, txProvider, blockProposalValidator, epochCache, config, metrics, dateProvider, telemetry);
|
|
117
|
+
let validatorKeyStore = NodeKeystoreAdapter.fromKeyStoreManager(keyStoreManager);
|
|
118
|
+
if (config.haSigningEnabled) {
|
|
119
|
+
// If maxStuckDutiesAgeMs is not explicitly set, compute it from Aztec slot duration
|
|
120
|
+
const haConfig = {
|
|
121
|
+
...config,
|
|
122
|
+
maxStuckDutiesAgeMs: config.maxStuckDutiesAgeMs ?? epochCache.getL1Constants().slotDuration * 2 * 1000
|
|
123
|
+
};
|
|
124
|
+
const { signer } = await createHASigner(haConfig);
|
|
125
|
+
validatorKeyStore = new HAKeyStore(validatorKeyStore, signer);
|
|
126
|
+
}
|
|
127
|
+
const validator = new ValidatorClient(validatorKeyStore, epochCache, p2pClient, blockProposalHandler, blockSource, checkpointsBuilder, worldState, l1ToL2MessageSource, config, blobClient, dateProvider, telemetry);
|
|
117
128
|
return validator;
|
|
118
129
|
}
|
|
119
130
|
getValidatorAddresses() {
|
|
@@ -122,8 +133,8 @@ const SLASHABLE_BLOCK_PROPOSAL_VALIDATION_RESULT = [
|
|
|
122
133
|
getBlockProposalHandler() {
|
|
123
134
|
return this.blockProposalHandler;
|
|
124
135
|
}
|
|
125
|
-
signWithAddress(addr, msg) {
|
|
126
|
-
return this.keyStore.signTypedDataWithAddress(addr, msg);
|
|
136
|
+
signWithAddress(addr, msg, context) {
|
|
137
|
+
return this.keyStore.signTypedDataWithAddress(addr, msg, context);
|
|
127
138
|
}
|
|
128
139
|
getCoinbaseForAttestor(attestor) {
|
|
129
140
|
return this.keyStore.getCoinbaseAddress(attestor);
|
|
@@ -145,6 +156,7 @@ const SLASHABLE_BLOCK_PROPOSAL_VALIDATION_RESULT = [
|
|
|
145
156
|
this.log.warn(`Validator client already started`);
|
|
146
157
|
return;
|
|
147
158
|
}
|
|
159
|
+
await this.keyStore.start();
|
|
148
160
|
await this.registerHandlers();
|
|
149
161
|
const myAddresses = this.getValidatorAddresses();
|
|
150
162
|
const inCommittee = await this.epochCache.filterInCommittee('now', myAddresses);
|
|
@@ -157,6 +169,7 @@ const SLASHABLE_BLOCK_PROPOSAL_VALIDATION_RESULT = [
|
|
|
157
169
|
}
|
|
158
170
|
async stop() {
|
|
159
171
|
await this.epochCacheUpdateLoop.stop();
|
|
172
|
+
await this.keyStore.stop();
|
|
160
173
|
}
|
|
161
174
|
/** Register handlers on the p2p client */ async registerHandlers() {
|
|
162
175
|
if (!this.hasRegisteredHandlers) {
|
|
@@ -181,6 +194,9 @@ const SLASHABLE_BLOCK_PROPOSAL_VALIDATION_RESULT = [
|
|
|
181
194
|
* @returns true if the proposal is valid, false otherwise
|
|
182
195
|
*/ async validateBlockProposal(proposal, proposalSender) {
|
|
183
196
|
const slotNumber = proposal.slotNumber;
|
|
197
|
+
// Note: During escape hatch, we still want to "validate" proposals for observability,
|
|
198
|
+
// but we intentionally reject them and disable slashing invalid block and attestation flow.
|
|
199
|
+
const escapeHatchOpen = await this.epochCache.isEscapeHatchOpenAtSlot(slotNumber);
|
|
184
200
|
const proposer = proposal.getSender();
|
|
185
201
|
// Reject proposals with invalid signatures
|
|
186
202
|
if (!proposer) {
|
|
@@ -203,7 +219,7 @@ const SLASHABLE_BLOCK_PROPOSAL_VALIDATION_RESULT = [
|
|
|
203
219
|
// In fisherman mode, we always reexecute to validate proposals.
|
|
204
220
|
const { validatorReexecute, slashBroadcastedInvalidBlockPenalty, alwaysReexecuteBlockProposals, fishermanMode } = this.config;
|
|
205
221
|
const shouldReexecute = fishermanMode || slashBroadcastedInvalidBlockPenalty > 0n && validatorReexecute || partOfCommittee && validatorReexecute || alwaysReexecuteBlockProposals || this.blobClient.canUpload();
|
|
206
|
-
const validationResult = await this.blockProposalHandler.handleBlockProposal(proposal, proposalSender, !!shouldReexecute);
|
|
222
|
+
const validationResult = await this.blockProposalHandler.handleBlockProposal(proposal, proposalSender, !!shouldReexecute && !escapeHatchOpen);
|
|
207
223
|
if (!validationResult.isValid) {
|
|
208
224
|
this.log.warn(`Block proposal validation failed: ${validationResult.reason}`, proposalInfo);
|
|
209
225
|
const reason = validationResult.reason || 'unknown';
|
|
@@ -222,7 +238,7 @@ const SLASHABLE_BLOCK_PROPOSAL_VALIDATION_RESULT = [
|
|
|
222
238
|
this.metrics.incFailedAttestationsNodeIssue(1, reason, partOfCommittee);
|
|
223
239
|
}
|
|
224
240
|
// Slash invalid block proposals (can happen even when not in committee)
|
|
225
|
-
if (validationResult.reason && SLASHABLE_BLOCK_PROPOSAL_VALIDATION_RESULT.includes(validationResult.reason) && slashBroadcastedInvalidBlockPenalty > 0n) {
|
|
241
|
+
if (!escapeHatchOpen && validationResult.reason && SLASHABLE_BLOCK_PROPOSAL_VALIDATION_RESULT.includes(validationResult.reason) && slashBroadcastedInvalidBlockPenalty > 0n) {
|
|
226
242
|
this.log.warn(`Slashing proposer for invalid block proposal`, proposalInfo);
|
|
227
243
|
this.slashInvalidBlock(proposal);
|
|
228
244
|
}
|
|
@@ -231,11 +247,13 @@ const SLASHABLE_BLOCK_PROPOSAL_VALIDATION_RESULT = [
|
|
|
231
247
|
this.log.info(`Validated block proposal for slot ${slotNumber}`, {
|
|
232
248
|
...proposalInfo,
|
|
233
249
|
inCommittee: partOfCommittee,
|
|
234
|
-
fishermanMode: this.config.fishermanMode || false
|
|
250
|
+
fishermanMode: this.config.fishermanMode || false,
|
|
251
|
+
escapeHatchOpen
|
|
235
252
|
});
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
253
|
+
if (escapeHatchOpen) {
|
|
254
|
+
this.log.warn(`Escape hatch open for slot ${slotNumber}, rejecting block proposal`, proposalInfo);
|
|
255
|
+
return false;
|
|
256
|
+
}
|
|
239
257
|
return true;
|
|
240
258
|
}
|
|
241
259
|
/**
|
|
@@ -246,6 +264,11 @@ const SLASHABLE_BLOCK_PROPOSAL_VALIDATION_RESULT = [
|
|
|
246
264
|
*/ async attestToCheckpointProposal(proposal, _proposalSender) {
|
|
247
265
|
const slotNumber = proposal.slotNumber;
|
|
248
266
|
const proposer = proposal.getSender();
|
|
267
|
+
// If escape hatch is open for this slot's epoch, do not attest.
|
|
268
|
+
if (await this.epochCache.isEscapeHatchOpenAtSlot(slotNumber)) {
|
|
269
|
+
this.log.warn(`Escape hatch open for slot ${slotNumber}, skipping checkpoint attestation handling`);
|
|
270
|
+
return undefined;
|
|
271
|
+
}
|
|
249
272
|
// Reject proposals with invalid signatures
|
|
250
273
|
if (!proposer) {
|
|
251
274
|
this.log.warn(`Received checkpoint proposal with invalid signature for slot ${slotNumber}`);
|
|
@@ -265,16 +288,9 @@ const SLASHABLE_BLOCK_PROPOSAL_VALIDATION_RESULT = [
|
|
|
265
288
|
txHashes: proposal.txHashes.map((t)=>t.toString()),
|
|
266
289
|
fishermanMode: this.config.fishermanMode || false
|
|
267
290
|
});
|
|
268
|
-
// TODO(palla/mbps): Remove this once checkpoint validation is stable.
|
|
269
|
-
// Check that we have successfully validated a block for this slot before attesting to the checkpoint.
|
|
270
|
-
if (!this.validatedBlockSlots.has(slotNumber)) {
|
|
271
|
-
this.log.warn(`No validated block found for slot ${slotNumber}, refusing to attest to checkpoint`, proposalInfo);
|
|
272
|
-
return undefined;
|
|
273
|
-
}
|
|
274
291
|
// Validate the checkpoint proposal before attesting (unless skipCheckpointProposalValidation is set)
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
this.log.verbose(`Skipping checkpoint proposal validation for slot ${slotNumber}`, proposalInfo);
|
|
292
|
+
if (this.config.skipCheckpointProposalValidation) {
|
|
293
|
+
this.log.warn(`Skipping checkpoint proposal validation for slot ${slotNumber}`, proposalInfo);
|
|
278
294
|
} else {
|
|
279
295
|
const validationResult = await this.validateCheckpointProposal(proposal, proposalInfo);
|
|
280
296
|
if (!validationResult.isValid) {
|
|
@@ -333,7 +349,7 @@ const SLASHABLE_BLOCK_PROPOSAL_VALIDATION_RESULT = [
|
|
|
333
349
|
* @returns Validation result with isValid flag and reason if invalid.
|
|
334
350
|
*/ async validateCheckpointProposal(proposal, proposalInfo) {
|
|
335
351
|
const slot = proposal.slotNumber;
|
|
336
|
-
const timeoutSeconds = 10;
|
|
352
|
+
const timeoutSeconds = 10; // TODO(palla/mbps): This should map to the timetable settings
|
|
337
353
|
// Wait for last block to sync by archive
|
|
338
354
|
let lastBlockHeader;
|
|
339
355
|
try {
|
|
@@ -362,18 +378,8 @@ const SLASHABLE_BLOCK_PROPOSAL_VALIDATION_RESULT = [
|
|
|
362
378
|
reason: 'last_block_not_found'
|
|
363
379
|
};
|
|
364
380
|
}
|
|
365
|
-
// Get the last full block to determine checkpoint number
|
|
366
|
-
const lastBlock = await this.blockSource.getL2BlockNew(lastBlockHeader.getBlockNumber());
|
|
367
|
-
if (!lastBlock) {
|
|
368
|
-
this.log.warn(`Last block ${lastBlockHeader.getBlockNumber()} not found`, proposalInfo);
|
|
369
|
-
return {
|
|
370
|
-
isValid: false,
|
|
371
|
-
reason: 'last_block_not_found'
|
|
372
|
-
};
|
|
373
|
-
}
|
|
374
|
-
const checkpointNumber = lastBlock.checkpointNumber;
|
|
375
381
|
// Get all full blocks for the slot and checkpoint
|
|
376
|
-
const blocks = await this.getBlocksForSlot(slot
|
|
382
|
+
const blocks = await this.blockSource.getBlocksForSlot(slot);
|
|
377
383
|
if (blocks.length === 0) {
|
|
378
384
|
this.log.warn(`No blocks found for slot ${slot}`, proposalInfo);
|
|
379
385
|
return {
|
|
@@ -388,14 +394,21 @@ const SLASHABLE_BLOCK_PROPOSAL_VALIDATION_RESULT = [
|
|
|
388
394
|
// Get checkpoint constants from first block
|
|
389
395
|
const firstBlock = blocks[0];
|
|
390
396
|
const constants = this.extractCheckpointConstants(firstBlock);
|
|
397
|
+
const checkpointNumber = firstBlock.checkpointNumber;
|
|
391
398
|
// Get L1-to-L2 messages for this checkpoint
|
|
392
399
|
const l1ToL2Messages = await this.l1ToL2MessageSource.getL1ToL2Messages(checkpointNumber);
|
|
400
|
+
// Compute the previous checkpoint out hashes for the epoch.
|
|
401
|
+
// TODO: There can be a more efficient way to get the previous checkpoint out hashes without having to fetch the
|
|
402
|
+
// actual checkpoints and the blocks/txs in them.
|
|
403
|
+
const epoch = getEpochAtSlot(slot, this.epochCache.getL1Constants());
|
|
404
|
+
const previousCheckpoints = (await this.blockSource.getCheckpointsForEpoch(epoch)).filter((b)=>b.number < checkpointNumber).sort((a, b)=>a.number - b.number);
|
|
405
|
+
const previousCheckpointOutHashes = previousCheckpoints.map((c)=>c.getCheckpointOutHash());
|
|
393
406
|
// Fork world state at the block before the first block
|
|
394
407
|
const parentBlockNumber = BlockNumber(firstBlock.number - 1);
|
|
395
408
|
const fork = await this.worldState.fork(parentBlockNumber);
|
|
396
409
|
try {
|
|
397
410
|
// Create checkpoint builder with all existing blocks
|
|
398
|
-
const checkpointBuilder = await this.checkpointsBuilder.openCheckpoint(checkpointNumber, constants, l1ToL2Messages, fork, blocks);
|
|
411
|
+
const checkpointBuilder = await this.checkpointsBuilder.openCheckpoint(checkpointNumber, constants, l1ToL2Messages, previousCheckpointOutHashes, fork, blocks, this.log.getBindings());
|
|
399
412
|
// Complete the checkpoint to get computed values
|
|
400
413
|
const computedCheckpoint = await checkpointBuilder.completeCheckpoint();
|
|
401
414
|
// Compare checkpoint header with proposal
|
|
@@ -422,6 +435,27 @@ const SLASHABLE_BLOCK_PROPOSAL_VALIDATION_RESULT = [
|
|
|
422
435
|
reason: 'archive_mismatch'
|
|
423
436
|
};
|
|
424
437
|
}
|
|
438
|
+
// Check that the accumulated epoch out hash matches the value in the proposal.
|
|
439
|
+
// The epoch out hash is the accumulated hash of all checkpoint out hashes in the epoch.
|
|
440
|
+
const checkpointOutHash = computedCheckpoint.getCheckpointOutHash();
|
|
441
|
+
const computedEpochOutHash = accumulateCheckpointOutHashes([
|
|
442
|
+
...previousCheckpointOutHashes,
|
|
443
|
+
checkpointOutHash
|
|
444
|
+
]);
|
|
445
|
+
const proposalEpochOutHash = proposal.checkpointHeader.epochOutHash;
|
|
446
|
+
if (!computedEpochOutHash.equals(proposalEpochOutHash)) {
|
|
447
|
+
this.log.warn(`Epoch out hash mismatch`, {
|
|
448
|
+
proposalEpochOutHash: proposalEpochOutHash.toString(),
|
|
449
|
+
computedEpochOutHash: computedEpochOutHash.toString(),
|
|
450
|
+
checkpointOutHash: checkpointOutHash.toString(),
|
|
451
|
+
previousCheckpointOutHashes: previousCheckpointOutHashes.map((h)=>h.toString()),
|
|
452
|
+
...proposalInfo
|
|
453
|
+
});
|
|
454
|
+
return {
|
|
455
|
+
isValid: false,
|
|
456
|
+
reason: 'out_hash_mismatch'
|
|
457
|
+
};
|
|
458
|
+
}
|
|
425
459
|
this.log.verbose(`Checkpoint proposal validation successful for slot ${slot}`, proposalInfo);
|
|
426
460
|
return {
|
|
427
461
|
isValid: true
|
|
@@ -431,36 +465,6 @@ const SLASHABLE_BLOCK_PROPOSAL_VALIDATION_RESULT = [
|
|
|
431
465
|
}
|
|
432
466
|
}
|
|
433
467
|
/**
|
|
434
|
-
* Get all full blocks for a given slot and checkpoint by walking backwards from the last block.
|
|
435
|
-
* Returns blocks in ascending order (earliest to latest).
|
|
436
|
-
* TODO(palla/mbps): Add getL2BlocksForSlot() to L2BlockSource interface for efficiency.
|
|
437
|
-
*/ async getBlocksForSlot(slot, lastBlockHeader, checkpointNumber) {
|
|
438
|
-
const blocks = [];
|
|
439
|
-
let currentHeader = lastBlockHeader;
|
|
440
|
-
const { genesisArchiveRoot } = await this.blockSource.getGenesisValues();
|
|
441
|
-
while(currentHeader.getSlot() === slot){
|
|
442
|
-
const block = await this.blockSource.getL2BlockNew(currentHeader.getBlockNumber());
|
|
443
|
-
if (!block) {
|
|
444
|
-
this.log.warn(`Block ${currentHeader.getBlockNumber()} not found while getting blocks for slot ${slot}`);
|
|
445
|
-
break;
|
|
446
|
-
}
|
|
447
|
-
if (block.checkpointNumber !== checkpointNumber) {
|
|
448
|
-
break;
|
|
449
|
-
}
|
|
450
|
-
blocks.unshift(block);
|
|
451
|
-
const prevArchive = currentHeader.lastArchive.root;
|
|
452
|
-
if (prevArchive.equals(genesisArchiveRoot)) {
|
|
453
|
-
break;
|
|
454
|
-
}
|
|
455
|
-
const prevHeader = await this.blockSource.getBlockHeaderByArchive(prevArchive);
|
|
456
|
-
if (!prevHeader || prevHeader.getSlot() !== slot) {
|
|
457
|
-
break;
|
|
458
|
-
}
|
|
459
|
-
currentHeader = prevHeader;
|
|
460
|
-
}
|
|
461
|
-
return blocks;
|
|
462
|
-
}
|
|
463
|
-
/**
|
|
464
468
|
* Extract checkpoint global variables from a block.
|
|
465
469
|
*/ extractCheckpointConstants(block) {
|
|
466
470
|
const gv = block.header.globalVariables;
|
|
@@ -482,13 +486,7 @@ const SLASHABLE_BLOCK_PROPOSAL_VALIDATION_RESULT = [
|
|
|
482
486
|
this.log.warn(`Failed to get last block header for blob upload`, proposalInfo);
|
|
483
487
|
return;
|
|
484
488
|
}
|
|
485
|
-
|
|
486
|
-
const lastBlock = await this.blockSource.getL2BlockNew(lastBlockHeader.getBlockNumber());
|
|
487
|
-
if (!lastBlock) {
|
|
488
|
-
this.log.warn(`Failed to get last block for blob upload`, proposalInfo);
|
|
489
|
-
return;
|
|
490
|
-
}
|
|
491
|
-
const blocks = await this.getBlocksForSlot(proposal.slotNumber, lastBlockHeader, lastBlock.checkpointNumber);
|
|
489
|
+
const blocks = await this.blockSource.getBlocksForSlot(proposal.slotNumber);
|
|
492
490
|
if (blocks.length === 0) {
|
|
493
491
|
this.log.warn(`No blocks found for blob upload`, proposalInfo);
|
|
494
492
|
return;
|
|
@@ -526,7 +524,7 @@ const SLASHABLE_BLOCK_PROPOSAL_VALIDATION_RESULT = [
|
|
|
526
524
|
}
|
|
527
525
|
]);
|
|
528
526
|
}
|
|
529
|
-
async createBlockProposal(blockHeader, indexWithinCheckpoint, inHash, archive, txs, proposerAddress, options) {
|
|
527
|
+
async createBlockProposal(blockHeader, indexWithinCheckpoint, inHash, archive, txs, proposerAddress, options = {}) {
|
|
530
528
|
// TODO(palla/mbps): Prevent double proposals properly
|
|
531
529
|
// if (this.previousProposal?.slotNumber === blockHeader.globalVariables.slotNumber) {
|
|
532
530
|
// this.log.verbose(`Already made a proposal for the same slot, skipping proposal`);
|
|
@@ -540,15 +538,15 @@ const SLASHABLE_BLOCK_PROPOSAL_VALIDATION_RESULT = [
|
|
|
540
538
|
this.previousProposal = newProposal;
|
|
541
539
|
return newProposal;
|
|
542
540
|
}
|
|
543
|
-
async createCheckpointProposal(checkpointHeader, archive, lastBlockInfo, proposerAddress, options) {
|
|
541
|
+
async createCheckpointProposal(checkpointHeader, archive, lastBlockInfo, proposerAddress, options = {}) {
|
|
544
542
|
this.log.info(`Assembling checkpoint proposal for slot ${checkpointHeader.slotNumber}`);
|
|
545
543
|
return await this.validationService.createCheckpointProposal(checkpointHeader, archive, lastBlockInfo, proposerAddress, options);
|
|
546
544
|
}
|
|
547
545
|
async broadcastBlockProposal(proposal) {
|
|
548
546
|
await this.p2pClient.broadcastProposal(proposal);
|
|
549
547
|
}
|
|
550
|
-
async signAttestationsAndSigners(attestationsAndSigners, proposer) {
|
|
551
|
-
return await this.validationService.signAttestationsAndSigners(attestationsAndSigners, proposer);
|
|
548
|
+
async signAttestationsAndSigners(attestationsAndSigners, proposer, slot, blockNumber) {
|
|
549
|
+
return await this.validationService.signAttestationsAndSigners(attestationsAndSigners, proposer, slot, blockNumber);
|
|
552
550
|
}
|
|
553
551
|
async collectOwnAttestations(proposal) {
|
|
554
552
|
const slot = proposal.slotNumber;
|
|
@@ -630,7 +628,11 @@ const SLASHABLE_BLOCK_PROPOSAL_VALIDATION_RESULT = [
|
|
|
630
628
|
return Buffer.alloc(0);
|
|
631
629
|
}
|
|
632
630
|
const payloadToSign = authRequest.getPayloadToSign();
|
|
633
|
-
|
|
631
|
+
// AUTH_REQUEST doesn't require HA protection - multiple signatures are safe
|
|
632
|
+
const context = {
|
|
633
|
+
dutyType: DutyType.AUTH_REQUEST
|
|
634
|
+
};
|
|
635
|
+
const signature = await this.keyStore.signMessageWithAddress(addressToUse, payloadToSign, context);
|
|
634
636
|
const authResponse = new AuthResponse(statusMessage, signature);
|
|
635
637
|
return authResponse.toBuffer();
|
|
636
638
|
}
|
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.86469d5",
|
|
4
4
|
"main": "dest/index.js",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"exports": {
|
|
@@ -64,31 +64,35 @@
|
|
|
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.
|
|
67
|
+
"@aztec/blob-client": "0.0.1-commit.86469d5",
|
|
68
|
+
"@aztec/blob-lib": "0.0.1-commit.86469d5",
|
|
69
|
+
"@aztec/constants": "0.0.1-commit.86469d5",
|
|
70
|
+
"@aztec/epoch-cache": "0.0.1-commit.86469d5",
|
|
71
|
+
"@aztec/ethereum": "0.0.1-commit.86469d5",
|
|
72
|
+
"@aztec/foundation": "0.0.1-commit.86469d5",
|
|
73
|
+
"@aztec/node-keystore": "0.0.1-commit.86469d5",
|
|
74
|
+
"@aztec/noir-protocol-circuits-types": "0.0.1-commit.86469d5",
|
|
75
|
+
"@aztec/p2p": "0.0.1-commit.86469d5",
|
|
76
|
+
"@aztec/protocol-contracts": "0.0.1-commit.86469d5",
|
|
77
|
+
"@aztec/prover-client": "0.0.1-commit.86469d5",
|
|
78
|
+
"@aztec/simulator": "0.0.1-commit.86469d5",
|
|
79
|
+
"@aztec/slasher": "0.0.1-commit.86469d5",
|
|
80
|
+
"@aztec/stdlib": "0.0.1-commit.86469d5",
|
|
81
|
+
"@aztec/telemetry-client": "0.0.1-commit.86469d5",
|
|
82
|
+
"@aztec/validator-ha-signer": "0.0.1-commit.86469d5",
|
|
82
83
|
"koa": "^2.16.1",
|
|
83
84
|
"koa-router": "^13.1.1",
|
|
84
85
|
"tslib": "^2.4.0",
|
|
85
86
|
"viem": "npm:@aztec/viem@2.38.2"
|
|
86
87
|
},
|
|
87
88
|
"devDependencies": {
|
|
89
|
+
"@aztec/archiver": "0.0.1-commit.86469d5",
|
|
90
|
+
"@aztec/world-state": "0.0.1-commit.86469d5",
|
|
91
|
+
"@electric-sql/pglite": "^0.3.14",
|
|
88
92
|
"@jest/globals": "^30.0.0",
|
|
89
93
|
"@types/jest": "^30.0.0",
|
|
90
94
|
"@types/node": "^22.15.17",
|
|
91
|
-
"@typescript/native-preview": "7.0.0-dev.
|
|
95
|
+
"@typescript/native-preview": "7.0.0-dev.20260113.1",
|
|
92
96
|
"jest": "^30.0.0",
|
|
93
97
|
"jest-mock-extended": "^4.0.0",
|
|
94
98
|
"ts-node": "^10.9.1",
|
|
@@ -1,17 +1,22 @@
|
|
|
1
1
|
import { INITIAL_L2_BLOCK_NUM } from '@aztec/constants';
|
|
2
|
+
import type { EpochCache } from '@aztec/epoch-cache';
|
|
2
3
|
import { BlockNumber, CheckpointNumber, SlotNumber } from '@aztec/foundation/branded-types';
|
|
4
|
+
import { chunkBy } from '@aztec/foundation/collection';
|
|
3
5
|
import { Fr } from '@aztec/foundation/curves/bn254';
|
|
4
6
|
import { TimeoutError } from '@aztec/foundation/error';
|
|
5
7
|
import { createLogger } from '@aztec/foundation/log';
|
|
6
8
|
import { retryUntil } from '@aztec/foundation/retry';
|
|
7
9
|
import { DateProvider, Timer } from '@aztec/foundation/timer';
|
|
8
10
|
import type { P2P, PeerId } from '@aztec/p2p';
|
|
9
|
-
import { TxProvider } from '@aztec/p2p';
|
|
10
11
|
import { BlockProposalValidator } from '@aztec/p2p/msg_validators';
|
|
11
|
-
import type {
|
|
12
|
-
import { getTimestampForSlot } from '@aztec/stdlib/epoch-helpers';
|
|
13
|
-
import type { ValidatorClientFullConfig, WorldStateSynchronizer } from '@aztec/stdlib/interfaces/server';
|
|
14
|
-
import {
|
|
12
|
+
import type { L2Block, L2BlockSink, L2BlockSource } from '@aztec/stdlib/block';
|
|
13
|
+
import { getEpochAtSlot, getTimestampForSlot } from '@aztec/stdlib/epoch-helpers';
|
|
14
|
+
import type { ITxProvider, ValidatorClientFullConfig, WorldStateSynchronizer } from '@aztec/stdlib/interfaces/server';
|
|
15
|
+
import {
|
|
16
|
+
type L1ToL2MessageSource,
|
|
17
|
+
computeCheckpointOutHash,
|
|
18
|
+
computeInHashFromL1ToL2Messages,
|
|
19
|
+
} from '@aztec/stdlib/messaging';
|
|
15
20
|
import type { BlockProposal } from '@aztec/stdlib/p2p';
|
|
16
21
|
import { BlockHeader, type CheckpointGlobalVariables, type FailedTx, type Tx } from '@aztec/stdlib/tx';
|
|
17
22
|
import {
|
|
@@ -39,7 +44,7 @@ export type BlockProposalValidationFailureReason =
|
|
|
39
44
|
| 'unknown_error';
|
|
40
45
|
|
|
41
46
|
type ReexecuteTransactionsResult = {
|
|
42
|
-
block:
|
|
47
|
+
block: L2Block;
|
|
43
48
|
failedTxs: FailedTx[];
|
|
44
49
|
reexecutionTimeMs: number;
|
|
45
50
|
totalManaUsed: number;
|
|
@@ -72,8 +77,9 @@ export class BlockProposalHandler {
|
|
|
72
77
|
private worldState: WorldStateSynchronizer,
|
|
73
78
|
private blockSource: L2BlockSource & L2BlockSink,
|
|
74
79
|
private l1ToL2MessageSource: L1ToL2MessageSource,
|
|
75
|
-
private txProvider:
|
|
80
|
+
private txProvider: ITxProvider,
|
|
76
81
|
private blockProposalValidator: BlockProposalValidator,
|
|
82
|
+
private epochCache: EpochCache,
|
|
77
83
|
private config: ValidatorClientFullConfig,
|
|
78
84
|
private metrics?: ValidatorMetrics,
|
|
79
85
|
private dateProvider: DateProvider = new DateProvider(),
|
|
@@ -140,8 +146,8 @@ export class BlockProposalHandler {
|
|
|
140
146
|
|
|
141
147
|
// Check that the proposal is from the current proposer, or the next proposer
|
|
142
148
|
// This should have been handled by the p2p layer, but we double check here out of caution
|
|
143
|
-
const
|
|
144
|
-
if (
|
|
149
|
+
const validationResult = await this.blockProposalValidator.validate(proposal);
|
|
150
|
+
if (validationResult.result !== 'accept') {
|
|
145
151
|
this.log.warn(`Proposal is not valid, skipping processing`, proposalInfo);
|
|
146
152
|
return { isValid: false, reason: 'invalid_proposal' };
|
|
147
153
|
}
|
|
@@ -153,9 +159,9 @@ export class BlockProposalHandler {
|
|
|
153
159
|
return { isValid: false, reason: 'parent_block_not_found' };
|
|
154
160
|
}
|
|
155
161
|
|
|
156
|
-
// Check that the parent block's slot is
|
|
157
|
-
if (parentBlockHeader !== 'genesis' && parentBlockHeader.getSlot()
|
|
158
|
-
this.log.warn(`Parent block slot is greater than
|
|
162
|
+
// Check that the parent block's slot is not greater than the proposal's slot.
|
|
163
|
+
if (parentBlockHeader !== 'genesis' && parentBlockHeader.getSlot() > slotNumber) {
|
|
164
|
+
this.log.warn(`Parent block slot is greater than proposal slot, skipping processing`, {
|
|
159
165
|
parentBlockSlot: parentBlockHeader.getSlot().toString(),
|
|
160
166
|
proposalSlot: slotNumber.toString(),
|
|
161
167
|
...proposalInfo,
|
|
@@ -212,6 +218,18 @@ export class BlockProposalHandler {
|
|
|
212
218
|
// Try re-executing the transactions in the proposal if needed
|
|
213
219
|
let reexecutionResult;
|
|
214
220
|
if (shouldReexecute) {
|
|
221
|
+
// Compute the previous checkpoint out hashes for the epoch.
|
|
222
|
+
// TODO(leila/mbps): There can be a more efficient way to get the previous checkpoint out
|
|
223
|
+
// hashes without having to fetch all the blocks.
|
|
224
|
+
const epoch = getEpochAtSlot(slotNumber, this.epochCache.getL1Constants());
|
|
225
|
+
const checkpointedBlocks = (await this.blockSource.getCheckpointedBlocksForEpoch(epoch))
|
|
226
|
+
.filter(b => b.block.number < blockNumber)
|
|
227
|
+
.sort((a, b) => a.block.number - b.block.number);
|
|
228
|
+
const blocksByCheckpoint = chunkBy(checkpointedBlocks, b => b.checkpointNumber);
|
|
229
|
+
const previousCheckpointOutHashes = blocksByCheckpoint.map(checkpointBlocks =>
|
|
230
|
+
computeCheckpointOutHash(checkpointBlocks.map(b => b.block.body.txEffects.map(tx => tx.l2ToL1Msgs))),
|
|
231
|
+
);
|
|
232
|
+
|
|
215
233
|
try {
|
|
216
234
|
this.log.verbose(`Re-executing transactions in the proposal`, proposalInfo);
|
|
217
235
|
reexecutionResult = await this.reexecuteTransactions(
|
|
@@ -220,6 +238,7 @@ export class BlockProposalHandler {
|
|
|
220
238
|
checkpointNumber,
|
|
221
239
|
txs,
|
|
222
240
|
l1ToL2Messages,
|
|
241
|
+
previousCheckpointOutHashes,
|
|
223
242
|
);
|
|
224
243
|
} catch (error) {
|
|
225
244
|
this.log.error(`Error reexecuting txs while processing block proposal`, error, proposalInfo);
|
|
@@ -229,7 +248,6 @@ export class BlockProposalHandler {
|
|
|
229
248
|
}
|
|
230
249
|
|
|
231
250
|
// If we succeeded, push this block into the archiver (unless disabled)
|
|
232
|
-
// TODO(palla/mbps): Change default to false once block sync is stable.
|
|
233
251
|
if (reexecutionResult?.block && this.config.skipPushProposedBlocksToArchiver === false) {
|
|
234
252
|
await this.blockSource.addBlock(reexecutionResult?.block);
|
|
235
253
|
}
|
|
@@ -297,7 +315,7 @@ export class BlockProposalHandler {
|
|
|
297
315
|
// TODO(palla/mbps): The block header should include the checkpoint number to avoid this lookup,
|
|
298
316
|
// or at least the L2BlockSource should return a different struct that includes it.
|
|
299
317
|
const parentBlockNumber = parentBlockHeader.getBlockNumber();
|
|
300
|
-
const parentBlock = await this.blockSource.
|
|
318
|
+
const parentBlock = await this.blockSource.getL2Block(parentBlockNumber);
|
|
301
319
|
if (!parentBlock) {
|
|
302
320
|
this.log.warn(`Parent block ${parentBlockNumber} not found in archiver`, proposalInfo);
|
|
303
321
|
return { reason: 'invalid_proposal' };
|
|
@@ -338,7 +356,7 @@ export class BlockProposalHandler {
|
|
|
338
356
|
*/
|
|
339
357
|
private validateNonFirstBlockInCheckpoint(
|
|
340
358
|
proposal: BlockProposal,
|
|
341
|
-
parentBlock:
|
|
359
|
+
parentBlock: L2Block,
|
|
342
360
|
proposalInfo: object,
|
|
343
361
|
): CheckpointComputationResult | undefined {
|
|
344
362
|
const proposalGlobals = proposal.blockHeader.globalVariables;
|
|
@@ -414,31 +432,7 @@ export class BlockProposalHandler {
|
|
|
414
432
|
|
|
415
433
|
private getReexecutionDeadline(slot: SlotNumber, config: { l1GenesisTime: bigint; slotDuration: number }): Date {
|
|
416
434
|
const nextSlotTimestampSeconds = Number(getTimestampForSlot(SlotNumber(slot + 1), config));
|
|
417
|
-
|
|
418
|
-
return new Date(nextSlotTimestampSeconds * 1000 - msNeededForPropagationAndPublishing);
|
|
419
|
-
}
|
|
420
|
-
|
|
421
|
-
/**
|
|
422
|
-
* Gets all prior blocks in the same checkpoint (same slot and checkpoint number) up to but not including upToBlockNumber.
|
|
423
|
-
*/
|
|
424
|
-
private async getBlocksInCheckpoint(
|
|
425
|
-
slot: SlotNumber,
|
|
426
|
-
upToBlockNumber: BlockNumber,
|
|
427
|
-
checkpointNumber: CheckpointNumber,
|
|
428
|
-
): Promise<L2BlockNew[]> {
|
|
429
|
-
const blocks: L2BlockNew[] = [];
|
|
430
|
-
let currentBlockNumber = BlockNumber(upToBlockNumber - 1);
|
|
431
|
-
|
|
432
|
-
while (currentBlockNumber >= INITIAL_L2_BLOCK_NUM) {
|
|
433
|
-
const block = await this.blockSource.getL2BlockNew(currentBlockNumber);
|
|
434
|
-
if (!block || block.header.getSlot() !== slot || block.checkpointNumber !== checkpointNumber) {
|
|
435
|
-
break;
|
|
436
|
-
}
|
|
437
|
-
blocks.unshift(block);
|
|
438
|
-
currentBlockNumber = BlockNumber(currentBlockNumber - 1);
|
|
439
|
-
}
|
|
440
|
-
|
|
441
|
-
return blocks;
|
|
435
|
+
return new Date(nextSlotTimestampSeconds * 1000);
|
|
442
436
|
}
|
|
443
437
|
|
|
444
438
|
private getReexecuteFailureReason(err: any) {
|
|
@@ -459,6 +453,7 @@ export class BlockProposalHandler {
|
|
|
459
453
|
checkpointNumber: CheckpointNumber,
|
|
460
454
|
txs: Tx[],
|
|
461
455
|
l1ToL2Messages: Fr[],
|
|
456
|
+
previousCheckpointOutHashes: Fr[],
|
|
462
457
|
): Promise<ReexecuteTransactionsResult> {
|
|
463
458
|
const { blockHeader, txHashes } = proposal;
|
|
464
459
|
|
|
@@ -473,11 +468,13 @@ export class BlockProposalHandler {
|
|
|
473
468
|
const slot = proposal.slotNumber;
|
|
474
469
|
const config = this.checkpointsBuilder.getConfig();
|
|
475
470
|
|
|
476
|
-
// Get prior blocks in this checkpoint (same slot
|
|
477
|
-
const
|
|
471
|
+
// Get prior blocks in this checkpoint (same slot before current block)
|
|
472
|
+
const allBlocksInSlot = await this.blockSource.getBlocksForSlot(slot);
|
|
473
|
+
const priorBlocks = allBlocksInSlot.filter(b => b.number < blockNumber && b.header.getSlot() === slot);
|
|
478
474
|
|
|
479
475
|
// Fork before the block to be built
|
|
480
476
|
const parentBlockNumber = BlockNumber(blockNumber - 1);
|
|
477
|
+
await this.worldState.syncImmediate(parentBlockNumber);
|
|
481
478
|
using fork = await this.worldState.fork(parentBlockNumber);
|
|
482
479
|
|
|
483
480
|
// Build checkpoint constants from proposal (excludes blockNumber and timestamp which are per-block)
|
|
@@ -495,8 +492,10 @@ export class BlockProposalHandler {
|
|
|
495
492
|
checkpointNumber,
|
|
496
493
|
constants,
|
|
497
494
|
l1ToL2Messages,
|
|
495
|
+
previousCheckpointOutHashes,
|
|
498
496
|
fork,
|
|
499
497
|
priorBlocks,
|
|
498
|
+
this.log.getBindings(),
|
|
500
499
|
);
|
|
501
500
|
|
|
502
501
|
// Build the new block
|