@@ -9,6 +9,7 @@ import { OffenseType } from '@aztec/stdlib/slashing';
99import {
1010 makeBlockHeader ,
1111 makeBlockProposal ,
12+ makeCheckpointAttestation ,
1213 makeCheckpointHeader ,
1314 makeCheckpointProposal ,
1415} from '@aztec/stdlib/testing' ;
@@ -21,14 +22,15 @@ import { WANT_TO_SLASH_EVENT, type WantToSlashArgs } from '../watcher.js';
2122import { BroadcastedInvalidCheckpointProposalWatcher } from './broadcasted_invalid_checkpoint_proposal_watcher.js' ;
2223
2324describe ( 'BroadcastedInvalidCheckpointProposalWatcher' , ( ) => {
24- let p2pClient : MockProxy < Pick < P2PClient , 'getProposalsForSlot' > > ;
25+ let p2pClient : MockProxy < Pick < P2PClient , 'getCheckpointAttestationsForSlot' | ' getProposalsForSlot'> > ;
2526 let epochCache : MockProxy < Pick < EpochCacheInterface , 'getCurrentAndNextSlot' | 'getL1Constants' > > ;
2627 let config : SlasherConfig ;
2728 let watcher : BroadcastedInvalidCheckpointProposalWatcher ;
2829 let handler : jest . MockedFunction < ( args : WantToSlashArgs [ ] ) => void > ;
2930
3031 beforeEach ( ( ) => {
31- p2pClient = mock < Pick < P2PClient , 'getProposalsForSlot' > > ( ) ;
32+ p2pClient = mock < Pick < P2PClient , 'getCheckpointAttestationsForSlot' | 'getProposalsForSlot' > > ( ) ;
33+ p2pClient . getCheckpointAttestationsForSlot . mockResolvedValue ( [ ] ) ;
3234 epochCache = mock < Pick < EpochCacheInterface , 'getCurrentAndNextSlot' | 'getL1Constants' > > ( ) ;
3335 epochCache . getCurrentAndNextSlot . mockReturnValue ( { currentSlot : SlotNumber ( 12 ) , nextSlot : SlotNumber ( 13 ) } ) ;
3436 epochCache . getL1Constants . mockReturnValue ( {
@@ -39,6 +41,7 @@ describe('BroadcastedInvalidCheckpointProposalWatcher', () => {
3941 config = {
4042 ...DefaultSlasherConfig ,
4143 slashBroadcastedInvalidCheckpointProposalPenalty : 11n ,
44+ slashAttestInvalidCheckpointProposalPenalty : 13n ,
4245 } ;
4346 watcher = new BroadcastedInvalidCheckpointProposalWatcher ( p2pClient , epochCache , config , 4 ) ;
4447 handler = jest . fn ( ) ;
@@ -108,6 +111,154 @@ describe('BroadcastedInvalidCheckpointProposalWatcher', () => {
108111 ] ) ;
109112 } ) ;
110113
114+ it ( 'slashes attesters when retained attestations exist for a truncated checkpoint proposal slot' , async ( ) => {
115+ const signer = Secp256k1Signer . random ( ) ;
116+ const attester = Secp256k1Signer . random ( ) ;
117+ const slot = SlotNumber ( 10 ) ;
118+ const blocks = await makeBlocks ( signer , slot , 4 ) ;
119+ const checkpoint = await makeCheckpointCore ( signer , slot , blocks [ 1 ] ) ;
120+ const attestation = makeCheckpointAttestation ( {
121+ header : makeCheckpointHeader ( 1 , { slotNumber : slot } ) ,
122+ attesterSigner : attester ,
123+ } ) ;
124+ mockProposals ( slot , blocks , [ checkpoint ] ) ;
125+ p2pClient . getCheckpointAttestationsForSlot . mockResolvedValue ( [ attestation ] ) ;
126+
127+ await watcher . scanSlot ( slot ) ;
128+
129+ expect ( handler ) . toHaveBeenCalledWith ( [
130+ {
131+ validator : signer . address ,
132+ amount : 11n ,
133+ offenseType : OffenseType . BROADCASTED_INVALID_CHECKPOINT_PROPOSAL ,
134+ epochOrSlot : 10n ,
135+ } ,
136+ {
137+ validator : attester . address ,
138+ amount : config . slashAttestInvalidCheckpointProposalPenalty ,
139+ offenseType : OffenseType . ATTESTED_TO_INVALID_CHECKPOINT_PROPOSAL ,
140+ epochOrSlot : 10n ,
141+ } ,
142+ ] ) ;
143+ } ) ;
144+
145+ it ( 'slashes attesters when proposer checkpoint slashing is disabled' , async ( ) => {
146+ watcher . updateConfig ( { slashBroadcastedInvalidCheckpointProposalPenalty : 0n } ) ;
147+ const signer = Secp256k1Signer . random ( ) ;
148+ const attester = Secp256k1Signer . random ( ) ;
149+ const slot = SlotNumber ( 10 ) ;
150+ const blocks = await makeBlocks ( signer , slot , 4 ) ;
151+ const checkpoint = await makeCheckpointCore ( signer , slot , blocks [ 1 ] ) ;
152+ const attestation = makeCheckpointAttestation ( {
153+ header : makeCheckpointHeader ( 1 , { slotNumber : slot } ) ,
154+ attesterSigner : attester ,
155+ } ) ;
156+ mockProposals ( slot , blocks , [ checkpoint ] ) ;
157+ p2pClient . getCheckpointAttestationsForSlot . mockResolvedValue ( [ attestation ] ) ;
158+
159+ await watcher . scanSlot ( slot ) ;
160+
161+ expect ( handler ) . toHaveBeenCalledWith ( [
162+ {
163+ validator : attester . address ,
164+ amount : config . slashAttestInvalidCheckpointProposalPenalty ,
165+ offenseType : OffenseType . ATTESTED_TO_INVALID_CHECKPOINT_PROPOSAL ,
166+ epochOrSlot : 10n ,
167+ } ,
168+ ] ) ;
169+ } ) ;
170+
171+ it ( 'does not slash attesters when bad attestation slashing is disabled' , async ( ) => {
172+ watcher . updateConfig ( { slashAttestInvalidCheckpointProposalPenalty : 0n } ) ;
173+ const signer = Secp256k1Signer . random ( ) ;
174+ const attester = Secp256k1Signer . random ( ) ;
175+ const slot = SlotNumber ( 10 ) ;
176+ const blocks = await makeBlocks ( signer , slot , 4 ) ;
177+ const checkpoint = await makeCheckpointCore ( signer , slot , blocks [ 1 ] ) ;
178+ const attestation = makeCheckpointAttestation ( {
179+ header : makeCheckpointHeader ( 1 , { slotNumber : slot } ) ,
180+ attesterSigner : attester ,
181+ } ) ;
182+ mockProposals ( slot , blocks , [ checkpoint ] ) ;
183+ p2pClient . getCheckpointAttestationsForSlot . mockResolvedValue ( [ attestation ] ) ;
184+
185+ await watcher . scanSlot ( slot ) ;
186+
187+ expect ( handler ) . toHaveBeenCalledWith ( [
188+ {
189+ validator : signer . address ,
190+ amount : 11n ,
191+ offenseType : OffenseType . BROADCASTED_INVALID_CHECKPOINT_PROPOSAL ,
192+ epochOrSlot : 10n ,
193+ } ,
194+ ] ) ;
195+ } ) ;
196+
197+ it ( 'does not emit duplicate bad attestation offenses on repeated scans' , async ( ) => {
198+ watcher . updateConfig ( { slashBroadcastedInvalidCheckpointProposalPenalty : 0n } ) ;
199+ const signer = Secp256k1Signer . random ( ) ;
200+ const attester = Secp256k1Signer . random ( ) ;
201+ const slot = SlotNumber ( 10 ) ;
202+ const blocks = await makeBlocks ( signer , slot , 4 ) ;
203+ const checkpoint = await makeCheckpointCore ( signer , slot , blocks [ 1 ] ) ;
204+ const attestation = makeCheckpointAttestation ( {
205+ header : makeCheckpointHeader ( 1 , { slotNumber : slot } ) ,
206+ attesterSigner : attester ,
207+ } ) ;
208+ mockProposals ( slot , blocks , [ checkpoint ] ) ;
209+ p2pClient . getCheckpointAttestationsForSlot . mockResolvedValue ( [ attestation ] ) ;
210+
211+ await watcher . scanSlot ( slot ) ;
212+ await watcher . scanSlot ( slot ) ;
213+
214+ expect ( handler ) . toHaveBeenCalledTimes ( 1 ) ;
215+ } ) ;
216+
217+ it ( 'does not emit bad attestation offenses for equivocated checkpoint proposal slots' , async ( ) => {
218+ watcher . updateConfig ( { slashBroadcastedInvalidCheckpointProposalPenalty : 0n } ) ;
219+ const signer = Secp256k1Signer . random ( ) ;
220+ const attester = Secp256k1Signer . random ( ) ;
221+ const slot = SlotNumber ( 10 ) ;
222+ const blocks = await makeBlocks ( signer , slot , 4 ) ;
223+ const truncatedCheckpoint = await makeCheckpointCore ( signer , slot , blocks [ 1 ] ) ;
224+ const otherCheckpoint = await makeCheckpointCore ( signer , slot , blocks [ 3 ] ) ;
225+ const attestation = makeCheckpointAttestation ( {
226+ header : makeCheckpointHeader ( 1 , { slotNumber : slot } ) ,
227+ attesterSigner : attester ,
228+ } ) ;
229+ mockProposals ( slot , blocks , [ truncatedCheckpoint , otherCheckpoint ] ) ;
230+ p2pClient . getCheckpointAttestationsForSlot . mockResolvedValue ( [ attestation ] ) ;
231+
232+ await watcher . scanSlot ( slot ) ;
233+
234+ expect ( handler ) . not . toHaveBeenCalled ( ) ;
235+ } ) ;
236+
237+ it ( 'does not emit bad attestation offenses for equivocated block proposal slots' , async ( ) => {
238+ watcher . updateConfig ( { slashBroadcastedInvalidCheckpointProposalPenalty : 0n } ) ;
239+ const signer = Secp256k1Signer . random ( ) ;
240+ const attester = Secp256k1Signer . random ( ) ;
241+ const slot = SlotNumber ( 10 ) ;
242+ const blocks = await makeBlocks ( signer , slot , 4 ) ;
243+ const equivocatedBlock = await makeBlockProposal ( {
244+ signer,
245+ blockHeader : makeBlockHeader ( 99 , { slotNumber : slot } ) ,
246+ archiveRoot : Fr . random ( ) ,
247+ indexWithinCheckpoint : IndexWithinCheckpoint ( 2 ) ,
248+ } ) ;
249+ const checkpoint = await makeCheckpointCore ( signer , slot , blocks [ 1 ] ) ;
250+ const attestation = makeCheckpointAttestation ( {
251+ header : makeCheckpointHeader ( 1 , { slotNumber : slot } ) ,
252+ attesterSigner : attester ,
253+ } ) ;
254+ mockProposals ( slot , [ ...blocks , equivocatedBlock ] , [ checkpoint ] ) ;
255+ p2pClient . getCheckpointAttestationsForSlot . mockResolvedValue ( [ attestation ] ) ;
256+
257+ await watcher . scanSlot ( slot ) ;
258+
259+ expect ( handler ) . not . toHaveBeenCalled ( ) ;
260+ } ) ;
261+
111262 it ( 'slashes when a higher-index proposal arrives after an earlier non-slashing scan' , async ( ) => {
112263 const signer = Secp256k1Signer . random ( ) ;
113264 const slot = SlotNumber ( 10 ) ;
0 commit comments