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 }