github.com/Finschia/ostracon@v1.1.5/statesync/reactor_test.go (about) 1 package statesync 2 3 import ( 4 "testing" 5 "time" 6 7 "github.com/gogo/protobuf/proto" 8 "github.com/stretchr/testify/assert" 9 "github.com/stretchr/testify/mock" 10 "github.com/stretchr/testify/require" 11 12 abci "github.com/tendermint/tendermint/abci/types" 13 tmstate "github.com/tendermint/tendermint/proto/tendermint/state" 14 ssproto "github.com/tendermint/tendermint/proto/tendermint/statesync" 15 tmversion "github.com/tendermint/tendermint/proto/tendermint/version" 16 17 "github.com/Finschia/ostracon/config" 18 "github.com/Finschia/ostracon/libs/log" 19 "github.com/Finschia/ostracon/p2p" 20 p2pmocks "github.com/Finschia/ostracon/p2p/mocks" 21 "github.com/Finschia/ostracon/proxy" 22 proxymocks "github.com/Finschia/ostracon/proxy/mocks" 23 sm "github.com/Finschia/ostracon/state" 24 "github.com/Finschia/ostracon/statesync/mocks" 25 "github.com/Finschia/ostracon/types" 26 "github.com/Finschia/ostracon/version" 27 ) 28 29 type Peer struct { 30 *p2pmocks.Peer 31 *p2pmocks.EnvelopeSender 32 } 33 34 func TestReactor_Receive_ChunkRequest(t *testing.T) { 35 testcases := map[string]struct { 36 request *ssproto.ChunkRequest 37 chunk []byte 38 expectResponse *ssproto.ChunkResponse 39 }{ 40 "chunk is returned": { 41 &ssproto.ChunkRequest{Height: 1, Format: 1, Index: 1}, 42 []byte{1, 2, 3}, 43 &ssproto.ChunkResponse{Height: 1, Format: 1, Index: 1, Chunk: []byte{1, 2, 3}}}, 44 "empty chunk is returned, as nil": { 45 &ssproto.ChunkRequest{Height: 1, Format: 1, Index: 1}, 46 []byte{}, 47 &ssproto.ChunkResponse{Height: 1, Format: 1, Index: 1, Chunk: nil}}, 48 "nil (missing) chunk is returned as missing": { 49 &ssproto.ChunkRequest{Height: 1, Format: 1, Index: 1}, 50 nil, 51 &ssproto.ChunkResponse{Height: 1, Format: 1, Index: 1, Missing: true}, 52 }, 53 } 54 55 for name, tc := range testcases { 56 tc := tc 57 t.Run(name, func(t *testing.T) { 58 // Mock ABCI connection to return local snapshots 59 conn := &proxymocks.AppConnSnapshot{} 60 conn.On("LoadSnapshotChunkSync", abci.RequestLoadSnapshotChunk{ 61 Height: tc.request.Height, 62 Format: tc.request.Format, 63 Chunk: tc.request.Index, 64 }).Return(&abci.ResponseLoadSnapshotChunk{Chunk: tc.chunk}, nil) 65 66 // Mock peer to store response, if found 67 peer := &Peer{Peer: &p2pmocks.Peer{}, EnvelopeSender: &p2pmocks.EnvelopeSender{}} 68 peer.Peer.On("ID").Return(p2p.ID("id")) 69 var response *ssproto.ChunkResponse 70 if tc.expectResponse != nil { 71 peer.EnvelopeSender.On("SendEnvelope", mock.MatchedBy(func(i interface{}) bool { 72 e, ok := i.(p2p.Envelope) 73 return ok && e.ChannelID == ChunkChannel 74 })).Run(func(args mock.Arguments) { 75 e := args[0].(p2p.Envelope) 76 77 // Marshal to simulate a wire roundtrip. 78 bz, err := proto.Marshal(e.Message) 79 require.NoError(t, err) 80 err = proto.Unmarshal(bz, e.Message) 81 require.NoError(t, err) 82 response = e.Message.(*ssproto.ChunkResponse) 83 }).Return(true) 84 } 85 86 // Start a reactor and send a ssproto.ChunkRequest, then wait for and check response 87 cfg := config.DefaultStateSyncConfig() 88 r := NewReactor(*cfg, conn, nil, true, 1000) 89 err := r.Start() 90 require.NoError(t, err) 91 t.Cleanup(func() { 92 if err := r.Stop(); err != nil { 93 t.Error(err) 94 } 95 }) 96 97 r.ReceiveEnvelope(p2p.Envelope{ 98 ChannelID: ChunkChannel, 99 Src: peer, 100 Message: tc.request, 101 }) 102 time.Sleep(100 * time.Millisecond) 103 assert.Equal(t, tc.expectResponse, response) 104 105 conn.AssertExpectations(t) 106 peer.Peer.AssertExpectations(t) 107 peer.EnvelopeSender.AssertExpectations(t) 108 }) 109 } 110 } 111 112 func TestReactor_Receive_SnapshotsRequest(t *testing.T) { 113 testcases := map[string]struct { 114 snapshots []*abci.Snapshot 115 expectResponses []*ssproto.SnapshotsResponse 116 }{ 117 "no snapshots": {nil, []*ssproto.SnapshotsResponse{}}, 118 ">10 unordered snapshots": { 119 []*abci.Snapshot{ 120 {Height: 1, Format: 2, Chunks: 7, Hash: []byte{1, 2}, Metadata: []byte{1}}, 121 {Height: 2, Format: 2, Chunks: 7, Hash: []byte{2, 2}, Metadata: []byte{2}}, 122 {Height: 3, Format: 2, Chunks: 7, Hash: []byte{3, 2}, Metadata: []byte{3}}, 123 {Height: 1, Format: 1, Chunks: 7, Hash: []byte{1, 1}, Metadata: []byte{4}}, 124 {Height: 2, Format: 1, Chunks: 7, Hash: []byte{2, 1}, Metadata: []byte{5}}, 125 {Height: 3, Format: 1, Chunks: 7, Hash: []byte{3, 1}, Metadata: []byte{6}}, 126 {Height: 1, Format: 4, Chunks: 7, Hash: []byte{1, 4}, Metadata: []byte{7}}, 127 {Height: 2, Format: 4, Chunks: 7, Hash: []byte{2, 4}, Metadata: []byte{8}}, 128 {Height: 3, Format: 4, Chunks: 7, Hash: []byte{3, 4}, Metadata: []byte{9}}, 129 {Height: 1, Format: 3, Chunks: 7, Hash: []byte{1, 3}, Metadata: []byte{10}}, 130 {Height: 2, Format: 3, Chunks: 7, Hash: []byte{2, 3}, Metadata: []byte{11}}, 131 {Height: 3, Format: 3, Chunks: 7, Hash: []byte{3, 3}, Metadata: []byte{12}}, 132 }, 133 []*ssproto.SnapshotsResponse{ 134 {Height: 3, Format: 4, Chunks: 7, Hash: []byte{3, 4}, Metadata: []byte{9}}, 135 {Height: 3, Format: 3, Chunks: 7, Hash: []byte{3, 3}, Metadata: []byte{12}}, 136 {Height: 3, Format: 2, Chunks: 7, Hash: []byte{3, 2}, Metadata: []byte{3}}, 137 {Height: 3, Format: 1, Chunks: 7, Hash: []byte{3, 1}, Metadata: []byte{6}}, 138 {Height: 2, Format: 4, Chunks: 7, Hash: []byte{2, 4}, Metadata: []byte{8}}, 139 {Height: 2, Format: 3, Chunks: 7, Hash: []byte{2, 3}, Metadata: []byte{11}}, 140 {Height: 2, Format: 2, Chunks: 7, Hash: []byte{2, 2}, Metadata: []byte{2}}, 141 {Height: 2, Format: 1, Chunks: 7, Hash: []byte{2, 1}, Metadata: []byte{5}}, 142 {Height: 1, Format: 4, Chunks: 7, Hash: []byte{1, 4}, Metadata: []byte{7}}, 143 {Height: 1, Format: 3, Chunks: 7, Hash: []byte{1, 3}, Metadata: []byte{10}}, 144 }, 145 }, 146 } 147 148 for name, tc := range testcases { 149 tc := tc 150 t.Run(name, func(t *testing.T) { 151 // Mock ABCI connection to return local snapshots 152 conn := &proxymocks.AppConnSnapshot{} 153 conn.On("ListSnapshotsSync", abci.RequestListSnapshots{}).Return(&abci.ResponseListSnapshots{ 154 Snapshots: tc.snapshots, 155 }, nil) 156 157 // Mock peer to catch responses and store them in a slice 158 responses := []*ssproto.SnapshotsResponse{} 159 peer := &Peer{Peer: &p2pmocks.Peer{}, EnvelopeSender: &p2pmocks.EnvelopeSender{}} 160 if len(tc.expectResponses) > 0 { 161 peer.Peer.On("ID").Return(p2p.ID("id")) 162 peer.EnvelopeSender.On("SendEnvelope", mock.MatchedBy(func(i interface{}) bool { 163 e, ok := i.(p2p.Envelope) 164 return ok && e.ChannelID == SnapshotChannel 165 })).Run(func(args mock.Arguments) { 166 e := args[0].(p2p.Envelope) 167 168 // Marshal to simulate a wire roundtrip. 169 bz, err := proto.Marshal(e.Message) 170 require.NoError(t, err) 171 err = proto.Unmarshal(bz, e.Message) 172 require.NoError(t, err) 173 responses = append(responses, e.Message.(*ssproto.SnapshotsResponse)) 174 }).Return(true) 175 } 176 177 // Start a reactor and send a SnapshotsRequestMessage, then wait for and check responses 178 cfg := config.DefaultStateSyncConfig() 179 r := NewReactor(*cfg, conn, nil, true, 1000) 180 err := r.Start() 181 require.NoError(t, err) 182 t.Cleanup(func() { 183 if err := r.Stop(); err != nil { 184 t.Error(err) 185 } 186 }) 187 188 r.ReceiveEnvelope(p2p.Envelope{ 189 ChannelID: SnapshotChannel, 190 Src: peer, 191 Message: &ssproto.SnapshotsRequest{}, 192 }) 193 time.Sleep(100 * time.Millisecond) 194 assert.Equal(t, tc.expectResponses, responses) 195 196 conn.AssertExpectations(t) 197 peer.Peer.AssertExpectations(t) 198 peer.EnvelopeSender.AssertExpectations(t) 199 }) 200 } 201 } 202 203 func TestLegacyReactorReceiveBasic(t *testing.T) { 204 cfg := config.DefaultStateSyncConfig() 205 conn := &proxymocks.AppConnSnapshot{} 206 reactor := NewReactor(*cfg, conn, nil, true, 1000) 207 peer := p2p.CreateRandomPeer(false) 208 209 reactor.InitPeer(peer) 210 reactor.AddPeer(peer) 211 m := &ssproto.ChunkRequest{Height: 1, Format: 1, Index: 1} 212 wm := m.Wrap() 213 msg, err := proto.Marshal(wm) 214 assert.NoError(t, err) 215 216 assert.NotPanics(t, func() { 217 reactor.Receive(ChunkChannel, peer, msg) 218 }) 219 } 220 221 func makeTestStateSyncReactor( 222 t *testing.T, appHash string, height int64, snapshot *snapshot, chunks []*chunk) *Reactor { 223 connSnapshot := makeMockAppConnSnapshot(appHash, snapshot, chunks) 224 connQuery := makeMockAppConnQuery(appHash, height) 225 226 p2pConfig := config.DefaultP2PConfig() 227 p2pConfig.PexReactor = true 228 p2pConfig.AllowDuplicateIP = true 229 230 name := "STATE_SYNC_REACTOR_FOR_TEST" 231 size := 2 232 reactors := make([]*Reactor, size) 233 initSwitch := func(i int, s *p2p.Switch, p2pConfig *config.P2PConfig) *p2p.Switch { 234 logger := log.TestingLogger() 235 cfg := config.DefaultStateSyncConfig() 236 reactors[i] = NewReactor(*cfg, connSnapshot, connQuery, true, 1000) 237 reactors[i].SetLogger(logger) 238 reactors[i].SetSwitch(s) 239 240 s.AddReactor(name, reactors[i]) 241 s.SetLogger(logger) 242 return s 243 } 244 switches := p2p.MakeConnectedSwitches(p2pConfig, size, initSwitch, p2p.Connect2Switches) 245 246 t.Cleanup(func() { 247 for i := 0; i < size; i++ { 248 if err := switches[i].Stop(); err != nil { 249 t.Error(err) 250 } 251 } 252 }) 253 254 return reactors[0] 255 } 256 257 func makeMockAppConnSnapshot(appHash string, snapshot *snapshot, chunks []*chunk) *proxymocks.AppConnSnapshot { 258 connSnapshot := &proxymocks.AppConnSnapshot{} 259 260 connSnapshot.On("OfferSnapshotSync", abci.RequestOfferSnapshot{ 261 Snapshot: &abci.Snapshot{ 262 Height: snapshot.Height, 263 Format: snapshot.Format, 264 Chunks: snapshot.Chunks, 265 Hash: snapshot.Hash, 266 }, 267 AppHash: []byte(appHash), 268 }).Return(&abci.ResponseOfferSnapshot{Result: abci.ResponseOfferSnapshot_ACCEPT}, nil) 269 270 connSnapshot.On("ListSnapshotsSync", abci.RequestListSnapshots{}).Return(&abci.ResponseListSnapshots{ 271 Snapshots: []*abci.Snapshot{{ 272 Height: snapshot.Height, 273 Format: snapshot.Format, 274 Chunks: snapshot.Chunks, 275 Hash: snapshot.Hash, 276 Metadata: snapshot.Metadata, 277 }}, 278 }, nil) 279 280 index := len(chunks) 281 for i := 0; i < index; i++ { 282 connSnapshot.On("ApplySnapshotChunkSync", 283 mock.Anything).Return(&abci.ResponseApplySnapshotChunk{Result: abci.ResponseApplySnapshotChunk_ACCEPT}, nil) 284 connSnapshot.On("LoadSnapshotChunkSync", abci.RequestLoadSnapshotChunk{ 285 Height: chunks[i].Height, Format: chunks[i].Format, Chunk: chunks[i].Index, 286 }).Return(&abci.ResponseLoadSnapshotChunk{Chunk: chunks[i].Chunk}, nil) 287 } 288 289 return connSnapshot 290 } 291 292 func makeMockAppConnQuery(appHash string, height int64) *proxymocks.AppConnQuery { 293 connQuery := &proxymocks.AppConnQuery{} 294 connQuery.On("InfoSync", proxy.RequestInfo).Return(&abci.ResponseInfo{ 295 AppVersion: testAppVersion, 296 LastBlockHeight: height, 297 LastBlockAppHash: []byte(appHash), 298 }, nil) 299 return connQuery 300 } 301 302 func makeTestStateAndCommit(appHash string, height int64) (sm.State, *types.Commit) { 303 blockHash := "block_hash" 304 305 state := sm.State{ 306 ChainID: "chain", 307 Version: tmstate.Version{ 308 Consensus: tmversion.Consensus{ 309 Block: version.BlockProtocol, 310 App: testAppVersion, 311 }, 312 313 Software: version.OCCoreSemVer, 314 }, 315 316 LastBlockHeight: height, 317 LastBlockID: types.BlockID{Hash: []byte(blockHash)}, 318 LastBlockTime: time.Now(), 319 LastResultsHash: []byte("last_results_hash"), 320 AppHash: []byte(appHash), 321 322 LastValidators: &types.ValidatorSet{}, 323 Validators: &types.ValidatorSet{}, 324 NextValidators: &types.ValidatorSet{}, 325 326 ConsensusParams: *types.DefaultConsensusParams(), 327 LastHeightConsensusParamsChanged: 1, 328 } 329 state.ConsensusParams.Version.AppVersion = testAppVersion 330 331 commit := &types.Commit{BlockID: types.BlockID{Hash: []byte(blockHash)}} 332 333 return state, commit 334 } 335 336 func makeMockStateProvider(appHash string, height uint64) *mocks.StateProvider { 337 state, commit := makeTestStateAndCommit(appHash, int64(height)) 338 339 stateProvider := &mocks.StateProvider{} 340 stateProvider.On("AppHash", mock.Anything, mock.Anything).Return([]byte(appHash), nil) 341 stateProvider.On("State", mock.Anything, height).Return(state, nil) 342 stateProvider.On("Commit", mock.Anything, height).Return(commit, nil) 343 return stateProvider 344 } 345 346 func TestSync(t *testing.T) { 347 // prepare 348 height, format := uint64(1), uint32(1) 349 appHash := "app_hash" 350 351 chunks := []*chunk{ 352 {Height: height, Format: format, Index: 0, Chunk: []byte{1, 1, 0}}, 353 {Height: height, Format: format, Index: 1, Chunk: []byte{1, 1, 1}}, 354 {Height: height, Format: format, Index: 2, Chunk: []byte{1, 1, 2}}, 355 } 356 snapshot := &snapshot{Height: height, Format: format, Chunks: uint32(len(chunks)), Hash: []byte{1, 2, 3}} 357 reactor := makeTestStateSyncReactor(t, appHash, int64(height), snapshot, chunks) 358 stateProvider := makeMockStateProvider(appHash, height) 359 360 // test 361 state, previousState, commit, err := reactor.Sync(stateProvider, 5*time.Second) 362 363 // verify 364 require.NoError(t, err) 365 require.NotNil(t, state) 366 require.NotNil(t, previousState) 367 require.NotNil(t, commit) 368 }