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  }