github.com/onflow/flow-go@v0.33.17/network/p2p/scoring/app_score_test.go (about) 1 package scoring_test 2 3 import ( 4 "context" 5 "testing" 6 "time" 7 8 "github.com/libp2p/go-libp2p/core/peer" 9 mocktestify "github.com/stretchr/testify/mock" 10 "github.com/stretchr/testify/require" 11 12 "github.com/onflow/flow-go/config" 13 "github.com/onflow/flow-go/model/flow" 14 "github.com/onflow/flow-go/module/id" 15 "github.com/onflow/flow-go/module/irrecoverable" 16 "github.com/onflow/flow-go/module/mock" 17 "github.com/onflow/flow-go/network/channels" 18 "github.com/onflow/flow-go/network/internal/p2pfixtures" 19 "github.com/onflow/flow-go/network/message" 20 "github.com/onflow/flow-go/network/p2p" 21 p2pconfig "github.com/onflow/flow-go/network/p2p/config" 22 p2ptest "github.com/onflow/flow-go/network/p2p/test" 23 flowpubsub "github.com/onflow/flow-go/network/validator/pubsub" 24 "github.com/onflow/flow-go/utils/unittest" 25 ) 26 27 // TestFullGossipSubConnectivity tests that when the entire network is running by honest nodes, 28 // pushing access nodes to the edges of the network (i.e., the access nodes are not in the mesh of any honest nodes) 29 // will not cause the network to partition, i.e., all honest nodes can still communicate with each other through GossipSub. 30 func TestFullGossipSubConnectivity(t *testing.T) { 31 ctx, cancel := context.WithCancel(context.Background()) 32 signalerCtx := irrecoverable.NewMockSignalerContext(t, ctx) 33 sporkId := unittest.IdentifierFixture() 34 idProvider := mock.NewIdentityProvider(t) 35 36 // two groups of non-access nodes and one group of access nodes. 37 groupOneNodes, groupOneIds := p2ptest.NodesFixture(t, sporkId, t.Name(), 5, 38 idProvider, 39 p2ptest.WithRole(flow.RoleConsensus)) 40 groupTwoNodes, groupTwoIds := p2ptest.NodesFixture(t, sporkId, t.Name(), 5, 41 idProvider, 42 p2ptest.WithRole(flow.RoleCollection)) 43 accessNodeGroup, accessNodeIds := p2ptest.NodesFixture(t, sporkId, t.Name(), 5, 44 idProvider, 45 p2ptest.WithRole(flow.RoleAccess)) 46 47 ids := append(append(groupOneIds, groupTwoIds...), accessNodeIds...) 48 nodes := append(append(groupOneNodes, groupTwoNodes...), accessNodeGroup...) 49 50 provider := id.NewFixedIdentityProvider(ids) 51 idProvider.On("ByPeerID", mocktestify.Anything).Return( 52 func(peerId peer.ID) *flow.Identity { 53 identity, _ := provider.ByPeerID(peerId) 54 return identity 55 }, func(peerId peer.ID) bool { 56 _, ok := provider.ByPeerID(peerId) 57 return ok 58 }) 59 p2ptest.StartNodes(t, signalerCtx, nodes) 60 defer p2ptest.StopNodes(t, nodes, cancel) 61 62 blockTopic := channels.TopicFromChannel(channels.PushBlocks, sporkId) 63 64 logger := unittest.Logger() 65 66 // all nodes subscribe to block topic (common topic among all roles) 67 // group one 68 groupOneSubs := make([]p2p.Subscription, len(groupOneNodes)) 69 var err error 70 for i, node := range groupOneNodes { 71 groupOneSubs[i], err = node.Subscribe(blockTopic, flowpubsub.TopicValidator(logger, unittest.AllowAllPeerFilter())) 72 require.NoError(t, err) 73 } 74 // group two 75 groupTwoSubs := make([]p2p.Subscription, len(groupTwoNodes)) 76 for i, node := range groupTwoNodes { 77 groupTwoSubs[i], err = node.Subscribe(blockTopic, flowpubsub.TopicValidator(logger, unittest.AllowAllPeerFilter())) 78 require.NoError(t, err) 79 } 80 // access node group 81 accessNodeSubs := make([]p2p.Subscription, len(accessNodeGroup)) 82 for i, node := range accessNodeGroup { 83 accessNodeSubs[i], err = node.Subscribe(blockTopic, flowpubsub.TopicValidator(logger, unittest.AllowAllPeerFilter())) 84 require.NoError(t, err) 85 } 86 87 // creates a topology as follows: 88 // groupOneNodes <--> accessNodeGroup <--> groupTwoNodes 89 p2ptest.LetNodesDiscoverEachOther(t, ctx, append(groupOneNodes, accessNodeGroup...), append(groupOneIds, accessNodeIds...)) 90 p2ptest.LetNodesDiscoverEachOther(t, ctx, append(groupTwoNodes, accessNodeGroup...), append(groupTwoIds, accessNodeIds...)) 91 92 // checks end-to-end message delivery works 93 // each node sends a distinct message to all and checks that all nodes receive it. 94 for _, node := range nodes { 95 outgoingMessageScope, err := message.NewOutgoingScope( 96 ids.NodeIDs(), 97 channels.TopicFromChannel(channels.PushBlocks, sporkId), 98 unittest.ProposalFixture(), 99 unittest.NetworkCodec().Encode, 100 message.ProtocolTypePubSub) 101 require.NoError(t, err) 102 require.NoError(t, node.Publish(ctx, outgoingMessageScope)) 103 104 // checks that the message is received by all nodes. 105 ctx1s, cancel1s := context.WithTimeout(ctx, 5*time.Second) 106 expectedReceivedData, err := outgoingMessageScope.Proto().Marshal() 107 require.NoError(t, err) 108 p2pfixtures.SubsMustReceiveMessage(t, ctx1s, expectedReceivedData, groupOneSubs) 109 p2pfixtures.SubsMustReceiveMessage(t, ctx1s, expectedReceivedData, accessNodeSubs) 110 p2pfixtures.SubsMustReceiveMessage(t, ctx1s, expectedReceivedData, groupTwoSubs) 111 112 cancel1s() 113 } 114 } 115 116 // TestFullGossipSubConnectivityAmongHonestNodesWithMaliciousMajority tests pushing access nodes to the edges of the network. 117 // This test proves that if access nodes are PUSHED to the edge of the network, even their malicious majority cannot partition 118 // the network of honest nodes. 119 // The scenario tests that whether two honest nodes are in each others topic mesh on GossipSub 120 // when the network topology is a complete graph (i.e., full topology) and a malicious majority of access nodes are present. 121 // The honest nodes (i.e., non-Access nodes) are enabled with peer scoring, then the honest nodes are enabled with peer scoring. 122 func TestFullGossipSubConnectivityAmongHonestNodesWithMaliciousMajority(t *testing.T) { 123 // Note: if this test is ever flaky, this means a bug in our scoring system. Please escalate to the team instead of skipping. 124 ctx, cancel := context.WithCancel(context.Background()) 125 signalerCtx := irrecoverable.NewMockSignalerContext(t, ctx) 126 sporkId := unittest.IdentifierFixture() 127 128 idProvider := mock.NewIdentityProvider(t) 129 defaultConfig, err := config.DefaultConfig() 130 require.NoError(t, err) 131 // override the default config to make the mesh tracer log more frequently 132 defaultConfig.NetworkConfig.GossipSub.RpcTracer.LocalMeshLogInterval = time.Second 133 134 con1Node, con1Id := p2ptest.NodeFixture(t, sporkId, t.Name(), idProvider, p2ptest.WithRole(flow.RoleConsensus), p2ptest.OverrideFlowConfig(defaultConfig)) 135 con2Node, con2Id := p2ptest.NodeFixture(t, sporkId, t.Name(), idProvider, p2ptest.WithRole(flow.RoleConsensus), p2ptest.OverrideFlowConfig(defaultConfig)) 136 137 // create > 2 * 12 malicious access nodes 138 // 12 is the maximum size of default GossipSub mesh. 139 // We want to make sure that it is unlikely for honest nodes to be in the same mesh without peer scoring. 140 accessNodeGroup, accessNodeIds := p2ptest.NodesFixture(t, sporkId, t.Name(), 30, 141 idProvider, 142 p2ptest.WithRole(flow.RoleAccess), 143 // overrides the default peer scoring parameters to mute GossipSub traffic from/to honest nodes. 144 p2ptest.EnablePeerScoringWithOverride(&p2p.PeerScoringConfigOverride{ 145 AppSpecificScoreParams: maliciousAppSpecificScore(flow.IdentityList{&con1Id, &con2Id}, defaultConfig.NetworkConfig.GossipSub.ScoringParameters.PeerScoring.Protocol), 146 }), 147 ) 148 149 allNodes := append([]p2p.LibP2PNode{con1Node, con2Node}, accessNodeGroup...) 150 allIds := append(flow.IdentityList{&con1Id, &con2Id}, accessNodeIds...) 151 152 provider := id.NewFixedIdentityProvider(allIds) 153 idProvider.On("ByPeerID", mocktestify.Anything).Return( 154 func(peerId peer.ID) *flow.Identity { 155 identity, _ := provider.ByPeerID(peerId) 156 return identity 157 }, func(peerId peer.ID) bool { 158 _, ok := provider.ByPeerID(peerId) 159 return ok 160 }).Maybe() 161 162 p2ptest.StartNodes(t, signalerCtx, allNodes) 163 defer p2ptest.StopNodes(t, allNodes, cancel) 164 165 blockTopic := channels.TopicFromChannel(channels.PushBlocks, sporkId) 166 167 // all nodes subscribe to block topic (common topic among all roles) 168 _, err = con1Node.Subscribe(blockTopic, flowpubsub.TopicValidator(unittest.Logger(), unittest.AllowAllPeerFilter())) 169 require.NoError(t, err) 170 171 _, err = con2Node.Subscribe(blockTopic, flowpubsub.TopicValidator(unittest.Logger(), unittest.AllowAllPeerFilter())) 172 require.NoError(t, err) 173 174 // access node group 175 accessNodeSubs := make([]p2p.Subscription, len(accessNodeGroup)) 176 for i, node := range accessNodeGroup { 177 sub, err := node.Subscribe(blockTopic, flowpubsub.TopicValidator(unittest.Logger(), unittest.AllowAllPeerFilter())) 178 require.NoError(t, err, "access node %d failed to subscribe to block topic", i) 179 accessNodeSubs[i] = sub 180 } 181 182 // let nodes reside on a full topology, hence no partition is caused by the topology. 183 p2ptest.LetNodesDiscoverEachOther(t, ctx, allNodes, allIds) 184 185 // checks whether con1 and con2 are in the same mesh 186 tick := time.Second // Set the tick duration as needed 187 timeout := 5 * time.Second // Set the timeout duration as needed 188 189 ticker := time.NewTicker(tick) 190 defer ticker.Stop() 191 timeoutCh := time.After(timeout) 192 193 con1HasCon2 := false // denotes whether con1 has con2 in its mesh 194 con2HasCon1 := false // denotes whether con2 has con1 in its mesh 195 for { 196 select { 197 case <-ticker.C: 198 con1BlockTopicPeers := con1Node.GetLocalMeshPeers(blockTopic) 199 for _, p := range con1BlockTopicPeers { 200 if p == con2Node.ID() { 201 con2HasCon1 = true 202 break // con1 has con2 in its mesh, break out of the current loop 203 } 204 } 205 206 con2BlockTopicPeers := con2Node.GetLocalMeshPeers(blockTopic) 207 for _, p := range con2BlockTopicPeers { 208 if p == con1Node.ID() { 209 con1HasCon2 = true 210 break // con2 has con1 in its mesh, break out of the current loop 211 } 212 } 213 214 if con2HasCon1 && con1HasCon2 { 215 return 216 } 217 218 case <-timeoutCh: 219 require.Fail(t, "timed out waiting for con1 to have con2 in its mesh; honest nodes are not on each others' topic mesh on GossipSub") 220 } 221 } 222 } 223 224 // maliciousAppSpecificScore returns a malicious app specific penalty function that rewards the malicious node and 225 // punishes the honest nodes. 226 func maliciousAppSpecificScore(honestIds flow.IdentityList, optionCfg p2pconfig.ProtocolLevelGossipSubScoreParams) func(peer.ID) float64 { 227 honestIdProvider := id.NewFixedIdentityProvider(honestIds) 228 return func(p peer.ID) float64 { 229 _, isHonest := honestIdProvider.ByPeerID(p) 230 if isHonest { 231 return optionCfg.AppSpecificScore.MaxAppSpecificPenalty 232 } 233 234 return optionCfg.AppSpecificScore.MaxAppSpecificReward 235 } 236 }