package statesync import ( "testing" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/mock" "github.com/stretchr/testify/require" "github.com/tendermint/tendermint/p2p" p2pmocks "github.com/tendermint/tendermint/p2p/mocks" "github.com/tendermint/tendermint/statesync/mocks" ) func TestSnapshot_Key(t *testing.T) { testcases := map[string]struct { modify func(*snapshot) }{ "new height": {func(s *snapshot) { s.Height = 9 }}, "new format": {func(s *snapshot) { s.Format = 9 }}, "new chunk count": {func(s *snapshot) { s.Chunks = 9 }}, "new hash": {func(s *snapshot) { s.Hash = []byte{9} }}, "no metadata": {func(s *snapshot) { s.Metadata = nil }}, } for name, tc := range testcases { tc := tc t.Run(name, func(t *testing.T) { s := snapshot{ Height: 3, Format: 1, Chunks: 7, Hash: []byte{1, 2, 3}, Metadata: []byte{255}, } before := s.Key() tc.modify(&s) after := s.Key() assert.NotEqual(t, before, after) }) } } func TestSnapshotPool_Add(t *testing.T) { stateProvider := &mocks.StateProvider{} stateProvider.On("AppHash", uint64(1)).Return([]byte("app_hash"), nil) peer := &p2pmocks.Peer{} peer.On("ID").Return(p2p.ID("id")) // Adding to the pool should work pool := newSnapshotPool(stateProvider) added, err := pool.Add(peer, &snapshot{ Height: 1, Format: 1, Chunks: 1, Hash: []byte{1}, }) require.NoError(t, err) assert.True(t, added) // Adding again from a different peer should return false otherPeer := &p2pmocks.Peer{} otherPeer.On("ID").Return(p2p.ID("other")) added, err = pool.Add(peer, &snapshot{ Height: 1, Format: 1, Chunks: 1, Hash: []byte{1}, }) require.NoError(t, err) assert.False(t, added) // The pool should have populated the snapshot with the trusted app hash snapshot := pool.Best() require.NotNil(t, snapshot) assert.Equal(t, []byte("app_hash"), snapshot.trustedAppHash) stateProvider.AssertExpectations(t) } func TestSnapshotPool_GetPeer(t *testing.T) { stateProvider := &mocks.StateProvider{} stateProvider.On("AppHash", mock.Anything).Return([]byte("app_hash"), nil) pool := newSnapshotPool(stateProvider) s := &snapshot{Height: 1, Format: 1, Chunks: 1, Hash: []byte{1}} peerA := &p2pmocks.Peer{} peerA.On("ID").Return(p2p.ID("a")) peerB := &p2pmocks.Peer{} peerB.On("ID").Return(p2p.ID("b")) _, err := pool.Add(peerA, s) require.NoError(t, err) _, err = pool.Add(peerB, s) require.NoError(t, err) _, err = pool.Add(peerA, &snapshot{Height: 2, Format: 1, Chunks: 1, Hash: []byte{1}}) require.NoError(t, err) // GetPeer currently picks a random peer, so lets run it until we've seen both. seenA := false seenB := false for !seenA || !seenB { peer := pool.GetPeer(s) switch peer.ID() { case p2p.ID("a"): seenA = true case p2p.ID("b"): seenB = true } } // GetPeer should return nil for an unknown snapshot peer := pool.GetPeer(&snapshot{Height: 9, Format: 9}) assert.Nil(t, peer) } func TestSnapshotPool_GetPeers(t *testing.T) { stateProvider := &mocks.StateProvider{} stateProvider.On("AppHash", mock.Anything).Return([]byte("app_hash"), nil) pool := newSnapshotPool(stateProvider) s := &snapshot{Height: 1, Format: 1, Chunks: 1, Hash: []byte{1}} peerA := &p2pmocks.Peer{} peerA.On("ID").Return(p2p.ID("a")) peerB := &p2pmocks.Peer{} peerB.On("ID").Return(p2p.ID("b")) _, err := pool.Add(peerA, s) require.NoError(t, err) _, err = pool.Add(peerB, s) require.NoError(t, err) _, err = pool.Add(peerA, &snapshot{Height: 2, Format: 1, Chunks: 1, Hash: []byte{2}}) require.NoError(t, err) peers := pool.GetPeers(s) assert.Len(t, peers, 2) assert.EqualValues(t, "a", peers[0].ID()) assert.EqualValues(t, "b", peers[1].ID()) } func TestSnapshotPool_Ranked_Best(t *testing.T) { stateProvider := &mocks.StateProvider{} stateProvider.On("AppHash", mock.Anything).Return([]byte("app_hash"), nil) pool := newSnapshotPool(stateProvider) // snapshots in expected order (best to worst). Highest height wins, then highest format. // Snapshots with different chunk hashes are considered different, and the most peers is // tie-breaker. expectSnapshots := []struct { snapshot *snapshot peers []string }{ {&snapshot{Height: 2, Format: 2, Chunks: 4, Hash: []byte{1, 3}}, []string{"a", "b", "c"}}, {&snapshot{Height: 2, Format: 2, Chunks: 5, Hash: []byte{1, 2}}, []string{"a"}}, {&snapshot{Height: 2, Format: 1, Chunks: 3, Hash: []byte{1, 2}}, []string{"a", "b"}}, {&snapshot{Height: 1, Format: 2, Chunks: 5, Hash: []byte{1, 2}}, []string{"a", "b"}}, {&snapshot{Height: 1, Format: 1, Chunks: 4, Hash: []byte{1, 2}}, []string{"a", "b", "c"}}, } // Add snapshots in reverse order, to make sure the pool enforces some order. for i := len(expectSnapshots) - 1; i >= 0; i-- { for _, peerID := range expectSnapshots[i].peers { peer := &p2pmocks.Peer{} peer.On("ID").Return(p2p.ID(peerID)) _, err := pool.Add(peer, expectSnapshots[i].snapshot) require.NoError(t, err) } } // Ranked should return the snapshots in the same order ranked := pool.Ranked() assert.Len(t, ranked, len(expectSnapshots)) for i := range ranked { assert.Equal(t, expectSnapshots[i].snapshot, ranked[i]) } // Check that best snapshots are returned in expected order for i := range expectSnapshots { snapshot := expectSnapshots[i].snapshot require.Equal(t, snapshot, pool.Best()) pool.Reject(snapshot) } assert.Nil(t, pool.Best()) } func TestSnapshotPool_Reject(t *testing.T) { stateProvider := &mocks.StateProvider{} stateProvider.On("AppHash", mock.Anything).Return([]byte("app_hash"), nil) pool := newSnapshotPool(stateProvider) peer := &p2pmocks.Peer{} peer.On("ID").Return(p2p.ID("id")) snapshots := []*snapshot{ {Height: 2, Format: 2, Chunks: 1, Hash: []byte{1, 2}}, {Height: 2, Format: 1, Chunks: 1, Hash: []byte{1, 2}}, {Height: 1, Format: 2, Chunks: 1, Hash: []byte{1, 2}}, {Height: 1, Format: 1, Chunks: 1, Hash: []byte{1, 2}}, } for _, s := range snapshots { _, err := pool.Add(peer, s) require.NoError(t, err) } pool.Reject(snapshots[0]) assert.Equal(t, snapshots[1:], pool.Ranked()) added, err := pool.Add(peer, snapshots[0]) require.NoError(t, err) assert.False(t, added) added, err = pool.Add(peer, &snapshot{Height: 3, Format: 3, Chunks: 1, Hash: []byte{1}}) require.NoError(t, err) assert.True(t, added) } // nolint: dupl func TestSnapshotPool_RejectFormat(t *testing.T) { stateProvider := &mocks.StateProvider{} stateProvider.On("AppHash", mock.Anything).Return([]byte("app_hash"), nil) pool := newSnapshotPool(stateProvider) peer := &p2pmocks.Peer{} peer.On("ID").Return(p2p.ID("id")) snapshots := []*snapshot{ {Height: 2, Format: 2, Chunks: 1, Hash: []byte{1, 2}}, {Height: 2, Format: 1, Chunks: 1, Hash: []byte{1, 2}}, {Height: 1, Format: 2, Chunks: 1, Hash: []byte{1, 2}}, {Height: 1, Format: 1, Chunks: 1, Hash: []byte{1, 2}}, } for _, s := range snapshots { _, err := pool.Add(peer, s) require.NoError(t, err) } pool.RejectFormat(1) assert.Equal(t, []*snapshot{snapshots[0], snapshots[2]}, pool.Ranked()) added, err := pool.Add(peer, &snapshot{Height: 3, Format: 1, Chunks: 1, Hash: []byte{1}}) require.NoError(t, err) assert.False(t, added) assert.Equal(t, []*snapshot{snapshots[0], snapshots[2]}, pool.Ranked()) added, err = pool.Add(peer, &snapshot{Height: 3, Format: 3, Chunks: 1, Hash: []byte{1}}) require.NoError(t, err) assert.True(t, added) } func TestSnapshotPool_RejectPeer(t *testing.T) { stateProvider := &mocks.StateProvider{} stateProvider.On("AppHash", mock.Anything).Return([]byte("app_hash"), nil) pool := newSnapshotPool(stateProvider) peerA := &p2pmocks.Peer{} peerA.On("ID").Return(p2p.ID("a")) peerB := &p2pmocks.Peer{} peerB.On("ID").Return(p2p.ID("b")) s1 := &snapshot{Height: 1, Format: 1, Chunks: 1, Hash: []byte{1}} s2 := &snapshot{Height: 2, Format: 1, Chunks: 1, Hash: []byte{2}} s3 := &snapshot{Height: 3, Format: 1, Chunks: 1, Hash: []byte{2}} _, err := pool.Add(peerA, s1) require.NoError(t, err) _, err = pool.Add(peerA, s2) require.NoError(t, err) _, err = pool.Add(peerB, s2) require.NoError(t, err) _, err = pool.Add(peerB, s3) require.NoError(t, err) pool.RejectPeer(peerA.ID()) assert.Empty(t, pool.GetPeers(s1)) peers2 := pool.GetPeers(s2) assert.Len(t, peers2, 1) assert.EqualValues(t, "b", peers2[0].ID()) peers3 := pool.GetPeers(s2) assert.Len(t, peers3, 1) assert.EqualValues(t, "b", peers3[0].ID()) // it should no longer be possible to add the peer back _, err = pool.Add(peerA, s1) require.NoError(t, err) assert.Empty(t, pool.GetPeers(s1)) } func TestSnapshotPool_RemovePeer(t *testing.T) { stateProvider := &mocks.StateProvider{} stateProvider.On("AppHash", mock.Anything).Return([]byte("app_hash"), nil) pool := newSnapshotPool(stateProvider) peerA := &p2pmocks.Peer{} peerA.On("ID").Return(p2p.ID("a")) peerB := &p2pmocks.Peer{} peerB.On("ID").Return(p2p.ID("b")) s1 := &snapshot{Height: 1, Format: 1, Chunks: 1, Hash: []byte{1}} s2 := &snapshot{Height: 2, Format: 1, Chunks: 1, Hash: []byte{2}} _, err := pool.Add(peerA, s1) require.NoError(t, err) _, err = pool.Add(peerA, s2) require.NoError(t, err) _, err = pool.Add(peerB, s1) require.NoError(t, err) pool.RemovePeer(peerA.ID()) peers1 := pool.GetPeers(s1) assert.Len(t, peers1, 1) assert.EqualValues(t, "b", peers1[0].ID()) peers2 := pool.GetPeers(s2) assert.Empty(t, peers2) // it should still be possible to add the peer back _, err = pool.Add(peerA, s1) require.NoError(t, err) peers1 = pool.GetPeers(s1) assert.Len(t, peers1, 2) assert.EqualValues(t, "a", peers1[0].ID()) assert.EqualValues(t, "b", peers1[1].ID()) }