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