@@ -955,6 +955,93 @@ describe('LibP2PService', () => {
955955 expect ( storedBlock ) . toBeDefined ( ) ;
956956 } ) ;
957957
958+ it ( 'skipCheckpointProposalValidation: attests before (not gated by) slow last-block processing' , async ( ) => {
959+ // Recreate the service in skip-validation mode and re-register the checkpoint/block callbacks on it.
960+ service = createTestLibP2PServiceWithPools (
961+ mockPeerManager ,
962+ reportMessageValidationResultSpy ,
963+ attestationPool ,
964+ mockTxPool ,
965+ mockEpochCache ,
966+ { skipCheckpointProposalValidation : true } ,
967+ ) ;
968+ service . registerBlockReceivedCallback ( blockReceivedCallback as any ) ;
969+ service . registerValidatorCheckpointReceivedCallback ( validatorCheckpointReceivedCallback as any ) ;
970+ service . registerAllNodesCheckpointReceivedCallback ( allNodesCheckpointReceivedCallback as any ) ;
971+
972+ const checkpointHeader = makeCheckpointHeader ( 1 , { slotNumber : targetSlot } ) ;
973+ const blockHeader = makeBlockHeader ( 1 , { slotNumber : targetSlot } ) ;
974+ const proposal = await makeCheckpointProposal ( { signer, checkpointHeader, lastBlock : { blockHeader } } ) ;
975+
976+ // Block processing hangs until released, simulating waiting for the parent block up to the
977+ // re-execution deadline. In skip mode the attestation must not be blocked behind it.
978+ let releaseBlock ! : ( ) => void ;
979+ blockReceivedCallback . mockReturnValue (
980+ new Promise < boolean > ( resolve => {
981+ releaseBlock = ( ) => resolve ( true ) ;
982+ } ) ,
983+ ) ;
984+
985+ // Resolves once the checkpoint attestation callback runs; if it were serialized behind the hung block
986+ // processing this would never resolve and the test would time out.
987+ let signalCheckpoint ! : ( ) => void ;
988+ const checkpointInvoked = new Promise < void > ( resolve => {
989+ signalCheckpoint = resolve ;
990+ } ) ;
991+ validatorCheckpointReceivedCallback . mockImplementation ( ( ) => {
992+ signalCheckpoint ( ) ;
993+ return Promise . resolve ( [ ] ) ;
994+ } ) ;
995+
996+ const handled = service . handleGossipedCheckpointProposal ( proposal . toBuffer ( ) , 'msg-1' , mockPeerId ) ;
997+
998+ await checkpointInvoked ;
999+ expect ( validatorCheckpointReceivedCallback ) . toHaveBeenCalledTimes ( 1 ) ;
1000+
1001+ releaseBlock ( ) ;
1002+ await handled ;
1003+ expect ( blockReceivedCallback ) . toHaveBeenCalledTimes ( 1 ) ;
1004+ } ) ;
1005+
1006+ it ( 'default: processes the last block before the checkpoint proposal' , async ( ) => {
1007+ const checkpointHeader = makeCheckpointHeader ( 1 , { slotNumber : targetSlot } ) ;
1008+ const blockHeader = makeBlockHeader ( 1 , { slotNumber : targetSlot } ) ;
1009+ const proposal = await makeCheckpointProposal ( { signer, checkpointHeader, lastBlock : { blockHeader } } ) ;
1010+
1011+ // Block processing hangs until released and signals when it starts; with validation enabled the
1012+ // checkpoint callback must wait for the block to finish.
1013+ let releaseBlock ! : ( ) => void ;
1014+ let signalBlockStarted ! : ( ) => void ;
1015+ const blockStarted = new Promise < void > ( resolve => {
1016+ signalBlockStarted = resolve ;
1017+ } ) ;
1018+ blockReceivedCallback . mockImplementation ( ( ) => {
1019+ signalBlockStarted ( ) ;
1020+ return new Promise < boolean > ( resolve => {
1021+ releaseBlock = ( ) => resolve ( true ) ;
1022+ } ) ;
1023+ } ) ;
1024+
1025+ let checkpointInvoked = false ;
1026+ validatorCheckpointReceivedCallback . mockImplementation ( ( ) => {
1027+ checkpointInvoked = true ;
1028+ return Promise . resolve ( [ ] ) ;
1029+ } ) ;
1030+
1031+ const handled = service . handleGossipedCheckpointProposal ( proposal . toBuffer ( ) , 'msg-1' , mockPeerId ) ;
1032+
1033+ // Wait until block processing is in flight (hung), then flush microtasks. The checkpoint callback
1034+ // must not have run, since it is gated behind the block.
1035+ await blockStarted ;
1036+ await new Promise ( resolve => setImmediate ( resolve ) ) ;
1037+ expect ( checkpointInvoked ) . toBe ( false ) ;
1038+
1039+ // Once the block completes, the checkpoint proposal is processed.
1040+ releaseBlock ( ) ;
1041+ await handled ;
1042+ expect ( checkpointInvoked ) . toBe ( true ) ;
1043+ } ) ;
1044+
9581045 it ( 'lastBlock processed even when checkpoint cap exceeded' , async ( ) => {
9591046 const checkpointHeader = makeCheckpointHeader ( 1 , { slotNumber : targetSlot } ) ;
9601047 const blockHeader = makeBlockHeader ( 1 , { slotNumber : targetSlot } ) ;
@@ -1525,6 +1612,7 @@ function createTestLibP2PServiceWithPools(
15251612 attestationPool : AttestationPool ,
15261613 mockTxPool : MockProxy < TxPoolV2 > ,
15271614 mockEpochCache : MockProxy < EpochCacheInterface > ,
1615+ configOverrides ?: Partial < P2PConfig > ,
15281616) : TestLibP2PService {
15291617 const mockNode = mock < PubSubLibp2p > ( ) ;
15301618 mockNode . services = {
@@ -1539,5 +1627,6 @@ function createTestLibP2PServiceWithPools(
15391627 attestationPool,
15401628 txPool : mockTxPool ,
15411629 epochCache : mockEpochCache ,
1630+ configOverrides,
15421631 } ) ;
15431632}
0 commit comments