@@ -21,6 +21,7 @@ import (
2121 "math"
2222 "math/big"
2323 "math/rand"
24+ "sync"
2425 "testing"
2526 "time"
2627
@@ -36,6 +37,7 @@ import (
3637 "github.com/XinFinOrg/XDPoSChain/eth/ethconfig"
3738 "github.com/XinFinOrg/XDPoSChain/event"
3839 "github.com/XinFinOrg/XDPoSChain/p2p"
40+ "github.com/XinFinOrg/XDPoSChain/p2p/enode"
3941 "github.com/XinFinOrg/XDPoSChain/params"
4042)
4143
@@ -765,3 +767,138 @@ func daoChallengeChainConfig(daoForkSupport bool) *params.ChainConfig {
765767
766768 return config
767769}
770+
771+ // waitForPeerRegistration blocks until the peer with the given id has been
772+ // registered by the protocol manager's handle goroutine.
773+ func waitForPeerRegistration (t * testing.T , pm * ProtocolManager , id string ) {
774+ t .Helper ()
775+ deadline := time .After (2 * time .Second )
776+ for pm .peers .Peer (id ) == nil {
777+ select {
778+ case <- deadline :
779+ t .Fatalf ("test peer %s was not registered in time" , id )
780+ case <- time .After (10 * time .Millisecond ):
781+ }
782+ }
783+ }
784+
785+ // TestProtocolManagerRemovePeerIdempotent verifies that removePeer is a no-op
786+ // once the peer is gone, so duplicate removals (BFT broadcast loops, DAO fork
787+ // timers and normal teardown) cannot re-run the unregister sequence.
788+ func TestProtocolManagerRemovePeerIdempotent (t * testing.T ) {
789+ pm , _ := newTestProtocolManagerMust (t , downloader .FullSync , 0 , nil , nil )
790+ defer pm .Stop ()
791+
792+ // Register a peer through the normal protocol handshake path.
793+ tp , errc := newTestPeer ("test-peer" , xdc165 , pm , true )
794+ defer tp .close ()
795+ defer tp .app .Close ()
796+ defer func () {
797+ select {
798+ case <- errc :
799+ default :
800+ }
801+ }()
802+ waitForPeerRegistration (t , pm , tp .id )
803+
804+ if pm .peers .Len () != 1 {
805+ t .Fatalf ("peer set size mismatch: got %d want 1" , pm .peers .Len ())
806+ }
807+ // The first removal performs the full unregister sequence.
808+ pm .removePeer (tp .id )
809+ if pm .peers .Peer (tp .id ) != nil {
810+ t .Fatal ("peer still registered after first removePeer" )
811+ }
812+ if pm .peers .Len () != 0 {
813+ t .Fatalf ("peer set size mismatch after removal: got %d want 0" , pm .peers .Len ())
814+ }
815+ // A duplicate removal must be a silent no-op. Note this second call is
816+ // already short-circuited by the peer == nil lookup above (the first
817+ // removal took the peer out of the set), so it does NOT exercise
818+ // markRemoved's atomic branch. That branch is covered by
819+ // TestPeerMarkRemovedOnce and TestProtocolManagerRemovePeerConcurrent.
820+ pm .removePeer (tp .id )
821+ if pm .peers .Len () != 0 {
822+ t .Fatalf ("peer set size mismatch after second removePeer: got %d want 0" , pm .peers .Len ())
823+ }
824+ }
825+
826+ // TestProtocolManagerRemovePeerConcurrent verifies that concurrent removePeer
827+ // calls for the same peer (BFT broadcast loops, DAO fork timers and normal
828+ // teardown racing each other) remove the peer exactly once, without panicking
829+ // or racing on the removal flag.
830+ func TestProtocolManagerRemovePeerConcurrent (t * testing.T ) {
831+ pm , _ := newTestProtocolManagerMust (t , downloader .FullSync , 0 , nil , nil )
832+ defer pm .Stop ()
833+
834+ // Register a peer through the normal protocol handshake path.
835+ tp , errc := newTestPeer ("test-peer" , xdc165 , pm , true )
836+ defer tp .close ()
837+ defer tp .app .Close ()
838+ defer func () {
839+ select {
840+ case <- errc :
841+ default :
842+ }
843+ }()
844+ waitForPeerRegistration (t , pm , tp .id )
845+
846+ var wg sync.WaitGroup
847+ for i := 0 ; i < 16 ; i ++ {
848+ wg .Add (1 )
849+ go func () {
850+ defer wg .Done ()
851+ pm .removePeer (tp .id )
852+ }()
853+ }
854+ wg .Wait ()
855+
856+ if pm .peers .Peer (tp .id ) != nil {
857+ t .Fatal ("peer still registered after concurrent removePeer calls" )
858+ }
859+ if pm .peers .Len () != 0 {
860+ t .Fatalf ("peer set size mismatch after concurrent removal: got %d want 0" , pm .peers .Len ())
861+ }
862+ }
863+
864+ // TestRegisterDownloaderPeerUndoesRacedRemoval reproduces the window in handle()
865+ // between pm.peers.Register and the downloader registration, where a BFT
866+ // broadcaster (BroadcastVote / BroadcastTimeout / BroadcastSyncInfo) can remove
867+ // the peer. removePeer claims the removal, but its downloader.UnregisterPeer
868+ // hits errNotRegistered (the peer is not yet registered in the downloader), so
869+ // without the recheck in registerDownloaderPeer the downloader would keep a
870+ // stale entry that blocks a reconnect of the same node id (errAlreadyRegistered).
871+ func TestRegisterDownloaderPeerUndoesRacedRemoval (t * testing.T ) {
872+ pm , _ := newTestProtocolManagerMust (t , downloader .FullSync , 0 , nil , nil )
873+ defer pm .Stop ()
874+
875+ // Build a real peer and register it in pm.peers only — the same state a
876+ // peer is in while handle() is between pm.peers.Register and the downloader
877+ // registration.
878+ app , net := p2p .MsgPipe ()
879+ defer app .Close ()
880+ var id enode.ID
881+ rand .Read (id [:])
882+ p := pm .newPeer (xdc165 , p2p .NewPeer (id , "race-peer" , nil ), net , pm .txpool .Get )
883+ if err := pm .peers .Register (p ); err != nil {
884+ t .Fatalf ("failed to register test peer: %v" , err )
885+ }
886+ if pm .peers .Len () != 1 {
887+ t .Fatalf ("peer set size mismatch: got %d want 1" , pm .peers .Len ())
888+ }
889+ // A BFT broadcaster's failing send removes the peer inside the window.
890+ pm .removePeer (p .id )
891+ if pm .peers .Peer (p .id ) != nil {
892+ t .Fatal ("peer still present after removePeer" )
893+ }
894+ // handle() now completes the downloader registration; the recheck must
895+ // detect the claimed removal, undo the registration and abort the handshake
896+ // with the same disconnect reason removePeer uses.
897+ if err := pm .registerDownloaderPeer (p ); err != p2p .DiscUselessPeer {
898+ t .Fatalf ("registerDownloaderPeer should abort with DiscUselessPeer a handshake whose removal was already claimed, got: %v" , err )
899+ }
900+ // A reconnect of the same node id must not be blocked by a stale entry.
901+ if err := pm .downloader .RegisterPeer (p .id , p .version , p ); err != nil {
902+ t .Fatalf ("reconnect blocked by stale downloader entry: %v" , err )
903+ }
904+ }
0 commit comments