@aztec/validator-client 0.0.1-commit.e6bd8901 → 0.0.1-commit.ec7ac5448
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 +60 -18
- package/dest/checkpoint_builder.d.ts +26 -14
- package/dest/checkpoint_builder.d.ts.map +1 -1
- package/dest/checkpoint_builder.js +136 -45
- package/dest/config.d.ts +1 -1
- package/dest/config.d.ts.map +1 -1
- package/dest/config.js +28 -9
- package/dest/duties/validation_service.d.ts +3 -4
- package/dest/duties/validation_service.d.ts.map +1 -1
- package/dest/duties/validation_service.js +11 -22
- package/dest/factory.d.ts +7 -4
- package/dest/factory.d.ts.map +1 -1
- package/dest/factory.js +6 -5
- package/dest/index.d.ts +2 -3
- package/dest/index.d.ts.map +1 -1
- package/dest/index.js +1 -2
- package/dest/key_store/ha_key_store.d.ts +1 -1
- package/dest/key_store/ha_key_store.d.ts.map +1 -1
- package/dest/key_store/ha_key_store.js +3 -3
- package/dest/metrics.d.ts +12 -3
- package/dest/metrics.d.ts.map +1 -1
- package/dest/metrics.js +46 -5
- package/dest/proposal_handler.d.ts +107 -0
- package/dest/proposal_handler.d.ts.map +1 -0
- package/dest/proposal_handler.js +966 -0
- package/dest/validator.d.ts +41 -20
- package/dest/validator.d.ts.map +1 -1
- package/dest/validator.js +183 -199
- package/package.json +19 -17
- package/src/checkpoint_builder.ts +183 -50
- package/src/config.ts +28 -9
- package/src/duties/validation_service.ts +18 -24
- package/src/factory.ts +9 -3
- package/src/index.ts +1 -2
- package/src/key_store/ha_key_store.ts +3 -3
- package/src/metrics.ts +63 -6
- package/src/proposal_handler.ts +1033 -0
- package/src/validator.ts +248 -229
- package/dest/block_proposal_handler.d.ts +0 -64
- package/dest/block_proposal_handler.d.ts.map +0 -1
- package/dest/block_proposal_handler.js +0 -545
- 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 -18
- package/dest/tx_validator/tx_validator_factory.d.ts.map +0 -1
- package/dest/tx_validator/tx_validator_factory.js +0 -54
- package/src/block_proposal_handler.ts +0 -554
- 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 -135
package/src/validator.ts
CHANGED
|
@@ -9,53 +9,56 @@ import {
|
|
|
9
9
|
SlotNumber,
|
|
10
10
|
} from '@aztec/foundation/branded-types';
|
|
11
11
|
import { Fr } from '@aztec/foundation/curves/bn254';
|
|
12
|
-
import { TimeoutError } from '@aztec/foundation/error';
|
|
13
12
|
import type { EthAddress } from '@aztec/foundation/eth-address';
|
|
14
13
|
import type { Signature } from '@aztec/foundation/eth-signature';
|
|
15
14
|
import { type LogData, type Logger, createLogger } from '@aztec/foundation/log';
|
|
16
|
-
import { retryUntil } from '@aztec/foundation/retry';
|
|
17
15
|
import { RunningPromise } from '@aztec/foundation/running-promise';
|
|
18
16
|
import { sleep } from '@aztec/foundation/sleep';
|
|
19
17
|
import { DateProvider } from '@aztec/foundation/timer';
|
|
20
18
|
import type { KeystoreManager } from '@aztec/node-keystore';
|
|
21
|
-
import type { P2P, PeerId
|
|
19
|
+
import type { DuplicateAttestationInfo, DuplicateProposalInfo, P2P, PeerId } from '@aztec/p2p';
|
|
22
20
|
import { AuthRequest, AuthResponse, BlockProposalValidator, ReqRespSubProtocol } from '@aztec/p2p';
|
|
23
21
|
import { OffenseType, WANT_TO_SLASH_EVENT, type Watcher, type WatcherEmitter } from '@aztec/slasher';
|
|
24
22
|
import type { AztecAddress } from '@aztec/stdlib/aztec-address';
|
|
25
|
-
import type { CommitteeAttestationsAndSigners,
|
|
23
|
+
import type { CommitteeAttestationsAndSigners, L2BlockSink, L2BlockSource } from '@aztec/stdlib/block';
|
|
26
24
|
import { getEpochAtSlot } from '@aztec/stdlib/epoch-helpers';
|
|
27
25
|
import type {
|
|
28
|
-
|
|
26
|
+
ITxProvider,
|
|
29
27
|
Validator,
|
|
30
28
|
ValidatorClientFullConfig,
|
|
31
29
|
WorldStateSynchronizer,
|
|
32
30
|
} from '@aztec/stdlib/interfaces/server';
|
|
33
31
|
import type { L1ToL2MessageSource } from '@aztec/stdlib/messaging';
|
|
34
|
-
import
|
|
35
|
-
BlockProposal,
|
|
36
|
-
BlockProposalOptions,
|
|
37
|
-
CheckpointAttestation,
|
|
38
|
-
|
|
39
|
-
|
|
32
|
+
import {
|
|
33
|
+
type BlockProposal,
|
|
34
|
+
type BlockProposalOptions,
|
|
35
|
+
type CheckpointAttestation,
|
|
36
|
+
CheckpointProposal,
|
|
37
|
+
type CheckpointProposalCore,
|
|
38
|
+
type CheckpointProposalOptions,
|
|
40
39
|
} from '@aztec/stdlib/p2p';
|
|
41
|
-
import { CheckpointProposal } from '@aztec/stdlib/p2p';
|
|
42
40
|
import type { CheckpointHeader } from '@aztec/stdlib/rollup';
|
|
43
|
-
import type { BlockHeader,
|
|
41
|
+
import type { BlockHeader, Tx } from '@aztec/stdlib/tx';
|
|
44
42
|
import { AttestationTimeoutError } from '@aztec/stdlib/validators';
|
|
45
43
|
import { type TelemetryClient, type Tracer, getTelemetryClient } from '@aztec/telemetry-client';
|
|
46
|
-
import {
|
|
47
|
-
|
|
44
|
+
import {
|
|
45
|
+
createHASigner,
|
|
46
|
+
createLocalSignerWithProtection,
|
|
47
|
+
createSignerFromSharedDb,
|
|
48
|
+
} from '@aztec/validator-ha-signer/factory';
|
|
49
|
+
import { DutyType, type SigningContext, type SlashingProtectionDatabase } from '@aztec/validator-ha-signer/types';
|
|
50
|
+
import type { ValidatorHASigner } from '@aztec/validator-ha-signer/validator-ha-signer';
|
|
48
51
|
|
|
49
52
|
import { EventEmitter } from 'events';
|
|
50
53
|
import type { TypedDataDefinition } from 'viem';
|
|
51
54
|
|
|
52
|
-
import { BlockProposalHandler, type BlockProposalValidationFailureReason } from './block_proposal_handler.js';
|
|
53
55
|
import type { FullNodeCheckpointsBuilder } from './checkpoint_builder.js';
|
|
54
56
|
import { ValidationService } from './duties/validation_service.js';
|
|
55
57
|
import { HAKeyStore } from './key_store/ha_key_store.js';
|
|
56
58
|
import type { ExtendedValidatorKeyStore } from './key_store/interface.js';
|
|
57
59
|
import { NodeKeystoreAdapter } from './key_store/node_keystore_adapter.js';
|
|
58
60
|
import { ValidatorMetrics } from './metrics.js';
|
|
61
|
+
import { type BlockProposalValidationFailureReason, ProposalHandler } from './proposal_handler.js';
|
|
59
62
|
|
|
60
63
|
// We maintain a set of proposers who have proposed invalid blocks.
|
|
61
64
|
// Just cap the set to avoid unbounded growth.
|
|
@@ -75,34 +78,37 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
|
|
|
75
78
|
private validationService: ValidationService;
|
|
76
79
|
private metrics: ValidatorMetrics;
|
|
77
80
|
private log: Logger;
|
|
78
|
-
|
|
79
81
|
// Whether it has already registered handlers on the p2p client
|
|
80
82
|
private hasRegisteredHandlers = false;
|
|
81
83
|
|
|
82
|
-
|
|
83
|
-
private
|
|
84
|
+
/** Tracks the last block proposal we created, to detect duplicate proposal attempts. */
|
|
85
|
+
private lastProposedBlock?: BlockProposal;
|
|
86
|
+
|
|
87
|
+
/** Tracks the last checkpoint proposal we created. */
|
|
88
|
+
private lastProposedCheckpoint?: CheckpointProposal;
|
|
84
89
|
|
|
85
90
|
private lastEpochForCommitteeUpdateLoop: EpochNumber | undefined;
|
|
86
91
|
private epochCacheUpdateLoop: RunningPromise;
|
|
92
|
+
/** Tracks the last epoch in which each attester successfully submitted at least one attestation. */
|
|
93
|
+
private lastAttestedEpochByAttester: Map<string, EpochNumber> = new Map();
|
|
87
94
|
|
|
88
95
|
private proposersOfInvalidBlocks: Set<string> = new Set();
|
|
89
96
|
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
// eslint-disable-next-line aztec-custom/no-non-primitive-in-collections
|
|
93
|
-
private validatedBlockSlots: Set<SlotNumber> = new Set();
|
|
97
|
+
/** Tracks the last checkpoint proposal we attested to, to prevent equivocation. */
|
|
98
|
+
private lastAttestedProposal?: CheckpointProposalCore;
|
|
94
99
|
|
|
95
100
|
protected constructor(
|
|
96
101
|
private keyStore: ExtendedValidatorKeyStore,
|
|
97
102
|
private epochCache: EpochCache,
|
|
98
103
|
private p2pClient: P2P,
|
|
99
|
-
private
|
|
104
|
+
private proposalHandler: ProposalHandler,
|
|
100
105
|
private blockSource: L2BlockSource,
|
|
101
106
|
private checkpointsBuilder: FullNodeCheckpointsBuilder,
|
|
102
107
|
private worldState: WorldStateSynchronizer,
|
|
103
108
|
private l1ToL2MessageSource: L1ToL2MessageSource,
|
|
104
109
|
private config: ValidatorClientFullConfig,
|
|
105
110
|
private blobClient: BlobClientInterface,
|
|
111
|
+
private slashingProtectionSigner: ValidatorHASigner,
|
|
106
112
|
private dateProvider: DateProvider = new DateProvider(),
|
|
107
113
|
telemetry: TelemetryClient = getTelemetryClient(),
|
|
108
114
|
log = createLogger('validator'),
|
|
@@ -156,6 +162,7 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
|
|
|
156
162
|
this.log.trace(`No committee found for slot`);
|
|
157
163
|
return;
|
|
158
164
|
}
|
|
165
|
+
this.metrics.setCurrentEpoch(epoch);
|
|
159
166
|
if (epoch !== this.lastEpochForCommitteeUpdateLoop) {
|
|
160
167
|
const me = this.getValidatorAddresses();
|
|
161
168
|
const committeeSet = new Set(committee.map(v => v.toString()));
|
|
@@ -184,17 +191,19 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
|
|
|
184
191
|
p2pClient: P2P,
|
|
185
192
|
blockSource: L2BlockSource & L2BlockSink,
|
|
186
193
|
l1ToL2MessageSource: L1ToL2MessageSource,
|
|
187
|
-
txProvider:
|
|
194
|
+
txProvider: ITxProvider,
|
|
188
195
|
keyStoreManager: KeystoreManager,
|
|
189
196
|
blobClient: BlobClientInterface,
|
|
190
197
|
dateProvider: DateProvider = new DateProvider(),
|
|
191
198
|
telemetry: TelemetryClient = getTelemetryClient(),
|
|
199
|
+
slashingProtectionDb?: SlashingProtectionDatabase,
|
|
192
200
|
) {
|
|
193
201
|
const metrics = new ValidatorMetrics(telemetry);
|
|
194
202
|
const blockProposalValidator = new BlockProposalValidator(epochCache, {
|
|
195
203
|
txsPermitted: !config.disableTransactions,
|
|
204
|
+
maxTxsPerBlock: config.validateMaxTxsPerBlock,
|
|
196
205
|
});
|
|
197
|
-
const
|
|
206
|
+
const proposalHandler = new ProposalHandler(
|
|
198
207
|
checkpointsBuilder,
|
|
199
208
|
worldState,
|
|
200
209
|
blockSource,
|
|
@@ -203,33 +212,53 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
|
|
|
203
212
|
blockProposalValidator,
|
|
204
213
|
epochCache,
|
|
205
214
|
config,
|
|
215
|
+
blobClient,
|
|
206
216
|
metrics,
|
|
207
217
|
dateProvider,
|
|
208
218
|
telemetry,
|
|
209
219
|
);
|
|
210
220
|
|
|
211
|
-
|
|
212
|
-
|
|
221
|
+
const nodeKeystoreAdapter = NodeKeystoreAdapter.fromKeyStoreManager(keyStoreManager);
|
|
222
|
+
let slashingProtectionSigner: ValidatorHASigner;
|
|
223
|
+
if (slashingProtectionDb) {
|
|
224
|
+
// Shared database mode: use a pre-existing database (e.g. for testing HA setups).
|
|
225
|
+
({ signer: slashingProtectionSigner } = createSignerFromSharedDb(slashingProtectionDb, config, {
|
|
226
|
+
telemetryClient: telemetry,
|
|
227
|
+
dateProvider,
|
|
228
|
+
}));
|
|
229
|
+
} else if (config.haSigningEnabled) {
|
|
230
|
+
// Multi-node HA mode: use PostgreSQL-backed distributed locking.
|
|
213
231
|
// If maxStuckDutiesAgeMs is not explicitly set, compute it from Aztec slot duration
|
|
214
232
|
const haConfig = {
|
|
215
233
|
...config,
|
|
216
234
|
maxStuckDutiesAgeMs: config.maxStuckDutiesAgeMs ?? epochCache.getL1Constants().slotDuration * 2 * 1000,
|
|
217
235
|
};
|
|
218
|
-
|
|
219
|
-
|
|
236
|
+
({ signer: slashingProtectionSigner } = await createHASigner(haConfig, {
|
|
237
|
+
telemetryClient: telemetry,
|
|
238
|
+
dateProvider,
|
|
239
|
+
}));
|
|
240
|
+
} else {
|
|
241
|
+
// Single-node mode: use LMDB-backed local signing protection.
|
|
242
|
+
// This prevents double-signing if the node crashes and restarts mid-proposal.
|
|
243
|
+
({ signer: slashingProtectionSigner } = await createLocalSignerWithProtection(config, {
|
|
244
|
+
telemetryClient: telemetry,
|
|
245
|
+
dateProvider,
|
|
246
|
+
}));
|
|
220
247
|
}
|
|
248
|
+
const validatorKeyStore: ExtendedValidatorKeyStore = new HAKeyStore(nodeKeystoreAdapter, slashingProtectionSigner);
|
|
221
249
|
|
|
222
250
|
const validator = new ValidatorClient(
|
|
223
251
|
validatorKeyStore,
|
|
224
252
|
epochCache,
|
|
225
253
|
p2pClient,
|
|
226
|
-
|
|
254
|
+
proposalHandler,
|
|
227
255
|
blockSource,
|
|
228
256
|
checkpointsBuilder,
|
|
229
257
|
worldState,
|
|
230
258
|
l1ToL2MessageSource,
|
|
231
259
|
config,
|
|
232
260
|
blobClient,
|
|
261
|
+
slashingProtectionSigner,
|
|
233
262
|
dateProvider,
|
|
234
263
|
telemetry,
|
|
235
264
|
);
|
|
@@ -243,8 +272,8 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
|
|
|
243
272
|
.filter(addr => !this.config.disabledValidators.some(disabled => disabled.equals(addr)));
|
|
244
273
|
}
|
|
245
274
|
|
|
246
|
-
public
|
|
247
|
-
return this.
|
|
275
|
+
public getProposalHandler() {
|
|
276
|
+
return this.proposalHandler;
|
|
248
277
|
}
|
|
249
278
|
|
|
250
279
|
public signWithAddress(addr: EthAddress, msg: TypedDataDefinition, context: SigningContext) {
|
|
@@ -267,6 +296,12 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
|
|
|
267
296
|
this.config = { ...this.config, ...config };
|
|
268
297
|
}
|
|
269
298
|
|
|
299
|
+
public reloadKeystore(newManager: KeystoreManager): void {
|
|
300
|
+
const newAdapter = NodeKeystoreAdapter.fromKeyStoreManager(newManager);
|
|
301
|
+
this.keyStore = new HAKeyStore(newAdapter, this.slashingProtectionSigner);
|
|
302
|
+
this.validationService = new ValidationService(this.keyStore, this.log.createChild('validation-service'));
|
|
303
|
+
}
|
|
304
|
+
|
|
270
305
|
public async start() {
|
|
271
306
|
if (this.epochCacheUpdateLoop.isRunning()) {
|
|
272
307
|
this.log.warn(`Validator client already started`);
|
|
@@ -311,7 +346,17 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
|
|
|
311
346
|
checkpoint: CheckpointProposalCore,
|
|
312
347
|
proposalSender: PeerId,
|
|
313
348
|
): Promise<CheckpointAttestation[] | undefined> => this.attestToCheckpointProposal(checkpoint, proposalSender);
|
|
314
|
-
this.p2pClient.
|
|
349
|
+
this.p2pClient.registerValidatorCheckpointProposalHandler(checkpointHandler);
|
|
350
|
+
|
|
351
|
+
// Duplicate proposal handler - triggers slashing for equivocation
|
|
352
|
+
this.p2pClient.registerDuplicateProposalCallback((info: DuplicateProposalInfo) => {
|
|
353
|
+
this.handleDuplicateProposal(info);
|
|
354
|
+
});
|
|
355
|
+
|
|
356
|
+
// Duplicate attestation handler - triggers slashing for attestation equivocation
|
|
357
|
+
this.p2pClient.registerDuplicateAttestationCallback((info: DuplicateAttestationInfo) => {
|
|
358
|
+
this.handleDuplicateAttestation(info);
|
|
359
|
+
});
|
|
315
360
|
|
|
316
361
|
const myAddresses = this.getValidatorAddresses();
|
|
317
362
|
this.p2pClient.registerThisValidatorAddresses(myAddresses);
|
|
@@ -340,6 +385,14 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
|
|
|
340
385
|
return false;
|
|
341
386
|
}
|
|
342
387
|
|
|
388
|
+
// Log self-proposals from HA peers (same validator key on different nodes)
|
|
389
|
+
if (this.getValidatorAddresses().some(addr => addr.equals(proposer))) {
|
|
390
|
+
this.log.verbose(`Processing block proposal from HA peer for slot ${slotNumber}`, {
|
|
391
|
+
proposer: proposer.toString(),
|
|
392
|
+
slotNumber,
|
|
393
|
+
});
|
|
394
|
+
}
|
|
395
|
+
|
|
343
396
|
// Check if we're in the committee (for metrics purposes)
|
|
344
397
|
const inCommittee = await this.epochCache.filterInCommittee(slotNumber, this.getValidatorAddresses());
|
|
345
398
|
const partOfCommittee = inCommittee.length > 0;
|
|
@@ -353,25 +406,25 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
|
|
|
353
406
|
|
|
354
407
|
// Reexecute txs if we are part of the committee, or if slashing is enabled, or if we are configured to always reexecute.
|
|
355
408
|
// In fisherman mode, we always reexecute to validate proposals.
|
|
356
|
-
const {
|
|
357
|
-
this.config;
|
|
409
|
+
const { slashBroadcastedInvalidBlockPenalty, alwaysReexecuteBlockProposals, fishermanMode } = this.config;
|
|
358
410
|
const shouldReexecute =
|
|
359
411
|
fishermanMode ||
|
|
360
|
-
|
|
361
|
-
|
|
412
|
+
slashBroadcastedInvalidBlockPenalty > 0n ||
|
|
413
|
+
partOfCommittee ||
|
|
362
414
|
alwaysReexecuteBlockProposals ||
|
|
363
415
|
this.blobClient.canUpload();
|
|
364
416
|
|
|
365
|
-
const validationResult = await this.
|
|
417
|
+
const validationResult = await this.proposalHandler.handleBlockProposal(
|
|
366
418
|
proposal,
|
|
367
419
|
proposalSender,
|
|
368
420
|
!!shouldReexecute && !escapeHatchOpen,
|
|
369
421
|
);
|
|
370
422
|
|
|
371
423
|
if (!validationResult.isValid) {
|
|
372
|
-
this.log.warn(`Block proposal validation failed: ${validationResult.reason}`, proposalInfo);
|
|
373
|
-
|
|
374
424
|
const reason = validationResult.reason || 'unknown';
|
|
425
|
+
|
|
426
|
+
this.log.warn(`Block proposal validation failed: ${reason}`, proposalInfo);
|
|
427
|
+
|
|
375
428
|
// Classify failure reason: bad proposal vs node issue
|
|
376
429
|
const badProposalReasons: BlockProposalValidationFailureReason[] = [
|
|
377
430
|
'invalid_proposal',
|
|
@@ -413,10 +466,6 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
|
|
|
413
466
|
return false;
|
|
414
467
|
}
|
|
415
468
|
|
|
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
469
|
return true;
|
|
421
470
|
}
|
|
422
471
|
|
|
@@ -430,61 +479,50 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
|
|
|
430
479
|
proposal: CheckpointProposalCore,
|
|
431
480
|
_proposalSender: PeerId,
|
|
432
481
|
): Promise<CheckpointAttestation[] | undefined> {
|
|
433
|
-
const
|
|
482
|
+
const proposalSlotNumber = proposal.slotNumber;
|
|
434
483
|
const proposer = proposal.getSender();
|
|
435
484
|
|
|
436
485
|
// If escape hatch is open for this slot's epoch, do not attest.
|
|
437
|
-
if (await this.epochCache.isEscapeHatchOpenAtSlot(
|
|
438
|
-
this.log.warn(`Escape hatch open for slot ${
|
|
486
|
+
if (await this.epochCache.isEscapeHatchOpenAtSlot(proposalSlotNumber)) {
|
|
487
|
+
this.log.warn(`Escape hatch open for slot ${proposalSlotNumber}, skipping checkpoint attestation handling`);
|
|
439
488
|
return undefined;
|
|
440
489
|
}
|
|
441
490
|
|
|
442
|
-
//
|
|
443
|
-
if (
|
|
444
|
-
this.log.
|
|
491
|
+
// Ignore proposals from ourselves (may happen in HA setups)
|
|
492
|
+
if (proposer && this.getValidatorAddresses().some(addr => addr.equals(proposer))) {
|
|
493
|
+
this.log.debug(`Ignoring block proposal from self for slot ${proposalSlotNumber}`, {
|
|
494
|
+
proposer: proposer.toString(),
|
|
495
|
+
proposalSlotNumber,
|
|
496
|
+
});
|
|
445
497
|
return undefined;
|
|
446
498
|
}
|
|
447
499
|
|
|
448
|
-
// Check that I have any address in
|
|
449
|
-
const inCommittee = await this.epochCache.filterInCommittee(
|
|
500
|
+
// Check that I have any address in the committee where this checkpoint will land before attesting
|
|
501
|
+
const inCommittee = await this.epochCache.filterInCommittee(proposalSlotNumber, this.getValidatorAddresses());
|
|
450
502
|
const partOfCommittee = inCommittee.length > 0;
|
|
451
503
|
|
|
452
504
|
const proposalInfo = {
|
|
453
|
-
|
|
505
|
+
proposalSlotNumber,
|
|
454
506
|
archive: proposal.archive.toString(),
|
|
455
|
-
proposer: proposer
|
|
456
|
-
txCount: proposal.txHashes.length,
|
|
507
|
+
proposer: proposer?.toString(),
|
|
457
508
|
};
|
|
458
|
-
this.log.info(`Received checkpoint proposal for slot ${
|
|
509
|
+
this.log.info(`Received checkpoint proposal for slot ${proposalSlotNumber}`, {
|
|
459
510
|
...proposalInfo,
|
|
460
|
-
txHashes: proposal.txHashes.map(t => t.toString()),
|
|
461
511
|
fishermanMode: this.config.fishermanMode || false,
|
|
462
512
|
});
|
|
463
513
|
|
|
464
|
-
//
|
|
465
|
-
//
|
|
466
|
-
if (
|
|
467
|
-
this.log.warn(`
|
|
468
|
-
return undefined;
|
|
469
|
-
}
|
|
470
|
-
|
|
471
|
-
// 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);
|
|
514
|
+
// Validate the checkpoint proposal before attesting (unless skipCheckpointProposalValidation is set).
|
|
515
|
+
// Uses the cached result from the all-nodes callback if available (avoids double validation).
|
|
516
|
+
if (this.config.skipCheckpointProposalValidation) {
|
|
517
|
+
this.log.warn(`Skipping checkpoint proposal validation for slot ${proposalSlotNumber}`, proposalInfo);
|
|
475
518
|
} else {
|
|
476
|
-
const validationResult = await this.
|
|
519
|
+
const validationResult = await this.proposalHandler.handleCheckpointProposal(proposal, proposalInfo);
|
|
477
520
|
if (!validationResult.isValid) {
|
|
478
521
|
this.log.warn(`Checkpoint proposal validation failed: ${validationResult.reason}`, proposalInfo);
|
|
479
522
|
return undefined;
|
|
480
523
|
}
|
|
481
524
|
}
|
|
482
525
|
|
|
483
|
-
// Upload blobs to filestore if we can (fire and forget)
|
|
484
|
-
if (this.blobClient.canUpload()) {
|
|
485
|
-
void this.uploadBlobsForCheckpoint(proposal, proposalInfo);
|
|
486
|
-
}
|
|
487
|
-
|
|
488
526
|
// Check that I have any address in current committee before attesting
|
|
489
527
|
// In fisherman mode, we still create attestations for validation even if not in committee
|
|
490
528
|
if (!partOfCommittee && !this.config.fishermanMode) {
|
|
@@ -493,14 +531,28 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
|
|
|
493
531
|
}
|
|
494
532
|
|
|
495
533
|
// Provided all of the above checks pass, we can attest to the proposal
|
|
496
|
-
this.log.info(
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
534
|
+
this.log.info(
|
|
535
|
+
`${partOfCommittee ? 'Attesting to' : 'Validated'} checkpoint proposal for slot ${proposalSlotNumber}`,
|
|
536
|
+
{
|
|
537
|
+
...proposalInfo,
|
|
538
|
+
inCommittee: partOfCommittee,
|
|
539
|
+
fishermanMode: this.config.fishermanMode || false,
|
|
540
|
+
},
|
|
541
|
+
);
|
|
501
542
|
|
|
502
543
|
this.metrics.incSuccessfulAttestations(inCommittee.length);
|
|
503
544
|
|
|
545
|
+
// Track epoch participation per attester: count each (attester, epoch) pair at most once
|
|
546
|
+
const proposalEpoch = getEpochAtSlot(proposalSlotNumber, this.epochCache.getL1Constants());
|
|
547
|
+
for (const attester of inCommittee) {
|
|
548
|
+
const key = attester.toString();
|
|
549
|
+
const lastEpoch = this.lastAttestedEpochByAttester.get(key);
|
|
550
|
+
if (lastEpoch === undefined || proposalEpoch > lastEpoch) {
|
|
551
|
+
this.lastAttestedEpochByAttester.set(key, proposalEpoch);
|
|
552
|
+
this.metrics.incAttestedEpochCount(attester);
|
|
553
|
+
}
|
|
554
|
+
}
|
|
555
|
+
|
|
504
556
|
// Determine which validators should attest
|
|
505
557
|
let attestors: EthAddress[];
|
|
506
558
|
if (partOfCommittee) {
|
|
@@ -519,167 +571,59 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
|
|
|
519
571
|
|
|
520
572
|
if (this.config.fishermanMode) {
|
|
521
573
|
// bail out early and don't save attestations to the pool in fisherman mode
|
|
522
|
-
this.log.info(`Creating checkpoint attestations for slot ${
|
|
574
|
+
this.log.info(`Creating checkpoint attestations for slot ${proposalSlotNumber}`, {
|
|
523
575
|
...proposalInfo,
|
|
524
576
|
attestors: attestors.map(a => a.toString()),
|
|
525
577
|
});
|
|
526
578
|
return undefined;
|
|
527
579
|
}
|
|
528
580
|
|
|
529
|
-
return this.createCheckpointAttestationsFromProposal(proposal, attestors);
|
|
530
|
-
}
|
|
531
|
-
|
|
532
|
-
private async createCheckpointAttestationsFromProposal(
|
|
533
|
-
proposal: CheckpointProposalCore,
|
|
534
|
-
attestors: EthAddress[] = [],
|
|
535
|
-
): Promise<CheckpointAttestation[]> {
|
|
536
|
-
const attestations = await this.validationService.attestToCheckpointProposal(proposal, attestors);
|
|
537
|
-
await this.p2pClient.addCheckpointAttestations(attestations);
|
|
538
|
-
return attestations;
|
|
581
|
+
return await this.createCheckpointAttestationsFromProposal(proposal, attestors);
|
|
539
582
|
}
|
|
540
583
|
|
|
541
584
|
/**
|
|
542
|
-
*
|
|
543
|
-
* @returns
|
|
585
|
+
* Checks if we should attest to a slot based on equivocation prevention rules.
|
|
586
|
+
* @returns true if we should attest, false if we should skip
|
|
544
587
|
*/
|
|
545
|
-
private
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
const timeoutSeconds = 10;
|
|
588
|
+
private shouldAttestToSlot(slotNumber: SlotNumber): boolean {
|
|
589
|
+
// If attestToEquivocatedProposals is true, always allow
|
|
590
|
+
if (this.config.attestToEquivocatedProposals) {
|
|
591
|
+
return true;
|
|
592
|
+
}
|
|
551
593
|
|
|
552
|
-
//
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
async () => {
|
|
557
|
-
await this.blockSource.syncImmediate();
|
|
558
|
-
return this.blockSource.getBlockHeaderByArchive(proposal.archive);
|
|
559
|
-
},
|
|
560
|
-
`waiting for block with archive ${proposal.archive.toString()} for slot ${slot}`,
|
|
561
|
-
timeoutSeconds,
|
|
562
|
-
0.5,
|
|
594
|
+
// Check if incoming slot is strictly greater than last attested
|
|
595
|
+
if (this.lastAttestedProposal && slotNumber <= this.lastAttestedProposal.slotNumber) {
|
|
596
|
+
this.log.warn(
|
|
597
|
+
`Refusing to process a proposal for slot ${slotNumber} given we already attested to a proposal for slot ${this.lastAttestedProposal.slotNumber}`,
|
|
563
598
|
);
|
|
564
|
-
|
|
565
|
-
if (err instanceof TimeoutError) {
|
|
566
|
-
this.log.warn(`Timed out waiting for block with archive matching checkpoint proposal`, proposalInfo);
|
|
567
|
-
return { isValid: false, reason: 'last_block_not_found' };
|
|
568
|
-
}
|
|
569
|
-
this.log.error(`Error fetching last block for checkpoint proposal`, err, proposalInfo);
|
|
570
|
-
return { isValid: false, reason: 'block_fetch_error' };
|
|
599
|
+
return false;
|
|
571
600
|
}
|
|
572
601
|
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
return { isValid: false, reason: 'last_block_not_found' };
|
|
576
|
-
}
|
|
602
|
+
return true;
|
|
603
|
+
}
|
|
577
604
|
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
605
|
+
private async createCheckpointAttestationsFromProposal(
|
|
606
|
+
proposal: CheckpointProposalCore,
|
|
607
|
+
attestors: EthAddress[] = [],
|
|
608
|
+
): Promise<CheckpointAttestation[] | undefined> {
|
|
609
|
+
// Equivocation check: must happen right before signing to minimize the race window
|
|
610
|
+
if (!this.shouldAttestToSlot(proposal.slotNumber)) {
|
|
611
|
+
return undefined;
|
|
583
612
|
}
|
|
584
613
|
|
|
585
|
-
this.
|
|
586
|
-
...proposalInfo,
|
|
587
|
-
blockNumbers: blocks.map(b => b.number),
|
|
588
|
-
});
|
|
589
|
-
|
|
590
|
-
// Get checkpoint constants from first block
|
|
591
|
-
const firstBlock = blocks[0];
|
|
592
|
-
const constants = this.extractCheckpointConstants(firstBlock);
|
|
593
|
-
const checkpointNumber = firstBlock.checkpointNumber;
|
|
594
|
-
|
|
595
|
-
// Get L1-to-L2 messages for this checkpoint
|
|
596
|
-
const l1ToL2Messages = await this.l1ToL2MessageSource.getL1ToL2Messages(checkpointNumber);
|
|
597
|
-
|
|
598
|
-
// Compute the previous checkpoint out hashes for the epoch.
|
|
599
|
-
// TODO: There can be a more efficient way to get the previous checkpoint out hashes without having to fetch the
|
|
600
|
-
// actual checkpoints and the blocks/txs in them.
|
|
601
|
-
const epoch = getEpochAtSlot(slot, this.epochCache.getL1Constants());
|
|
602
|
-
const previousCheckpoints = (await this.blockSource.getCheckpointsForEpoch(epoch))
|
|
603
|
-
.filter(b => b.number < checkpointNumber)
|
|
604
|
-
.sort((a, b) => a.number - b.number);
|
|
605
|
-
const previousCheckpointOutHashes = previousCheckpoints.map(c => c.getCheckpointOutHash());
|
|
606
|
-
|
|
607
|
-
// Fork world state at the block before the first block
|
|
608
|
-
const parentBlockNumber = BlockNumber(firstBlock.number - 1);
|
|
609
|
-
const fork = await this.worldState.fork(parentBlockNumber);
|
|
610
|
-
|
|
611
|
-
try {
|
|
612
|
-
// Create checkpoint builder with all existing blocks
|
|
613
|
-
const checkpointBuilder = await this.checkpointsBuilder.openCheckpoint(
|
|
614
|
-
checkpointNumber,
|
|
615
|
-
constants,
|
|
616
|
-
l1ToL2Messages,
|
|
617
|
-
previousCheckpointOutHashes,
|
|
618
|
-
fork,
|
|
619
|
-
blocks,
|
|
620
|
-
);
|
|
621
|
-
|
|
622
|
-
// Complete the checkpoint to get computed values
|
|
623
|
-
const computedCheckpoint = await checkpointBuilder.completeCheckpoint();
|
|
624
|
-
|
|
625
|
-
// Compare checkpoint header with proposal
|
|
626
|
-
if (!computedCheckpoint.header.equals(proposal.checkpointHeader)) {
|
|
627
|
-
this.log.warn(`Checkpoint header mismatch`, {
|
|
628
|
-
...proposalInfo,
|
|
629
|
-
computed: computedCheckpoint.header.toInspect(),
|
|
630
|
-
proposal: proposal.checkpointHeader.toInspect(),
|
|
631
|
-
});
|
|
632
|
-
return { isValid: false, reason: 'checkpoint_header_mismatch' };
|
|
633
|
-
}
|
|
634
|
-
|
|
635
|
-
// Compare archive root with proposal
|
|
636
|
-
if (!computedCheckpoint.archive.root.equals(proposal.archive)) {
|
|
637
|
-
this.log.warn(`Archive root mismatch`, {
|
|
638
|
-
...proposalInfo,
|
|
639
|
-
computed: computedCheckpoint.archive.root.toString(),
|
|
640
|
-
proposal: proposal.archive.toString(),
|
|
641
|
-
});
|
|
642
|
-
return { isValid: false, reason: 'archive_mismatch' };
|
|
643
|
-
}
|
|
614
|
+
const attestations = await this.validationService.attestToCheckpointProposal(proposal, attestors);
|
|
644
615
|
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
const proposalOutHash = proposal.checkpointHeader.epochOutHash;
|
|
648
|
-
if (!computedOutHash.equals(proposalOutHash)) {
|
|
649
|
-
this.log.warn(`Epoch out hash mismatch`, {
|
|
650
|
-
proposalOutHash: proposalOutHash.toString(),
|
|
651
|
-
computedOutHash: computedOutHash.toString(),
|
|
652
|
-
...proposalInfo,
|
|
653
|
-
});
|
|
654
|
-
return { isValid: false, reason: 'out_hash_mismatch' };
|
|
655
|
-
}
|
|
616
|
+
// Track the proposal we attested to (to prevent equivocation)
|
|
617
|
+
this.lastAttestedProposal = proposal;
|
|
656
618
|
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
} finally {
|
|
660
|
-
await fork.close();
|
|
661
|
-
}
|
|
662
|
-
}
|
|
663
|
-
|
|
664
|
-
/**
|
|
665
|
-
* Extract checkpoint global variables from a block.
|
|
666
|
-
*/
|
|
667
|
-
private extractCheckpointConstants(block: L2Block): CheckpointGlobalVariables {
|
|
668
|
-
const gv = block.header.globalVariables;
|
|
669
|
-
return {
|
|
670
|
-
chainId: gv.chainId,
|
|
671
|
-
version: gv.version,
|
|
672
|
-
slotNumber: gv.slotNumber,
|
|
673
|
-
coinbase: gv.coinbase,
|
|
674
|
-
feeRecipient: gv.feeRecipient,
|
|
675
|
-
gasFees: gv.gasFees,
|
|
676
|
-
};
|
|
619
|
+
await this.p2pClient.addOwnCheckpointAttestations(attestations);
|
|
620
|
+
return attestations;
|
|
677
621
|
}
|
|
678
622
|
|
|
679
623
|
/**
|
|
680
624
|
* Uploads blobs for a checkpoint to the filestore (fire and forget).
|
|
681
625
|
*/
|
|
682
|
-
|
|
626
|
+
protected async uploadBlobsForCheckpoint(proposal: CheckpointProposalCore, proposalInfo: LogData): Promise<void> {
|
|
683
627
|
try {
|
|
684
628
|
const lastBlockHeader = await this.blockSource.getBlockHeaderByArchive(proposal.archive);
|
|
685
629
|
if (!lastBlockHeader) {
|
|
@@ -694,7 +638,7 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
|
|
|
694
638
|
}
|
|
695
639
|
|
|
696
640
|
const blobFields = blocks.flatMap(b => b.toBlobFields());
|
|
697
|
-
const blobs: Blob[] = getBlobsPerL1Block(blobFields);
|
|
641
|
+
const blobs: Blob[] = await getBlobsPerL1Block(blobFields);
|
|
698
642
|
await this.blobClient.sendBlobsToFilestore(blobs);
|
|
699
643
|
this.log.debug(`Uploaded ${blobs.length} blobs to filestore for checkpoint at slot ${proposal.slotNumber}`, {
|
|
700
644
|
...proposalInfo,
|
|
@@ -732,6 +676,52 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
|
|
|
732
676
|
]);
|
|
733
677
|
}
|
|
734
678
|
|
|
679
|
+
/**
|
|
680
|
+
* Handle detection of a duplicate proposal (equivocation).
|
|
681
|
+
* Emits a slash event when a proposer sends multiple proposals for the same position.
|
|
682
|
+
*/
|
|
683
|
+
private handleDuplicateProposal(info: DuplicateProposalInfo): void {
|
|
684
|
+
const { slot, proposer, type } = info;
|
|
685
|
+
|
|
686
|
+
this.log.warn(`Triggering slash event for duplicate ${type} proposal from ${proposer.toString()} at slot ${slot}`, {
|
|
687
|
+
proposer: proposer.toString(),
|
|
688
|
+
slot,
|
|
689
|
+
type,
|
|
690
|
+
});
|
|
691
|
+
|
|
692
|
+
// Emit slash event
|
|
693
|
+
this.emit(WANT_TO_SLASH_EVENT, [
|
|
694
|
+
{
|
|
695
|
+
validator: proposer,
|
|
696
|
+
amount: this.config.slashDuplicateProposalPenalty,
|
|
697
|
+
offenseType: OffenseType.DUPLICATE_PROPOSAL,
|
|
698
|
+
epochOrSlot: BigInt(slot),
|
|
699
|
+
},
|
|
700
|
+
]);
|
|
701
|
+
}
|
|
702
|
+
|
|
703
|
+
/**
|
|
704
|
+
* Handle detection of a duplicate attestation (equivocation).
|
|
705
|
+
* Emits a slash event when an attester signs attestations for different proposals at the same slot.
|
|
706
|
+
*/
|
|
707
|
+
private handleDuplicateAttestation(info: DuplicateAttestationInfo): void {
|
|
708
|
+
const { slot, attester } = info;
|
|
709
|
+
|
|
710
|
+
this.log.warn(`Triggering slash event for duplicate attestation from ${attester.toString()} at slot ${slot}`, {
|
|
711
|
+
attester: attester.toString(),
|
|
712
|
+
slot,
|
|
713
|
+
});
|
|
714
|
+
|
|
715
|
+
this.emit(WANT_TO_SLASH_EVENT, [
|
|
716
|
+
{
|
|
717
|
+
validator: attester,
|
|
718
|
+
amount: this.config.slashDuplicateAttestationPenalty,
|
|
719
|
+
offenseType: OffenseType.DUPLICATE_ATTESTATION,
|
|
720
|
+
epochOrSlot: BigInt(slot),
|
|
721
|
+
},
|
|
722
|
+
]);
|
|
723
|
+
}
|
|
724
|
+
|
|
735
725
|
async createBlockProposal(
|
|
736
726
|
blockHeader: BlockHeader,
|
|
737
727
|
indexWithinCheckpoint: IndexWithinCheckpoint,
|
|
@@ -739,13 +729,21 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
|
|
|
739
729
|
archive: Fr,
|
|
740
730
|
txs: Tx[],
|
|
741
731
|
proposerAddress: EthAddress | undefined,
|
|
742
|
-
options: BlockProposalOptions,
|
|
732
|
+
options: BlockProposalOptions = {},
|
|
743
733
|
): Promise<BlockProposal> {
|
|
744
|
-
//
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
734
|
+
// Validate that we're not creating a proposal for an older or equal position
|
|
735
|
+
if (this.lastProposedBlock) {
|
|
736
|
+
const lastSlot = this.lastProposedBlock.slotNumber;
|
|
737
|
+
const lastIndex = this.lastProposedBlock.indexWithinCheckpoint;
|
|
738
|
+
const newSlot = blockHeader.globalVariables.slotNumber;
|
|
739
|
+
|
|
740
|
+
if (newSlot < lastSlot || (newSlot === lastSlot && indexWithinCheckpoint <= lastIndex)) {
|
|
741
|
+
throw new Error(
|
|
742
|
+
`Cannot create block proposal for slot ${newSlot} index ${indexWithinCheckpoint}: ` +
|
|
743
|
+
`already proposed block for slot ${lastSlot} index ${lastIndex}`,
|
|
744
|
+
);
|
|
745
|
+
}
|
|
746
|
+
}
|
|
749
747
|
|
|
750
748
|
this.log.info(
|
|
751
749
|
`Assembling block proposal for block ${blockHeader.globalVariables.blockNumber} slot ${blockHeader.globalVariables.slotNumber}`,
|
|
@@ -762,25 +760,42 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
|
|
|
762
760
|
broadcastInvalidBlockProposal: this.config.broadcastInvalidBlockProposal,
|
|
763
761
|
},
|
|
764
762
|
);
|
|
765
|
-
this.
|
|
763
|
+
this.lastProposedBlock = newProposal;
|
|
766
764
|
return newProposal;
|
|
767
765
|
}
|
|
768
766
|
|
|
769
767
|
async createCheckpointProposal(
|
|
770
768
|
checkpointHeader: CheckpointHeader,
|
|
771
769
|
archive: Fr,
|
|
772
|
-
|
|
770
|
+
feeAssetPriceModifier: bigint,
|
|
771
|
+
lastBlockProposal: BlockProposal | undefined,
|
|
773
772
|
proposerAddress: EthAddress | undefined,
|
|
774
|
-
options: CheckpointProposalOptions,
|
|
773
|
+
options: CheckpointProposalOptions = {},
|
|
775
774
|
): Promise<CheckpointProposal> {
|
|
775
|
+
// Validate that we're not creating a proposal for an older or equal slot
|
|
776
|
+
if (this.lastProposedCheckpoint) {
|
|
777
|
+
const lastSlot = this.lastProposedCheckpoint.slotNumber;
|
|
778
|
+
const newSlot = checkpointHeader.slotNumber;
|
|
779
|
+
|
|
780
|
+
if (newSlot <= lastSlot) {
|
|
781
|
+
throw new Error(
|
|
782
|
+
`Cannot create checkpoint proposal for slot ${newSlot}: ` +
|
|
783
|
+
`already proposed checkpoint for slot ${lastSlot}`,
|
|
784
|
+
);
|
|
785
|
+
}
|
|
786
|
+
}
|
|
787
|
+
|
|
776
788
|
this.log.info(`Assembling checkpoint proposal for slot ${checkpointHeader.slotNumber}`);
|
|
777
|
-
|
|
789
|
+
const newProposal = await this.validationService.createCheckpointProposal(
|
|
778
790
|
checkpointHeader,
|
|
779
791
|
archive,
|
|
780
|
-
|
|
792
|
+
feeAssetPriceModifier,
|
|
793
|
+
lastBlockProposal,
|
|
781
794
|
proposerAddress,
|
|
782
795
|
options,
|
|
783
796
|
);
|
|
797
|
+
this.lastProposedCheckpoint = newProposal;
|
|
798
|
+
return newProposal;
|
|
784
799
|
}
|
|
785
800
|
|
|
786
801
|
async broadcastBlockProposal(proposal: BlockProposal): Promise<void> {
|
|
@@ -802,6 +817,10 @@ export class ValidatorClient extends (EventEmitter as new () => WatcherEmitter)
|
|
|
802
817
|
this.log.debug(`Collecting ${inCommittee.length} self-attestations for slot ${slot}`, { inCommittee });
|
|
803
818
|
const attestations = await this.createCheckpointAttestationsFromProposal(proposal, inCommittee);
|
|
804
819
|
|
|
820
|
+
if (!attestations) {
|
|
821
|
+
return [];
|
|
822
|
+
}
|
|
823
|
+
|
|
805
824
|
// We broadcast our own attestations to our peers so, in case our block does not get mined on L1,
|
|
806
825
|
// other nodes can see that our validators did attest to this block proposal, and do not slash us
|
|
807
826
|
// due to inactivity for missed attestations.
|