github.com/celestiaorg/celestia-node@v0.15.0-beta.1/share/p2p/peers/manager_test.go (about) 1 package peers 2 3 import ( 4 "context" 5 "sync" 6 "testing" 7 "time" 8 9 "github.com/ipfs/go-datastore" 10 dssync "github.com/ipfs/go-datastore/sync" 11 dht "github.com/libp2p/go-libp2p-kad-dht" 12 pubsub "github.com/libp2p/go-libp2p-pubsub" 13 "github.com/libp2p/go-libp2p/core/host" 14 "github.com/libp2p/go-libp2p/core/peer" 15 routingdisc "github.com/libp2p/go-libp2p/p2p/discovery/routing" 16 "github.com/libp2p/go-libp2p/p2p/net/conngater" 17 mocknet "github.com/libp2p/go-libp2p/p2p/net/mock" 18 "github.com/stretchr/testify/require" 19 "github.com/tendermint/tendermint/libs/rand" 20 21 libhead "github.com/celestiaorg/go-header" 22 23 "github.com/celestiaorg/celestia-node/header" 24 "github.com/celestiaorg/celestia-node/share" 25 "github.com/celestiaorg/celestia-node/share/p2p/discovery" 26 "github.com/celestiaorg/celestia-node/share/p2p/shrexsub" 27 ) 28 29 func TestManager(t *testing.T) { 30 t.Run("Validate pool by headerSub", func(t *testing.T) { 31 ctx, cancel := context.WithTimeout(context.Background(), time.Second*5) 32 t.Cleanup(cancel) 33 34 // create headerSub mock 35 h := testHeader() 36 headerSub := newSubLock(h, nil) 37 38 // start test manager 39 manager, err := testManager(ctx, headerSub) 40 require.NoError(t, err) 41 42 // wait until header is requested from header sub 43 err = headerSub.wait(ctx, 1) 44 require.NoError(t, err) 45 46 // check validation 47 require.True(t, manager.pools[h.DataHash.String()].isValidatedDataHash.Load()) 48 stopManager(t, manager) 49 }) 50 51 t.Run("Validate pool by shrex.Getter", func(t *testing.T) { 52 ctx, cancel := context.WithTimeout(context.Background(), time.Second*5) 53 t.Cleanup(cancel) 54 55 h := testHeader() 56 headerSub := newSubLock(h, nil) 57 58 // start test manager 59 manager, err := testManager(ctx, headerSub) 60 require.NoError(t, err) 61 62 peerID, msg := peer.ID("peer1"), newShrexSubMsg(h) 63 result := manager.Validate(ctx, peerID, msg) 64 require.Equal(t, pubsub.ValidationIgnore, result) 65 66 pID, _, err := manager.Peer(ctx, h.DataHash.Bytes(), h.Height()) 67 require.NoError(t, err) 68 require.Equal(t, peerID, pID) 69 70 // check pool validation 71 require.True(t, manager.getPool(h.DataHash.String()).isValidatedDataHash.Load()) 72 }) 73 74 t.Run("validator", func(t *testing.T) { 75 ctx, cancel := context.WithTimeout(context.Background(), time.Second*5) 76 t.Cleanup(cancel) 77 78 // create headerSub mock 79 h := testHeader() 80 headerSub := newSubLock(h, nil) 81 82 // start test manager 83 manager, err := testManager(ctx, headerSub) 84 require.NoError(t, err) 85 86 // own messages should be be accepted 87 msg := newShrexSubMsg(h) 88 result := manager.Validate(ctx, manager.host.ID(), msg) 89 require.Equal(t, pubsub.ValidationAccept, result) 90 91 // normal messages should be ignored 92 peerID := peer.ID("peer1") 93 result = manager.Validate(ctx, peerID, msg) 94 require.Equal(t, pubsub.ValidationIgnore, result) 95 96 // mark peer as misbehaved to blacklist it 97 pID, done, err := manager.Peer(ctx, h.DataHash.Bytes(), h.Height()) 98 require.NoError(t, err) 99 require.Equal(t, peerID, pID) 100 manager.params.EnableBlackListing = true 101 done(ResultBlacklistPeer) 102 103 // new messages from misbehaved peer should be Rejected 104 result = manager.Validate(ctx, pID, msg) 105 require.Equal(t, pubsub.ValidationReject, result) 106 107 stopManager(t, manager) 108 }) 109 110 t.Run("cleanup", func(t *testing.T) { 111 ctx, cancel := context.WithTimeout(context.Background(), time.Second*5) 112 t.Cleanup(cancel) 113 114 // create headerSub mock 115 h := testHeader() 116 headerSub := newSubLock(h) 117 118 // start test manager 119 manager, err := testManager(ctx, headerSub) 120 require.NoError(t, err) 121 require.NoError(t, headerSub.wait(ctx, 1)) 122 123 // set syncTimeout to 0 to allow cleanup to find outdated datahash 124 manager.params.PoolValidationTimeout = 0 125 126 // create unvalidated pool 127 peerID := peer.ID("peer1") 128 msg := shrexsub.Notification{ 129 DataHash: share.DataHash("datahash1datahash1datahash1datahash1datahash1"), 130 Height: 2, 131 } 132 manager.Validate(ctx, peerID, msg) 133 134 // create validated pool 135 validDataHash := share.DataHash("datahash2") 136 manager.fullNodes.add("full") // add FN to unblock Peer call 137 manager.Peer(ctx, validDataHash, h.Height()) //nolint:errcheck 138 require.Len(t, manager.pools, 3) 139 140 // trigger cleanup 141 blacklisted := manager.cleanUp() 142 require.Contains(t, blacklisted, peerID) 143 require.Len(t, manager.pools, 2) 144 145 // messages with blacklisted hash should be rejected right away 146 peerID2 := peer.ID("peer2") 147 result := manager.Validate(ctx, peerID2, msg) 148 require.Equal(t, pubsub.ValidationReject, result) 149 150 // check blacklisted pools 151 require.True(t, manager.isBlacklistedHash(msg.DataHash)) 152 require.False(t, manager.isBlacklistedHash(validDataHash)) 153 }) 154 155 t.Run("no peers from shrex.Sub, get from discovery", func(t *testing.T) { 156 ctx, cancel := context.WithTimeout(context.Background(), time.Second*5) 157 t.Cleanup(cancel) 158 159 // create headerSub mock 160 h := testHeader() 161 headerSub := newSubLock(h) 162 163 // start test manager 164 manager, err := testManager(ctx, headerSub) 165 require.NoError(t, err) 166 167 // add peers to fullnodes, imitating discovery add 168 peers := []peer.ID{"peer1", "peer2", "peer3"} 169 manager.fullNodes.add(peers...) 170 171 peerID, _, err := manager.Peer(ctx, h.DataHash.Bytes(), h.Height()) 172 require.NoError(t, err) 173 require.Contains(t, peers, peerID) 174 175 stopManager(t, manager) 176 }) 177 178 t.Run("no peers from shrex.Sub and from discovery. Wait", func(t *testing.T) { 179 ctx, cancel := context.WithTimeout(context.Background(), time.Second*5) 180 t.Cleanup(cancel) 181 182 // create headerSub mock 183 h := testHeader() 184 headerSub := newSubLock(h) 185 186 // start test manager 187 manager, err := testManager(ctx, headerSub) 188 require.NoError(t, err) 189 190 // make sure peers are not returned before timeout 191 timeoutCtx, cancel := context.WithTimeout(context.Background(), time.Millisecond) 192 t.Cleanup(cancel) 193 _, _, err = manager.Peer(timeoutCtx, h.DataHash.Bytes(), h.Height()) 194 require.ErrorIs(t, err, context.DeadlineExceeded) 195 196 peers := []peer.ID{"peer1", "peer2", "peer3"} 197 198 // launch wait routine 199 doneCh := make(chan struct{}) 200 go func() { 201 defer close(doneCh) 202 peerID, _, err := manager.Peer(ctx, h.DataHash.Bytes(), h.Height()) 203 require.NoError(t, err) 204 require.Contains(t, peers, peerID) 205 }() 206 207 // send peers 208 manager.fullNodes.add(peers...) 209 210 // wait for peer to be received 211 select { 212 case <-doneCh: 213 case <-ctx.Done(): 214 require.NoError(t, ctx.Err()) 215 } 216 217 stopManager(t, manager) 218 }) 219 220 t.Run("shrexSub sends a message lower than first headerSub header height, headerSub first", func(t *testing.T) { 221 ctx, cancel := context.WithTimeout(context.Background(), time.Second*5) 222 t.Cleanup(cancel) 223 224 h := testHeader() 225 h.RawHeader.Height = 100 226 headerSub := newSubLock(h, nil) 227 228 // start test manager 229 manager, err := testManager(ctx, headerSub) 230 require.NoError(t, err) 231 232 // unlock headerSub to read first header 233 require.NoError(t, headerSub.wait(ctx, 1)) 234 // pool will be created for first headerSub header datahash 235 require.Len(t, manager.pools, 1) 236 237 // create shrexSub msg with height lower than first header from headerSub 238 msg := shrexsub.Notification{ 239 DataHash: share.DataHash("datahash"), 240 Height: h.Height() - 1, 241 } 242 result := manager.Validate(ctx, "peer", msg) 243 require.Equal(t, pubsub.ValidationIgnore, result) 244 // pool will be created for first shrexSub message 245 require.Len(t, manager.pools, 2) 246 247 blacklisted := manager.cleanUp() 248 require.Empty(t, blacklisted) 249 // trigger cleanup and outdated pool should be removed 250 require.Len(t, manager.pools, 1) 251 }) 252 253 t.Run("shrexSub sends a message lower than first headerSub header height, shrexSub first", func(t *testing.T) { 254 ctx, cancel := context.WithTimeout(context.Background(), time.Second*5) 255 t.Cleanup(cancel) 256 257 h := testHeader() 258 h.RawHeader.Height = 100 259 headerSub := newSubLock(h, nil) 260 261 // start test manager 262 manager, err := testManager(ctx, headerSub) 263 require.NoError(t, err) 264 265 // create shrexSub msg with height lower than first header from headerSub 266 msg := shrexsub.Notification{ 267 DataHash: share.DataHash("datahash"), 268 Height: h.Height() - 1, 269 } 270 result := manager.Validate(ctx, "peer", msg) 271 require.Equal(t, pubsub.ValidationIgnore, result) 272 273 // pool will be created for first shrexSub message 274 require.Len(t, manager.pools, 1) 275 276 // unlock headerSub to allow it to send next message 277 require.NoError(t, headerSub.wait(ctx, 1)) 278 // second pool should be created 279 require.Len(t, manager.pools, 2) 280 281 // trigger cleanup and outdated pool should be removed 282 blacklisted := manager.cleanUp() 283 require.Len(t, manager.pools, 1) 284 285 // check that no peers or hashes were blacklisted 286 manager.params.PoolValidationTimeout = 0 287 require.Len(t, blacklisted, 0) 288 require.Len(t, manager.blacklistedHashes, 0) 289 }) 290 291 t.Run("pools store window", func(t *testing.T) { 292 ctx, cancel := context.WithTimeout(context.Background(), time.Second*5) 293 t.Cleanup(cancel) 294 295 h := testHeader() 296 h.RawHeader.Height = storedPoolsAmount * 2 297 headerSub := newSubLock(h, nil) 298 299 // start test manager 300 manager, err := testManager(ctx, headerSub) 301 require.NoError(t, err) 302 303 // unlock headerSub to read first header 304 require.NoError(t, headerSub.wait(ctx, 1)) 305 // pool will be created for first headerSub header datahash 306 require.Len(t, manager.pools, 1) 307 308 // create shrexSub msg with height lower than storedPoolsAmount 309 msg := shrexsub.Notification{ 310 DataHash: share.DataHash("datahash"), 311 Height: h.Height() - storedPoolsAmount - 3, 312 } 313 result := manager.Validate(ctx, "peer", msg) 314 require.Equal(t, pubsub.ValidationIgnore, result) 315 316 // shrexSub message should be discarded and amount of pools should not change 317 require.Len(t, manager.pools, 1) 318 }) 319 } 320 321 func TestIntegration(t *testing.T) { 322 t.Run("get peer from shrexsub", func(t *testing.T) { 323 nw, err := mocknet.FullMeshLinked(2) 324 require.NoError(t, err) 325 ctx, cancel := context.WithTimeout(context.Background(), time.Second*5) 326 t.Cleanup(cancel) 327 328 bnPubSub, err := shrexsub.NewPubSub(ctx, nw.Hosts()[0], "test") 329 require.NoError(t, err) 330 331 fnPubSub, err := shrexsub.NewPubSub(ctx, nw.Hosts()[1], "test") 332 require.NoError(t, err) 333 334 require.NoError(t, bnPubSub.Start(ctx)) 335 require.NoError(t, fnPubSub.Start(ctx)) 336 337 fnPeerManager, err := testManager(ctx, newSubLock()) 338 require.NoError(t, err) 339 fnPeerManager.host = nw.Hosts()[1] 340 341 require.NoError(t, fnPubSub.AddValidator(fnPeerManager.Validate)) 342 _, err = fnPubSub.Subscribe() 343 require.NoError(t, err) 344 345 time.Sleep(time.Millisecond * 100) 346 require.NoError(t, nw.ConnectAllButSelf()) 347 time.Sleep(time.Millisecond * 100) 348 349 // broadcast from BN 350 randHash := rand.Bytes(32) 351 require.NoError(t, bnPubSub.Broadcast(ctx, shrexsub.Notification{ 352 DataHash: randHash, 353 Height: 1, 354 })) 355 356 // FN should get message 357 gotPeer, _, err := fnPeerManager.Peer(ctx, randHash, 13) 358 require.NoError(t, err) 359 360 // check that gotPeer matched bridge node 361 require.Equal(t, nw.Hosts()[0].ID(), gotPeer) 362 }) 363 364 t.Run("get peer from discovery", func(t *testing.T) { 365 fullNodesTag := "fullNodes" 366 nw, err := mocknet.FullMeshConnected(3) 367 require.NoError(t, err) 368 ctx, cancel := context.WithTimeout(context.Background(), time.Second*10) 369 t.Cleanup(cancel) 370 371 // set up bootstrapper 372 bsHost := nw.Hosts()[0] 373 bs := host.InfoFromHost(bsHost) 374 opts := []dht.Option{ 375 dht.Mode(dht.ModeAuto), 376 dht.BootstrapPeers(*bs), 377 dht.RoutingTableRefreshPeriod(time.Second), 378 } 379 380 bsOpts := opts 381 bsOpts = append(bsOpts, 382 dht.Mode(dht.ModeServer), // it must accept incoming connections 383 dht.BootstrapPeers(), // no bootstrappers for a bootstrapper ¯\_(ツ)_/¯ 384 ) 385 bsRouter, err := dht.New(ctx, bsHost, bsOpts...) 386 require.NoError(t, err) 387 require.NoError(t, bsRouter.Bootstrap(ctx)) 388 389 // set up broadcaster node 390 bnHost := nw.Hosts()[1] 391 bnRouter, err := dht.New(ctx, bnHost, opts...) 392 require.NoError(t, err) 393 394 params := discovery.DefaultParameters() 395 params.AdvertiseInterval = time.Second 396 397 bnDisc, err := discovery.NewDiscovery( 398 params, 399 bnHost, 400 routingdisc.NewRoutingDiscovery(bnRouter), 401 fullNodesTag, 402 ) 403 require.NoError(t, err) 404 405 // set up full node / receiver node 406 fnHost := nw.Hosts()[2] 407 fnRouter, err := dht.New(ctx, fnHost, opts...) 408 require.NoError(t, err) 409 410 // init peer manager for full node 411 connGater, err := conngater.NewBasicConnectionGater(dssync.MutexWrap(datastore.NewMapDatastore())) 412 require.NoError(t, err) 413 fnPeerManager, err := NewManager( 414 DefaultParameters(), 415 nil, 416 connGater, 417 ) 418 require.NoError(t, err) 419 420 waitCh := make(chan struct{}) 421 checkDiscoveredPeer := func(peerID peer.ID, isAdded bool) { 422 defer close(waitCh) 423 // check that obtained peer id is BN 424 require.Equal(t, bnHost.ID(), peerID) 425 } 426 427 // set up discovery for full node with hook to peer manager and check discovered peer 428 params = discovery.DefaultParameters() 429 params.AdvertiseInterval = time.Second 430 params.PeersLimit = 10 431 432 fnDisc, err := discovery.NewDiscovery( 433 params, 434 fnHost, 435 routingdisc.NewRoutingDiscovery(fnRouter), 436 fullNodesTag, 437 discovery.WithOnPeersUpdate(fnPeerManager.UpdateFullNodePool), 438 discovery.WithOnPeersUpdate(checkDiscoveredPeer), 439 ) 440 require.NoError(t, fnDisc.Start(ctx)) 441 t.Cleanup(func() { 442 err = fnDisc.Stop(ctx) 443 require.NoError(t, err) 444 }) 445 446 require.NoError(t, bnRouter.Bootstrap(ctx)) 447 require.NoError(t, fnRouter.Bootstrap(ctx)) 448 449 go bnDisc.Advertise(ctx) 450 451 select { 452 case <-waitCh: 453 require.Contains(t, fnPeerManager.fullNodes.peersList, bnHost.ID()) 454 case <-ctx.Done(): 455 require.NoError(t, ctx.Err()) 456 } 457 458 }) 459 } 460 461 func testManager(ctx context.Context, headerSub libhead.Subscriber[*header.ExtendedHeader]) (*Manager, error) { 462 host, err := mocknet.New().GenPeer() 463 if err != nil { 464 return nil, err 465 } 466 shrexSub, err := shrexsub.NewPubSub(ctx, host, "test") 467 if err != nil { 468 return nil, err 469 } 470 471 connGater, err := conngater.NewBasicConnectionGater(dssync.MutexWrap(datastore.NewMapDatastore())) 472 if err != nil { 473 return nil, err 474 } 475 manager, err := NewManager( 476 DefaultParameters(), 477 host, 478 connGater, 479 WithShrexSubPools(shrexSub, headerSub), 480 ) 481 482 if err != nil { 483 return nil, err 484 } 485 err = manager.Start(ctx) 486 return manager, err 487 } 488 489 func stopManager(t *testing.T, m *Manager) { 490 closeCtx, cancel := context.WithTimeout(context.Background(), time.Second) 491 t.Cleanup(cancel) 492 require.NoError(t, m.Stop(closeCtx)) 493 } 494 495 func testHeader() *header.ExtendedHeader { 496 return &header.ExtendedHeader{ 497 RawHeader: header.RawHeader{ 498 Height: 1, 499 DataHash: rand.Bytes(32), 500 }, 501 } 502 } 503 504 type subLock struct { 505 next chan struct{} 506 wg *sync.WaitGroup 507 expected []*header.ExtendedHeader 508 } 509 510 func (s subLock) wait(ctx context.Context, count int) error { 511 s.wg.Add(count) 512 for i := 0; i < count; i++ { 513 err := s.release(ctx) 514 if err != nil { 515 return err 516 } 517 } 518 s.wg.Wait() 519 return nil 520 } 521 522 func (s subLock) release(ctx context.Context) error { 523 select { 524 case s.next <- struct{}{}: 525 return nil 526 case <-ctx.Done(): 527 return ctx.Err() 528 } 529 } 530 531 func newSubLock(expected ...*header.ExtendedHeader) *subLock { 532 wg := &sync.WaitGroup{} 533 wg.Add(1) 534 return &subLock{ 535 next: make(chan struct{}), 536 expected: expected, 537 wg: wg, 538 } 539 } 540 541 func (s *subLock) Subscribe() (libhead.Subscription[*header.ExtendedHeader], error) { 542 return s, nil 543 } 544 545 func (s *subLock) SetVerifier(func(context.Context, *header.ExtendedHeader) error) error { 546 panic("implement me") 547 } 548 549 func (s *subLock) NextHeader(ctx context.Context) (*header.ExtendedHeader, error) { 550 s.wg.Done() 551 552 // wait for call to be unlocked by release 553 select { 554 case <-s.next: 555 h := s.expected[0] 556 s.expected = s.expected[1:] 557 return h, nil 558 case <-ctx.Done(): 559 return nil, ctx.Err() 560 } 561 } 562 563 func (s *subLock) Cancel() { 564 } 565 566 func newShrexSubMsg(h *header.ExtendedHeader) shrexsub.Notification { 567 return shrexsub.Notification{ 568 DataHash: h.DataHash.Bytes(), 569 Height: h.Height(), 570 } 571 }