package statesync import ( "context" "fmt" "strings" "sync" "testing" "time" "github.com/fortytw2/leaktest" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "github.com/tendermint/tendermint/internal/p2p" ssproto "github.com/tendermint/tendermint/proto/tendermint/statesync" "github.com/tendermint/tendermint/types" ) func TestDispatcherBasic(t *testing.T) { t.Cleanup(leaktest.Check(t)) ch := make(chan p2p.Envelope, 100) closeCh := make(chan struct{}) defer close(closeCh) d := newDispatcher(ch, 1*time.Second) go handleRequests(t, d, ch, closeCh) peers := createPeerSet(5) for _, peer := range peers { d.addPeer(peer) } wg := sync.WaitGroup{} // make a bunch of async requests and require that the correct responses are // given for i := 1; i < 10; i++ { wg.Add(1) go func(height int64) { defer wg.Done() lb, peer, err := d.LightBlock(context.Background(), height) require.NoError(t, err) require.NotNil(t, lb) require.Equal(t, lb.Height, height) require.Contains(t, peers, peer) }(int64(i)) } wg.Wait() } func TestDispatcherReturnsNoBlock(t *testing.T) { t.Cleanup(leaktest.Check(t)) ch := make(chan p2p.Envelope, 100) d := newDispatcher(ch, 1*time.Second) peerFromSet := createPeerSet(1)[0] d.addPeer(peerFromSet) doneCh := make(chan struct{}) go func() { <-ch require.NoError(t, d.respond(nil, peerFromSet)) close(doneCh) }() lb, peerResult, err := d.LightBlock(context.Background(), 1) <-doneCh require.Nil(t, lb) require.Nil(t, err) require.Equal(t, peerFromSet, peerResult) } func TestDispatcherErrorsWhenNoPeers(t *testing.T) { t.Cleanup(leaktest.Check(t)) ch := make(chan p2p.Envelope, 100) d := newDispatcher(ch, 1*time.Second) lb, peerResult, err := d.LightBlock(context.Background(), 1) require.Nil(t, lb) require.Empty(t, peerResult) require.Equal(t, errNoConnectedPeers, err) } func TestDispatcherReturnsBlockOncePeerAvailable(t *testing.T) { t.Cleanup(leaktest.Check(t)) dispatcherRequestCh := make(chan p2p.Envelope, 100) d := newDispatcher(dispatcherRequestCh, 1*time.Second) peerFromSet := createPeerSet(1)[0] d.addPeer(peerFromSet) ctx := context.Background() wrapped, cancelFunc := context.WithCancel(ctx) doneCh := make(chan struct{}) go func() { lb, peerResult, err := d.LightBlock(wrapped, 1) require.Nil(t, lb) require.Equal(t, peerFromSet, peerResult) require.Nil(t, err) // calls to dispatcher.Lightblock write into the dispatcher's requestCh. // we read from the requestCh here to unblock the requestCh for future // calls. <-dispatcherRequestCh close(doneCh) }() cancelFunc() <-doneCh go func() { <-dispatcherRequestCh lb := &types.LightBlock{} asProto, err := lb.ToProto() require.Nil(t, err) err = d.respond(asProto, peerFromSet) require.Nil(t, err) }() lb, peerResult, err := d.LightBlock(context.Background(), 1) require.NotNil(t, lb) require.Equal(t, peerFromSet, peerResult) require.Nil(t, err) } func TestDispatcherProviders(t *testing.T) { t.Cleanup(leaktest.Check(t)) ch := make(chan p2p.Envelope, 100) chainID := "state-sync-test" closeCh := make(chan struct{}) defer close(closeCh) d := newDispatcher(ch, 1*time.Second) go handleRequests(t, d, ch, closeCh) peers := createPeerSet(5) for _, peer := range peers { d.addPeer(peer) } providers := d.Providers(chainID, 5*time.Second) require.Len(t, providers, 5) for i, p := range providers { bp, ok := p.(*blockProvider) require.True(t, ok) assert.Equal(t, bp.String(), string(peers[i])) lb, err := p.LightBlock(context.Background(), 10) assert.Error(t, err) assert.Nil(t, lb) } } func TestPeerListBasic(t *testing.T) { t.Cleanup(leaktest.Check(t)) peerList := newPeerList() assert.Zero(t, peerList.Len()) numPeers := 10 peerSet := createPeerSet(numPeers) for _, peer := range peerSet { peerList.Append(peer) } for idx, peer := range peerList.Peers() { assert.Equal(t, peer, peerSet[idx]) } assert.Equal(t, numPeers, peerList.Len()) half := numPeers / 2 for i := 0; i < half; i++ { assert.Equal(t, peerSet[i], peerList.Pop(ctx)) } assert.Equal(t, half, peerList.Len()) peerList.Remove(types.NodeID("lp")) assert.Equal(t, half, peerList.Len()) peerList.Remove(peerSet[half]) half++ assert.Equal(t, peerSet[half], peerList.Pop(ctx)) } func TestPeerListBlocksWhenEmpty(t *testing.T) { t.Cleanup(leaktest.Check(t)) peerList := newPeerList() require.Zero(t, peerList.Len()) doneCh := make(chan struct{}) ctx, cancel := context.WithCancel(context.Background()) defer cancel() go func() { peerList.Pop(ctx) close(doneCh) }() select { case <-doneCh: t.Error("empty peer list should not have returned result") case <-time.After(100 * time.Millisecond): } } func TestEmptyPeerListReturnsWhenContextCanceled(t *testing.T) { t.Cleanup(leaktest.Check(t)) peerList := newPeerList() require.Zero(t, peerList.Len()) doneCh := make(chan struct{}) ctx := context.Background() wrapped, cancel := context.WithCancel(ctx) go func() { peerList.Pop(wrapped) close(doneCh) }() select { case <-doneCh: t.Error("empty peer list should not have returned result") case <-time.After(100 * time.Millisecond): } cancel() select { case <-doneCh: case <-time.After(100 * time.Millisecond): t.Error("peer list should have returned after context canceled") } } func TestPeerListConcurrent(t *testing.T) { t.Cleanup(leaktest.Check(t)) peerList := newPeerList() numPeers := 10 wg := sync.WaitGroup{} // we run a set of goroutines requesting the next peer in the list. As the // peer list hasn't been populated each these go routines should block for i := 0; i < numPeers/2; i++ { go func() { _ = peerList.Pop(ctx) wg.Done() }() } // now we add the peers to the list, this should allow the previously // blocked go routines to unblock for _, peer := range createPeerSet(numPeers) { wg.Add(1) peerList.Append(peer) } // we request the second half of the peer set for i := 0; i < numPeers/2; i++ { go func() { _ = peerList.Pop(ctx) wg.Done() }() } // we use a context with cancel and a separate go routine to wait for all // the other goroutines to close. ctx, cancel := context.WithCancel(context.Background()) go func() { wg.Wait(); cancel() }() select { case <-time.After(time.Second): // not all of the blocked go routines waiting on peers have closed after // one second. This likely means the list got blocked. t.Failed() case <-ctx.Done(): // there should be no peers remaining require.Equal(t, 0, peerList.Len()) } } // handleRequests is a helper function usually run in a separate go routine to // imitate the expected responses of the reactor wired to the dispatcher func handleRequests(t *testing.T, d *dispatcher, ch chan p2p.Envelope, closeCh chan struct{}) { t.Helper() for { select { case request := <-ch: height := request.Message.(*ssproto.LightBlockRequest).Height peer := request.To resp := mockLBResp(t, peer, int64(height), time.Now()) block, _ := resp.block.ToProto() require.NoError(t, d.respond(block, resp.peer)) case <-closeCh: return } } } func createPeerSet(num int) []types.NodeID { peers := make([]types.NodeID, num) for i := 0; i < num; i++ { peers[i], _ = types.NewNodeID(strings.Repeat(fmt.Sprintf("%d", i), 2*types.NodeIDByteLength)) } return peers }