diff --git a/yarn-project/epoch-cache/src/epoch_cache.test.ts b/yarn-project/epoch-cache/src/epoch_cache.test.ts index f21d7a3b77c..f9218b3a9ec 100644 --- a/yarn-project/epoch-cache/src/epoch_cache.test.ts +++ b/yarn-project/epoch-cache/src/epoch_cache.test.ts @@ -102,7 +102,7 @@ describe('EpochCache', () => { expect(nextNextProposer).toEqual(testCommittee[0]); }); - it('Should request to update the validator set when on the epoch boundary', async () => { + it('should request to update the validator set when on the epoch boundary', async () => { // Set initial time to a known slot const initialTime = Number(l1GenesisTime) * 1000; // Convert to milliseconds jest.setSystemTime(initialTime); diff --git a/yarn-project/epoch-cache/src/epoch_cache.ts b/yarn-project/epoch-cache/src/epoch_cache.ts index 962f59e3d3a..b0c019c6b63 100644 --- a/yarn-project/epoch-cache/src/epoch_cache.ts +++ b/yarn-project/epoch-cache/src/epoch_cache.ts @@ -8,6 +8,7 @@ import { RollupContract, createEthereumChain } from '@aztec/ethereum'; import { EthAddress } from '@aztec/foundation/eth-address'; import { type Logger, createLogger } from '@aztec/foundation/log'; +import { EventEmitter } from 'node:events'; import { createPublicClient, encodeAbiParameters, http, keccak256 } from 'viem'; import { type EpochCacheConfig, getEpochCacheConfigEnvVars } from './config.js'; @@ -28,7 +29,7 @@ type EpochAndSlot = { * * Note: This class is very dependent on the system clock being in sync. */ -export class EpochCache { +export class EpochCache extends EventEmitter<{ committeeChanged: [EthAddress[], bigint] }> { private committee: EthAddress[]; private cachedEpoch: bigint; private cachedSampleSeed: bigint; @@ -40,6 +41,7 @@ export class EpochCache { initialSampleSeed: bigint = 0n, private readonly l1constants: L1RollupConstants = EmptyL1RollupConstants, ) { + super(); this.committee = initialValidators; this.cachedSampleSeed = initialSampleSeed; @@ -111,14 +113,19 @@ export class EpochCache { const { epoch: calculatedEpoch, ts } = nextSlot ? this.getEpochAndSlotInNextSlot() : this.getEpochAndSlotNow(); if (calculatedEpoch !== this.cachedEpoch) { - this.log.debug(`Epoch changed, updating validator set`, { calculatedEpoch, cachedEpoch: this.cachedEpoch }); - this.cachedEpoch = calculatedEpoch; + this.log.debug(`Updating validator set for new epoch ${calculatedEpoch}`, { + epoch: calculatedEpoch, + previousEpoch: this.cachedEpoch, + }); const [committeeAtTs, sampleSeedAtTs] = await Promise.all([ this.rollup.getCommitteeAt(ts), this.rollup.getSampleSeedAt(ts), ]); this.committee = committeeAtTs.map((v: `0x${string}`) => EthAddress.fromString(v)); + this.cachedEpoch = calculatedEpoch; this.cachedSampleSeed = sampleSeedAtTs; + this.log.debug(`Updated validator set for epoch ${calculatedEpoch}`, { commitee: this.committee }); + this.emit('committeeChanged', this.committee, calculatedEpoch); } return this.committee; diff --git a/yarn-project/sequencer-client/src/client/sequencer-client.ts b/yarn-project/sequencer-client/src/client/sequencer-client.ts index 98ff97db320..ba9987262c2 100644 --- a/yarn-project/sequencer-client/src/client/sequencer-client.ts +++ b/yarn-project/sequencer-client/src/client/sequencer-client.ts @@ -81,7 +81,7 @@ export class SequencerClient { telemetryClient, config, ); - + await validatorClient?.start(); await sequencer.start(); return new SequencerClient(sequencer); } diff --git a/yarn-project/sequencer-client/src/sequencer/sequencer.ts b/yarn-project/sequencer-client/src/sequencer/sequencer.ts index f868980cf5e..7b3216d1829 100644 --- a/yarn-project/sequencer-client/src/sequencer/sequencer.ts +++ b/yarn-project/sequencer-client/src/sequencer/sequencer.ts @@ -204,6 +204,7 @@ export class Sequencer { */ public async stop(): Promise { this.log.debug(`Stopping sequencer`); + await this.validatorClient?.stop(); await this.runningPromise?.stop(); this.publisher.interrupt(); this.setState(SequencerState.STOPPED, 0n, true /** force */); diff --git a/yarn-project/validator-client/src/validator.ts b/yarn-project/validator-client/src/validator.ts index 179b7307294..6e2890334f8 100644 --- a/yarn-project/validator-client/src/validator.ts +++ b/yarn-project/validator-client/src/validator.ts @@ -11,6 +11,7 @@ import { type EpochCache } from '@aztec/epoch-cache'; import { Buffer32 } from '@aztec/foundation/buffer'; import { type Fr } from '@aztec/foundation/fields'; import { createLogger } from '@aztec/foundation/log'; +import { RunningPromise } from '@aztec/foundation/running-promise'; import { sleep } from '@aztec/foundation/sleep'; import { type Timer } from '@aztec/foundation/timer'; import { type P2P } from '@aztec/p2p'; @@ -68,6 +69,8 @@ export class ValidatorClient extends WithTracer implements Validator { // Callback registered to: sequencer.buildBlock private blockBuilder?: BlockBuilderCallback = undefined; + private epochCacheUpdateLoop: RunningPromise; + constructor( private keyStore: ValidatorKeyStore, private epochCache: EpochCache, @@ -81,7 +84,23 @@ export class ValidatorClient extends WithTracer implements Validator { this.metrics = new ValidatorMetrics(telemetry); this.validationService = new ValidationService(keyStore); - this.log.verbose('Initialized validator'); + + // Refresh epoch cache every second to trigger commiteeChanged event + this.epochCacheUpdateLoop = new RunningPromise(async () => { + await this.epochCache.getCommittee().catch(err => log.error('Error updating validator committee', err)); + }, 1000); + + // Listen to commiteeChanged event to alert operator when their validator has entered the committee + this.epochCache.on('committeeChanged', (newCommittee, epochNumber) => { + const me = this.keyStore.getAddress(); + if (newCommittee.some(addr => addr.equals(me))) { + this.log.info(`Validator ${me.toString()} is on the validator committee for epoch ${epochNumber}`); + } else { + this.log.verbose(`Validator ${me.toString()} not on the validator committee for epoch ${epochNumber}`); + } + }); + + this.log.verbose(`Initialized validator with address ${this.keyStore.getAddress().toString()}`); } static new( @@ -102,14 +121,25 @@ export class ValidatorClient extends WithTracer implements Validator { return validator; } - public start() { + public async start() { // Sync the committee from the smart contract // https://github.com/AztecProtocol/aztec-packages/issues/7962 - this.log.info('Validator started'); + const me = this.keyStore.getAddress(); + const inCommittee = await this.epochCache.isInCommittee(me); + if (inCommittee) { + this.log.info(`Started validator with address ${me.toString()} in current validator committee`); + } else { + this.log.info(`Started validator with address ${me.toString()}`); + } + this.epochCacheUpdateLoop.start(); return Promise.resolve(); } + public async stop() { + await this.epochCacheUpdateLoop.stop(); + } + public registerBlockProposalHandler() { const handler = (block: BlockProposal): Promise => { return this.attestToProposal(block);