@@ -10,7 +10,10 @@ describe('TallySlashingHelpers', () => {
1010 const mockValidator4 = EthAddress . fromString ( '0x4567890123456789012345678901234567890123' ) ;
1111
1212 describe ( 'getSlashConsensusVotesFromOffenses' , ( ) => {
13- const settings = { slashingAmounts : [ 10n , 20n , 30n ] as [ bigint , bigint , bigint ] } ;
13+ const settings = {
14+ slashingAmounts : [ 10n , 20n , 30n ] as [ bigint , bigint , bigint ] ,
15+ epochDuration : 32 ,
16+ } ;
1417
1518 it ( 'creates votes based on offenses and committees' , ( ) => {
1619 const offenses : Offense [ ] = [
@@ -35,12 +38,13 @@ describe('TallySlashingHelpers', () => {
3538 ] ;
3639
3740 const committees = [ [ mockValidator1 , mockValidator2 , mockValidator3 ] ] ;
38- const votes = getSlashConsensusVotesFromOffenses ( offenses , committees , settings ) ;
41+ const epochsForCommittees = [ 5n ] ; // Committee for epoch 5
42+ const votes = getSlashConsensusVotesFromOffenses ( offenses , committees , epochsForCommittees , settings ) ;
3943
4044 expect ( votes ) . toHaveLength ( 3 ) ;
41- expect ( votes [ 0 ] ) . toEqual ( 3 ) ; // 30 / 10 = 3 slash units for validator1
42- expect ( votes [ 1 ] ) . toEqual ( 0 ) ; // 5 / 10 = 0 slash units for validator2
43- expect ( votes [ 2 ] ) . toEqual ( 0 ) ; // 0 / 10 = 0 slash units for validator3
45+ expect ( votes [ 0 ] ) . toEqual ( 2 ) ; // Only 25n from epoch 5 offense for validator1
46+ expect ( votes [ 1 ] ) . toEqual ( 0 ) ; // Offense is in slot 10, which is epoch 0, not 5
47+ expect ( votes [ 2 ] ) . toEqual ( 0 ) ; // No offenses for validator3
4448 } ) ;
4549
4650 it ( 'caps slash units at maximum per validator' , ( ) => {
@@ -54,7 +58,8 @@ describe('TallySlashingHelpers', () => {
5458 ] ;
5559
5660 const committees = [ [ mockValidator1 ] ] ;
57- const votes = getSlashConsensusVotesFromOffenses ( offenses , committees , settings ) ;
61+ const epochsForCommittees = [ 5n ] ;
62+ const votes = getSlashConsensusVotesFromOffenses ( offenses , committees , epochsForCommittees , settings ) ;
5863
5964 expect ( votes ) . toHaveLength ( 1 ) ;
6065 expect ( votes [ 0 ] ) . toEqual ( 3 ) ; // Capped at MAX_SLASH_UNITS_PER_VALIDATOR
@@ -81,7 +86,8 @@ describe('TallySlashingHelpers', () => {
8186 [ mockValidator3 , mockValidator4 ] ,
8287 ] ;
8388
84- const votes = getSlashConsensusVotesFromOffenses ( offenses , committees , settings ) ;
89+ const epochsForCommittees = [ 5n , 6n ] ; // Committees for epochs 5 and 6
90+ const votes = getSlashConsensusVotesFromOffenses ( offenses , committees , epochsForCommittees , settings ) ;
8591
8692 expect ( votes ) . toHaveLength ( 4 ) ;
8793 expect ( votes [ 0 ] ) . toEqual ( 2 ) ; // validator1 in committee1
@@ -90,7 +96,7 @@ describe('TallySlashingHelpers', () => {
9096 expect ( votes [ 3 ] ) . toEqual ( 3 ) ; // validator4 in committee2
9197 } ) ;
9298
93- it ( 'does not repeat slashes for the same validator in different committees ' , ( ) => {
99+ it ( 'correctly handles validators appearing in multiple committees with different epochs ' , ( ) => {
94100 const offenses : Offense [ ] = [
95101 {
96102 validator : mockValidator1 ,
@@ -110,13 +116,14 @@ describe('TallySlashingHelpers', () => {
110116 [ mockValidator1 , mockValidator2 ] ,
111117 [ mockValidator1 , mockValidator3 ] ,
112118 ] ;
113- const votes = getSlashConsensusVotesFromOffenses ( offenses , committees , settings ) ;
119+ const epochsForCommittees = [ 5n , 6n ] ;
120+ const votes = getSlashConsensusVotesFromOffenses ( offenses , committees , epochsForCommittees , settings ) ;
114121
115122 expect ( votes ) . toHaveLength ( 4 ) ;
116- expect ( votes [ 0 ] ) . toEqual ( 3 ) ; // validator1 in committee1
117- expect ( votes [ 1 ] ) . toEqual ( 0 ) ; // validator2 in committee1
118- expect ( votes [ 2 ] ) . toEqual ( 0 ) ; // validator1 in committee2
119- expect ( votes [ 3 ] ) . toEqual ( 0 ) ; // validator3 in committee2
123+ expect ( votes [ 0 ] ) . toEqual ( 2 ) ; // validator1 in committee1, epoch 5 offense (20n)
124+ expect ( votes [ 1 ] ) . toEqual ( 0 ) ; // validator2 in committee1, no offenses
125+ expect ( votes [ 2 ] ) . toEqual ( 1 ) ; // validator1 in committee2, epoch 6 offense (10n)
126+ expect ( votes [ 3 ] ) . toEqual ( 0 ) ; // validator3 in committee2, no offenses
120127 } ) ;
121128
122129 it ( 'returns empty votes for empty committees' , ( ) => {
@@ -130,7 +137,8 @@ describe('TallySlashingHelpers', () => {
130137 ] ;
131138
132139 const committees : EthAddress [ ] [ ] = [ ] ;
133- const votes = getSlashConsensusVotesFromOffenses ( offenses , committees , settings ) ;
140+ const epochsForCommittees : bigint [ ] = [ ] ;
141+ const votes = getSlashConsensusVotesFromOffenses ( offenses , committees , epochsForCommittees , settings ) ;
134142
135143 expect ( votes ) . toEqual ( [ ] ) ;
136144 } ) ;
@@ -146,12 +154,211 @@ describe('TallySlashingHelpers', () => {
146154 ] ;
147155
148156 const committees = [ [ mockValidator2 , mockValidator3 ] ] ;
149- const votes = getSlashConsensusVotesFromOffenses ( offenses , committees , settings ) ;
157+ const epochsForCommittees = [ 5n ] ;
158+ const votes = getSlashConsensusVotesFromOffenses ( offenses , committees , epochsForCommittees , settings ) ;
150159
151160 expect ( votes ) . toHaveLength ( 2 ) ;
152161 expect ( votes [ 0 ] ) . toEqual ( 0 ) ; // validator2 has no offenses
153162 expect ( votes [ 1 ] ) . toEqual ( 0 ) ; // validator3 has no offenses
154163 } ) ;
164+
165+ it ( 'handles offenses without epochOrSlot (slashValidatorsAlways)' , ( ) => {
166+ const offenses = [
167+ {
168+ validator : mockValidator1 ,
169+ amount : 30n ,
170+ offenseType : OffenseType . UNKNOWN ,
171+ epochOrSlot : undefined , // No epoch/slot for always-slash validators
172+ } ,
173+ {
174+ validator : mockValidator2 ,
175+ amount : 10n ,
176+ offenseType : OffenseType . INACTIVITY ,
177+ epochOrSlot : 5n ,
178+ } ,
179+ ] ;
180+
181+ const committees = [
182+ [ mockValidator1 , mockValidator2 ] ,
183+ [ mockValidator1 , mockValidator3 ] ,
184+ ] ;
185+ const epochsForCommittees = [ 5n , 6n ] ;
186+ const votes = getSlashConsensusVotesFromOffenses ( offenses , committees , epochsForCommittees , settings ) ;
187+
188+ expect ( votes ) . toHaveLength ( 4 ) ;
189+ expect ( votes [ 0 ] ) . toEqual ( 3 ) ; // validator1 in committee1, always-slash (30n)
190+ expect ( votes [ 1 ] ) . toEqual ( 1 ) ; // validator2 in committee1, epoch 5 offense (10n)
191+ expect ( votes [ 2 ] ) . toEqual ( 3 ) ; // validator1 in committee2, always-slash (30n)
192+ expect ( votes [ 3 ] ) . toEqual ( 0 ) ; // validator3 in committee2, no offenses
193+ } ) ;
194+
195+ it ( 'correctly converts slot-based offenses to epochs' , ( ) => {
196+ const offenses : Offense [ ] = [
197+ {
198+ validator : mockValidator1 ,
199+ amount : 15n ,
200+ offenseType : OffenseType . PROPOSED_INSUFFICIENT_ATTESTATIONS , // slot-based
201+ epochOrSlot : 64n , // slot 64 = epoch 2 (64/32)
202+ } ,
203+ {
204+ validator : mockValidator2 ,
205+ amount : 20n ,
206+ offenseType : OffenseType . PROPOSED_INSUFFICIENT_ATTESTATIONS , // slot-based
207+ epochOrSlot : 95n , // slot 95 = epoch 2 (95/32 = 2.96... -> 2)
208+ } ,
209+ ] ;
210+
211+ const committees = [ [ mockValidator1 , mockValidator2 , mockValidator3 ] ] ;
212+ const epochsForCommittees = [ 2n ] ; // Committee for epoch 2
213+ const votes = getSlashConsensusVotesFromOffenses ( offenses , committees , epochsForCommittees , settings ) ;
214+
215+ expect ( votes ) . toHaveLength ( 3 ) ;
216+ expect ( votes [ 0 ] ) . toEqual ( 1 ) ; // validator1: 15n offense maps to epoch 2
217+ expect ( votes [ 1 ] ) . toEqual ( 2 ) ; // validator2: 20n offense maps to epoch 2
218+ expect ( votes [ 2 ] ) . toEqual ( 0 ) ; // validator3: no offenses
219+ } ) ;
220+
221+ it ( 'handles mixed epoch and slot-based offenses resolving to same epoch' , ( ) => {
222+ const offenses : Offense [ ] = [
223+ {
224+ validator : mockValidator1 ,
225+ amount : 10n ,
226+ offenseType : OffenseType . INACTIVITY , // epoch-based
227+ epochOrSlot : 2n , // epoch 2
228+ } ,
229+ {
230+ validator : mockValidator1 ,
231+ amount : 15n ,
232+ offenseType : OffenseType . PROPOSED_INSUFFICIENT_ATTESTATIONS , // slot-based
233+ epochOrSlot : 75n , // slot 75 = epoch 2 (75/32 = 2.34... -> 2)
234+ } ,
235+ ] ;
236+
237+ const committees = [ [ mockValidator1 , mockValidator2 ] ] ;
238+ const epochsForCommittees = [ 2n ] ;
239+ const votes = getSlashConsensusVotesFromOffenses ( offenses , committees , epochsForCommittees , settings ) ;
240+
241+ expect ( votes ) . toHaveLength ( 2 ) ;
242+ expect ( votes [ 0 ] ) . toEqual ( 2 ) ; // validator1: 10n + 15n = 25n total for epoch 2
243+ expect ( votes [ 1 ] ) . toEqual ( 0 ) ; // validator2: no offenses
244+ } ) ;
245+
246+ it ( 'sums multiple offenses for same validator in same epoch' , ( ) => {
247+ const offenses : Offense [ ] = [
248+ {
249+ validator : mockValidator1 ,
250+ amount : 8n ,
251+ offenseType : OffenseType . INACTIVITY ,
252+ epochOrSlot : 3n ,
253+ } ,
254+ {
255+ validator : mockValidator1 ,
256+ amount : 7n ,
257+ offenseType : OffenseType . DATA_WITHHOLDING ,
258+ epochOrSlot : 3n ,
259+ } ,
260+ {
261+ validator : mockValidator1 ,
262+ amount : 5n ,
263+ offenseType : OffenseType . VALID_EPOCH_PRUNED ,
264+ epochOrSlot : 3n ,
265+ } ,
266+ ] ;
267+
268+ const committees = [ [ mockValidator1 , mockValidator2 ] ] ;
269+ const epochsForCommittees = [ 3n ] ;
270+ const votes = getSlashConsensusVotesFromOffenses ( offenses , committees , epochsForCommittees , settings ) ;
271+
272+ expect ( votes ) . toHaveLength ( 2 ) ;
273+ expect ( votes [ 0 ] ) . toEqual ( 2 ) ; // validator1: 8n + 7n + 5n = 20n total
274+ expect ( votes [ 1 ] ) . toEqual ( 0 ) ; // validator2: no offenses
275+ } ) ;
276+
277+ it ( 'handles always-slash validator with additional epoch-specific offenses' , ( ) => {
278+ const offenses = [
279+ {
280+ validator : mockValidator1 ,
281+ amount : 20n , // always-slash
282+ offenseType : OffenseType . UNKNOWN ,
283+ epochOrSlot : undefined ,
284+ } ,
285+ {
286+ validator : mockValidator1 ,
287+ amount : 15n , // epoch-specific
288+ offenseType : OffenseType . INACTIVITY ,
289+ epochOrSlot : 5n ,
290+ } ,
291+ ] ;
292+
293+ const committees = [
294+ [ mockValidator1 , mockValidator2 ] ,
295+ [ mockValidator1 , mockValidator3 ] ,
296+ ] ;
297+ const epochsForCommittees = [ 5n , 6n ] ;
298+ const votes = getSlashConsensusVotesFromOffenses ( offenses , committees , epochsForCommittees , settings ) ;
299+
300+ expect ( votes ) . toHaveLength ( 4 ) ;
301+ expect ( votes [ 0 ] ) . toEqual ( 3 ) ; // validator1 committee1: 20n(always) + 15n(epoch5) = 35n
302+ expect ( votes [ 1 ] ) . toEqual ( 0 ) ; // validator2: no offenses
303+ expect ( votes [ 2 ] ) . toEqual ( 2 ) ; // validator1 committee2: 20n(always) only
304+ expect ( votes [ 3 ] ) . toEqual ( 0 ) ; // validator3: no offenses
305+ } ) ;
306+
307+ it ( 'handles epoch boundary conditions' , ( ) => {
308+ const offenses : Offense [ ] = [
309+ {
310+ validator : mockValidator1 ,
311+ amount : 15n ,
312+ offenseType : OffenseType . PROPOSED_INSUFFICIENT_ATTESTATIONS , // slot-based
313+ epochOrSlot : 31n , // slot 31 = epoch 0 (31/32 = 0.96... -> 0)
314+ } ,
315+ {
316+ validator : mockValidator2 ,
317+ amount : 20n ,
318+ offenseType : OffenseType . PROPOSED_INSUFFICIENT_ATTESTATIONS , // slot-based
319+ epochOrSlot : 32n , // slot 32 = epoch 1 (32/32 = 1)
320+ } ,
321+ ] ;
322+
323+ const committees = [
324+ [ mockValidator1 , mockValidator2 ] ,
325+ [ mockValidator1 , mockValidator2 ] ,
326+ ] ;
327+ const epochsForCommittees = [ 0n , 1n ] ;
328+ const votes = getSlashConsensusVotesFromOffenses ( offenses , committees , epochsForCommittees , settings ) ;
329+
330+ expect ( votes ) . toHaveLength ( 4 ) ;
331+ expect ( votes [ 0 ] ) . toEqual ( 1 ) ; // validator1 epoch0: 15n offense
332+ expect ( votes [ 1 ] ) . toEqual ( 0 ) ; // validator2 epoch0: no matching offenses
333+ expect ( votes [ 2 ] ) . toEqual ( 0 ) ; // validator1 epoch1: no matching offenses
334+ expect ( votes [ 3 ] ) . toEqual ( 2 ) ; // validator2 epoch1: 20n offense
335+ } ) ;
336+
337+ it ( 'handles zero amount offenses' , ( ) => {
338+ const offenses : Offense [ ] = [
339+ {
340+ validator : mockValidator1 ,
341+ amount : 0n ,
342+ offenseType : OffenseType . INACTIVITY ,
343+ epochOrSlot : 5n ,
344+ } ,
345+ {
346+ validator : mockValidator2 ,
347+ amount : 15n ,
348+ offenseType : OffenseType . INACTIVITY ,
349+ epochOrSlot : 5n ,
350+ } ,
351+ ] ;
352+
353+ const committees = [ [ mockValidator1 , mockValidator2 , mockValidator3 ] ] ;
354+ const epochsForCommittees = [ 5n ] ;
355+ const votes = getSlashConsensusVotesFromOffenses ( offenses , committees , epochsForCommittees , settings ) ;
356+
357+ expect ( votes ) . toHaveLength ( 3 ) ;
358+ expect ( votes [ 0 ] ) . toEqual ( 0 ) ; // validator1: 0n amount = 0 slash units
359+ expect ( votes [ 1 ] ) . toEqual ( 1 ) ; // validator2: 15n amount = 1 slash unit
360+ expect ( votes [ 2 ] ) . toEqual ( 0 ) ; // validator3: no offenses
361+ } ) ;
155362 } ) ;
156363
157364 describe ( 'encodeSlashConsensusVotes' , ( ) => {
0 commit comments