github.com/hechain20/hechain@v0.0.0-20220316014945-b544036ba106/orderer/common/channelparticipation/restapi_test.go (about) 1 /* 2 Copyright hechain. All Rights Reserved. 3 4 SPDX-License-Identifier: Apache-2.0 5 */ 6 7 package channelparticipation_test 8 9 import ( 10 "bytes" 11 "encoding/json" 12 "fmt" 13 "mime/multipart" 14 "net/http" 15 "net/http/httptest" 16 "os" 17 "path" 18 "testing" 19 20 "github.com/hechain20/hechain/orderer/common/channelparticipation" 21 "github.com/hechain20/hechain/orderer/common/channelparticipation/mocks" 22 "github.com/hechain20/hechain/orderer/common/localconfig" 23 "github.com/hechain20/hechain/orderer/common/types" 24 "github.com/hechain20/hechain/protoutil" 25 "github.com/hyperledger/fabric-protos-go/common" 26 "github.com/pkg/errors" 27 "github.com/stretchr/testify/require" 28 ) 29 30 func TestNewHTTPHandler(t *testing.T) { 31 config := localconfig.ChannelParticipation{ 32 Enabled: false, 33 } 34 h := channelparticipation.NewHTTPHandler(config, &mocks.ChannelManagement{}) 35 require.NotNilf(t, h, "cannot create handler") 36 } 37 38 func TestHTTPHandler_ServeHTTP_Disabled(t *testing.T) { 39 config := localconfig.ChannelParticipation{Enabled: false} 40 _, h := setup(config, t) 41 42 resp := httptest.NewRecorder() 43 req := httptest.NewRequest("GET", channelparticipation.URLBaseV1, nil) 44 h.ServeHTTP(resp, req) 45 checkErrorResponse(t, http.StatusServiceUnavailable, "channel participation API is disabled", resp) 46 } 47 48 func TestHTTPHandler_ServeHTTP_InvalidMethods(t *testing.T) { 49 config := localconfig.ChannelParticipation{Enabled: true} 50 _, h := setup(config, t) 51 invalidMethods := []string{http.MethodConnect, http.MethodHead, http.MethodOptions, http.MethodPatch, http.MethodPut, http.MethodTrace} 52 53 t.Run("on /channels/ch-id", func(t *testing.T) { 54 invalidMethodsExt := append(invalidMethods, http.MethodPost) 55 for _, method := range invalidMethodsExt { 56 resp := httptest.NewRecorder() 57 req := httptest.NewRequest(method, path.Join(channelparticipation.URLBaseV1Channels, "ch-id"), nil) 58 h.ServeHTTP(resp, req) 59 checkErrorResponse(t, http.StatusMethodNotAllowed, fmt.Sprintf("invalid request method: %s", method), resp) 60 require.Equal(t, "GET, DELETE", resp.Result().Header.Get("Allow"), "%s", method) 61 } 62 }) 63 64 t.Run("on /channels", func(t *testing.T) { 65 invalidMethodsExt := append(invalidMethods, http.MethodDelete) 66 for _, method := range invalidMethodsExt { 67 resp := httptest.NewRecorder() 68 req := httptest.NewRequest(method, channelparticipation.URLBaseV1Channels, nil) 69 h.ServeHTTP(resp, req) 70 checkErrorResponse(t, http.StatusMethodNotAllowed, fmt.Sprintf("invalid request method: %s", method), resp) 71 require.Equal(t, "GET, POST", resp.Result().Header.Get("Allow"), "%s", method) 72 } 73 }) 74 } 75 76 func TestHTTPHandler_ServeHTTP_ListErrors(t *testing.T) { 77 config := localconfig.ChannelParticipation{Enabled: true} 78 _, h := setup(config, t) 79 80 t.Run("bad base", func(t *testing.T) { 81 resp := httptest.NewRecorder() 82 req := httptest.NewRequest(http.MethodGet, "/oops", nil) 83 h.ServeHTTP(resp, req) 84 require.Equal(t, http.StatusNotFound, resp.Result().StatusCode) 85 }) 86 87 t.Run("bad resource", func(t *testing.T) { 88 resp := httptest.NewRecorder() 89 req := httptest.NewRequest(http.MethodGet, channelparticipation.URLBaseV1+"oops", nil) 90 h.ServeHTTP(resp, req) 91 require.Equal(t, http.StatusNotFound, resp.Result().StatusCode) 92 }) 93 94 t.Run("bad channel ID", func(t *testing.T) { 95 resp := httptest.NewRecorder() 96 req := httptest.NewRequest(http.MethodGet, channelparticipation.URLBaseV1Channels+"/no/slash", nil) 97 h.ServeHTTP(resp, req) 98 require.Equal(t, http.StatusNotFound, resp.Result().StatusCode) 99 }) 100 101 t.Run("illegal character in channel ID", func(t *testing.T) { 102 resp := httptest.NewRecorder() 103 req := httptest.NewRequest(http.MethodGet, channelparticipation.URLBaseV1Channels+"/Oops", nil) 104 h.ServeHTTP(resp, req) 105 checkErrorResponse(t, http.StatusBadRequest, "invalid channel ID: 'Oops' contains illegal characters", resp) 106 }) 107 108 t.Run("bad Accept header", func(t *testing.T) { 109 resp := httptest.NewRecorder() 110 req := httptest.NewRequest(http.MethodGet, channelparticipation.URLBaseV1Channels+"/ok", nil) 111 req.Header.Set("Accept", "text/html") 112 h.ServeHTTP(resp, req) 113 checkErrorResponse(t, http.StatusNotAcceptable, "response Content-Type is application/json only", resp) 114 }) 115 } 116 117 func TestHTTPHandler_ServeHTTP_ListAll(t *testing.T) { 118 config := localconfig.ChannelParticipation{Enabled: true} 119 fakeManager, h := setup(config, t) 120 121 t.Run("two channels", func(t *testing.T) { 122 list := types.ChannelList{ 123 Channels: []types.ChannelInfoShort{ 124 {Name: "app-channel1", URL: ""}, 125 {Name: "app-channel2", URL: ""}, 126 }, 127 SystemChannel: &types.ChannelInfoShort{Name: "system-channel", URL: ""}, 128 } 129 fakeManager.ChannelListReturns(list) 130 resp := httptest.NewRecorder() 131 req := httptest.NewRequest(http.MethodGet, channelparticipation.URLBaseV1Channels, nil) 132 h.ServeHTTP(resp, req) 133 require.Equal(t, http.StatusOK, resp.Result().StatusCode) 134 require.Equal(t, "application/json", resp.Result().Header.Get("Content-Type")) 135 require.Equal(t, "no-store", resp.Result().Header.Get("Cache-Control")) 136 137 listAll := &types.ChannelList{} 138 err := json.Unmarshal(resp.Body.Bytes(), listAll) 139 require.NoError(t, err, "cannot be unmarshaled") 140 require.Equal(t, 2, len(listAll.Channels)) 141 require.Equal(t, list.SystemChannel, listAll.SystemChannel) 142 m := make(map[string]bool) 143 for _, item := range listAll.Channels { 144 m[item.Name] = true 145 require.Equal(t, channelparticipation.URLBaseV1Channels+"/"+item.Name, item.URL) 146 } 147 require.True(t, m["app-channel1"]) 148 require.True(t, m["app-channel2"]) 149 }) 150 151 t.Run("no channels, empty channels", func(t *testing.T) { 152 list := types.ChannelList{ 153 Channels: []types.ChannelInfoShort{}, 154 } 155 fakeManager.ChannelListReturns(list) 156 resp := httptest.NewRecorder() 157 req := httptest.NewRequest(http.MethodGet, channelparticipation.URLBaseV1Channels, nil) 158 h.ServeHTTP(resp, req) 159 require.Equal(t, http.StatusOK, resp.Result().StatusCode) 160 require.Equal(t, "application/json", resp.Result().Header.Get("Content-Type")) 161 require.Equal(t, "no-store", resp.Result().Header.Get("Cache-Control")) 162 163 listAll := &types.ChannelList{} 164 err := json.Unmarshal(resp.Body.Bytes(), listAll) 165 require.NoError(t, err, "cannot be unmarshaled") 166 require.Equal(t, 0, len(listAll.Channels)) 167 require.NotNil(t, listAll.Channels) 168 require.Nil(t, listAll.SystemChannel) 169 }) 170 171 t.Run("no channels, Accept ok", func(t *testing.T) { 172 list := types.ChannelList{} 173 fakeManager.ChannelListReturns(list) 174 175 for _, accept := range []string{"application/json", "application/*", "*/*"} { 176 resp := httptest.NewRecorder() 177 req := httptest.NewRequest(http.MethodGet, channelparticipation.URLBaseV1Channels, nil) 178 req.Header.Set("Accept", accept) 179 h.ServeHTTP(resp, req) 180 require.Equal(t, http.StatusOK, resp.Result().StatusCode, "Accept: %s", accept) 181 require.Equal(t, "application/json", resp.Result().Header.Get("Content-Type")) 182 require.Equal(t, "no-store", resp.Result().Header.Get("Cache-Control")) 183 184 listAll := &types.ChannelList{} 185 err := json.Unmarshal(resp.Body.Bytes(), listAll) 186 require.NoError(t, err, "cannot be unmarshaled") 187 require.Equal(t, 0, len(listAll.Channels)) 188 require.Nil(t, listAll.Channels) 189 require.Nil(t, listAll.SystemChannel) 190 } 191 }) 192 193 t.Run("redirect from base V1 URL", func(t *testing.T) { 194 resp := httptest.NewRecorder() 195 req := httptest.NewRequest(http.MethodGet, channelparticipation.URLBaseV1, nil) 196 h.ServeHTTP(resp, req) 197 require.Equal(t, http.StatusFound, resp.Result().StatusCode) 198 require.Equal(t, channelparticipation.URLBaseV1Channels, resp.Result().Header.Get("Location")) 199 }) 200 } 201 202 func TestHTTPHandler_ServeHTTP_ListSingle(t *testing.T) { 203 config := localconfig.ChannelParticipation{Enabled: true} 204 fakeManager, h := setup(config, t) 205 require.NotNilf(t, h, "cannot create handler") 206 207 t.Run("channel exists", func(t *testing.T) { 208 fakeManager.ChannelInfoReturns(types.ChannelInfo{ 209 Name: "app-channel", 210 ConsensusRelation: "consenter", 211 Status: "active", 212 Height: 3, 213 }, nil) 214 resp := httptest.NewRecorder() 215 req := httptest.NewRequest(http.MethodGet, channelparticipation.URLBaseV1Channels+"/app-channel", nil) 216 h.ServeHTTP(resp, req) 217 require.Equal(t, http.StatusOK, resp.Result().StatusCode) 218 require.Equal(t, "application/json", resp.Result().Header.Get("Content-Type")) 219 require.Equal(t, "no-store", resp.Result().Header.Get("Cache-Control")) 220 221 infoResp := types.ChannelInfo{} 222 err := json.Unmarshal(resp.Body.Bytes(), &infoResp) 223 require.NoError(t, err, "cannot be unmarshaled") 224 require.Equal(t, types.ChannelInfo{ 225 Name: "app-channel", 226 URL: channelparticipation.URLBaseV1Channels + "/app-channel", 227 ConsensusRelation: "consenter", 228 Status: "active", 229 Height: 3, 230 }, infoResp) 231 }) 232 233 t.Run("channel does not exists", func(t *testing.T) { 234 fakeManager.ChannelInfoReturns(types.ChannelInfo{}, errors.New("not found")) 235 resp := httptest.NewRecorder() 236 req := httptest.NewRequest(http.MethodGet, channelparticipation.URLBaseV1Channels+"/app-channel", nil) 237 h.ServeHTTP(resp, req) 238 checkErrorResponse(t, http.StatusNotFound, "not found", resp) 239 }) 240 } 241 242 func TestHTTPHandler_ServeHTTP_Join(t *testing.T) { 243 config := localconfig.ChannelParticipation{ 244 Enabled: true, 245 MaxRequestBodySize: 1024 * 1024, 246 } 247 248 t.Run("created ok", func(t *testing.T) { 249 fakeManager, h := setup(config, t) 250 fakeManager.JoinChannelReturns(types.ChannelInfo{ 251 Name: "app-channel", 252 ConsensusRelation: "consenter", 253 Status: "active", 254 Height: 1, 255 }, nil) 256 257 resp := httptest.NewRecorder() 258 req := genJoinRequestFormData(t, validBlockBytes("ch-id")) 259 h.ServeHTTP(resp, req) 260 require.Equal(t, http.StatusCreated, resp.Result().StatusCode) 261 require.Equal(t, "application/json", resp.Result().Header.Get("Content-Type")) 262 263 infoResp := types.ChannelInfo{} 264 err := json.Unmarshal(resp.Body.Bytes(), &infoResp) 265 require.NoError(t, err, "cannot be unmarshaled") 266 require.Equal(t, types.ChannelInfo{ 267 Name: "app-channel", 268 URL: channelparticipation.URLBaseV1Channels + "/app-channel", 269 ConsensusRelation: "consenter", 270 Status: "active", 271 Height: 1, 272 }, infoResp) 273 }) 274 275 t.Run("Error: System Channel Exists", func(t *testing.T) { 276 fakeManager, h := setup(config, t) 277 fakeManager.JoinChannelReturns(types.ChannelInfo{}, types.ErrSystemChannelExists) 278 resp := httptest.NewRecorder() 279 req := genJoinRequestFormData(t, validBlockBytes("ch-id")) 280 h.ServeHTTP(resp, req) 281 checkErrorResponse(t, http.StatusMethodNotAllowed, "cannot join: system channel exists", resp) 282 require.Equal(t, "GET", resp.Result().Header.Get("Allow")) 283 }) 284 285 t.Run("Error: Channel Exists", func(t *testing.T) { 286 fakeManager, h := setup(config, t) 287 fakeManager.JoinChannelReturns(types.ChannelInfo{}, types.ErrChannelAlreadyExists) 288 resp := httptest.NewRecorder() 289 req := genJoinRequestFormData(t, validBlockBytes("ch-id")) 290 h.ServeHTTP(resp, req) 291 checkErrorResponse(t, http.StatusMethodNotAllowed, "cannot join: channel already exists", resp) 292 require.Equal(t, "GET, DELETE", resp.Result().Header.Get("Allow")) 293 }) 294 295 t.Run("Error: App Channels Exist", func(t *testing.T) { 296 fakeManager, h := setup(config, t) 297 fakeManager.JoinChannelReturns(types.ChannelInfo{}, types.ErrAppChannelsAlreadyExists) 298 resp := httptest.NewRecorder() 299 req := genJoinRequestFormData(t, validBlockBytes("ch-id")) 300 h.ServeHTTP(resp, req) 301 checkErrorResponse(t, http.StatusForbidden, "cannot join: application channels already exist", resp) 302 }) 303 304 t.Run("Error: Channel Pending Removal", func(t *testing.T) { 305 fakeManager, h := setup(config, t) 306 fakeManager.JoinChannelReturns(types.ChannelInfo{}, types.ErrChannelPendingRemoval) 307 resp := httptest.NewRecorder() 308 req := genJoinRequestFormData(t, validBlockBytes("ch-id")) 309 h.ServeHTTP(resp, req) 310 checkErrorResponse(t, http.StatusConflict, "cannot join: channel pending removal", resp) 311 }) 312 313 t.Run("bad body - not a block", func(t *testing.T) { 314 _, h := setup(config, t) 315 resp := httptest.NewRecorder() 316 req := genJoinRequestFormData(t, []byte{1, 2, 3, 4}) 317 h.ServeHTTP(resp, req) 318 checkErrorResponse(t, http.StatusBadRequest, "cannot unmarshal file part config-block into a block: proto: common.Block: illegal tag 0 (wire type 1)", resp) 319 }) 320 321 t.Run("bad body - invalid join block", func(t *testing.T) { 322 _, h := setup(config, t) 323 resp := httptest.NewRecorder() 324 req := genJoinRequestFormData(t, []byte{}) 325 h.ServeHTTP(resp, req) 326 checkErrorResponse(t, http.StatusBadRequest, "invalid join block: block is not a config block", resp) 327 }) 328 329 t.Run("content type mismatch", func(t *testing.T) { 330 _, h := setup(config, t) 331 resp := httptest.NewRecorder() 332 req := httptest.NewRequest(http.MethodPost, channelparticipation.URLBaseV1Channels, nil) 333 req.Header.Set("Content-Type", "text/plain") 334 h.ServeHTTP(resp, req) 335 checkErrorResponse(t, http.StatusBadRequest, "unsupported Content-Type: [text/plain]", resp) 336 }) 337 338 t.Run("form-data: bad form - no boundary", func(t *testing.T) { 339 _, h := setup(config, t) 340 resp := httptest.NewRecorder() 341 342 joinBody := new(bytes.Buffer) 343 writer := multipart.NewWriter(joinBody) 344 part, err := writer.CreateFormFile(channelparticipation.FormDataConfigBlockKey, "join-config.block") 345 require.NoError(t, err) 346 part.Write([]byte{}) 347 err = writer.Close() 348 require.NoError(t, err) 349 350 req := httptest.NewRequest(http.MethodPost, channelparticipation.URLBaseV1Channels, joinBody) 351 req.Header.Set("Content-Type", "multipart/form-data") // missing boundary 352 353 h.ServeHTTP(resp, req) 354 checkErrorResponse(t, http.StatusBadRequest, "cannot read form from request body: multipart: boundary is empty", resp) 355 }) 356 357 t.Run("form-data: bad form - no key", func(t *testing.T) { 358 _, h := setup(config, t) 359 resp := httptest.NewRecorder() 360 361 joinBody := new(bytes.Buffer) 362 writer := multipart.NewWriter(joinBody) 363 part, err := writer.CreateFormFile("bad-key", "join-config.block") 364 require.NoError(t, err) 365 part.Write([]byte{}) 366 err = writer.Close() 367 require.NoError(t, err) 368 369 req := httptest.NewRequest(http.MethodPost, channelparticipation.URLBaseV1Channels, joinBody) 370 req.Header.Set("Content-Type", writer.FormDataContentType()) 371 372 h.ServeHTTP(resp, req) 373 checkErrorResponse(t, http.StatusBadRequest, "form does not contains part key: config-block", resp) 374 }) 375 376 t.Run("form-data: bad form - too many parts", func(t *testing.T) { 377 _, h := setup(config, t) 378 resp := httptest.NewRecorder() 379 380 joinBody := new(bytes.Buffer) 381 writer := multipart.NewWriter(joinBody) 382 part, err := writer.CreateFormFile(channelparticipation.FormDataConfigBlockKey, "join-config.block") 383 require.NoError(t, err) 384 part.Write([]byte{}) 385 part, err = writer.CreateFormField("not-wanted") 386 require.NoError(t, err) 387 part.Write([]byte("something")) 388 err = writer.Close() 389 require.NoError(t, err) 390 391 req := httptest.NewRequest(http.MethodPost, channelparticipation.URLBaseV1Channels, joinBody) 392 req.Header.Set("Content-Type", writer.FormDataContentType()) 393 394 h.ServeHTTP(resp, req) 395 checkErrorResponse(t, http.StatusBadRequest, "form contains too many parts", resp) 396 }) 397 398 t.Run("body larger that MaxRequestBodySize", func(t *testing.T) { 399 config := localconfig.ChannelParticipation{ 400 Enabled: true, 401 MaxRequestBodySize: 1, 402 } 403 _, h := setup(config, t) 404 resp := httptest.NewRecorder() 405 req := genJoinRequestFormData(t, []byte{1, 2, 3, 4}) 406 h.ServeHTTP(resp, req) 407 checkErrorResponse(t, http.StatusBadRequest, "cannot read form from request body: multipart: NextPart: http: request body too large", resp) 408 }) 409 } 410 411 func TestHTTPHandler_ServeHTTP_Remove(t *testing.T) { 412 config := localconfig.ChannelParticipation{Enabled: true} 413 fakeManager, h := setup(config, t) 414 415 type testDef struct { 416 name string 417 channel string 418 fakeReturns error 419 expectedCode int 420 expectedErr error 421 } 422 423 testCases := []testDef{ 424 { 425 name: "success", 426 channel: "my-channel", 427 fakeReturns: nil, 428 expectedCode: http.StatusNoContent, 429 expectedErr: nil, 430 }, 431 { 432 name: "bad channel ID", 433 channel: "My-Channel", 434 fakeReturns: nil, 435 expectedCode: http.StatusBadRequest, 436 expectedErr: errors.New("invalid channel ID: 'My-Channel' contains illegal characters"), 437 }, 438 { 439 name: "channel does not exist", 440 channel: "my-channel", 441 fakeReturns: types.ErrChannelNotExist, 442 expectedCode: http.StatusNotFound, 443 expectedErr: errors.Wrap(types.ErrChannelNotExist, "cannot remove"), 444 }, 445 { 446 name: "some other error", 447 channel: "my-channel", 448 fakeReturns: os.ErrInvalid, 449 expectedCode: http.StatusBadRequest, 450 expectedErr: errors.Wrap(os.ErrInvalid, "cannot remove"), 451 }, 452 } 453 454 for _, testCase := range testCases { 455 t.Run(testCase.name, func(t *testing.T) { 456 fakeManager.RemoveChannelReturns(testCase.fakeReturns) 457 resp := httptest.NewRecorder() 458 target := path.Join(channelparticipation.URLBaseV1Channels, testCase.channel) 459 req := httptest.NewRequest(http.MethodDelete, target, nil) 460 h.ServeHTTP(resp, req) 461 462 if testCase.expectedErr == nil { 463 require.Equal(t, testCase.expectedCode, resp.Result().StatusCode) 464 require.Equal(t, 0, resp.Body.Len(), "empty body") 465 } else { 466 checkErrorResponse(t, testCase.expectedCode, testCase.expectedErr.Error(), resp) 467 } 468 }) 469 } 470 471 t.Run("Error: System Channel Exists", func(t *testing.T) { 472 fakeManager, h := setup(config, t) 473 fakeManager.RemoveChannelReturns(types.ErrSystemChannelExists) 474 resp := httptest.NewRecorder() 475 req := httptest.NewRequest(http.MethodDelete, path.Join(channelparticipation.URLBaseV1Channels, "my-channel"), nil) 476 h.ServeHTTP(resp, req) 477 checkErrorResponse(t, http.StatusMethodNotAllowed, "cannot remove: system channel exists", resp) 478 require.Equal(t, "GET", resp.Result().Header.Get("Allow")) 479 }) 480 481 t.Run("Error: Channel Pending Removal", func(t *testing.T) { 482 fakeManager, h := setup(config, t) 483 fakeManager.RemoveChannelReturns(types.ErrChannelPendingRemoval) 484 resp := httptest.NewRecorder() 485 req := httptest.NewRequest(http.MethodDelete, path.Join(channelparticipation.URLBaseV1Channels, "my-channel"), nil) 486 h.ServeHTTP(resp, req) 487 checkErrorResponse(t, http.StatusConflict, "cannot remove: channel pending removal", resp) 488 }) 489 } 490 491 func setup(config localconfig.ChannelParticipation, t *testing.T) (*mocks.ChannelManagement, *channelparticipation.HTTPHandler) { 492 fakeManager := &mocks.ChannelManagement{} 493 h := channelparticipation.NewHTTPHandler(config, fakeManager) 494 require.NotNilf(t, h, "cannot create handler") 495 return fakeManager, h 496 } 497 498 func checkErrorResponse(t *testing.T, expectedCode int, expectedErrMsg string, resp *httptest.ResponseRecorder) { 499 require.Equal(t, expectedCode, resp.Result().StatusCode) 500 501 headerArray, headerOK := resp.Result().Header["Content-Type"] 502 require.True(t, headerOK) 503 require.Len(t, headerArray, 1) 504 require.Equal(t, "application/json", headerArray[0]) 505 506 decoder := json.NewDecoder(resp.Body) 507 respErr := &types.ErrorResponse{} 508 err := decoder.Decode(respErr) 509 require.NoError(t, err, "body: %s", resp.Body.String()) 510 require.Equal(t, expectedErrMsg, respErr.Error) 511 } 512 513 func genJoinRequestFormData(t *testing.T, blockBytes []byte) *http.Request { 514 joinBody := new(bytes.Buffer) 515 writer := multipart.NewWriter(joinBody) 516 part, err := writer.CreateFormFile(channelparticipation.FormDataConfigBlockKey, "join-config.block") 517 require.NoError(t, err) 518 part.Write(blockBytes) 519 err = writer.Close() 520 require.NoError(t, err) 521 522 req := httptest.NewRequest(http.MethodPost, channelparticipation.URLBaseV1Channels, joinBody) 523 req.Header.Set("Content-Type", writer.FormDataContentType()) 524 525 return req 526 } 527 528 func validBlockBytes(channelID string) []byte { 529 blockBytes := protoutil.MarshalOrPanic(blockWithGroups(map[string]*common.ConfigGroup{ 530 "Application": {}, 531 }, channelID)) 532 return blockBytes 533 }