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  }