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