github.com/onflow/flow-go@v0.35.7-crescendo-preview.23-atree-inlining/network/p2p/scoring/subscription_validator_test.go (about)

     1  package scoring_test
     2  
     3  import (
     4  	"context"
     5  	"math/rand"
     6  	"regexp"
     7  	"testing"
     8  	"time"
     9  
    10  	"github.com/onflow/flow-go/config"
    11  	"github.com/onflow/flow-go/network/message"
    12  	"github.com/onflow/flow-go/network/p2p"
    13  	p2ptest "github.com/onflow/flow-go/network/p2p/test"
    14  	flowpubsub "github.com/onflow/flow-go/network/validator/pubsub"
    15  
    16  	"github.com/libp2p/go-libp2p/core/peer"
    17  	mocktestify "github.com/stretchr/testify/mock"
    18  	"github.com/stretchr/testify/require"
    19  
    20  	"github.com/onflow/flow-go/model/flow"
    21  	"github.com/onflow/flow-go/model/messages"
    22  	"github.com/onflow/flow-go/module/id"
    23  	"github.com/onflow/flow-go/module/irrecoverable"
    24  	"github.com/onflow/flow-go/module/mock"
    25  	"github.com/onflow/flow-go/network/channels"
    26  	"github.com/onflow/flow-go/network/internal/p2pfixtures"
    27  	mockp2p "github.com/onflow/flow-go/network/p2p/mock"
    28  	"github.com/onflow/flow-go/network/p2p/scoring"
    29  	"github.com/onflow/flow-go/utils/unittest"
    30  )
    31  
    32  // TestSubscriptionProvider_GetSubscribedTopics tests that when a peer has not subscribed to
    33  // any topic, the subscription validator returns no error.
    34  func TestSubscriptionValidator_NoSubscribedTopic(t *testing.T) {
    35  	sp := mockp2p.NewSubscriptionProvider(t)
    36  	sv := scoring.NewSubscriptionValidator(unittest.Logger(), sp)
    37  
    38  	// mocks peer 1 not subscribed to any topic.
    39  	peer1 := unittest.PeerIdFixture(t)
    40  	sp.On("GetSubscribedTopics", peer1).Return([]string{})
    41  
    42  	// as peer 1 has not subscribed to any topic, the subscription validator should return no error regardless of the
    43  	// role.
    44  	for _, role := range flow.Roles() {
    45  		require.NoError(t, sv.CheckSubscribedToAllowedTopics(peer1, role))
    46  	}
    47  }
    48  
    49  // TestSubscriptionValidator_UnknownChannel tests that when a peer has subscribed to an unknown
    50  // topic, the subscription validator returns an error.
    51  func TestSubscriptionValidator_UnknownChannel(t *testing.T) {
    52  	sp := mockp2p.NewSubscriptionProvider(t)
    53  	sv := scoring.NewSubscriptionValidator(unittest.Logger(), sp)
    54  
    55  	// mocks peer 1 not subscribed to an unknown topic.
    56  	peer1 := unittest.PeerIdFixture(t)
    57  	sp.On("GetSubscribedTopics", peer1).Return([]string{"unknown-topic-1", "unknown-topic-2"})
    58  
    59  	// as peer 1 has subscribed to unknown topics, the subscription validator should return an error
    60  	// regardless of the role.
    61  	for _, role := range flow.Roles() {
    62  		err := sv.CheckSubscribedToAllowedTopics(peer1, role)
    63  		require.Error(t, err)
    64  		require.True(t, p2p.IsInvalidSubscriptionError(err))
    65  	}
    66  }
    67  
    68  // TestSubscriptionValidator_ValidSubscription tests that when a peer has subscribed to valid
    69  // topics based on its Flow protocol role, the subscription validator returns no error.
    70  func TestSubscriptionValidator_ValidSubscriptions(t *testing.T) {
    71  	sp := mockp2p.NewSubscriptionProvider(t)
    72  	sv := scoring.NewSubscriptionValidator(unittest.Logger(), sp)
    73  
    74  	for _, role := range flow.Roles() {
    75  		peerId := unittest.PeerIdFixture(t)
    76  		// allowed channels for the role excluding the test channels.
    77  		allowedChannels := channels.ChannelsByRole(role).ExcludePattern(regexp.MustCompile("^(test).*"))
    78  		sporkID := unittest.IdentifierFixture()
    79  
    80  		allowedTopics := make([]string, 0, len(allowedChannels))
    81  		for _, channel := range allowedChannels {
    82  			allowedTopics = append(allowedTopics, channels.TopicFromChannel(channel, sporkID).String())
    83  		}
    84  
    85  		// peer should pass the subscription validator as it has subscribed to any subset of its allowed topics.
    86  		for i := range allowedTopics {
    87  			sp.On("GetSubscribedTopics", peerId).Return(allowedTopics[:i+1])
    88  			require.NoError(t, sv.CheckSubscribedToAllowedTopics(peerId, role))
    89  		}
    90  	}
    91  }
    92  
    93  // TestSubscriptionValidator_SubscribeToAllTopics tests that regardless of its role when a peer has subscribed to all
    94  // topics, the subscription validator returns an error.
    95  //
    96  // Note: this test is to ensure that the subscription validator is not bypassed by subscribing to all topics.
    97  // It also based on the assumption that within the list of all topics, there are guaranteed to be some that are not allowed.
    98  // If this assumption is not true, this test will fail. Hence, the test should be updated accordingly if the assumption
    99  // is no longer true.
   100  func TestSubscriptionValidator_SubscribeToAllTopics(t *testing.T) {
   101  	sp := mockp2p.NewSubscriptionProvider(t)
   102  	sv := scoring.NewSubscriptionValidator(unittest.Logger(), sp)
   103  
   104  	allChannels := channels.Channels().ExcludePattern(regexp.MustCompile("^(test).*"))
   105  	sporkID := unittest.IdentifierFixture()
   106  	allTopics := make([]string, 0, len(allChannels))
   107  	for _, channel := range allChannels {
   108  		allTopics = append(allTopics, channels.TopicFromChannel(channel, sporkID).String())
   109  	}
   110  
   111  	for _, role := range flow.Roles() {
   112  		peerId := unittest.PeerIdFixture(t)
   113  		sp.On("GetSubscribedTopics", peerId).Return(allTopics)
   114  		err := sv.CheckSubscribedToAllowedTopics(peerId, role)
   115  		require.Error(t, err, role)
   116  		require.True(t, p2p.IsInvalidSubscriptionError(err), role)
   117  	}
   118  }
   119  
   120  // TestSubscriptionValidator_InvalidSubscription tests that when a peer has subscribed to invalid
   121  // topics based on its Flow protocol role, the subscription validator returns an error.
   122  func TestSubscriptionValidator_InvalidSubscriptions(t *testing.T) {
   123  	sp := mockp2p.NewSubscriptionProvider(t)
   124  	sv := scoring.NewSubscriptionValidator(unittest.Logger(), sp)
   125  
   126  	for _, role := range flow.Roles() {
   127  		peerId := unittest.PeerIdFixture(t)
   128  		unauthorizedChannels := channels.Channels(). // all channels
   129  								ExcludeChannels(channels.ChannelsByRole(role)). // excluding the channels for the role
   130  								ExcludePattern(regexp.MustCompile("^(test).*")) // excluding the test channels.
   131  		sporkID := unittest.IdentifierFixture()
   132  		unauthorizedTopics := make([]string, 0, len(unauthorizedChannels))
   133  		for _, channel := range unauthorizedChannels {
   134  			unauthorizedTopics = append(unauthorizedTopics, channels.TopicFromChannel(channel, sporkID).String())
   135  		}
   136  
   137  		// peer should NOT pass subscription validator as it has subscribed to any subset of its unauthorized topics,
   138  		// regardless of the role.
   139  		for i := range unauthorizedTopics {
   140  			sp.On("GetSubscribedTopics", peerId).Return(unauthorizedTopics[:i+1])
   141  			err := sv.CheckSubscribedToAllowedTopics(peerId, role)
   142  			require.Error(t, err, role)
   143  			require.True(t, p2p.IsInvalidSubscriptionError(err), role)
   144  		}
   145  	}
   146  }
   147  
   148  // TestSubscriptionValidator_Integration tests that when a peer is subscribed to an invalid topic, it is penalized
   149  // by the subscription validator of other peers on that same (invalid) topic,
   150  // and they prevent the peer from sending messages on that topic.
   151  // This test is an integration test that tests the subscription validator in the context of the pubsub.
   152  //
   153  // Scenario:
   154  // Part-1:
   155  // 1. Two verification nodes and one consensus node are created.
   156  // 2. All nodes subscribe to a legit shared topic (PushBlocks).
   157  // 3. Consensus node publishes a block on this topic.
   158  // 4. Test checks that all nodes receive the block.
   159  //
   160  // Part-2:
   161  // 1. Consensus node subscribes to an invalid topic, which it is not supposed to (RequestChunks).
   162  // 2. Consensus node publishes a block on the PushBlocks topic.
   163  // 3. Test checks that none of the nodes receive the block.
   164  // 4. Verification node also publishes a chunk request on the RequestChunks channel.
   165  // 5. Test checks that consensus node does not receive the chunk request while the other verification node does.
   166  func TestSubscriptionValidator_Integration(t *testing.T) {
   167  	unittest.SkipUnless(t, unittest.TEST_FLAKY, "flakey tests")
   168  	ctx, cancel := context.WithCancel(context.Background())
   169  	signalerCtx := irrecoverable.NewMockSignalerContext(t, ctx)
   170  
   171  	cfg, err := config.DefaultConfig()
   172  	require.NoError(t, err)
   173  	// set a low update interval to speed up the test
   174  	cfg.NetworkConfig.GossipSub.SubscriptionProvider.UpdateInterval = 10 * time.Millisecond
   175  	cfg.NetworkConfig.GossipSub.ScoringParameters.ScoringRegistryParameters.AppSpecificScore.ScoreTTL = 10 * time.Millisecond
   176  	// score tracer interval is set to 500 milliseconds to speed up the test, it should be shorter than the heartbeat interval (1 second) of gossipsub to catch the score updates in time.
   177  	cfg.NetworkConfig.GossipSub.RpcTracer.ScoreTracerInterval = 500 * time.Millisecond
   178  
   179  	sporkId := unittest.IdentifierFixture()
   180  
   181  	idProvider := mock.NewIdentityProvider(t)
   182  	// one consensus node.
   183  	conNode, conId := p2ptest.NodeFixture(t, sporkId, t.Name(),
   184  		idProvider,
   185  		p2ptest.WithLogger(unittest.Logger()),
   186  		p2ptest.OverrideFlowConfig(cfg),
   187  		p2ptest.WithRole(flow.RoleConsensus))
   188  
   189  	// two verification node.
   190  	verNode1, verId1 := p2ptest.NodeFixture(t, sporkId, t.Name(),
   191  		idProvider,
   192  		p2ptest.WithLogger(unittest.Logger()),
   193  		p2ptest.OverrideFlowConfig(cfg),
   194  		p2ptest.WithRole(flow.RoleVerification))
   195  
   196  	verNode2, verId2 := p2ptest.NodeFixture(t, sporkId, t.Name(),
   197  		idProvider,
   198  		p2ptest.WithLogger(unittest.Logger()),
   199  		p2ptest.OverrideFlowConfig(cfg),
   200  		p2ptest.WithRole(flow.RoleVerification))
   201  
   202  	// suppress peer provider error
   203  	peerProvider := func() peer.IDSlice {
   204  		return []peer.ID{conNode.ID(), verNode1.ID(), verNode2.ID()}
   205  	}
   206  	verNode1.WithPeersProvider(peerProvider)
   207  	verNode2.WithPeersProvider(peerProvider)
   208  	conNode.WithPeersProvider(peerProvider)
   209  
   210  	ids := flow.IdentityList{&conId, &verId1, &verId2}
   211  	nodes := []p2p.LibP2PNode{conNode, verNode1, verNode2}
   212  
   213  	provider := id.NewFixedIdentityProvider(ids)
   214  	idProvider.On("ByPeerID", mocktestify.Anything).Return(
   215  		func(peerId peer.ID) *flow.Identity {
   216  			identity, _ := provider.ByPeerID(peerId)
   217  			return identity
   218  		}, func(peerId peer.ID) bool {
   219  			_, ok := provider.ByPeerID(peerId)
   220  			return ok
   221  		})
   222  
   223  	p2ptest.StartNodes(t, signalerCtx, nodes)
   224  	defer p2ptest.StopNodes(t, nodes, cancel)
   225  
   226  	blockTopic := channels.TopicFromChannel(channels.PushBlocks, sporkId)
   227  
   228  	topicValidator := flowpubsub.TopicValidator(unittest.Logger(), unittest.AllowAllPeerFilter())
   229  
   230  	// wait for the subscriptions to be established
   231  	p2ptest.LetNodesDiscoverEachOther(t, ctx, nodes, ids)
   232  
   233  	// consensus node subscribes to the block topic.
   234  	conSub, err := conNode.Subscribe(blockTopic, topicValidator)
   235  	require.NoError(t, err)
   236  
   237  	// both verification nodes subscribe to the blocks and chunks topic (because they are allowed to).
   238  	ver1SubBlocks, err := verNode1.Subscribe(blockTopic, topicValidator)
   239  	require.NoError(t, err)
   240  
   241  	ver1SubChunks, err := verNode1.Subscribe(channels.TopicFromChannel(channels.RequestChunks, sporkId), topicValidator)
   242  	require.NoError(t, err)
   243  
   244  	ver2SubBlocks, err := verNode2.Subscribe(blockTopic, topicValidator)
   245  	require.NoError(t, err)
   246  
   247  	ver2SubChunks, err := verNode2.Subscribe(channels.TopicFromChannel(channels.RequestChunks, sporkId), topicValidator)
   248  	require.NoError(t, err)
   249  
   250  	// let the subscriptions be established
   251  	time.Sleep(2 * time.Second)
   252  
   253  	outgoingMessageScope, err := message.NewOutgoingScope(
   254  		ids.NodeIDs(),
   255  		channels.TopicFromChannel(channels.PushBlocks, sporkId),
   256  		unittest.ProposalFixture(),
   257  		unittest.NetworkCodec().Encode,
   258  		message.ProtocolTypePubSub)
   259  	require.NoError(t, err)
   260  	require.NoError(t, conNode.Publish(ctx, outgoingMessageScope))
   261  
   262  	// checks that the message is received by all nodes.
   263  	ctx1s, cancel1s := context.WithTimeout(ctx, 1*time.Second)
   264  	defer cancel1s()
   265  
   266  	expectedReceivedData, err := outgoingMessageScope.Proto().Marshal()
   267  	require.NoError(t, err)
   268  
   269  	p2pfixtures.SubsMustReceiveMessage(t, ctx1s, expectedReceivedData, []p2p.Subscription{conSub, ver1SubBlocks, ver2SubBlocks})
   270  
   271  	// now consensus node is doing something very bad!
   272  	// it is subscribing to a channel that it is not supposed to subscribe to.
   273  	conSubChunks, err := conNode.Subscribe(channels.TopicFromChannel(channels.RequestChunks, sporkId), topicValidator)
   274  	require.NoError(t, err)
   275  
   276  	invalidSubscriptionPenalty := cfg.NetworkConfig.GossipSub.ScoringParameters.PeerScoring.Protocol.AppSpecificScore.InvalidSubscriptionPenalty
   277  	require.Eventually(t, func() bool {
   278  		score, ok := verNode1.PeerScoreExposer().GetScore(conNode.ID())
   279  		return score == invalidSubscriptionPenalty && ok
   280  	}, 5*time.Second, 200*time.Millisecond)
   281  	require.Eventually(t, func() bool {
   282  		score, ok := verNode2.PeerScoreExposer().GetScore(conNode.ID())
   283  		return score == invalidSubscriptionPenalty && ok
   284  	}, 5*time.Second, 200*time.Millisecond)
   285  
   286  	// consensus node publishes another proposal, but this time, it should not reach verification node.
   287  	// since upon an unauthorized subscription, verification node should have slashed consensus node on
   288  	// the GossipSub scoring protocol.
   289  	outgoingMessageScope, err = message.NewOutgoingScope(
   290  		ids.NodeIDs(),
   291  		channels.TopicFromChannel(channels.PushBlocks, sporkId),
   292  		unittest.ProposalFixture(),
   293  		unittest.NetworkCodec().Encode,
   294  		message.ProtocolTypePubSub)
   295  	require.NoError(t, err)
   296  
   297  	ctx5s, cancel5s := context.WithTimeout(ctx, 5*time.Second)
   298  	defer cancel5s()
   299  	p2pfixtures.SubsMustEventuallyStopReceivingAnyMessage(t, ctx5s, []p2p.Subscription{ver1SubBlocks, ver2SubBlocks}, func(t *testing.T) {
   300  		require.NoError(t, conNode.Publish(ctx, outgoingMessageScope))
   301  	})
   302  
   303  	// moreover, a verification node publishing a message to the request chunk topic should not reach consensus node.
   304  	// however, both verification nodes should receive the message.
   305  	outgoingMessageScope, err = message.NewOutgoingScope(
   306  		ids.NodeIDs(),
   307  		channels.TopicFromChannel(channels.RequestChunks, sporkId),
   308  		&messages.ChunkDataRequest{
   309  			ChunkID: unittest.IdentifierFixture(),
   310  			Nonce:   rand.Uint64(),
   311  		},
   312  		unittest.NetworkCodec().Encode,
   313  		message.ProtocolTypePubSub)
   314  	require.NoError(t, err)
   315  
   316  	require.NoError(t, verNode1.Publish(ctx, outgoingMessageScope))
   317  
   318  	ctx1s, cancel1s = context.WithTimeout(ctx, 1*time.Second)
   319  	defer cancel1s()
   320  
   321  	expectedReceivedData, err = outgoingMessageScope.Proto().Marshal()
   322  	require.NoError(t, err)
   323  
   324  	p2pfixtures.SubsMustReceiveMessage(t, ctx1s, expectedReceivedData, []p2p.Subscription{ver1SubChunks, ver2SubChunks})
   325  	ctx5s, cancel5s = context.WithTimeout(ctx, 5*time.Second)
   326  	defer cancel5s()
   327  	p2pfixtures.SubsMustEventuallyStopReceivingAnyMessage(t, ctx5s, []p2p.Subscription{conSubChunks}, func(t *testing.T) {
   328  		require.NoError(t, verNode1.Publish(ctx, outgoingMessageScope))
   329  	})
   330  }