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