Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
79 changes: 79 additions & 0 deletions yarn-project/aztec-node/src/sentinel/sentinel.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -657,6 +657,81 @@ describe('sentinel', () => {
});
});

describe('escape hatch', () => {
it('processSlot skips tracking when escape hatch is open', async () => {
const validator1 = EthAddress.random();
const validator2 = EthAddress.random();
const committee = [validator1, validator2];

epochCache.getCommittee.mockResolvedValue({
committee,
seed: 0n,
epoch,
isEscapeHatchOpen: true,
});

const updateSpy = jest.spyOn(store, 'updateValidators');

await sentinel.doProcessSlot(slot);

// Should NOT have called updateValidators since escape hatch is open
expect(updateSpy).not.toHaveBeenCalled();
// But lastProcessedSlot should still advance
expect(sentinel.getLastProcessedSlot()).toEqual(slot);
});

it('processSlot tracks normally when escape hatch is closed', async () => {
const signers = times(4, Secp256k1Signer.random);
const validators = signers.map(s => s.address);
const committee = [...validators];

epochCache.getCommittee.mockResolvedValue({
committee,
seed: 0n,
epoch,
isEscapeHatchOpen: false,
});
epochCache.computeProposerIndex.mockReturnValue(0n);
p2p.getCheckpointAttestationsForSlot.mockResolvedValue([]);

const updateSpy = jest.spyOn(store, 'updateValidators');

await sentinel.doProcessSlot(slot);

// Should have called updateValidators since escape hatch is closed
expect(updateSpy).toHaveBeenCalled();
expect(sentinel.getLastProcessedSlot()).toEqual(slot);
});

it('handleChainProven skips proven performance when escape hatch is open', async () => {
const blockNumber = BlockNumber(15);
const blockHash = '0xblockhash';
const mockBlock = await L2Block.random(blockNumber);
const blockSlot = mockBlock.header.getSlot();
const epochNumber = getEpochAtSlot(blockSlot, l1Constants);
const validator1 = EthAddress.random();

archiver.getBlockHeader.calledWith(blockNumber).mockResolvedValue(mockBlock.header);

epochCache.getCommittee.mockResolvedValue({
committee: [validator1],
seed: 0n,
epoch: epochNumber,
isEscapeHatchOpen: true,
});

const emitSpy = jest.spyOn(sentinel, 'emit');
const updateProvenSpy = jest.spyOn(store, 'updateProvenPerformance');

await sentinel.handleChainProven({ type: 'chain-proven', block: { number: blockNumber, hash: blockHash } });

// Should have stored empty performance (no offenses during escape hatch)
expect(updateProvenSpy).toHaveBeenCalledWith(epochNumber, {});
// Should NOT have emitted any slash events
expect(emitSpy).not.toHaveBeenCalled();
});
});

describe('consecutive epoch inactivity', () => {
let validator1: EthAddress;
let validator2: EthAddress;
Expand Down Expand Up @@ -905,4 +980,8 @@ class TestSentinel extends Sentinel {
) {
return super.checkPastInactivity(validator, currentEpoch, requiredConsecutiveEpochs);
}

public doProcessSlot(slot: SlotNumber) {
return super.processSlot(slot);
}
}
13 changes: 11 additions & 2 deletions yarn-project/aztec-node/src/sentinel/sentinel.ts
Original file line number Diff line number Diff line change
Expand Up @@ -158,7 +158,11 @@ export class Sentinel extends (EventEmitter as new () => WatcherEmitter) impleme

protected async computeProvenPerformance(epoch: EpochNumber): Promise<ValidatorsEpochPerformance> {
const [fromSlot, toSlot] = getSlotRangeForEpoch(epoch, this.epochCache.getL1Constants());
const { committee } = await this.epochCache.getCommittee(fromSlot);
const { committee, isEscapeHatchOpen } = await this.epochCache.getCommittee(fromSlot);
if (isEscapeHatchOpen) {
this.logger.info(`Skipping proven performance for epoch ${epoch} - escape hatch is open`);
return {};
}
if (!committee) {
this.logger.trace(`No committee found for slot ${fromSlot}`);
return {};
Expand Down Expand Up @@ -327,7 +331,12 @@ export class Sentinel extends (EventEmitter as new () => WatcherEmitter) impleme
* and updates overall stats.
*/
protected async processSlot(slot: SlotNumber) {
const { epoch, seed, committee } = await this.epochCache.getCommittee(slot);
const { epoch, seed, committee, isEscapeHatchOpen } = await this.epochCache.getCommittee(slot);
if (isEscapeHatchOpen) {
this.logger.info(`Skipping slot ${slot} at epoch ${epoch} - escape hatch is open`);
this.lastProcessedSlot = slot;
return;
}
if (!committee || committee.length === 0) {
this.logger.trace(`No committee found for slot ${slot} at epoch ${epoch}`);
this.lastProcessedSlot = slot;
Expand Down
Loading