@aztec/validator-client 0.0.1-commit.2ed92850 → 0.0.1-commit.43597cc1

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.
@@ -1,9 +1,9 @@
1
1
  import { BlockNumber, CheckpointNumber } from '@aztec/foundation/branded-types';
2
2
  import { merge, pick } from '@aztec/foundation/collection';
3
3
  import { Fr } from '@aztec/foundation/curves/bn254';
4
- import { createLogger } from '@aztec/foundation/log';
4
+ import { type Logger, type LoggerBindings, createLogger } from '@aztec/foundation/log';
5
5
  import { bufferToHex } from '@aztec/foundation/string';
6
- import { DateProvider, Timer, elapsed } from '@aztec/foundation/timer';
6
+ import { DateProvider, elapsed } from '@aztec/foundation/timer';
7
7
  import { getDefaultAllowedSetupFunctions } from '@aztec/p2p/msg_validators';
8
8
  import { LightweightCheckpointBuilder } from '@aztec/prover-client/light';
9
9
  import {
@@ -24,6 +24,7 @@ import {
24
24
  type ICheckpointBlockBuilder,
25
25
  type ICheckpointsBuilder,
26
26
  type MerkleTreeWriteOperations,
27
+ NoValidTxsError,
27
28
  type PublicProcessorLimits,
28
29
  type WorldStateSynchronizer,
29
30
  } from '@aztec/stdlib/interfaces/server';
@@ -36,18 +37,13 @@ import { createValidatorForBlockBuilding } from './tx_validator/tx_validator_fac
36
37
  // Re-export for backward compatibility
37
38
  export type { BuildBlockInCheckpointResult } from '@aztec/stdlib/interfaces/server';
38
39
 
39
- const log = createLogger('checkpoint-builder');
40
-
41
- /** Result of building a block within a checkpoint. Extends the base interface with timer. */
42
- export interface BuildBlockInCheckpointResultWithTimer extends BuildBlockInCheckpointResult {
43
- blockBuildingTimer: Timer;
44
- }
45
-
46
40
  /**
47
41
  * Builder for a single checkpoint. Handles building blocks within the checkpoint
48
42
  * and completing it.
49
43
  */
50
44
  export class CheckpointBuilder implements ICheckpointBlockBuilder {
45
+ private log: Logger;
46
+
51
47
  constructor(
52
48
  private checkpointBuilder: LightweightCheckpointBuilder,
53
49
  private fork: MerkleTreeWriteOperations,
@@ -55,7 +51,13 @@ export class CheckpointBuilder implements ICheckpointBlockBuilder {
55
51
  private contractDataSource: ContractDataSource,
56
52
  private dateProvider: DateProvider,
57
53
  private telemetryClient: TelemetryClient,
58
- ) {}
54
+ bindings?: LoggerBindings,
55
+ ) {
56
+ this.log = createLogger('checkpoint-builder', {
57
+ ...bindings,
58
+ instanceId: `checkpoint-${checkpointBuilder.checkpointNumber}`,
59
+ });
60
+ }
59
61
 
60
62
  getConstantData(): CheckpointGlobalVariables {
61
63
  return this.checkpointBuilder.constants;
@@ -68,12 +70,11 @@ export class CheckpointBuilder implements ICheckpointBlockBuilder {
68
70
  pendingTxs: Iterable<Tx> | AsyncIterable<Tx>,
69
71
  blockNumber: BlockNumber,
70
72
  timestamp: bigint,
71
- opts: PublicProcessorLimits & { expectedEndState?: StateReference },
72
- ): Promise<BuildBlockInCheckpointResultWithTimer> {
73
- const blockBuildingTimer = new Timer();
73
+ opts: PublicProcessorLimits & { expectedEndState?: StateReference } = {},
74
+ ): Promise<BuildBlockInCheckpointResult> {
74
75
  const slot = this.checkpointBuilder.constants.slotNumber;
75
76
 
76
- log.verbose(`Building block ${blockNumber} for slot ${slot} within checkpoint`, {
77
+ this.log.verbose(`Building block ${blockNumber} for slot ${slot} within checkpoint`, {
77
78
  slot,
78
79
  blockNumber,
79
80
  ...opts,
@@ -97,6 +98,12 @@ export class CheckpointBuilder implements ICheckpointBlockBuilder {
97
98
  processor.process(pendingTxs, opts, validator),
98
99
  );
99
100
 
101
+ // Throw if we didn't collect a single valid tx and we're not allowed to build empty blocks
102
+ // (only the first block in a checkpoint can be empty)
103
+ if (processedTxs.length === 0 && this.checkpointBuilder.getBlockCount() > 0) {
104
+ throw new NoValidTxsError(failedTxs);
105
+ }
106
+
100
107
  // Add block to checkpoint
101
108
  const block = await this.checkpointBuilder.addBlock(globalVariables, processedTxs, {
102
109
  expectedEndState: opts.expectedEndState,
@@ -105,25 +112,28 @@ export class CheckpointBuilder implements ICheckpointBlockBuilder {
105
112
  // How much public gas was processed
106
113
  const publicGas = processedTxs.reduce((acc, tx) => acc.add(tx.gasUsed.publicGas), Gas.empty());
107
114
 
108
- const res = {
115
+ this.log.debug('Built block within checkpoint', {
116
+ header: block.header.toInspect(),
117
+ processedTxs: processedTxs.map(tx => tx.hash.toString()),
118
+ failedTxs: failedTxs.map(tx => tx.tx.txHash.toString()),
119
+ });
120
+
121
+ return {
109
122
  block,
110
123
  publicGas,
111
124
  publicProcessorDuration,
112
125
  numTxs: processedTxs.length,
113
126
  failedTxs,
114
- blockBuildingTimer,
115
127
  usedTxs,
116
128
  usedTxBlobFields,
117
129
  };
118
- log.debug('Built block within checkpoint', res.block.header);
119
- return res;
120
130
  }
121
131
 
122
132
  /** Completes the checkpoint and returns it. */
123
133
  async completeCheckpoint(): Promise<Checkpoint> {
124
134
  const checkpoint = await this.checkpointBuilder.completeCheckpoint();
125
135
 
126
- log.verbose(`Completed checkpoint ${checkpoint.number}`, {
136
+ this.log.verbose(`Completed checkpoint ${checkpoint.number}`, {
127
137
  checkpointNumber: checkpoint.number,
128
138
  numBlocks: checkpoint.blocks.length,
129
139
  archiveRoot: checkpoint.archive.root.toString(),
@@ -139,14 +149,16 @@ export class CheckpointBuilder implements ICheckpointBlockBuilder {
139
149
 
140
150
  protected async makeBlockBuilderDeps(globalVariables: GlobalVariables, fork: MerkleTreeWriteOperations) {
141
151
  const txPublicSetupAllowList = this.config.txPublicSetupAllowList ?? (await getDefaultAllowedSetupFunctions());
142
- const contractsDB = new PublicContractsDB(this.contractDataSource);
152
+ const contractsDB = new PublicContractsDB(this.contractDataSource, this.log.getBindings());
143
153
  const guardedFork = new GuardedMerkleTreeOperations(fork);
144
154
 
155
+ const bindings = this.log.getBindings();
145
156
  const publicTxSimulator = createPublicTxSimulatorForBlockBuilding(
146
157
  guardedFork,
147
158
  contractsDB,
148
159
  globalVariables,
149
160
  this.telemetryClient,
161
+ bindings,
150
162
  );
151
163
 
152
164
  const processor = new PublicProcessor(
@@ -156,7 +168,7 @@ export class CheckpointBuilder implements ICheckpointBlockBuilder {
156
168
  publicTxSimulator,
157
169
  this.dateProvider,
158
170
  this.telemetryClient,
159
- undefined,
171
+ createLogger('simulator:public-processor', bindings),
160
172
  this.config,
161
173
  );
162
174
 
@@ -165,6 +177,7 @@ export class CheckpointBuilder implements ICheckpointBlockBuilder {
165
177
  this.contractDataSource,
166
178
  globalVariables,
167
179
  txPublicSetupAllowList,
180
+ this.log.getBindings(),
168
181
  );
169
182
 
170
183
  return {
@@ -176,13 +189,17 @@ export class CheckpointBuilder implements ICheckpointBlockBuilder {
176
189
 
177
190
  /** Factory for creating checkpoint builders. */
178
191
  export class FullNodeCheckpointsBuilder implements ICheckpointsBuilder {
192
+ private log: Logger;
193
+
179
194
  constructor(
180
195
  private config: FullNodeBlockBuilderConfig & Pick<L1RollupConstants, 'l1GenesisTime' | 'slotDuration'>,
181
196
  private worldState: WorldStateSynchronizer,
182
197
  private contractDataSource: ContractDataSource,
183
198
  private dateProvider: DateProvider,
184
199
  private telemetryClient: TelemetryClient = getTelemetryClient(),
185
- ) {}
200
+ ) {
201
+ this.log = createLogger('checkpoint-builder');
202
+ }
186
203
 
187
204
  public getConfig(): FullNodeBlockBuilderConfig {
188
205
  return this.config;
@@ -201,11 +218,12 @@ export class FullNodeCheckpointsBuilder implements ICheckpointsBuilder {
201
218
  l1ToL2Messages: Fr[],
202
219
  previousCheckpointOutHashes: Fr[],
203
220
  fork: MerkleTreeWriteOperations,
221
+ bindings?: LoggerBindings,
204
222
  ): Promise<CheckpointBuilder> {
205
223
  const stateReference = await fork.getStateReference();
206
224
  const archiveTree = await fork.getTreeInfo(MerkleTreeId.ARCHIVE);
207
225
 
208
- log.verbose(`Building new checkpoint ${checkpointNumber}`, {
226
+ this.log.verbose(`Building new checkpoint ${checkpointNumber}`, {
209
227
  checkpointNumber,
210
228
  msgCount: l1ToL2Messages.length,
211
229
  initialStateReference: stateReference.toInspect(),
@@ -219,6 +237,7 @@ export class FullNodeCheckpointsBuilder implements ICheckpointsBuilder {
219
237
  l1ToL2Messages,
220
238
  previousCheckpointOutHashes,
221
239
  fork,
240
+ bindings,
222
241
  );
223
242
 
224
243
  return new CheckpointBuilder(
@@ -228,6 +247,7 @@ export class FullNodeCheckpointsBuilder implements ICheckpointsBuilder {
228
247
  this.contractDataSource,
229
248
  this.dateProvider,
230
249
  this.telemetryClient,
250
+ bindings,
231
251
  );
232
252
  }
233
253
 
@@ -241,15 +261,23 @@ export class FullNodeCheckpointsBuilder implements ICheckpointsBuilder {
241
261
  previousCheckpointOutHashes: Fr[],
242
262
  fork: MerkleTreeWriteOperations,
243
263
  existingBlocks: L2Block[] = [],
264
+ bindings?: LoggerBindings,
244
265
  ): Promise<CheckpointBuilder> {
245
266
  const stateReference = await fork.getStateReference();
246
267
  const archiveTree = await fork.getTreeInfo(MerkleTreeId.ARCHIVE);
247
268
 
248
269
  if (existingBlocks.length === 0) {
249
- return this.startCheckpoint(checkpointNumber, constants, l1ToL2Messages, previousCheckpointOutHashes, fork);
270
+ return this.startCheckpoint(
271
+ checkpointNumber,
272
+ constants,
273
+ l1ToL2Messages,
274
+ previousCheckpointOutHashes,
275
+ fork,
276
+ bindings,
277
+ );
250
278
  }
251
279
 
252
- log.verbose(`Resuming checkpoint ${checkpointNumber} with ${existingBlocks.length} existing blocks`, {
280
+ this.log.verbose(`Resuming checkpoint ${checkpointNumber} with ${existingBlocks.length} existing blocks`, {
253
281
  checkpointNumber,
254
282
  msgCount: l1ToL2Messages.length,
255
283
  existingBlockCount: existingBlocks.length,
@@ -265,6 +293,7 @@ export class FullNodeCheckpointsBuilder implements ICheckpointsBuilder {
265
293
  previousCheckpointOutHashes,
266
294
  fork,
267
295
  existingBlocks,
296
+ bindings,
268
297
  );
269
298
 
270
299
  return new CheckpointBuilder(
@@ -274,6 +303,7 @@ export class FullNodeCheckpointsBuilder implements ICheckpointsBuilder {
274
303
  this.contractDataSource,
275
304
  this.dateProvider,
276
305
  this.telemetryClient,
306
+ bindings,
277
307
  );
278
308
  }
279
309
 
package/src/config.ts CHANGED
@@ -65,10 +65,9 @@ export const validatorClientConfigMappings: ConfigMappingsType<ValidatorClientCo
65
65
  'Whether to run in fisherman mode: validates all proposals and attestations but does not broadcast attestations or participate in consensus.',
66
66
  ...booleanConfigHelper(false),
67
67
  },
68
- // TODO(palla/mbps): Change default to false once checkpoint validation is stable
69
68
  skipCheckpointProposalValidation: {
70
- description: 'Skip checkpoint proposal validation and always attest (default: true)',
71
- defaultValue: true,
69
+ description: 'Skip checkpoint proposal validation and always attest (default: false)',
70
+ defaultValue: false,
72
71
  },
73
72
  skipPushProposedBlocksToArchiver: {
74
73
  description: 'Skip pushing re-executed blocks to archiver (default: false)',
@@ -256,8 +256,8 @@ export class HAKeyStore implements ExtendedValidatorKeyStore {
256
256
  /**
257
257
  * Start the high-availability key store
258
258
  */
259
- public start(): Promise<void> {
260
- return Promise.resolve(this.haSigner.start());
259
+ public async start() {
260
+ await this.haSigner.start();
261
261
  }
262
262
 
263
263
  /**
package/src/metrics.ts CHANGED
@@ -6,8 +6,11 @@ import {
6
6
  Metrics,
7
7
  type TelemetryClient,
8
8
  type UpDownCounter,
9
+ createUpDownCounterWithDefault,
9
10
  } from '@aztec/telemetry-client';
10
11
 
12
+ import type { BlockProposalValidationFailureReason } from './block_proposal_handler.js';
13
+
11
14
  export class ValidatorMetrics {
12
15
  private failedReexecutionCounter: UpDownCounter;
13
16
  private successfulAttestationsCount: UpDownCounter;
@@ -21,16 +24,44 @@ export class ValidatorMetrics {
21
24
  constructor(telemetryClient: TelemetryClient) {
22
25
  const meter = telemetryClient.getMeter('Validator');
23
26
 
24
- this.failedReexecutionCounter = meter.createUpDownCounter(Metrics.VALIDATOR_FAILED_REEXECUTION_COUNT);
27
+ this.failedReexecutionCounter = createUpDownCounterWithDefault(meter, Metrics.VALIDATOR_FAILED_REEXECUTION_COUNT, {
28
+ [Attributes.STATUS]: ['failed'],
29
+ });
25
30
 
26
- this.successfulAttestationsCount = meter.createUpDownCounter(Metrics.VALIDATOR_ATTESTATION_SUCCESS_COUNT);
31
+ this.successfulAttestationsCount = createUpDownCounterWithDefault(
32
+ meter,
33
+ Metrics.VALIDATOR_ATTESTATION_SUCCESS_COUNT,
34
+ );
27
35
 
28
- this.failedAttestationsBadProposalCount = meter.createUpDownCounter(
36
+ this.failedAttestationsBadProposalCount = createUpDownCounterWithDefault(
37
+ meter,
29
38
  Metrics.VALIDATOR_ATTESTATION_FAILED_BAD_PROPOSAL_COUNT,
39
+ {
40
+ [Attributes.ERROR_TYPE]: [
41
+ 'invalid_proposal',
42
+ 'state_mismatch',
43
+ 'failed_txs',
44
+ 'in_hash_mismatch',
45
+ 'parent_block_wrong_slot',
46
+ ],
47
+ [Attributes.IS_COMMITTEE_MEMBER]: [true, false],
48
+ },
30
49
  );
31
50
 
32
- this.failedAttestationsNodeIssueCount = meter.createUpDownCounter(
51
+ this.failedAttestationsNodeIssueCount = createUpDownCounterWithDefault(
52
+ meter,
33
53
  Metrics.VALIDATOR_ATTESTATION_FAILED_NODE_ISSUE_COUNT,
54
+ {
55
+ [Attributes.ERROR_TYPE]: [
56
+ 'parent_block_not_found',
57
+ 'global_variables_mismatch',
58
+ 'block_number_already_exists',
59
+ 'txs_not_available',
60
+ 'timeout',
61
+ 'unknown_error',
62
+ ],
63
+ [Attributes.IS_COMMITTEE_MEMBER]: [true, false],
64
+ },
34
65
  );
35
66
 
36
67
  this.reexMana = meter.createHistogram(Metrics.VALIDATOR_RE_EXECUTION_MANA);
@@ -58,14 +89,22 @@ export class ValidatorMetrics {
58
89
  this.successfulAttestationsCount.add(num);
59
90
  }
60
91
 
61
- public incFailedAttestationsBadProposal(num: number, reason: string, inCommittee: boolean) {
92
+ public incFailedAttestationsBadProposal(
93
+ num: number,
94
+ reason: BlockProposalValidationFailureReason,
95
+ inCommittee: boolean,
96
+ ) {
62
97
  this.failedAttestationsBadProposalCount.add(num, {
63
98
  [Attributes.ERROR_TYPE]: reason,
64
99
  [Attributes.IS_COMMITTEE_MEMBER]: inCommittee,
65
100
  });
66
101
  }
67
102
 
68
- public incFailedAttestationsNodeIssue(num: number, reason: string, inCommittee: boolean) {
103
+ public incFailedAttestationsNodeIssue(
104
+ num: number,
105
+ reason: BlockProposalValidationFailureReason,
106
+ inCommittee: boolean,
107
+ ) {
69
108
  this.failedAttestationsNodeIssueCount.add(num, {
70
109
  [Attributes.ERROR_TYPE]: reason,
71
110
  [Attributes.IS_COMMITTEE_MEMBER]: inCommittee,
@@ -1,5 +1,6 @@
1
1
  import { BlockNumber } from '@aztec/foundation/branded-types';
2
2
  import { Fr } from '@aztec/foundation/curves/bn254';
3
+ import type { LoggerBindings } from '@aztec/foundation/log';
3
4
  import { getVKTreeRoot } from '@aztec/noir-protocol-circuits-types/vk-tree';
4
5
  import {
5
6
  AggregateTxValidator,
@@ -53,32 +54,41 @@ export function createValidatorForAcceptingTxs(
53
54
  blockNumber: BlockNumber;
54
55
  txsPermitted: boolean;
55
56
  },
57
+ bindings?: LoggerBindings,
56
58
  ): TxValidator<Tx> {
57
59
  const validators: TxValidator<Tx>[] = [
58
- new TxPermittedValidator(txsPermitted),
59
- new SizeTxValidator(),
60
- new DataTxValidator(),
61
- new MetadataTxValidator({
62
- l1ChainId: new Fr(l1ChainId),
63
- rollupVersion: new Fr(rollupVersion),
64
- protocolContractsHash,
65
- vkTreeRoot: getVKTreeRoot(),
66
- }),
67
- new TimestampTxValidator({
68
- timestamp,
69
- blockNumber,
70
- }),
71
- new DoubleSpendTxValidator(new NullifierCache(db)),
72
- new PhasesTxValidator(contractDataSource, setupAllowList, timestamp),
73
- new BlockHeaderTxValidator(new ArchiveCache(db)),
60
+ new TxPermittedValidator(txsPermitted, bindings),
61
+ new SizeTxValidator(bindings),
62
+ new DataTxValidator(bindings),
63
+ new MetadataTxValidator(
64
+ {
65
+ l1ChainId: new Fr(l1ChainId),
66
+ rollupVersion: new Fr(rollupVersion),
67
+ protocolContractsHash,
68
+ vkTreeRoot: getVKTreeRoot(),
69
+ },
70
+ bindings,
71
+ ),
72
+ new TimestampTxValidator(
73
+ {
74
+ timestamp,
75
+ blockNumber,
76
+ },
77
+ bindings,
78
+ ),
79
+ new DoubleSpendTxValidator(new NullifierCache(db), bindings),
80
+ new PhasesTxValidator(contractDataSource, setupAllowList, timestamp, bindings),
81
+ new BlockHeaderTxValidator(new ArchiveCache(db), bindings),
74
82
  ];
75
83
 
76
84
  if (!skipFeeEnforcement) {
77
- validators.push(new GasTxValidator(new DatabasePublicStateSource(db), ProtocolContractAddress.FeeJuice, gasFees));
85
+ validators.push(
86
+ new GasTxValidator(new DatabasePublicStateSource(db), ProtocolContractAddress.FeeJuice, gasFees, bindings),
87
+ );
78
88
  }
79
89
 
80
90
  if (verifier) {
81
- validators.push(new TxProofValidator(verifier));
91
+ validators.push(new TxProofValidator(verifier, bindings));
82
92
  }
83
93
 
84
94
  return new AggregateTxValidator(...validators);
@@ -89,6 +99,7 @@ export function createValidatorForBlockBuilding(
89
99
  contractDataSource: ContractDataSource,
90
100
  globalVariables: GlobalVariables,
91
101
  setupAllowList: AllowedElement[],
102
+ bindings?: LoggerBindings,
92
103
  ): PublicProcessorValidator {
93
104
  const nullifierCache = new NullifierCache(db);
94
105
  const archiveCache = new ArchiveCache(db);
@@ -102,6 +113,7 @@ export function createValidatorForBlockBuilding(
102
113
  contractDataSource,
103
114
  globalVariables,
104
115
  setupAllowList,
116
+ bindings,
105
117
  ),
106
118
  nullifierCache,
107
119
  };
@@ -114,22 +126,29 @@ function preprocessValidator(
114
126
  contractDataSource: ContractDataSource,
115
127
  globalVariables: GlobalVariables,
116
128
  setupAllowList: AllowedElement[],
129
+ bindings?: LoggerBindings,
117
130
  ): TxValidator<Tx> {
118
131
  // We don't include the TxProofValidator nor the DataTxValidator here because they are already checked by the time we get to block building.
119
132
  return new AggregateTxValidator(
120
- new MetadataTxValidator({
121
- l1ChainId: globalVariables.chainId,
122
- rollupVersion: globalVariables.version,
123
- protocolContractsHash,
124
- vkTreeRoot: getVKTreeRoot(),
125
- }),
126
- new TimestampTxValidator({
127
- timestamp: globalVariables.timestamp,
128
- blockNumber: globalVariables.blockNumber,
129
- }),
130
- new DoubleSpendTxValidator(nullifierCache),
131
- new PhasesTxValidator(contractDataSource, setupAllowList, globalVariables.timestamp),
132
- new GasTxValidator(publicStateSource, ProtocolContractAddress.FeeJuice, globalVariables.gasFees),
133
- new BlockHeaderTxValidator(archiveCache),
133
+ new MetadataTxValidator(
134
+ {
135
+ l1ChainId: globalVariables.chainId,
136
+ rollupVersion: globalVariables.version,
137
+ protocolContractsHash,
138
+ vkTreeRoot: getVKTreeRoot(),
139
+ },
140
+ bindings,
141
+ ),
142
+ new TimestampTxValidator(
143
+ {
144
+ timestamp: globalVariables.timestamp,
145
+ blockNumber: globalVariables.blockNumber,
146
+ },
147
+ bindings,
148
+ ),
149
+ new DoubleSpendTxValidator(nullifierCache, bindings),
150
+ new PhasesTxValidator(contractDataSource, setupAllowList, globalVariables.timestamp, bindings),
151
+ new GasTxValidator(publicStateSource, ProtocolContractAddress.FeeJuice, globalVariables.gasFees, bindings),
152
+ new BlockHeaderTxValidator(archiveCache, bindings),
134
153
  );
135
154
  }
package/src/validator.ts CHANGED
@@ -18,7 +18,7 @@ import { RunningPromise } from '@aztec/foundation/running-promise';
18
18
  import { sleep } from '@aztec/foundation/sleep';
19
19
  import { DateProvider } from '@aztec/foundation/timer';
20
20
  import type { KeystoreManager } from '@aztec/node-keystore';
21
- import type { P2P, PeerId, TxProvider } from '@aztec/p2p';
21
+ import type { DuplicateProposalInfo, P2P, PeerId } from '@aztec/p2p';
22
22
  import { AuthRequest, AuthResponse, BlockProposalValidator, ReqRespSubProtocol } from '@aztec/p2p';
23
23
  import { OffenseType, WANT_TO_SLASH_EVENT, type Watcher, type WatcherEmitter } from '@aztec/slasher';
24
24
  import type { AztecAddress } from '@aztec/stdlib/aztec-address';
@@ -26,11 +26,12 @@ import type { CommitteeAttestationsAndSigners, L2Block, L2BlockSink, L2BlockSour
26
26
  import { getEpochAtSlot } from '@aztec/stdlib/epoch-helpers';
27
27
  import type {
28
28
  CreateCheckpointProposalLastBlockData,
29
+ ITxProvider,
29
30
  Validator,
30
31
  ValidatorClientFullConfig,
31
32
  WorldStateSynchronizer,
32
33
  } from '@aztec/stdlib/interfaces/server';
33
- import type { L1ToL2MessageSource } from '@aztec/stdlib/messaging';
34
+ import { type L1ToL2MessageSource, accumulateCheckpointOutHashes } from '@aztec/stdlib/messaging';
34
35
  import type {
35
36
  BlockProposal,
36
37
  BlockProposalOptions,
@@ -87,11 +88,6 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
87
88
 
88
89
  private proposersOfInvalidBlocks: Set<string> = new Set();
89
90
 
90
- // TODO(palla/mbps): Remove this once checkpoint validation is stable and we can validate all blocks properly.
91
- // Tracks slots for which we have successfully validated a block proposal, so we can attest to checkpoint proposals for those slots.
92
- // eslint-disable-next-line aztec-custom/no-non-primitive-in-collections
93
- private validatedBlockSlots: Set<SlotNumber> = new Set();
94
-
95
91
  protected constructor(
96
92
  private keyStore: ExtendedValidatorKeyStore,
97
93
  private epochCache: EpochCache,
@@ -184,7 +180,7 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
184
180
  p2pClient: P2P,
185
181
  blockSource: L2BlockSource & L2BlockSink,
186
182
  l1ToL2MessageSource: L1ToL2MessageSource,
187
- txProvider: TxProvider,
183
+ txProvider: ITxProvider,
188
184
  keyStoreManager: KeystoreManager,
189
185
  blobClient: BlobClientInterface,
190
186
  dateProvider: DateProvider = new DateProvider(),
@@ -313,6 +309,11 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
313
309
  ): Promise<CheckpointAttestation[] | undefined> => this.attestToCheckpointProposal(checkpoint, proposalSender);
314
310
  this.p2pClient.registerCheckpointProposalHandler(checkpointHandler);
315
311
 
312
+ // Duplicate proposal handler - triggers slashing for equivocation
313
+ this.p2pClient.registerDuplicateProposalCallback((info: DuplicateProposalInfo) => {
314
+ this.handleDuplicateProposal(info);
315
+ });
316
+
316
317
  const myAddresses = this.getValidatorAddresses();
317
318
  this.p2pClient.registerThisValidatorAddresses(myAddresses);
318
319
 
@@ -413,10 +414,6 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
413
414
  return false;
414
415
  }
415
416
 
416
- // TODO(palla/mbps): Remove this once checkpoint validation is stable.
417
- // Track that we successfully validated a block for this slot, so we can attest to checkpoint proposals for it.
418
- this.validatedBlockSlots.add(slotNumber);
419
-
420
417
  return true;
421
418
  }
422
419
 
@@ -461,17 +458,9 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
461
458
  fishermanMode: this.config.fishermanMode || false,
462
459
  });
463
460
 
464
- // TODO(palla/mbps): Remove this once checkpoint validation is stable.
465
- // Check that we have successfully validated a block for this slot before attesting to the checkpoint.
466
- if (!this.validatedBlockSlots.has(slotNumber)) {
467
- this.log.warn(`No validated block found for slot ${slotNumber}, refusing to attest to checkpoint`, proposalInfo);
468
- return undefined;
469
- }
470
-
471
461
  // Validate the checkpoint proposal before attesting (unless skipCheckpointProposalValidation is set)
472
- // TODO(palla/mbps): Change default to false once checkpoint validation is stable.
473
- if (this.config.skipCheckpointProposalValidation !== false) {
474
- this.log.verbose(`Skipping checkpoint proposal validation for slot ${slotNumber}`, proposalInfo);
462
+ if (this.config.skipCheckpointProposalValidation) {
463
+ this.log.warn(`Skipping checkpoint proposal validation for slot ${slotNumber}`, proposalInfo);
475
464
  } else {
476
465
  const validationResult = await this.validateCheckpointProposal(proposal, proposalInfo);
477
466
  if (!validationResult.isValid) {
@@ -534,7 +523,7 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
534
523
  attestors: EthAddress[] = [],
535
524
  ): Promise<CheckpointAttestation[]> {
536
525
  const attestations = await this.validationService.attestToCheckpointProposal(proposal, attestors);
537
- await this.p2pClient.addCheckpointAttestations(attestations);
526
+ await this.p2pClient.addOwnCheckpointAttestations(attestations);
538
527
  return attestations;
539
528
  }
540
529
 
@@ -547,7 +536,7 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
547
536
  proposalInfo: LogData,
548
537
  ): Promise<{ isValid: true } | { isValid: false; reason: string }> {
549
538
  const slot = proposal.slotNumber;
550
- const timeoutSeconds = 10;
539
+ const timeoutSeconds = 10; // TODO(palla/mbps): This should map to the timetable settings
551
540
 
552
541
  // Wait for last block to sync by archive
553
542
  let lastBlockHeader: BlockHeader | undefined;
@@ -617,6 +606,7 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
617
606
  previousCheckpointOutHashes,
618
607
  fork,
619
608
  blocks,
609
+ this.log.getBindings(),
620
610
  );
621
611
 
622
612
  // Complete the checkpoint to get computed values
@@ -642,13 +632,17 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
642
632
  return { isValid: false, reason: 'archive_mismatch' };
643
633
  }
644
634
 
645
- // Check that the accumulated out hash matches the value in the proposal.
646
- const computedOutHash = computedCheckpoint.getCheckpointOutHash();
647
- const proposalOutHash = proposal.checkpointHeader.epochOutHash;
648
- if (!computedOutHash.equals(proposalOutHash)) {
635
+ // Check that the accumulated epoch out hash matches the value in the proposal.
636
+ // The epoch out hash is the accumulated hash of all checkpoint out hashes in the epoch.
637
+ const checkpointOutHash = computedCheckpoint.getCheckpointOutHash();
638
+ const computedEpochOutHash = accumulateCheckpointOutHashes([...previousCheckpointOutHashes, checkpointOutHash]);
639
+ const proposalEpochOutHash = proposal.checkpointHeader.epochOutHash;
640
+ if (!computedEpochOutHash.equals(proposalEpochOutHash)) {
649
641
  this.log.warn(`Epoch out hash mismatch`, {
650
- proposalOutHash: proposalOutHash.toString(),
651
- computedOutHash: computedOutHash.toString(),
642
+ proposalEpochOutHash: proposalEpochOutHash.toString(),
643
+ computedEpochOutHash: computedEpochOutHash.toString(),
644
+ checkpointOutHash: checkpointOutHash.toString(),
645
+ previousCheckpointOutHashes: previousCheckpointOutHashes.map(h => h.toString()),
652
646
  ...proposalInfo,
653
647
  });
654
648
  return { isValid: false, reason: 'out_hash_mismatch' };
@@ -732,6 +726,30 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
732
726
  ]);
733
727
  }
734
728
 
729
+ /**
730
+ * Handle detection of a duplicate proposal (equivocation).
731
+ * Emits a slash event when a proposer sends multiple proposals for the same position.
732
+ */
733
+ private handleDuplicateProposal(info: DuplicateProposalInfo): void {
734
+ const { slot, proposer, type } = info;
735
+
736
+ this.log.warn(`Triggering slash event for duplicate ${type} proposal from ${proposer.toString()} at slot ${slot}`, {
737
+ proposer: proposer.toString(),
738
+ slot,
739
+ type,
740
+ });
741
+
742
+ // Emit slash event
743
+ this.emit(WANT_TO_SLASH_EVENT, [
744
+ {
745
+ validator: proposer,
746
+ amount: this.config.slashDuplicateProposalPenalty,
747
+ offenseType: OffenseType.DUPLICATE_PROPOSAL,
748
+ epochOrSlot: BigInt(slot),
749
+ },
750
+ ]);
751
+ }
752
+
735
753
  async createBlockProposal(
736
754
  blockHeader: BlockHeader,
737
755
  indexWithinCheckpoint: IndexWithinCheckpoint,
@@ -739,7 +757,7 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
739
757
  archive: Fr,
740
758
  txs: Tx[],
741
759
  proposerAddress: EthAddress | undefined,
742
- options: BlockProposalOptions,
760
+ options: BlockProposalOptions = {},
743
761
  ): Promise<BlockProposal> {
744
762
  // TODO(palla/mbps): Prevent double proposals properly
745
763
  // if (this.previousProposal?.slotNumber === blockHeader.globalVariables.slotNumber) {
@@ -771,7 +789,7 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
771
789
  archive: Fr,
772
790
  lastBlockInfo: CreateCheckpointProposalLastBlockData | undefined,
773
791
  proposerAddress: EthAddress | undefined,
774
- options: CheckpointProposalOptions,
792
+ options: CheckpointProposalOptions = {},
775
793
  ): Promise<CheckpointProposal> {
776
794
  this.log.info(`Assembling checkpoint proposal for slot ${checkpointHeader.slotNumber}`);
777
795
  return await this.validationService.createCheckpointProposal(