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