@@ -117,6 +117,7 @@ describe('ValidatorClient', () => {
117117 p2pClient . getCheckpointAttestationsForSlot . mockImplementation ( ( ) => Promise . resolve ( [ ] ) ) ;
118118 p2pClient . handleAuthRequestFromPeer . mockResolvedValue ( StatusMessage . random ( ) ) ;
119119 p2pClient . broadcastCheckpointAttestations . mockResolvedValue ( ) ;
120+ p2pClient . getProposalsForSlot . mockResolvedValue ( { blockProposals : [ ] , checkpointProposals : [ ] } ) ;
120121 checkpointsBuilder = mock < FullNodeCheckpointsBuilder > ( ) ;
121122 checkpointsBuilder . getConfig . mockReturnValue ( {
122123 l1GenesisTime : 1n ,
@@ -520,6 +521,121 @@ describe('ValidatorClient', () => {
520521 expect ( isValid ) . toBe ( true ) ;
521522 } ) ;
522523
524+ it ( 'does not push a block proposal beyond a retained checkpoint terminal block to the archiver' , async ( ) => {
525+ validatorClient . updateConfig ( { skipPushProposedBlocksToArchiver : false } ) ;
526+ validatorClient . getProposalHandler ( ) . register ( p2pClient , true ) ;
527+
528+ const signer = Secp256k1Signer . random ( ) ;
529+ const emptyInHash = computeInHashFromL1ToL2Messages ( [ ] ) ;
530+ const checkpointProposal = await makeCheckpointProposal ( {
531+ signer,
532+ checkpointHeader : makeCheckpointHeader ( 1 , { slotNumber : proposal . slotNumber , inHash : emptyInHash } ) ,
533+ archiveRoot : Fr . random ( ) ,
534+ lastBlock : {
535+ blockHeader : makeBlockHeader ( 1 , { blockNumber, slotNumber : proposal . slotNumber } ) ,
536+ indexWithinCheckpoint : IndexWithinCheckpoint ( 0 ) ,
537+ txHashes : proposal . txHashes ,
538+ } ,
539+ } ) ;
540+ const terminalBlock = checkpointProposal . getBlockProposal ( ) ! ;
541+
542+ const terminalGlobals = terminalBlock . blockHeader . globalVariables ;
543+ const laterBlockHeader = makeBlockHeader ( 2 , {
544+ lastArchive : new AppendOnlyTreeSnapshot ( terminalBlock . archive , terminalBlock . blockNumber ) ,
545+ blockNumber : BlockNumber ( terminalBlock . blockNumber + 1 ) ,
546+ slotNumber : proposal . slotNumber ,
547+ chainId : terminalGlobals . chainId ,
548+ version : terminalGlobals . version ,
549+ timestamp : terminalGlobals . timestamp ,
550+ coinbase : terminalGlobals . coinbase ,
551+ feeRecipient : terminalGlobals . feeRecipient ,
552+ gasFees : terminalGlobals . gasFees ,
553+ } ) ;
554+ const laterBlock = await makeBlockProposal ( {
555+ signer,
556+ blockHeader : laterBlockHeader ,
557+ indexWithinCheckpoint : IndexWithinCheckpoint ( 1 ) ,
558+ inHash : emptyInHash ,
559+ archiveRoot : Fr . random ( ) ,
560+ } ) ;
561+
562+ epochCache . getProposerAttesterAddressInSlot . mockResolvedValue ( signer . address ) ;
563+ p2pClient . getProposalsForSlot . mockResolvedValue ( {
564+ blockProposals : [ terminalBlock , laterBlock ] ,
565+ checkpointProposals : [ checkpointProposal . toCore ( ) ] ,
566+ } ) ;
567+
568+ const terminalBlockData = {
569+ header : terminalBlock . blockHeader ,
570+ archive : new AppendOnlyTreeSnapshot ( terminalBlock . archive , terminalBlock . blockNumber ) ,
571+ blockHash : BlockHash . random ( ) ,
572+ checkpointNumber : CheckpointNumber ( 1 ) ,
573+ indexWithinCheckpoint : terminalBlock . indexWithinCheckpoint ,
574+ } as unknown as BlockData ;
575+ blockSource . getBlockData . mockImplementation ( query =>
576+ Promise . resolve ( 'number' in query ? undefined : terminalBlockData ) ,
577+ ) ;
578+
579+ const blockAddedIfProcessed = {
580+ ...blockBuildResult . block ,
581+ header : laterBlock . blockHeader ,
582+ body : { txEffects : times ( laterBlock . txHashes . length , ( ) => TxEffect . empty ( ) ) } ,
583+ archive : new AppendOnlyTreeSnapshot ( laterBlock . archive , laterBlock . blockNumber ) ,
584+ checkpointNumber : CheckpointNumber ( 1 ) ,
585+ indexWithinCheckpoint : laterBlock . indexWithinCheckpoint ,
586+ } as unknown as L2Block ;
587+ mockCheckpointBuilder . buildBlock . mockResolvedValue ( {
588+ ...blockBuildResult ,
589+ block : blockAddedIfProcessed ,
590+ numTxs : laterBlock . txHashes . length ,
591+ } ) ;
592+ worldState . fork . mockResolvedValue ( {
593+ close : ( ) => Promise . resolve ( ) ,
594+ [ Symbol . asyncDispose ] : ( ) => Promise . resolve ( ) ,
595+ getTreeInfo : ( ) => Promise . resolve ( { root : laterBlock . blockHeader . lastArchive . root . toBuffer ( ) } ) ,
596+ } as never ) ;
597+
598+ const result = await validatorClient . getProposalHandler ( ) . handleBlockProposal ( laterBlock , sender , true ) ;
599+
600+ expect ( result ) . toMatchObject ( { isValid : false , reason : 'block_proposal_beyond_checkpoint' } ) ;
601+ expect ( blockSource . addBlock ) . not . toHaveBeenCalled ( ) ;
602+ } ) ;
603+
604+ it ( 'does not push a block proposal to the archiver when retained checkpoint proposals equivocate' , async ( ) => {
605+ validatorClient . updateConfig ( { skipPushProposedBlocksToArchiver : false } ) ;
606+ validatorClient . getProposalHandler ( ) . register ( p2pClient , true ) ;
607+
608+ const emptyInHash = computeInHashFromL1ToL2Messages ( [ ] ) ;
609+ const checkpointProposal = await makeCheckpointProposal ( {
610+ checkpointHeader : makeCheckpointHeader ( 1 , { slotNumber : proposal . slotNumber , inHash : emptyInHash } ) ,
611+ archiveRoot : Fr . random ( ) ,
612+ lastBlock : {
613+ blockHeader : makeBlockHeader ( 1 , { blockNumber, slotNumber : proposal . slotNumber } ) ,
614+ indexWithinCheckpoint : IndexWithinCheckpoint ( 0 ) ,
615+ txHashes : proposal . txHashes ,
616+ } ,
617+ } ) ;
618+ const equivocatedCheckpointProposal = await makeCheckpointProposal ( {
619+ checkpointHeader : makeCheckpointHeader ( 1 , { slotNumber : proposal . slotNumber , inHash : emptyInHash } ) ,
620+ archiveRoot : Fr . random ( ) ,
621+ lastBlock : {
622+ blockHeader : makeBlockHeader ( 1 , { blockNumber, slotNumber : proposal . slotNumber } ) ,
623+ indexWithinCheckpoint : IndexWithinCheckpoint ( 0 ) ,
624+ txHashes : proposal . txHashes ,
625+ } ,
626+ } ) ;
627+
628+ p2pClient . getProposalsForSlot . mockResolvedValue ( {
629+ blockProposals : [ proposal ] ,
630+ checkpointProposals : [ checkpointProposal . toCore ( ) , equivocatedCheckpointProposal . toCore ( ) ] ,
631+ } ) ;
632+
633+ const result = await validatorClient . getProposalHandler ( ) . handleBlockProposal ( proposal , sender , true ) ;
634+
635+ expect ( result ) . toMatchObject ( { isValid : false , reason : 'checkpoint_proposal_equivocation' } ) ;
636+ expect ( blockSource . addBlock ) . not . toHaveBeenCalled ( ) ;
637+ } ) ;
638+
523639 it ( 'uses the next wall-clock slot as the tx collection deadline for pipelined proposals' , async ( ) => {
524640 const pipelineOffsetInSlots = 1 ;
525641 epochCache . isProposerPipeliningEnabled . mockReturnValue ( true ) ;
0 commit comments