github.com/onflow/flow-go@v0.35.7-crescendo-preview.23-atree-inlining/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 132 // override the default config to make the mesh tracer log more frequently 133 defaultConfig.NetworkConfig.GossipSub.RpcTracer.LocalMeshLogInterval = time.Second 134 135 con1Node, con1Id := p2ptest.NodeFixture(t, sporkId, t.Name(), idProvider, p2ptest.WithRole(flow.RoleConsensus), p2ptest.OverrideFlowConfig(defaultConfig)) 136 con2Node, con2Id := p2ptest.NodeFixture(t, sporkId, t.Name(), idProvider, p2ptest.WithRole(flow.RoleConsensus), p2ptest.OverrideFlowConfig(defaultConfig)) 137 138 // create > 2 * 12 malicious access nodes 139 // 12 is the maximum size of default GossipSub mesh. 140 // We want to make sure that it is unlikely for honest nodes to be in the same mesh without peer scoring. 141 accessNodeGroup, accessNodeIds := p2ptest.NodesFixture(t, sporkId, t.Name(), 30, 142 idProvider, 143 p2ptest.WithRole(flow.RoleAccess), 144 // overrides the default peer scoring parameters to mute GossipSub traffic from/to honest nodes. 145 p2ptest.EnablePeerScoringWithOverride(&p2p.PeerScoringConfigOverride{ 146 AppSpecificScoreParams: maliciousAppSpecificScore(flow.IdentityList{&con1Id, &con2Id}, defaultConfig.NetworkConfig.GossipSub.ScoringParameters.PeerScoring.Protocol), 147 }), 148 ) 149 150 allNodes := append([]p2p.LibP2PNode{con1Node, con2Node}, accessNodeGroup...) 151 allIds := append(flow.IdentityList{&con1Id, &con2Id}, accessNodeIds...) 152 153 provider := id.NewFixedIdentityProvider(allIds) 154 idProvider.On("ByPeerID", mocktestify.Anything).Return( 155 func(peerId peer.ID) *flow.Identity { 156 identity, _ := provider.ByPeerID(peerId) 157 return identity 158 }, func(peerId peer.ID) bool { 159 _, ok := provider.ByPeerID(peerId) 160 return ok 161 }).Maybe() 162 163 p2ptest.StartNodes(t, signalerCtx, allNodes) 164 defer p2ptest.StopNodes(t, allNodes, cancel) 165 166 blockTopic := channels.TopicFromChannel(channels.PushBlocks, sporkId) 167 168 // all nodes subscribe to block topic (common topic among all roles) 169 _, err = con1Node.Subscribe(blockTopic, flowpubsub.TopicValidator(unittest.Logger(), unittest.AllowAllPeerFilter())) 170 require.NoError(t, err) 171 172 _, err = con2Node.Subscribe(blockTopic, flowpubsub.TopicValidator(unittest.Logger(), unittest.AllowAllPeerFilter())) 173 require.NoError(t, err) 174 175 // access node group 176 accessNodeSubs := make([]p2p.Subscription, len(accessNodeGroup)) 177 for i, node := range accessNodeGroup { 178 sub, err := node.Subscribe(blockTopic, flowpubsub.TopicValidator(unittest.Logger(), unittest.AllowAllPeerFilter())) 179 require.NoError(t, err, "access node %d failed to subscribe to block topic", i) 180 accessNodeSubs[i] = sub 181 } 182 183 // let nodes reside on a full topology, hence no partition is caused by the topology. 184 p2ptest.LetNodesDiscoverEachOther(t, ctx, allNodes, allIds) 185 186 // checks whether con1 and con2 are in the same mesh 187 tick := time.Second // Set the tick duration as needed 188 timeout := 5 * time.Second // Set the timeout duration as needed 189 190 ticker := time.NewTicker(tick) 191 defer ticker.Stop() 192 timeoutCh := time.After(timeout) 193 194 con1HasCon2 := false // denotes whether con1 has con2 in its mesh 195 con2HasCon1 := false // denotes whether con2 has con1 in its mesh 196 for { 197 select { 198 case <-ticker.C: 199 con1BlockTopicPeers := con1Node.GetLocalMeshPeers(blockTopic) 200 for _, p := range con1BlockTopicPeers { 201 if p == con2Node.ID() { 202 con2HasCon1 = true 203 break // con1 has con2 in its mesh, break out of the current loop 204 } 205 } 206 207 con2BlockTopicPeers := con2Node.GetLocalMeshPeers(blockTopic) 208 for _, p := range con2BlockTopicPeers { 209 if p == con1Node.ID() { 210 con1HasCon2 = true 211 break // con2 has con1 in its mesh, break out of the current loop 212 } 213 } 214 215 if con2HasCon1 && con1HasCon2 { 216 return 217 } 218 219 case <-timeoutCh: 220 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") 221 } 222 } 223 } 224 225 // maliciousAppSpecificScore returns a malicious app specific penalty function that rewards the malicious node and 226 // punishes the honest nodes. 227 func maliciousAppSpecificScore(honestIds flow.IdentityList, optionCfg p2pconfig.ProtocolLevelGossipSubScoreParams) func(peer.ID) float64 { 228 honestIdProvider := id.NewFixedIdentityProvider(honestIds) 229 return func(p peer.ID) float64 { 230 _, isHonest := honestIdProvider.ByPeerID(p) 231 if isHonest { 232 return optionCfg.AppSpecificScore.MaxAppSpecificPenalty 233 } 234 235 return optionCfg.AppSpecificScore.MaxAppSpecificReward 236 } 237 }