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