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

     1  package cache_test
     2  
     3  import (
     4  	"fmt"
     5  	"testing"
     6  
     7  	"github.com/dgraph-io/badger/v2"
     8  	"github.com/libp2p/go-libp2p/core/peer"
     9  	"github.com/stretchr/testify/mock"
    10  	"github.com/stretchr/testify/require"
    11  	"github.com/stretchr/testify/suite"
    12  	"go.uber.org/atomic"
    13  
    14  	"github.com/onflow/flow-go/model/flow"
    15  	"github.com/onflow/flow-go/model/flow/filter"
    16  	mocks "github.com/onflow/flow-go/module/mock"
    17  	"github.com/onflow/flow-go/network"
    18  	"github.com/onflow/flow-go/network/mocknetwork"
    19  	"github.com/onflow/flow-go/network/p2p/cache"
    20  	"github.com/onflow/flow-go/utils/unittest"
    21  )
    22  
    23  type NodeDisallowListWrapperTestSuite struct {
    24  	suite.Suite
    25  	DB       *badger.DB
    26  	provider *mocks.IdentityProvider
    27  
    28  	wrapper        *cache.NodeDisallowListingWrapper
    29  	updateConsumer *mocknetwork.DisallowListNotificationConsumer
    30  }
    31  
    32  func (s *NodeDisallowListWrapperTestSuite) SetupTest() {
    33  	s.DB, _ = unittest.TempBadgerDB(s.T())
    34  	s.provider = new(mocks.IdentityProvider)
    35  
    36  	var err error
    37  	s.updateConsumer = mocknetwork.NewDisallowListNotificationConsumer(s.T())
    38  	s.wrapper, err = cache.NewNodeDisallowListWrapper(s.provider, s.DB, func() network.DisallowListNotificationConsumer {
    39  		return s.updateConsumer
    40  	})
    41  	require.NoError(s.T(), err)
    42  }
    43  
    44  func TestNodeDisallowListWrapperTestSuite(t *testing.T) {
    45  	suite.Run(t, new(NodeDisallowListWrapperTestSuite))
    46  }
    47  
    48  // TestHonestNode verifies:
    49  // For nodes _not_ on the disallowList, the `cache.NodeDisallowListingWrapper` should forward
    50  // the identities from the wrapped `IdentityProvider` without modification.
    51  func (s *NodeDisallowListWrapperTestSuite) TestHonestNode() {
    52  	s.Run("ByNodeID", func() {
    53  		identity := unittest.IdentityFixture()
    54  		s.provider.On("ByNodeID", identity.NodeID).Return(identity, true)
    55  
    56  		i, found := s.wrapper.ByNodeID(identity.NodeID)
    57  		require.True(s.T(), found)
    58  		require.Equal(s.T(), i, identity)
    59  	})
    60  	s.Run("ByPeerID", func() {
    61  		identity := unittest.IdentityFixture()
    62  		peerID := (peer.ID)("some_peer_ID")
    63  		s.provider.On("ByPeerID", peerID).Return(identity, true)
    64  
    65  		i, found := s.wrapper.ByPeerID(peerID)
    66  		require.True(s.T(), found)
    67  		require.Equal(s.T(), i, identity)
    68  	})
    69  	s.Run("Identities", func() {
    70  		identities := unittest.IdentityListFixture(11)
    71  		f := filter.In(identities[3:4])
    72  		expectedFilteredIdentities := identities.Filter(f)
    73  		s.provider.On("Identities", mock.Anything).Return(
    74  			func(filter flow.IdentityFilter[flow.Identity]) flow.IdentityList {
    75  				return identities.Filter(filter)
    76  			},
    77  			nil,
    78  		)
    79  		require.Equal(s.T(), expectedFilteredIdentities, s.wrapper.Identities(f))
    80  	})
    81  }
    82  
    83  // TestDisallowListNode tests proper handling of identities _on_ the disallowList:
    84  //   - For any identity `i` with `i.NodeID ∈ disallowList`, the returned identity
    85  //     should have `i.Ejected` set to `true` (irrespective of the `Ejected`
    86  //     flag's initial returned by the wrapped `IdentityProvider`).
    87  //   - The wrapper should _copy_ the identity and _not_ write into the wrapped
    88  //     IdentityProvider's memory.
    89  //   - For `IdentityProvider.ByNodeID` and `IdentityProvider.ByPeerID`:
    90  //     whether or not the wrapper modifies the `Ejected` flag should depend only
    91  //     in the NodeID of the returned identity, irrespective of the second return
    92  //     value (boolean).
    93  //     While returning (non-nil identity, false) is not a defined return value,
    94  //     we expect the wrapper to nevertheless handle this case to increase its
    95  //     generality.
    96  func (s *NodeDisallowListWrapperTestSuite) TestDisallowListNode() {
    97  	blocklist := unittest.IdentityListFixture(11)
    98  	s.updateConsumer.On("OnDisallowListNotification", &network.DisallowListingUpdate{
    99  		FlowIds: blocklist.NodeIDs(),
   100  		Cause:   network.DisallowListedCauseAdmin,
   101  	}).Return().Once()
   102  	err := s.wrapper.Update(blocklist.NodeIDs())
   103  	require.NoError(s.T(), err)
   104  
   105  	index := atomic.NewInt32(0)
   106  	for _, b := range []bool{true, false} {
   107  		expectedfound := b
   108  
   109  		s.Run(fmt.Sprintf("IdentityProvider.ByNodeID returning (<non-nil identity>, %v)", expectedfound), func() {
   110  			originalIdentity := blocklist[index.Inc()]
   111  			s.provider.On("ByNodeID", originalIdentity.NodeID).Return(originalIdentity, expectedfound)
   112  
   113  			var expectedIdentity = *originalIdentity                                         // expected Identity is a copy of the original
   114  			expectedIdentity.EpochParticipationStatus = flow.EpochParticipationStatusEjected // with the `Ejected` flag set to true
   115  
   116  			i, found := s.wrapper.ByNodeID(originalIdentity.NodeID)
   117  			require.Equal(s.T(), expectedfound, found)
   118  			require.Equal(s.T(), &expectedIdentity, i)
   119  
   120  			// check that originalIdentity returned by wrapped `IdentityProvider` is _not_ modified
   121  			require.False(s.T(), originalIdentity.IsEjected())
   122  		})
   123  
   124  		s.Run(fmt.Sprintf("IdentityProvider.ByPeerID returning (<non-nil identity>, %v)", expectedfound), func() {
   125  			originalIdentity := blocklist[index.Inc()]
   126  			peerID := (peer.ID)(originalIdentity.NodeID.String())
   127  			s.provider.On("ByPeerID", peerID).Return(originalIdentity, expectedfound)
   128  
   129  			var expectedIdentity = *originalIdentity                                         // expected Identity is a copy of the original
   130  			expectedIdentity.EpochParticipationStatus = flow.EpochParticipationStatusEjected // with the `Ejected` flag set to true
   131  
   132  			i, found := s.wrapper.ByPeerID(peerID)
   133  			require.Equal(s.T(), expectedfound, found)
   134  			require.Equal(s.T(), &expectedIdentity, i)
   135  
   136  			// check that originalIdentity returned by `IdentityProvider` is _not_ modified by wrapper
   137  			require.False(s.T(), originalIdentity.IsEjected())
   138  		})
   139  	}
   140  
   141  	s.Run("Identities", func() {
   142  		blocklistLookup := blocklist.Lookup()
   143  		honestIdentities := unittest.IdentityListFixture(8)
   144  		combinedIdentities := honestIdentities.Union(blocklist)
   145  		combinedIdentities, err = combinedIdentities.Shuffle()
   146  		require.NoError(s.T(), err)
   147  		numIdentities := len(combinedIdentities)
   148  
   149  		s.provider.On("Identities", mock.Anything).Return(combinedIdentities)
   150  
   151  		noFilter := filter.Not(filter.In[flow.Identity](nil))
   152  		identities := s.wrapper.Identities(noFilter)
   153  
   154  		require.Equal(s.T(), numIdentities, len(identities)) // expected number resulting identities have the
   155  		for _, i := range identities {
   156  			_, isBlocked := blocklistLookup[i.NodeID]
   157  			require.Equal(s.T(), isBlocked, i.IsEjected())
   158  		}
   159  
   160  		// check that original `combinedIdentities` returned by `IdentityProvider` are _not_ modified by wrapper
   161  		require.Equal(s.T(), numIdentities, len(combinedIdentities)) // length of list should not be modified by wrapper
   162  		for _, i := range combinedIdentities {
   163  			require.False(s.T(), i.IsEjected()) // Ejected flag should still have the original value (false here)
   164  		}
   165  	})
   166  
   167  	// this tests the edge case where the  Identities func is invoked with the p2p.NotEjectedFilter. Block listed
   168  	// nodes are expected to be filtered from the identity list returned after setting the ejected field.
   169  	s.Run("Identities(p2p.NotEjectedFilter) should not return block listed nodes", func() {
   170  		blocklistLookup := blocklist.Lookup()
   171  		honestIdentities := unittest.IdentityListFixture(8)
   172  		combinedIdentities := honestIdentities.Union(blocklist)
   173  		combinedIdentities, err = combinedIdentities.Shuffle()
   174  		require.NoError(s.T(), err)
   175  		numIdentities := len(combinedIdentities)
   176  
   177  		s.provider.On("Identities", mock.Anything).Return(combinedIdentities)
   178  
   179  		identities := s.wrapper.Identities(filter.NotEjectedFilter)
   180  
   181  		require.Equal(s.T(), len(honestIdentities), len(identities)) // expected only honest nodes to be returned
   182  		for _, i := range identities {
   183  			_, isBlocked := blocklistLookup[i.NodeID]
   184  			require.False(s.T(), isBlocked)
   185  			require.False(s.T(), i.IsEjected())
   186  		}
   187  
   188  		// check that original `combinedIdentities` returned by `IdentityProvider` are _not_ modified by wrapper
   189  		require.Equal(s.T(), numIdentities, len(combinedIdentities)) // length of list should not be modified by wrapper
   190  		for _, i := range combinedIdentities {
   191  			require.False(s.T(), i.IsEjected()) // Ejected flag should still have the original value (false here)
   192  		}
   193  	})
   194  }
   195  
   196  // TestUnknownNode verifies that the wrapper forwards nil identities
   197  // irrespective of the boolean return values.
   198  func (s *NodeDisallowListWrapperTestSuite) TestUnknownNode() {
   199  	for _, b := range []bool{true, false} {
   200  		s.Run(fmt.Sprintf("IdentityProvider.ByNodeID returning (nil, %v)", b), func() {
   201  			id := unittest.IdentifierFixture()
   202  			s.provider.On("ByNodeID", id).Return(nil, b)
   203  
   204  			identity, found := s.wrapper.ByNodeID(id)
   205  			require.Equal(s.T(), b, found)
   206  			require.Nil(s.T(), identity)
   207  		})
   208  
   209  		s.Run(fmt.Sprintf("IdentityProvider.ByPeerID returning (nil, %v)", b), func() {
   210  			peerID := (peer.ID)(unittest.IdentifierFixture().String())
   211  			s.provider.On("ByPeerID", peerID).Return(nil, b)
   212  
   213  			identity, found := s.wrapper.ByPeerID(peerID)
   214  			require.Equal(s.T(), b, found)
   215  			require.Nil(s.T(), identity)
   216  		})
   217  	}
   218  }
   219  
   220  // TestDisallowListAddRemove checks that adding and subsequently removing a node from the disallowList
   221  // it in combination a no-op. We test two scenarious
   222  //   - Node whose original `Identity` has `Ejected = false`:
   223  //     After adding the node to the disallowList and then removing it again, the `Ejected` should be false.
   224  //   - Node whose original `Identity` has `EpochParticipationStatus = flow.EpochParticipationStatusEjected`:
   225  //     After adding the node to the disallowList and then removing it again, the `Ejected` should be still be true.
   226  func (s *NodeDisallowListWrapperTestSuite) TestDisallowListAddRemove() {
   227  	for _, originalParticipationStatus := range []flow.EpochParticipationStatus{flow.EpochParticipationStatusEjected, flow.EpochParticipationStatusActive} {
   228  		s.Run(fmt.Sprintf("Add & remove node with EpochParticipationStatus = %v", originalParticipationStatus), func() {
   229  			originalIdentity := unittest.IdentityFixture()
   230  			originalIdentity.EpochParticipationStatus = originalParticipationStatus
   231  			peerID := (peer.ID)(originalIdentity.NodeID.String())
   232  			s.provider.On("ByNodeID", originalIdentity.NodeID).Return(originalIdentity, true)
   233  			s.provider.On("ByPeerID", peerID).Return(originalIdentity, true)
   234  
   235  			// step 1: before putting node on disallowList,
   236  			// an Identity with `Ejected` equal to the original value should be returned
   237  			i, found := s.wrapper.ByNodeID(originalIdentity.NodeID)
   238  			require.True(s.T(), found)
   239  			require.Equal(s.T(), originalParticipationStatus, i.EpochParticipationStatus)
   240  
   241  			i, found = s.wrapper.ByPeerID(peerID)
   242  			require.True(s.T(), found)
   243  			require.Equal(s.T(), originalParticipationStatus, i.EpochParticipationStatus)
   244  
   245  			// step 2: _after_ putting node on disallowList,
   246  			// an Identity with `Ejected` equal to `true` should be returned
   247  			s.updateConsumer.On("OnDisallowListNotification", &network.DisallowListingUpdate{
   248  				FlowIds: flow.IdentifierList{originalIdentity.NodeID},
   249  				Cause:   network.DisallowListedCauseAdmin,
   250  			}).Return().Once()
   251  			err := s.wrapper.Update(flow.IdentifierList{originalIdentity.NodeID})
   252  			require.NoError(s.T(), err)
   253  
   254  			i, found = s.wrapper.ByNodeID(originalIdentity.NodeID)
   255  			require.True(s.T(), found)
   256  			require.True(s.T(), i.IsEjected())
   257  
   258  			i, found = s.wrapper.ByPeerID(peerID)
   259  			require.True(s.T(), found)
   260  			require.True(s.T(), i.IsEjected())
   261  
   262  			// step 3: after removing the node from the disallowList,
   263  			// an Identity with `Ejected` equal to the original value should be returned
   264  			s.updateConsumer.On("OnDisallowListNotification", &network.DisallowListingUpdate{
   265  				FlowIds: flow.IdentifierList{},
   266  				Cause:   network.DisallowListedCauseAdmin,
   267  			}).Return().Once()
   268  			err = s.wrapper.Update(flow.IdentifierList{})
   269  			require.NoError(s.T(), err)
   270  
   271  			i, found = s.wrapper.ByNodeID(originalIdentity.NodeID)
   272  			require.True(s.T(), found)
   273  			require.Equal(s.T(), originalParticipationStatus, i.EpochParticipationStatus)
   274  
   275  			i, found = s.wrapper.ByPeerID(peerID)
   276  			require.True(s.T(), found)
   277  			require.Equal(s.T(), originalParticipationStatus, i.EpochParticipationStatus)
   278  		})
   279  	}
   280  }
   281  
   282  // TestUpdate tests updating, clearing and retrieving the disallowList.
   283  // This test verifies that the wrapper updates _its own internal state_ correctly.
   284  // Note:
   285  // conceptually, the disallowList is a set, i.e. not order dependent.
   286  // The wrapper internally converts the list to a set and vice versa. Therefore
   287  // the order is not preserved by `GetDisallowList`. Consequently, we compare
   288  // map-based representations here.
   289  func (s *NodeDisallowListWrapperTestSuite) TestUpdate() {
   290  	disallowList1 := unittest.IdentifierListFixture(8)
   291  	disallowList2 := unittest.IdentifierListFixture(11)
   292  	disallowList3 := unittest.IdentifierListFixture(5)
   293  
   294  	s.updateConsumer.On("OnDisallowListNotification", &network.DisallowListingUpdate{
   295  		FlowIds: disallowList1,
   296  		Cause:   network.DisallowListedCauseAdmin,
   297  	}).Return().Once()
   298  	err := s.wrapper.Update(disallowList1)
   299  	require.NoError(s.T(), err)
   300  	require.Equal(s.T(), disallowList1.Lookup(), s.wrapper.GetDisallowList().Lookup())
   301  
   302  	s.updateConsumer.On("OnDisallowListNotification", &network.DisallowListingUpdate{
   303  		FlowIds: disallowList2,
   304  		Cause:   network.DisallowListedCauseAdmin,
   305  	}).Return().Once()
   306  	err = s.wrapper.Update(disallowList2)
   307  	require.NoError(s.T(), err)
   308  	require.Equal(s.T(), disallowList2.Lookup(), s.wrapper.GetDisallowList().Lookup())
   309  
   310  	s.updateConsumer.On("OnDisallowListNotification", &network.DisallowListingUpdate{
   311  		FlowIds: nil,
   312  		Cause:   network.DisallowListedCauseAdmin,
   313  	}).Return().Once()
   314  	err = s.wrapper.ClearDisallowList()
   315  	require.NoError(s.T(), err)
   316  	require.Empty(s.T(), s.wrapper.GetDisallowList())
   317  
   318  	s.updateConsumer.On("OnDisallowListNotification", &network.DisallowListingUpdate{
   319  		FlowIds: disallowList3,
   320  		Cause:   network.DisallowListedCauseAdmin,
   321  	}).Return().Once()
   322  	err = s.wrapper.Update(disallowList3)
   323  	require.NoError(s.T(), err)
   324  	require.Equal(s.T(), disallowList3.Lookup(), s.wrapper.GetDisallowList().Lookup())
   325  }
   326  
   327  // TestDataBasePersist verifies database interactions of the wrapper with the data base.
   328  // This test verifies that the disallowList updates are persisted across restarts.
   329  // To decouple this test from the lower-level data base design, we proceed as follows:
   330  //   - We do data-base operation through the exported methods from `NodeDisallowListingWrapper`
   331  //   - Then, we create a new `NodeDisallowListingWrapper` backed by the same data base. Since it is a
   332  //     new wrapper, it must read its state from the data base. Hence, if the new wrapper returns
   333  //     the correct data, we have strong evidence that data-base interactions are correct.
   334  //
   335  // Note: The wrapper internally converts the list to a set and vice versa. Therefore
   336  // the order is not preserved by `GetDisallowList`. Consequently, we compare
   337  // map-based representations here.
   338  func (s *NodeDisallowListWrapperTestSuite) TestDataBasePersist() {
   339  	disallowList1 := unittest.IdentifierListFixture(8)
   340  	disallowList2 := unittest.IdentifierListFixture(8)
   341  
   342  	s.Run("Get disallowList from empty database", func() {
   343  		require.Empty(s.T(), s.wrapper.GetDisallowList())
   344  	})
   345  
   346  	s.Run("Clear disallow-list on empty database", func() {
   347  		s.updateConsumer.On("OnDisallowListNotification", &network.DisallowListingUpdate{
   348  			FlowIds: nil,
   349  			Cause:   network.DisallowListedCauseAdmin,
   350  		}).Return().Once()
   351  		err := s.wrapper.ClearDisallowList() // No-op as data base does not contain any block list
   352  		require.NoError(s.T(), err)
   353  		require.Empty(s.T(), s.wrapper.GetDisallowList())
   354  
   355  		// newly created wrapper should read `disallowList` from data base during initialization
   356  		w, err := cache.NewNodeDisallowListWrapper(s.provider, s.DB, func() network.DisallowListNotificationConsumer {
   357  			return s.updateConsumer
   358  		})
   359  		require.NoError(s.T(), err)
   360  		require.Empty(s.T(), w.GetDisallowList())
   361  	})
   362  
   363  	s.Run("Update disallowList and init new wrapper from database", func() {
   364  		s.updateConsumer.On("OnDisallowListNotification", &network.DisallowListingUpdate{
   365  			FlowIds: disallowList1,
   366  			Cause:   network.DisallowListedCauseAdmin,
   367  		}).Return().Once()
   368  		err := s.wrapper.Update(disallowList1)
   369  		require.NoError(s.T(), err)
   370  
   371  		// newly created wrapper should read `disallowList` from data base during initialization
   372  		w, err := cache.NewNodeDisallowListWrapper(s.provider, s.DB, func() network.DisallowListNotificationConsumer {
   373  			return s.updateConsumer
   374  		})
   375  		require.NoError(s.T(), err)
   376  		require.Equal(s.T(), disallowList1.Lookup(), w.GetDisallowList().Lookup())
   377  	})
   378  
   379  	s.Run("Update and overwrite disallowList and then init new wrapper from database", func() {
   380  		s.updateConsumer.On("OnDisallowListNotification", &network.DisallowListingUpdate{
   381  			FlowIds: disallowList1,
   382  			Cause:   network.DisallowListedCauseAdmin,
   383  		}).Return().Once()
   384  		err := s.wrapper.Update(disallowList1)
   385  		require.NoError(s.T(), err)
   386  
   387  		s.updateConsumer.On("OnDisallowListNotification", &network.DisallowListingUpdate{
   388  			FlowIds: disallowList2,
   389  			Cause:   network.DisallowListedCauseAdmin,
   390  		}).Return().Once()
   391  		err = s.wrapper.Update(disallowList2)
   392  		require.NoError(s.T(), err)
   393  
   394  		// newly created wrapper should read initial state from data base
   395  		w, err := cache.NewNodeDisallowListWrapper(s.provider, s.DB, func() network.DisallowListNotificationConsumer {
   396  			return s.updateConsumer
   397  		})
   398  		require.NoError(s.T(), err)
   399  		require.Equal(s.T(), disallowList2.Lookup(), w.GetDisallowList().Lookup())
   400  	})
   401  
   402  	s.Run("Update & clear & update and then init new wrapper from database", func() {
   403  		// set disallowList ->
   404  		// newly created wrapper should now read this list from data base during initialization
   405  		s.updateConsumer.On("OnDisallowListNotification", &network.DisallowListingUpdate{
   406  			FlowIds: disallowList1,
   407  			Cause:   network.DisallowListedCauseAdmin,
   408  		}).Return().Once()
   409  		err := s.wrapper.Update(disallowList1)
   410  		require.NoError(s.T(), err)
   411  
   412  		w0, err := cache.NewNodeDisallowListWrapper(s.provider, s.DB, func() network.DisallowListNotificationConsumer {
   413  			return s.updateConsumer
   414  		})
   415  		require.NoError(s.T(), err)
   416  		require.Equal(s.T(), disallowList1.Lookup(), w0.GetDisallowList().Lookup())
   417  
   418  		// clear disallowList ->
   419  		// newly created wrapper should now read empty disallowList from data base during initialization
   420  		s.updateConsumer.On("OnDisallowListNotification", &network.DisallowListingUpdate{
   421  			FlowIds: nil,
   422  			Cause:   network.DisallowListedCauseAdmin,
   423  		}).Return().Once()
   424  		err = s.wrapper.ClearDisallowList()
   425  		require.NoError(s.T(), err)
   426  
   427  		w1, err := cache.NewNodeDisallowListWrapper(s.provider, s.DB, func() network.DisallowListNotificationConsumer {
   428  			return s.updateConsumer
   429  		})
   430  		require.NoError(s.T(), err)
   431  		require.Empty(s.T(), w1.GetDisallowList())
   432  
   433  		// set disallowList2 ->
   434  		// newly created wrapper should now read this list from data base during initialization
   435  		s.updateConsumer.On("OnDisallowListNotification", &network.DisallowListingUpdate{
   436  			FlowIds: disallowList2,
   437  			Cause:   network.DisallowListedCauseAdmin,
   438  		}).Return().Once()
   439  		err = s.wrapper.Update(disallowList2)
   440  		require.NoError(s.T(), err)
   441  
   442  		w2, err := cache.NewNodeDisallowListWrapper(s.provider, s.DB, func() network.DisallowListNotificationConsumer {
   443  			return s.updateConsumer
   444  		})
   445  		require.NoError(s.T(), err)
   446  		require.Equal(s.T(), disallowList2.Lookup(), w2.GetDisallowList().Lookup())
   447  	})
   448  }