@@ -564,6 +564,52 @@ func TestOrdererDeliverBFT(t *testing.T) {
564564 }
565565}
566566
567+ // TestOrdererDeliverBFTRepeatedPromotionProgresses is an end-to-end guard for the
568+ // "cascading suspicion can halt block delivery" bug (#632). With real gRPC streams, header-only
569+ // streams continuously race ahead of the data stream, so a freshly promoted data source typically
570+ // already has header-only blocks queued. Before the fix, such a stale header-only block from the
571+ // promoted source was processed as a (payload-less) data block, immediately re-suspecting it and
572+ // promoting yet another source — a cascade that stalls delivery.
573+ //
574+ // Here we force the current data source to withhold the next block every round (a short grace
575+ // period promotes a new source quickly). Delivery must keep progressing across repeated
576+ // promotions rather than stalling.
577+ func TestOrdererDeliverBFTRepeatedPromotionProgresses (t * testing.T ) {
578+ t .Parallel ()
579+ e := newDeliverOrdererTestEnv (t , deliverOrdererTestEnvParams {
580+ ftMode : ordererdial .BFT ,
581+ tlsMode : connection .NoneTLSMode ,
582+ })
583+ // A short grace period makes suspicion + promotion happen quickly.
584+ e .deliveryParams .SuspicionGracePeriodPerBlock = 100 * time .Millisecond
585+ stopDelivery := e .startDelivery (t )
586+ defer stopDelivery ()
587+ e .waitForDeliveryOfConfigBlock (t )
588+ require .Len (t , e .PartyStates , 3 )
589+
590+ //nolint:gosec // party IDs are small, non-negative values.
591+ prev := uint32 (test .GetIntMetricValue (t , e .deliveryParams .Metrics .CurrentDataSourceID ))
592+ for range 4 {
593+ expectedBlockNum := e .PrevBlock .Header .Number + 1
594+ // Withhold the next full data block only from the current source, forcing a promotion.
595+ for _ , p := range e .PartyStates {
596+ p .HoldFromBlock .Store (0 )
597+ }
598+ e .PartyStates [prev ].HoldFromBlock .Store (expectedBlockNum )
599+
600+ b := e .submit (t )
601+ require .NotEqual (t , prev , b .SourceID , "delivery must rotate away from the withholding source" )
602+ prev = b .SourceID
603+ }
604+
605+ // No data-stream restart should have been needed to clear a stale header-only block.
606+ // (Restarts here would be due to genuine suspicion/promotion, never a malformed data block.)
607+ malformedFromHeaders := test .GetIntMetricValue (t ,
608+ e .deliveryParams .Metrics .StreamErrorsTotal .WithLabelValues ("data" , strconv .FormatUint (uint64 (prev ), 10 ),
609+ "malformed_block" ))
610+ require .Zero (t , malformedFromHeaders , "a promoted source's queued header-only block must not be seen as malformed" )
611+ }
612+
567613type deliverOrdererTestEnvParams struct {
568614 tlsMode string
569615 ftMode string
0 commit comments