@@ -4,7 +4,7 @@ import { Secp256k1Signer } from '@aztec/foundation/crypto';
44import { EthAddress } from '@aztec/foundation/eth-address' ;
55import { AztecLMDBStoreV2 , openTmpStore } from '@aztec/kv-store/lmdb-v2' ;
66import type { P2PClient } from '@aztec/p2p' ;
7- import { OffenseType , WANT_TO_SLASH_EVENT } from '@aztec/slasher' ;
7+ import { OffenseType , WANT_TO_SLASH_EVENT , type WantToSlashArgs } from '@aztec/slasher' ;
88import type { SlasherConfig } from '@aztec/slasher/config' ;
99import {
1010 type L2BlockSource ,
@@ -24,7 +24,7 @@ import type {
2424} from '@aztec/stdlib/validators' ;
2525
2626import { jest } from '@jest/globals' ;
27- import { type MockProxy , mock , mockDeep } from 'jest-mock-extended' ;
27+ import { type MockProxy , mock } from 'jest-mock-extended' ;
2828
2929import { Sentinel } from './sentinel.js' ;
3030import { SentinelStore } from './store.js' ;
@@ -287,8 +287,8 @@ describe('sentinel', () => {
287287 jest . spyOn ( sentinel , 'computeStatsForValidator' ) . mockReturnValue ( {
288288 address : validator ,
289289 totalSlots : 2 ,
290- missedProposals : { count : 0 , currentStreak : 0 , rate : 0 } ,
291- missedAttestations : { count : 0 , currentStreak : 0 , rate : 0 } ,
290+ missedProposals : { count : 0 , currentStreak : 0 , rate : 0 , total : 0 } ,
291+ missedAttestations : { count : 0 , currentStreak : 0 , rate : 0 , total : 0 } ,
292292 history : mockHistory ,
293293 } ) ;
294294
@@ -298,8 +298,8 @@ describe('sentinel', () => {
298298 validator : {
299299 address : validator ,
300300 totalSlots : 2 ,
301- missedProposals : { count : 0 , currentStreak : 0 , rate : 0 } ,
302- missedAttestations : { count : 0 , currentStreak : 0 , rate : 0 } ,
301+ missedProposals : { count : 0 , currentStreak : 0 , rate : 0 , total : 0 } ,
302+ missedAttestations : { count : 0 , currentStreak : 0 , rate : 0 , total : 0 } ,
303303 history : mockHistory ,
304304 } ,
305305 allTimeProvenPerformance : mockProvenPerformance ,
@@ -316,8 +316,8 @@ describe('sentinel', () => {
316316 const computeStatsSpy = jest . spyOn ( sentinel , 'computeStatsForValidator' ) . mockReturnValue ( {
317317 address : validator ,
318318 totalSlots : 1 ,
319- missedProposals : { count : 0 , currentStreak : 0 , rate : 0 } ,
320- missedAttestations : { count : 0 , currentStreak : 0 , rate : 0 } ,
319+ missedProposals : { count : 0 , currentStreak : 0 , rate : 0 , total : 0 } ,
320+ missedAttestations : { count : 0 , currentStreak : 0 , rate : 0 , total : 0 } ,
321321 history : mockHistory ,
322322 } ) ;
323323
@@ -333,8 +333,8 @@ describe('sentinel', () => {
333333 const computeStatsSpy = jest . spyOn ( sentinel , 'computeStatsForValidator' ) . mockReturnValue ( {
334334 address : validator ,
335335 totalSlots : 1 ,
336- missedProposals : { count : 0 , currentStreak : 0 , rate : 0 } ,
337- missedAttestations : { count : 0 , currentStreak : 0 , rate : 0 } ,
336+ missedProposals : { count : 0 , currentStreak : 0 , rate : 0 , total : 0 } ,
337+ missedAttestations : { count : 0 , currentStreak : 0 , rate : 0 , total : 0 } ,
338338 history : mockHistory ,
339339 } ) ;
340340
@@ -357,8 +357,8 @@ describe('sentinel', () => {
357357 jest . spyOn ( sentinel , 'computeStatsForValidator' ) . mockReturnValue ( {
358358 address : validator ,
359359 totalSlots : 1 ,
360- missedProposals : { count : 0 , currentStreak : 0 , rate : 0 } ,
361- missedAttestations : { count : 0 , currentStreak : 0 , rate : 0 } ,
360+ missedProposals : { count : 0 , currentStreak : 0 , rate : 0 , total : 0 } ,
361+ missedAttestations : { count : 0 , currentStreak : 0 , rate : 0 , total : 0 } ,
362362 history : mockHistory ,
363363 } ) ;
364364
@@ -379,65 +379,52 @@ describe('sentinel', () => {
379379 const epochNumber = getEpochAtSlot ( slot , l1Constants ) ;
380380 const validator1 = EthAddress . random ( ) ;
381381 const validator2 = EthAddress . random ( ) ;
382- const headerSlots = times ( 5 , i => slot - BigInt ( i ) ) ;
383- const mockHeaders = headerSlots . map ( s => {
384- const header = mockDeep < PublishedL2Block [ 'block' ] [ 'header' ] > ( ) ;
385- header . getSlot . mockReturnValue ( s ) ;
386- return header ;
387- } ) ;
382+ const validator3 = EthAddress . random ( ) ;
383+ const headerSlots = times ( l1Constants . epochDuration , i => slot - BigInt ( i ) ) . reverse ( ) ;
388384
389385 epochCache . getEpochAndSlotNow . mockReturnValue ( { epoch : epochNumber , slot, ts, now : ts } ) ;
390386 archiver . getBlock . calledWith ( blockNumber ) . mockResolvedValue ( mockBlock . block ) ;
391387 archiver . getL1Constants . mockResolvedValue ( l1Constants ) ;
392-
393- archiver . getBlockHeadersForEpoch . calledWith ( epochNumber ) . mockResolvedValue ( mockHeaders as any ) ;
388+ epochCache . getL1Constants . mockReturnValue ( l1Constants ) ;
394389
395390 epochCache . getCommittee . mockResolvedValue ( {
396- committee : [ validator1 , validator2 ] ,
391+ committee : [ validator1 , validator2 , validator3 ] ,
397392 seed : 0n ,
398393 epoch : epochNumber ,
399394 } ) ;
400- const statsResult = {
395+
396+ const statsResult : ValidatorsStats = {
401397 stats : {
398+ // Validator 1 missed 1 attestation only, we won't slash them
402399 [ validator1 . toString ( ) ] : {
403400 address : validator1 ,
404401 totalSlots : headerSlots . length ,
405- missedProposals : { count : 0 , currentStreak : 0 , rate : 0 } ,
406- missedAttestations : { count : 1 , currentStreak : 0 , rate : 1 / 5 } ,
407- history : [
408- { slot : headerSlots [ 0 ] , status : 'attestation-sent' } ,
409- { slot : headerSlots [ 1 ] , status : 'attestation-missed' } ,
410- { slot : headerSlots [ 2 ] , status : 'attestation-sent' } ,
411- { slot : headerSlots [ 3 ] , status : 'attestation-sent' } ,
412- { slot : headerSlots [ 4 ] , status : 'attestation-sent' } ,
413- ] ,
414- } as ValidatorStats ,
402+ missedProposals : { count : 0 , currentStreak : 0 , rate : 0 , total : 0 } ,
403+ missedAttestations : { count : 1 , currentStreak : 0 , rate : 1 / 8 , total : 8 } ,
404+ history : [ ] ,
405+ } ,
406+ // Validator 2 missed 7 out of 8, we will slash them
415407 [ validator2 . toString ( ) ] : {
416408 address : validator2 ,
417409 totalSlots : headerSlots . length ,
418- missedProposals : { count : 0 , currentStreak : 0 , rate : 0 } ,
419- // We should only count the slots that are in the proven epoch (0, 1, 2)!!
420- missedAttestations : { count : 4 , currentStreak : 3 , rate : 4 / 5 } ,
421- history : [
422- { slot : headerSlots [ 0 ] , status : 'attestation-missed' } ,
423- { slot : headerSlots [ 1 ] , status : 'attestation-sent' } ,
424- { slot : headerSlots [ 2 ] , status : 'attestation-missed' } ,
425- { slot : headerSlots [ 3 ] , status : 'attestation-missed' } ,
426- { slot : headerSlots [ 4 ] , status : 'attestation-missed' } ,
427- ] ,
428- } as ValidatorStats ,
429- '0xNotAnAddress' : {
430- address : EthAddress . ZERO , // Placeholder
431- totalSlots : 0 ,
432- missedProposals : { count : 0 , currentStreak : 0 , rate : undefined } ,
433- missedAttestations : { count : 0 , currentStreak : 0 , rate : undefined } ,
410+ missedProposals : { count : 0 , currentStreak : 0 , rate : 0 , total : 0 } ,
411+ missedAttestations : { count : 7 , currentStreak : 3 , rate : 7 / 8 , total : 8 } ,
412+ history : [ ] ,
413+ } ,
414+ // Validator 3 missed 4 attestations out of 4, so we will slash them even though the epoch has 8 slots
415+ // This difference happens because we don't count attestations for a slot where there was no proposal
416+ [ validator3 . toString ( ) ] : {
417+ address : validator3 ,
418+ totalSlots : headerSlots . length ,
419+ missedProposals : { count : 0 , currentStreak : 0 , rate : 0 , total : 0 } ,
420+ missedAttestations : { count : 4 , currentStreak : 4 , rate : 4 / 4 , total : 4 } ,
434421 history : [ ] ,
435- } as ValidatorStats , // To test filtering
422+ } ,
436423 } ,
437424 lastProcessedSlot : slot ,
438425 initialSlot : 0n ,
439426 slotWindow : 15 ,
440- } as ValidatorsStats ;
427+ } ;
441428 const computeStatsSpy = jest . spyOn ( sentinel , 'computeStats' ) . mockResolvedValue ( statsResult ) ;
442429 const emitSpy = jest . spyOn ( sentinel , 'emit' ) ;
443430
@@ -446,20 +433,20 @@ describe('sentinel', () => {
446433 expect ( computeStatsSpy ) . toHaveBeenCalledWith ( {
447434 fromSlot : headerSlots [ 0 ] ,
448435 toSlot : headerSlots [ headerSlots . length - 1 ] ,
436+ validators : [ validator1 , validator2 , validator3 ] ,
437+ } ) ;
438+ const makeInactivitySlash = ( validator : EthAddress ) : WantToSlashArgs => ( {
439+ validator,
440+ amount : config . slashInactivityPenalty ,
441+ offenseType : OffenseType . INACTIVITY ,
442+ epochOrSlot : 1n ,
449443 } ) ;
450444
451445 expect ( emitSpy ) . toHaveBeenCalledTimes ( 1 ) ;
452- expect ( emitSpy ) . toHaveBeenCalledWith (
453- WANT_TO_SLASH_EVENT ,
454- expect . arrayContaining ( [
455- expect . objectContaining ( {
456- validator : validator2 ,
457- amount : config . slashInactivityPenalty ,
458- offenseType : OffenseType . INACTIVITY ,
459- epochOrSlot : epochNumber ,
460- } ) ,
461- ] ) ,
462- ) ;
446+ expect ( emitSpy ) . toHaveBeenCalledWith ( WANT_TO_SLASH_EVENT , [
447+ makeInactivitySlash ( validator2 ) ,
448+ makeInactivitySlash ( validator3 ) ,
449+ ] ) ;
463450 } ) ;
464451 } ) ;
465452
@@ -673,10 +660,6 @@ class TestSentinel extends Sentinel {
673660 return super . handleProvenPerformance ( epoch , performance ) ;
674661 }
675662
676- public override updateProvenPerformance ( epoch : bigint , performance : ValidatorsEpochPerformance ) {
677- return super . updateProvenPerformance ( epoch , performance ) ;
678- }
679-
680663 public override getValidatorStats ( validatorAddress : EthAddress , fromSlot ?: bigint , toSlot ?: bigint ) {
681664 return super . getValidatorStats ( validatorAddress , fromSlot , toSlot ) ;
682665 }
0 commit comments