@@ -516,7 +516,11 @@ func TestIntegration_SevenNodes(t *testing.T) {
516516 sharedNetwork := NewSharedNetwork [TestHash ](N )
517517
518518 nodes := make ([]* HotStuff2 [TestHash ], N )
519- committed := make ([][]Block [TestHash ], N )
519+ // Track committed blocks by height for each node to handle out-of-order commits
520+ committed := make ([]map [uint32 ]Block [TestHash ], N )
521+ for i := range N {
522+ committed [i ] = make (map [uint32 ]Block [TestHash ])
523+ }
520524 var commitMu sync.Mutex
521525
522526 for i := range N {
@@ -539,7 +543,7 @@ func TestIntegration_SevenNodes(t *testing.T) {
539543 idx := i
540544 onCommit := func (block Block [TestHash ]) {
541545 commitMu .Lock ()
542- committed [idx ] = append ( committed [ idx ], block )
546+ committed [idx ][ block . Height ()] = block
543547 commitMu .Unlock ()
544548 }
545549
@@ -591,6 +595,7 @@ done:
591595 commitMu .Lock ()
592596 defer commitMu .Unlock ()
593597
598+ // Find minimum committed count
594599 minCommitted := len (committed [0 ])
595600 for i := 1 ; i < N ; i ++ {
596601 if len (committed [i ]) < minCommitted {
@@ -601,13 +606,31 @@ done:
601606 t .Logf ("All %d nodes committed at least %d blocks" , N , minCommitted )
602607 assert .GreaterOrEqual (t , minCommitted , TARGET_BLOCKS )
603608
604- // Verify chain consistency
605- for i := 1 ; i < N ; i ++ {
606- for j := range minCommitted {
607- assert .True (t , committed [i ][j ].Hash ().Equals (committed [0 ][j ].Hash ()),
608- "node %d block %d should match node 0" , i , j )
609+ // Find common heights across all nodes
610+ commonHeights := make ([]uint32 , 0 )
611+ for height := range committed [0 ] {
612+ allHave := true
613+ for i := 1 ; i < N ; i ++ {
614+ if _ , ok := committed [i ][height ]; ! ok {
615+ allHave = false
616+ break
617+ }
618+ }
619+ if allHave {
620+ commonHeights = append (commonHeights , height )
609621 }
610622 }
623+
624+ // Verify chain consistency: all nodes must have the same block at each height
625+ for _ , height := range commonHeights {
626+ refBlock := committed [0 ][height ]
627+ for i := 1 ; i < N ; i ++ {
628+ assert .True (t , committed [i ][height ].Hash ().Equals (refBlock .Hash ()),
629+ "node %d block at height %d should match node 0" , i , height )
630+ }
631+ }
632+
633+ t .Logf ("Verified %d common heights across all nodes" , len (commonHeights ))
611634}
612635
613636// TestValidatorSet256 implements ValidatorSet for testing with TestHash256.
@@ -667,3 +690,81 @@ func (vs *TestValidatorSet256) GetLeader(view uint32) uint16 {
667690func (vs * TestValidatorSet256 ) F () int {
668691 return (vs .n - 1 ) / 3
669692}
693+
694+ // TestIntegration_SingleNode tests consensus with a single node (n=1).
695+ // This is a degenerate case where quorum=1, so the leader's own vote
696+ // should be sufficient to form a QC and make progress.
697+ func TestIntegration_SingleNode (t * testing.T ) {
698+ const N = 1
699+ const TARGET_BLOCKS = 5
700+
701+ validators , privKeys := NewTestValidatorSetWithKeys (N )
702+
703+ // Single node network (messages go nowhere, but that's fine for n=1)
704+ sharedNetwork := NewSharedNetwork [TestHash ](N )
705+
706+ storage := NewTestStorage ()
707+ executor := NewTestExecutor ()
708+ mockTimer := timer .NewMockTimer ()
709+
710+ cfg := & Config [TestHash ]{
711+ Logger : zap .NewNop (),
712+ Timer : mockTimer ,
713+ Validators : validators ,
714+ MyIndex : 0 ,
715+ PrivateKey : privKeys [0 ],
716+ CryptoScheme : "ed25519" ,
717+ Storage : storage ,
718+ Network : sharedNetwork .NodeNetwork (0 ),
719+ Executor : executor ,
720+ }
721+
722+ var committed []Block [TestHash ]
723+ var commitMu sync.Mutex
724+
725+ onCommit := func (block Block [TestHash ]) {
726+ commitMu .Lock ()
727+ committed = append (committed , block )
728+ commitMu .Unlock ()
729+ }
730+
731+ node , err := NewHotStuff2 (cfg , onCommit )
732+ require .NoError (t , err )
733+
734+ require .NoError (t , node .Start ())
735+
736+ // Wait for blocks to be committed (timeout after 5 seconds)
737+ timeout := time .After (5 * time .Second )
738+ ticker := time .NewTicker (50 * time .Millisecond )
739+ defer ticker .Stop ()
740+
741+ for {
742+ select {
743+ case <- timeout :
744+ commitMu .Lock ()
745+ count := len (committed )
746+ commitMu .Unlock ()
747+ t .Fatalf ("timeout waiting for blocks to commit, only got %d blocks" , count )
748+
749+ case <- ticker .C :
750+ commitMu .Lock ()
751+ count := len (committed )
752+ commitMu .Unlock ()
753+
754+ if count >= TARGET_BLOCKS {
755+ goto done
756+ }
757+ }
758+ }
759+
760+ done:
761+ node .Stop ()
762+ time .Sleep (50 * time .Millisecond )
763+ sharedNetwork .Close ()
764+
765+ commitMu .Lock ()
766+ defer commitMu .Unlock ()
767+
768+ t .Logf ("Single node committed %d blocks" , len (committed ))
769+ assert .GreaterOrEqual (t , len (committed ), TARGET_BLOCKS , "single node should commit at least %d blocks" , TARGET_BLOCKS )
770+ }
0 commit comments