@@ -33,6 +33,7 @@ import type { L2BlockBuiltStats } from '@aztec/stdlib/stats';
3333import { type FailedTx , Tx } from '@aztec/stdlib/tx' ;
3434import { AttestationTimeoutError } from '@aztec/stdlib/validators' ;
3535import type { ValidatorClient } from '@aztec/validator-client' ;
36+ import { DutyAlreadySignedError , SlashingProtectionError } from '@aztec/validator-ha-signer/errors' ;
3637
3738import type { GlobalVariableBuilder } from '../global_variable_builder/global_builder.js' ;
3839import type { InvalidateBlockRequest , SequencerPublisher } from '../publisher/sequencer-publisher.js' ;
@@ -207,21 +208,62 @@ export class CheckpointProposalJob {
207208 }
208209
209210 // TODO(palla/mbps): Wire this to the new p2p API once available, including the pendingBroadcast.block
210- const proposal = await this . validatorClient . createCheckpointProposal (
211- checkpoint . header ,
212- checkpoint . archive . root ,
213- pendingBroadcast ?. txs ?? [ ] ,
214- this . proposer ,
215- blockProposalOptions ,
216- ) ;
211+ let proposal : BlockProposal ;
212+ try {
213+ proposal = await this . validatorClient . createCheckpointProposal (
214+ checkpoint . header ,
215+ checkpoint . archive . root ,
216+ pendingBroadcast ?. txs ?? [ ] ,
217+ this . proposer ,
218+ blockProposalOptions ,
219+ ) ;
220+ } catch ( err ) {
221+ if ( err instanceof DutyAlreadySignedError ) {
222+ this . log . info ( `Checkpoint proposal for slot ${ this . slot } already signed by another HA node, yielding` , {
223+ slot : this . slot ,
224+ signedByNode : err . signedByNode ,
225+ } ) ;
226+ return undefined ;
227+ }
228+ if ( err instanceof SlashingProtectionError ) {
229+ this . log . warn ( `Checkpoint proposal for slot ${ this . slot } blocked by slashing protection` , {
230+ slot : this . slot ,
231+ existingSigningRoot : err . existingSigningRoot ,
232+ attemptedSigningRoot : err . attemptedSigningRoot ,
233+ } ) ;
234+ return undefined ;
235+ }
236+ throw err ;
237+ }
238+
217239 await this . p2pClient . broadcastProposal ( proposal ) ;
218240
219241 this . setStateFn ( SequencerState . COLLECTING_ATTESTATIONS , this . slot ) ;
220242 const attestations = await this . waitForAttestations ( proposal ) ;
221243
222244 // Proposer must sign over the attestations before pushing them to L1
223245 const signer = this . proposer ?? this . publisher . getSenderAddress ( ) ;
224- const attestationsSignature = await this . validatorClient . signAttestationsAndSigners ( attestations , signer ) ;
246+ let attestationsSignature : Signature ;
247+ try {
248+ attestationsSignature = await this . validatorClient . signAttestationsAndSigners ( attestations , signer ) ;
249+ } catch ( err ) {
250+ if ( err instanceof DutyAlreadySignedError ) {
251+ this . log . info ( `Attestations signature for slot ${ this . slot } already signed by another HA node, yielding` , {
252+ slot : this . slot ,
253+ signedByNode : err . signedByNode ,
254+ } ) ;
255+ return undefined ;
256+ }
257+ if ( err instanceof SlashingProtectionError ) {
258+ this . log . warn ( `Attestations signature for slot ${ this . slot } blocked by slashing protection` , {
259+ slot : this . slot ,
260+ existingSigningRoot : err . existingSigningRoot ,
261+ attemptedSigningRoot : err . attemptedSigningRoot ,
262+ } ) ;
263+ return undefined ;
264+ }
265+ throw err ;
266+ }
225267
226268 // Enqueue publishing the checkpoint to L1
227269 this . setStateFn ( SequencerState . PUBLISHING_CHECKPOINT , this . slot ) ;
@@ -331,15 +373,38 @@ export class CheckpointProposalJob {
331373 // If the block is the last one, we'll broadcast it along with the checkpoint at the end of the loop
332374 if ( ! this . config . fishermanMode ) {
333375 // TODO(palla/mbps): Wire this to the new p2p API once available
334- const proposal = await this . validatorClient . createBlockProposal (
335- block . header . globalVariables . blockNumber ,
336- ( await checkpointBuilder . getCheckpoint ( ) ) . header ,
337- block . archive . root ,
338- usedTxs ,
339- this . proposer ,
340- blockProposalOptions ,
341- ) ;
342- await this . p2pClient . broadcastProposal ( proposal ) ;
376+ try {
377+ const proposal = await this . validatorClient . createBlockProposal (
378+ block . header . globalVariables . blockNumber ,
379+ ( await checkpointBuilder . getCheckpoint ( ) ) . header ,
380+ block . archive . root ,
381+ usedTxs ,
382+ this . proposer ,
383+ blockProposalOptions ,
384+ ) ;
385+ await this . p2pClient . broadcastProposal ( proposal ) ;
386+ } catch ( err ) {
387+ if ( err instanceof DutyAlreadySignedError ) {
388+ this . log . info ( `Block proposal for slot ${ this . slot } already signed by another HA node, yielding` , {
389+ slot : this . slot ,
390+ blockNumber,
391+ signedByNode : err . signedByNode ,
392+ } ) ;
393+ // Another HA node is handling this slot, stop building
394+ break ;
395+ }
396+ if ( err instanceof SlashingProtectionError ) {
397+ this . log . warn ( `Block proposal for slot ${ this . slot } blocked by slashing protection` , {
398+ slot : this . slot ,
399+ blockNumber,
400+ existingSigningRoot : err . existingSigningRoot ,
401+ attemptedSigningRoot : err . attemptedSigningRoot ,
402+ } ) ;
403+ // Stop building to avoid further slashing issues
404+ break ;
405+ }
406+ throw err ;
407+ }
343408 }
344409
345410 // Wait until the next block's start time
0 commit comments