From 53c1b05509d4ef3b2a6c12472fe2812b413843f7 Mon Sep 17 00:00:00 2001 From: Fedor Partanskiy Date: Fri, 5 Sep 2025 14:05:58 +0300 Subject: [PATCH] added to OSN new handler for fetch a block of channel part 2 Signed-off-by: Fedor Partanskiy --- cmd/osnadmin/mocks/channel_management.go | 89 +++++++++++-- cmd/osnadmin/osnadmin_suite_test.go | 1 + .../mocks/channel_management.go | 89 +++++++++++-- .../common/channelparticipation/restapi.go | 115 +++++++++++++++-- .../channelparticipation/restapi_test.go | 119 ++++++++++++++++++ .../common/channelparticipation/validator.go | 20 +++ .../channelparticipation/validator_test.go | 60 +++++++++ orderer/common/cluster/rpc.go | 2 +- orderer/consensus/etcdraft/chain.go | 2 + swagger/swagger-fabric.json | 46 ++++++- tools/go.mod | 19 +-- tools/go.sum | 44 +++---- 12 files changed, 544 insertions(+), 62 deletions(-) diff --git a/cmd/osnadmin/mocks/channel_management.go b/cmd/osnadmin/mocks/channel_management.go index 68f4da07fec..30df5f019c5 100644 --- a/cmd/osnadmin/mocks/channel_management.go +++ b/cmd/osnadmin/mocks/channel_management.go @@ -32,6 +32,20 @@ type ChannelManagement struct { channelListReturnsOnCall map[int]struct { result1 types.ChannelList } + FetchBlockStub func(string, string) (*common.Block, error) + fetchBlockMutex sync.RWMutex + fetchBlockArgsForCall []struct { + arg1 string + arg2 string + } + fetchBlockReturns struct { + result1 *common.Block + result2 error + } + fetchBlockReturnsOnCall map[int]struct { + result1 *common.Block + result2 error + } JoinChannelStub func(string, *common.Block) (types.ChannelInfo, error) joinChannelMutex sync.RWMutex joinChannelArgsForCall []struct { @@ -192,6 +206,71 @@ func (fake *ChannelManagement) ChannelListReturnsOnCall(i int, result1 types.Cha }{result1} } +func (fake *ChannelManagement) FetchBlock(arg1 string, arg2 string) (*common.Block, error) { + fake.fetchBlockMutex.Lock() + ret, specificReturn := fake.fetchBlockReturnsOnCall[len(fake.fetchBlockArgsForCall)] + fake.fetchBlockArgsForCall = append(fake.fetchBlockArgsForCall, struct { + arg1 string + arg2 string + }{arg1, arg2}) + stub := fake.FetchBlockStub + fakeReturns := fake.fetchBlockReturns + fake.recordInvocation("FetchBlock", []interface{}{arg1, arg2}) + fake.fetchBlockMutex.Unlock() + if stub != nil { + return stub(arg1, arg2) + } + if specificReturn { + return ret.result1, ret.result2 + } + return fakeReturns.result1, fakeReturns.result2 +} + +func (fake *ChannelManagement) FetchBlockCallCount() int { + fake.fetchBlockMutex.RLock() + defer fake.fetchBlockMutex.RUnlock() + return len(fake.fetchBlockArgsForCall) +} + +func (fake *ChannelManagement) FetchBlockCalls(stub func(string, string) (*common.Block, error)) { + fake.fetchBlockMutex.Lock() + defer fake.fetchBlockMutex.Unlock() + fake.FetchBlockStub = stub +} + +func (fake *ChannelManagement) FetchBlockArgsForCall(i int) (string, string) { + fake.fetchBlockMutex.RLock() + defer fake.fetchBlockMutex.RUnlock() + argsForCall := fake.fetchBlockArgsForCall[i] + return argsForCall.arg1, argsForCall.arg2 +} + +func (fake *ChannelManagement) FetchBlockReturns(result1 *common.Block, result2 error) { + fake.fetchBlockMutex.Lock() + defer fake.fetchBlockMutex.Unlock() + fake.FetchBlockStub = nil + fake.fetchBlockReturns = struct { + result1 *common.Block + result2 error + }{result1, result2} +} + +func (fake *ChannelManagement) FetchBlockReturnsOnCall(i int, result1 *common.Block, result2 error) { + fake.fetchBlockMutex.Lock() + defer fake.fetchBlockMutex.Unlock() + fake.FetchBlockStub = nil + if fake.fetchBlockReturnsOnCall == nil { + fake.fetchBlockReturnsOnCall = make(map[int]struct { + result1 *common.Block + result2 error + }) + } + fake.fetchBlockReturnsOnCall[i] = struct { + result1 *common.Block + result2 error + }{result1, result2} +} + func (fake *ChannelManagement) JoinChannel(arg1 string, arg2 *common.Block) (types.ChannelInfo, error) { fake.joinChannelMutex.Lock() ret, specificReturn := fake.joinChannelReturnsOnCall[len(fake.joinChannelArgsForCall)] @@ -386,16 +465,6 @@ func (fake *ChannelManagement) UpdateChannelReturnsOnCall(i int, result1 types.C func (fake *ChannelManagement) Invocations() map[string][][]interface{} { fake.invocationsMutex.RLock() defer fake.invocationsMutex.RUnlock() - fake.channelInfoMutex.RLock() - defer fake.channelInfoMutex.RUnlock() - fake.channelListMutex.RLock() - defer fake.channelListMutex.RUnlock() - fake.joinChannelMutex.RLock() - defer fake.joinChannelMutex.RUnlock() - fake.removeChannelMutex.RLock() - defer fake.removeChannelMutex.RUnlock() - fake.updateChannelMutex.RLock() - defer fake.updateChannelMutex.RUnlock() copiedInvocations := map[string][][]interface{}{} for key, value := range fake.invocations { copiedInvocations[key] = value diff --git a/cmd/osnadmin/osnadmin_suite_test.go b/cmd/osnadmin/osnadmin_suite_test.go index 220a5b18fd3..cef21e5699b 100644 --- a/cmd/osnadmin/osnadmin_suite_test.go +++ b/cmd/osnadmin/osnadmin_suite_test.go @@ -23,6 +23,7 @@ type channelManagement interface { JoinChannel(channelID string, configBlock *cb.Block) (types.ChannelInfo, error) UpdateChannel(channelID string, configUpdateEnvelope *cb.Envelope) (types.ChannelInfo, error) RemoveChannel(channelID string) error + FetchBlock(channelID string, blockID string) (*cb.Block, error) } func TestOsnadmin(t *testing.T) { diff --git a/orderer/common/channelparticipation/mocks/channel_management.go b/orderer/common/channelparticipation/mocks/channel_management.go index 12af904bf16..956cf27aa73 100644 --- a/orderer/common/channelparticipation/mocks/channel_management.go +++ b/orderer/common/channelparticipation/mocks/channel_management.go @@ -33,6 +33,20 @@ type ChannelManagement struct { channelListReturnsOnCall map[int]struct { result1 types.ChannelList } + FetchBlockStub func(string, string) (*common.Block, error) + fetchBlockMutex sync.RWMutex + fetchBlockArgsForCall []struct { + arg1 string + arg2 string + } + fetchBlockReturns struct { + result1 *common.Block + result2 error + } + fetchBlockReturnsOnCall map[int]struct { + result1 *common.Block + result2 error + } JoinChannelStub func(string, *common.Block) (types.ChannelInfo, error) joinChannelMutex sync.RWMutex joinChannelArgsForCall []struct { @@ -193,6 +207,71 @@ func (fake *ChannelManagement) ChannelListReturnsOnCall(i int, result1 types.Cha }{result1} } +func (fake *ChannelManagement) FetchBlock(arg1 string, arg2 string) (*common.Block, error) { + fake.fetchBlockMutex.Lock() + ret, specificReturn := fake.fetchBlockReturnsOnCall[len(fake.fetchBlockArgsForCall)] + fake.fetchBlockArgsForCall = append(fake.fetchBlockArgsForCall, struct { + arg1 string + arg2 string + }{arg1, arg2}) + stub := fake.FetchBlockStub + fakeReturns := fake.fetchBlockReturns + fake.recordInvocation("FetchBlock", []interface{}{arg1, arg2}) + fake.fetchBlockMutex.Unlock() + if stub != nil { + return stub(arg1, arg2) + } + if specificReturn { + return ret.result1, ret.result2 + } + return fakeReturns.result1, fakeReturns.result2 +} + +func (fake *ChannelManagement) FetchBlockCallCount() int { + fake.fetchBlockMutex.RLock() + defer fake.fetchBlockMutex.RUnlock() + return len(fake.fetchBlockArgsForCall) +} + +func (fake *ChannelManagement) FetchBlockCalls(stub func(string, string) (*common.Block, error)) { + fake.fetchBlockMutex.Lock() + defer fake.fetchBlockMutex.Unlock() + fake.FetchBlockStub = stub +} + +func (fake *ChannelManagement) FetchBlockArgsForCall(i int) (string, string) { + fake.fetchBlockMutex.RLock() + defer fake.fetchBlockMutex.RUnlock() + argsForCall := fake.fetchBlockArgsForCall[i] + return argsForCall.arg1, argsForCall.arg2 +} + +func (fake *ChannelManagement) FetchBlockReturns(result1 *common.Block, result2 error) { + fake.fetchBlockMutex.Lock() + defer fake.fetchBlockMutex.Unlock() + fake.FetchBlockStub = nil + fake.fetchBlockReturns = struct { + result1 *common.Block + result2 error + }{result1, result2} +} + +func (fake *ChannelManagement) FetchBlockReturnsOnCall(i int, result1 *common.Block, result2 error) { + fake.fetchBlockMutex.Lock() + defer fake.fetchBlockMutex.Unlock() + fake.FetchBlockStub = nil + if fake.fetchBlockReturnsOnCall == nil { + fake.fetchBlockReturnsOnCall = make(map[int]struct { + result1 *common.Block + result2 error + }) + } + fake.fetchBlockReturnsOnCall[i] = struct { + result1 *common.Block + result2 error + }{result1, result2} +} + func (fake *ChannelManagement) JoinChannel(arg1 string, arg2 *common.Block) (types.ChannelInfo, error) { fake.joinChannelMutex.Lock() ret, specificReturn := fake.joinChannelReturnsOnCall[len(fake.joinChannelArgsForCall)] @@ -387,16 +466,6 @@ func (fake *ChannelManagement) UpdateChannelReturnsOnCall(i int, result1 types.C func (fake *ChannelManagement) Invocations() map[string][][]interface{} { fake.invocationsMutex.RLock() defer fake.invocationsMutex.RUnlock() - fake.channelInfoMutex.RLock() - defer fake.channelInfoMutex.RUnlock() - fake.channelListMutex.RLock() - defer fake.channelListMutex.RUnlock() - fake.joinChannelMutex.RLock() - defer fake.joinChannelMutex.RUnlock() - fake.removeChannelMutex.RLock() - defer fake.removeChannelMutex.RUnlock() - fake.updateChannelMutex.RLock() - defer fake.updateChannelMutex.RUnlock() copiedInvocations := map[string][][]interface{}{} for key, value := range fake.invocations { copiedInvocations[key] = value diff --git a/orderer/common/channelparticipation/restapi.go b/orderer/common/channelparticipation/restapi.go index f4ce12b0b83..2fbeb3f386e 100644 --- a/orderer/common/channelparticipation/restapi.go +++ b/orderer/common/channelparticipation/restapi.go @@ -31,8 +31,10 @@ const ( FormDataConfigBlockKey = "config-block" FormDataConfigUpdateEnvelopeKey = "config-update-envelope" - channelIDKey = "channelID" - urlWithChannelIDKey = URLBaseV1Channels + "/{" + channelIDKey + "}" + channelIDKey = "channelID" + blockIDKey = "blockID" + urlWithChannelIDKey = URLBaseV1Channels + "/{" + channelIDKey + "}" + urlWithChannelAndBlockIDKey = urlWithChannelIDKey + "/blocks/{" + blockIDKey + "}" ) //go:generate counterfeiter -o mocks/channel_management.go -fake-name ChannelManagement . ChannelManagement @@ -57,6 +59,9 @@ type ChannelManagement interface { // RemoveChannel instructs the orderer to remove a channel. RemoveChannel(channelID string) error + + // FetchBlock instructs the orderer to send a block of channel. + FetchBlock(channelID string, blockID string) (*cb.Block, error) } // HTTPHandler handles all the HTTP requests to the channel participation API. @@ -75,7 +80,7 @@ func NewHTTPHandler(config localconfig.ChannelParticipation, registrar ChannelMa router: mux.NewRouter(), } - // swagger:operation GET /v1/participation/channels/{channelID} channels listChannel + // swagger:operation GET /participation/v1/channels/{channelID} channels listChannel // --- // summary: Returns detailed channel information for a specific channel Ordering Service Node (OSN) has joined. // parameters: @@ -99,7 +104,7 @@ func NewHTTPHandler(config localconfig.ChannelParticipation, registrar ChannelMa handler.router.HandleFunc(urlWithChannelIDKey, handler.serveListOne).Methods(http.MethodGet) - // swagger:operation DELETE /v1/participation/channels/{channelID} channels removeChannel + // swagger:operation DELETE /participation/v1/channels/{channelID} channels removeChannel // --- // summary: Removes an Ordering Service Node (OSN) from a channel. // parameters: @@ -119,9 +124,8 @@ func NewHTTPHandler(config localconfig.ChannelParticipation, registrar ChannelMa // description: The channel is pending removal. handler.router.HandleFunc(urlWithChannelIDKey, handler.serveRemove).Methods(http.MethodDelete) - handler.router.HandleFunc(urlWithChannelIDKey, handler.serveNotAllowed) - // swagger:operation GET /v1/participation/channels channels listChannels + // swagger:operation GET /participation/v1/channels channels listChannels // --- // summary: Returns the complete list of channels an Ordering Service Node (OSN) has joined. // responses: @@ -139,7 +143,7 @@ func NewHTTPHandler(config localconfig.ChannelParticipation, registrar ChannelMa handler.router.HandleFunc(URLBaseV1Channels, handler.serveListAll).Methods(http.MethodGet) - // swagger:operation POST /v1/participation/channels channels joinChannel + // swagger:operation POST /participation/v1/channels channels joinChannel // --- // summary: Joins an Ordering Service Node (OSN) to a channel. // description: If a channel does not yet exist, it will be created. @@ -175,7 +179,7 @@ func NewHTTPHandler(config localconfig.ChannelParticipation, registrar ChannelMa "Content-Type", "multipart/form-data*") handler.router.HandleFunc(URLBaseV1Channels, handler.serveBadContentType).Methods(http.MethodPost) - // swagger:operation PUT /v1/participation/channels channels updateChannel + // swagger:operation PUT /participation/v1/channels channels updateChannel // --- // summary: Update config block of the Order Service Node (OSN) to the channel. // parameters: @@ -212,6 +216,36 @@ func NewHTTPHandler(config localconfig.ChannelParticipation, registrar ChannelMa "Content-Type", "multipart/form-data*") handler.router.HandleFunc(URLBaseV1Channels, handler.serveBadContentType).Methods(http.MethodPut) + // swagger:operation GET /participation/v1/channels/{channelID}/blocks/{blockID} channels fetchBlock + // --- + // summary: Returns a specific channel block for a specific Channel Ordering Service (OSN) node. + // parameters: + // - name: channelID + // in: path + // description: Channel ID + // required: true + // type: string + // - name: blockID + // in: path + // description: Block ID + // required: true + // type: string + // produces: + // - application/octet-stream + // responses: + // '200': + // description: Successful block download. + // schema: + // type: file + // '400': + // description: Bad request. + // '404': + // description: The channel or block does not exist. + + handler.router.HandleFunc(urlWithChannelAndBlockIDKey, handler.serveFetchBlock).Methods(http.MethodGet) + + handler.router.HandleFunc(urlWithChannelAndBlockIDKey, handler.serveNotAllowed) + handler.router.HandleFunc(urlWithChannelIDKey, handler.serveNotAllowed) handler.router.HandleFunc(URLBaseV1Channels, handler.serveNotAllowed) handler.router.HandleFunc(URLBaseV1, handler.redirectBaseV1).Methods(http.MethodGet) @@ -275,6 +309,41 @@ func (h *HTTPHandler) redirectBaseV1(resp http.ResponseWriter, req *http.Request http.Redirect(resp, req, URLBaseV1Channels, http.StatusFound) } +func (h *HTTPHandler) serveFetchBlock(resp http.ResponseWriter, req *http.Request) { + _, err := negotiateContentType(req) // Only application/json responses for now + if err != nil { + h.sendResponseJsonError(resp, http.StatusNotAcceptable, err) + return + } + + channelID, err := h.extractChannelID(req, resp) + if err != nil { + return + } + + blockID, err := h.extractBlockID(req, resp) + if err != nil { + h.sendResponseJsonError(resp, http.StatusBadRequest, err) + return + } + + block, err := h.registrar.FetchBlock(channelID, blockID) + if err != nil { + h.sendResponseJsonError(resp, http.StatusNotFound, err) + return + } + + blockBytes, err := proto.Marshal(block) + if err != nil { + h.sendResponseJsonError(resp, http.StatusInternalServerError, err) + return + } + + resp.Header().Set("Cache-Control", "no-store") + resp.Header().Set("Content-Type", "application/octet-stream") + h.sendResponseBlock(resp, blockBytes) +} + // Join a channel. // Expect multipart/form-data. func (h *HTTPHandler) serveJoin(resp http.ResponseWriter, req *http.Request) { @@ -459,6 +528,22 @@ func (h *HTTPHandler) extractChannelID(req *http.Request, resp http.ResponseWrit return channelID, nil } +func (h *HTTPHandler) extractBlockID(req *http.Request, resp http.ResponseWriter) (string, error) { + blockID, ok := mux.Vars(req)[blockIDKey] + if !ok { + err := errors.New("missing block ID") + h.sendResponseJsonError(resp, http.StatusInternalServerError, err) + return "", err + } + + if err := ValidateFetchBlockID(blockID); err != nil { + err = errors.WithMessage(err, "invalid block ID") + h.sendResponseJsonError(resp, http.StatusBadRequest, err) + return "", err + } + return blockID, nil +} + func (h *HTTPHandler) sendJoinError(err error, resp http.ResponseWriter) { h.logger.Debugf("Failed to JoinChannel: %s", err) switch err { @@ -544,6 +629,11 @@ func (h *HTTPHandler) serveBadContentType(resp http.ResponseWriter, req *http.Re func (h *HTTPHandler) serveNotAllowed(resp http.ResponseWriter, req *http.Request) { err := errors.Errorf("invalid request method: %s", req.Method) + if _, ok := mux.Vars(req)[blockIDKey]; ok { + h.sendResponseNotAllowed(resp, err, http.MethodGet) + return + } + if _, ok := mux.Vars(req)[channelIDKey]; ok { h.sendResponseNotAllowed(resp, err, http.MethodGet, http.MethodDelete) return @@ -588,6 +678,15 @@ func (h *HTTPHandler) sendResponseOK(resp http.ResponseWriter, content interface } } +func (h *HTTPHandler) sendResponseBlock(resp http.ResponseWriter, block []byte) { + // resp.Header().Set("Content-Type", "application/json") + resp.WriteHeader(http.StatusOK) + + if _, err := resp.Write(block); err != nil { + h.logger.Errorf("failed to write block, err: %s", err) + } +} + func (h *HTTPHandler) sendResponseCreated(resp http.ResponseWriter, location string, content interface{}) { encoder := json.NewEncoder(resp) resp.Header().Set("Location", location) diff --git a/orderer/common/channelparticipation/restapi_test.go b/orderer/common/channelparticipation/restapi_test.go index 4c7f21c4017..8b4116ae458 100644 --- a/orderer/common/channelparticipation/restapi_test.go +++ b/orderer/common/channelparticipation/restapi_test.go @@ -25,6 +25,7 @@ import ( "github.com/hyperledger/fabric/protoutil" "github.com/pkg/errors" "github.com/stretchr/testify/require" + "google.golang.org/protobuf/proto" ) func TestNewHTTPHandler(t *testing.T) { @@ -50,6 +51,17 @@ func TestHTTPHandler_ServeHTTP_InvalidMethods(t *testing.T) { _, h := setup(config, t) invalidMethods := []string{http.MethodConnect, http.MethodHead, http.MethodOptions, http.MethodPatch, http.MethodTrace} + t.Run("on /channels/ch-id/blocks/b-id", func(t *testing.T) { + invalidMethodsExt := append(invalidMethods, http.MethodPost, http.MethodDelete, http.MethodPut) + for _, method := range invalidMethodsExt { + resp := httptest.NewRecorder() + req := httptest.NewRequest(method, path.Join(channelparticipation.URLBaseV1Channels, "ch-id", "blocks", "b-id"), nil) + h.ServeHTTP(resp, req) + checkErrorResponse(t, http.StatusMethodNotAllowed, fmt.Sprintf("invalid request method: %s", method), resp) + require.Equal(t, "GET", resp.Result().Header.Get("Allow"), "%s", method) + } + }) + t.Run("on /channels/ch-id", func(t *testing.T) { invalidMethodsExt := append(invalidMethods, http.MethodPost) for _, method := range invalidMethodsExt { @@ -112,6 +124,49 @@ func TestHTTPHandler_ServeHTTP_ListErrors(t *testing.T) { h.ServeHTTP(resp, req) checkErrorResponse(t, http.StatusNotAcceptable, "response Content-Type is application/json only", resp) }) + + t.Run("bad resource", func(t *testing.T) { + resp := httptest.NewRecorder() + req := httptest.NewRequest(http.MethodGet, channelparticipation.URLBaseV1Channels+"/ch-id/oops", nil) + h.ServeHTTP(resp, req) + require.Equal(t, http.StatusNotFound, resp.Result().StatusCode) + }) + + t.Run("bad resource", func(t *testing.T) { + resp := httptest.NewRecorder() + req := httptest.NewRequest(http.MethodGet, channelparticipation.URLBaseV1Channels+"/ch-id/blocks", nil) + h.ServeHTTP(resp, req) + require.Equal(t, http.StatusNotFound, resp.Result().StatusCode) + }) + + t.Run("bad resource", func(t *testing.T) { + resp := httptest.NewRecorder() + req := httptest.NewRequest(http.MethodGet, channelparticipation.URLBaseV1Channels+"/ch-id/blocks/", nil) + h.ServeHTTP(resp, req) + require.Equal(t, http.StatusNotFound, resp.Result().StatusCode) + }) + + t.Run("bad channel ID", func(t *testing.T) { + resp := httptest.NewRecorder() + req := httptest.NewRequest(http.MethodGet, channelparticipation.URLBaseV1Channels+"/ch-id/blocks/no/slash", nil) + h.ServeHTTP(resp, req) + require.Equal(t, http.StatusNotFound, resp.Result().StatusCode) + }) + + t.Run("illegal character in channel ID", func(t *testing.T) { + resp := httptest.NewRecorder() + req := httptest.NewRequest(http.MethodGet, channelparticipation.URLBaseV1Channels+"/ch-id/blocks/blabla", nil) + h.ServeHTTP(resp, req) + checkErrorResponse(t, http.StatusBadRequest, "invalid block ID: 'blabla' not equal ", resp) + }) + + t.Run("bad Accept header", func(t *testing.T) { + resp := httptest.NewRecorder() + req := httptest.NewRequest(http.MethodGet, channelparticipation.URLBaseV1Channels+"/ch-id/blocks/0", nil) + req.Header.Set("Accept", "text/html") + h.ServeHTTP(resp, req) + checkErrorResponse(t, http.StatusNotAcceptable, "response Content-Type is application/json only", resp) + }) } func TestHTTPHandler_ServeHTTP_ListAll(t *testing.T) { @@ -641,6 +696,70 @@ func TestHTTPHandler_ServeHTTP_Update(t *testing.T) { }) } +func TestHTTPHandler_ServeHTTP_Fetch(t *testing.T) { + config := localconfig.ChannelParticipation{ + Enabled: true, + MaxRequestBodySize: 1024 * 1024, + } + + t.Run("fetch ok", func(t *testing.T) { + block := blockWithGroups(map[string]*common.ConfigGroup{ + "Application": {}, + }, "ch-id") + fakeManager, h := setup(config, t) + fakeManager.FetchBlockReturns(block, nil) + + resp := httptest.NewRecorder() + req := httptest.NewRequest(http.MethodGet, channelparticipation.URLBaseV1Channels+"/app-channel/blocks/oldest", nil) + + h.ServeHTTP(resp, req) + require.Equal(t, http.StatusOK, resp.Result().StatusCode) + require.Equal(t, "application/octet-stream", resp.Result().Header.Get("Content-Type")) + require.Equal(t, "no-store", resp.Result().Header.Get("Cache-Control")) + + b := &common.Block{} + err := proto.Unmarshal(resp.Body.Bytes(), b) + require.NoError(t, err, "cannot be unmarshaled") + require.True(t, proto.Equal(block, b)) + }) + + t.Run("Error: Channel not exists", func(t *testing.T) { + fakeManager, h := setup(config, t) + fakeManager.FetchBlockReturns(nil, types.ErrChannelNotExist) + resp := httptest.NewRecorder() + req := httptest.NewRequest(http.MethodGet, channelparticipation.URLBaseV1Channels+"/app-channel/blocks/oldest", nil) + h.ServeHTTP(resp, req) + checkErrorResponse(t, http.StatusNotFound, "channel does not exist", resp) + }) + + t.Run("Error: Channels is folower", func(t *testing.T) { + fakeManager, h := setup(config, t) + fakeManager.FetchBlockReturns(nil, types.ErrChannelNotReady) + resp := httptest.NewRecorder() + req := httptest.NewRequest(http.MethodGet, channelparticipation.URLBaseV1Channels+"/app-channel/blocks/oldest", nil) + h.ServeHTTP(resp, req) + checkErrorResponse(t, http.StatusNotFound, "channel is not ready, he is a follower", resp) + }) + + t.Run("Error: Channel Pending Removal", func(t *testing.T) { + fakeManager, h := setup(config, t) + fakeManager.FetchBlockReturns(nil, types.ErrChannelPendingRemoval) + resp := httptest.NewRecorder() + req := httptest.NewRequest(http.MethodGet, channelparticipation.URLBaseV1Channels+"/app-channel/blocks/oldest", nil) + h.ServeHTTP(resp, req) + checkErrorResponse(t, http.StatusNotFound, "channel pending removal", resp) + }) + + t.Run("Error: block ID is bad", func(t *testing.T) { + fakeManager, h := setup(config, t) + fakeManager.FetchBlockReturns(nil, types.ErrChannelPendingRemoval) + resp := httptest.NewRecorder() + req := httptest.NewRequest(http.MethodGet, channelparticipation.URLBaseV1Channels+"/app-channel/blocks/blabla", nil) + h.ServeHTTP(resp, req) + checkErrorResponse(t, http.StatusBadRequest, "invalid block ID: 'blabla' not equal ", resp) + }) +} + func setup(config localconfig.ChannelParticipation, t *testing.T) (*mocks.ChannelManagement, *channelparticipation.HTTPHandler) { fakeManager := &mocks.ChannelManagement{} h := channelparticipation.NewHTTPHandler(config, fakeManager) diff --git a/orderer/common/channelparticipation/validator.go b/orderer/common/channelparticipation/validator.go index 778f8f5365b..3a0f32e6e0e 100644 --- a/orderer/common/channelparticipation/validator.go +++ b/orderer/common/channelparticipation/validator.go @@ -8,6 +8,7 @@ package channelparticipation import ( "bytes" + "strconv" "github.com/hyperledger/fabric-lib-go/bccsp/factory" cb "github.com/hyperledger/fabric-protos-go-apiv2/common" @@ -104,3 +105,22 @@ func ValidateUpdateConfigEnvelope(env *cb.Envelope) (channelID string, err error return configUpdate.ChannelId, nil } + +// ValidateFetchBlockID checks the block id. He can be: newest|oldest|config|(number) +func ValidateFetchBlockID(blockID string) error { + // Length + if len(blockID) <= 0 { + return errors.Errorf("block ID illegal, cannot be empty") + } + + if blockID == "newest" || blockID == "oldest" || blockID == "config" { + return nil + } + + _, err := strconv.Atoi(blockID) + if err == nil { + return nil + } + + return errors.Errorf("'%s' not equal ", blockID) +} diff --git a/orderer/common/channelparticipation/validator_test.go b/orderer/common/channelparticipation/validator_test.go index 870bf8023c3..bec886f275d 100644 --- a/orderer/common/channelparticipation/validator_test.go +++ b/orderer/common/channelparticipation/validator_test.go @@ -158,6 +158,66 @@ func TestValidateUpdateEnvelope(t *testing.T) { } } +func TestValidateFetchBlockID(t *testing.T) { + tests := []struct { + testName string + blockID string + expectedErr error + }{ + { + testName: "block ID is empty", + blockID: "", + expectedErr: errors.New("block ID illegal, cannot be empty"), + }, + { + testName: "block ID is oldest", + blockID: "oldest", + expectedErr: nil, + }, + { + testName: "block ID is newest", + blockID: "newest", + expectedErr: nil, + }, + { + testName: "block ID is config", + blockID: "config", + expectedErr: nil, + }, + { + testName: "block ID is 0", + blockID: "0", + expectedErr: nil, + }, + { + testName: "block ID is 99", + blockID: "99", + expectedErr: nil, + }, + { + testName: "block ID is blabla", + blockID: "blabla", + expectedErr: errors.New("'blabla' not equal "), + }, + { + testName: "block ID is 1n0", + blockID: "1n0", + expectedErr: errors.New("'1n0' not equal "), + }, + } + + for _, test := range tests { + t.Run(test.testName, func(t *testing.T) { + err := channelparticipation.ValidateFetchBlockID(test.blockID) + if test.expectedErr != nil { + require.EqualError(t, err, test.expectedErr.Error()) + } else { + require.NoError(t, err) + } + }) + } +} + func blockWithGroups(groups map[string]*cb.ConfigGroup, channelID string) *cb.Block { block := protoutil.NewBlock(0, []byte{}) block.Data = &cb.BlockData{ diff --git a/orderer/common/cluster/rpc.go b/orderer/common/cluster/rpc.go index 3fcef30ade7..fa8038ea182 100644 --- a/orderer/common/cluster/rpc.go +++ b/orderer/common/cluster/rpc.go @@ -141,7 +141,7 @@ func (s *RPC) submitSent(start time.Time, to uint64, msg *orderer.SubmitRequest) } func (s *RPC) consensusSent(start time.Time, to uint64, msg *orderer.ConsensusRequest) { - s.Logger.Debugf("Sending msg of %d bytes to %d on channel %s took %v", len(msg.Payload), to, s.Channel, time.Since(start)) + s.Logger.Debugf("Sending msg of %d bytes (with metadata %d bytes) to %d on channel %s took %v", len(msg.Payload), len(msg.Metadata), to, s.Channel, time.Since(start)) } // getOrCreateStream obtains a Submit stream for the given destination node diff --git a/orderer/consensus/etcdraft/chain.go b/orderer/consensus/etcdraft/chain.go index 8235fff3545..88dad1a62b3 100644 --- a/orderer/consensus/etcdraft/chain.go +++ b/orderer/consensus/etcdraft/chain.go @@ -535,6 +535,7 @@ func (c *Chain) Consensus(req *orderer.ConsensusRequest, sender uint64) error { c.Metrics.ActiveNodes.Set(float64(len(clusterMetadata.ActiveNodes))) c.ActiveNodes.Store(clusterMetadata.ActiveNodes) + c.logger.Infof("Store ActiveNodes %+v", clusterMetadata.ActiveNodes) return nil } @@ -1543,6 +1544,7 @@ func (c *Chain) ValidateConsensusMetadata(oldOrdererConfig, newOrdererConfig cha active := c.ActiveNodes.Load().([]uint64) if changes.UnacceptableQuorumLoss(active) { + c.logger.Debugf("%d out of %d nodes are alive - %+v", len(active), len(dummyOldConsentersMap), active) return errors.Errorf("%d out of %d nodes are alive, configuration will result in quorum loss", len(active), len(dummyOldConsentersMap)) } diff --git a/swagger/swagger-fabric.json b/swagger/swagger-fabric.json index 9ebbb7b10f5..ea351fa9789 100644 --- a/swagger/swagger-fabric.json +++ b/swagger/swagger-fabric.json @@ -90,7 +90,7 @@ } } }, - "/v1/participation/channels": { + "/participation/v1/channels": { "get": { "tags": [ "channels" @@ -217,7 +217,7 @@ } } }, - "/v1/participation/channels/{channelID}": { + "/participation/v1/channels/{channelID}": { "get": { "tags": [ "channels" @@ -283,6 +283,48 @@ } } }, + "/participation/v1/channels/{channelID}/blocks/{blockID}": { + "get": { + "produces": [ + "application/octet-stream" + ], + "tags": [ + "channels" + ], + "summary": "Returns a specific channel block for a specific Channel Ordering Service (OSN) node.", + "operationId": "fetchBlock", + "parameters": [ + { + "type": "string", + "description": "Channel ID", + "name": "channelID", + "in": "path", + "required": true + }, + { + "type": "string", + "description": "Block ID", + "name": "blockID", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "Successful block download.", + "schema": { + "type": "file" + } + }, + "400": { + "description": "Bad request." + }, + "404": { + "description": "The channel or block does not exist." + } + } + } + }, "/version": { "get": { "tags": [ diff --git a/tools/go.mod b/tools/go.mod index ee7eed5a8e0..d09f9a81428 100644 --- a/tools/go.mod +++ b/tools/go.mod @@ -43,7 +43,7 @@ require ( github.com/go-stack/stack v1.8.0 // indirect github.com/go-swagger/go-swagger v0.25.0 // indirect github.com/go-task/slim-sprig/v3 v3.0.0 // indirect - github.com/google/go-cmp v0.6.0 // indirect + github.com/google/go-cmp v0.7.0 // indirect github.com/google/pprof v0.0.0-20240827171923-fa2c70bbbfe5 // indirect github.com/google/uuid v1.6.0 // indirect github.com/gorilla/handlers v1.4.2 // indirect @@ -61,7 +61,7 @@ require ( github.com/mailru/easyjson v0.7.7 // indirect github.com/mattn/go-colorable v0.1.13 // indirect github.com/mattn/go-isatty v0.0.17 // indirect - github.com/maxbrunsfeld/counterfeiter/v6 v6.8.1 // indirect + github.com/maxbrunsfeld/counterfeiter/v6 v6.11.3 // indirect github.com/mitchellh/go-homedir v1.1.0 // indirect github.com/mitchellh/mapstructure v1.5.0 // indirect github.com/onsi/ginkgo/v2 v2.20.2 // indirect @@ -81,13 +81,14 @@ require ( go.mongodb.org/mongo-driver v1.5.1 // indirect golang.org/x/exp/typeparams v0.0.0-20231108232855-2478ac86f678 // indirect golang.org/x/lint v0.0.0-20210508222113-6edffad5e616 // indirect - golang.org/x/mod v0.23.0 // indirect - golang.org/x/net v0.39.0 // indirect - golang.org/x/sync v0.13.0 // indirect - golang.org/x/sys v0.32.0 // indirect - golang.org/x/term v0.31.0 // indirect - golang.org/x/text v0.24.0 // indirect - golang.org/x/tools v0.30.0 // indirect + golang.org/x/mod v0.26.0 // indirect + golang.org/x/net v0.42.0 // indirect + golang.org/x/sync v0.16.0 // indirect + golang.org/x/sys v0.34.0 // indirect + golang.org/x/term v0.33.0 // indirect + golang.org/x/text v0.27.0 // indirect + golang.org/x/tools v0.35.0 // indirect + golang.org/x/tools/go/expect v0.1.1-deprecated // indirect google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.5.1 // indirect google.golang.org/protobuf v1.34.2 // indirect gopkg.in/ini.v1 v1.67.0 // indirect diff --git a/tools/go.sum b/tools/go.sum index 51695e21ce7..1de056aefa5 100644 --- a/tools/go.sum +++ b/tools/go.sum @@ -208,8 +208,8 @@ github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/ github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= -github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= +github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= @@ -315,8 +315,8 @@ github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/ github.com/mattn/go-isatty v0.0.17 h1:BTarxUcIeDqL27Mc+vyvdWYSL28zpIhv3RoTdsLMPng= github.com/mattn/go-isatty v0.0.17/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= -github.com/maxbrunsfeld/counterfeiter/v6 v6.8.1 h1:NicmruxkeqHjDv03SfSxqmaLuisddudfP3h5wdXFbhM= -github.com/maxbrunsfeld/counterfeiter/v6 v6.8.1/go.mod h1:eyp4DdUJAKkr9tvxR3jWhw2mDK7CWABMG5r9uyaKC7I= +github.com/maxbrunsfeld/counterfeiter/v6 v6.11.3 h1:Eaq36EIyJNp7b3qDhjV7jmDVq/yPeW2v4pTqzGbOGB4= +github.com/maxbrunsfeld/counterfeiter/v6 v6.11.3/go.mod h1:6KKUoQBZBW6PDXJtNfqeEjPXMj/ITTk+cWK9t9uS5+E= github.com/miekg/dns v1.0.14/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg= github.com/mitchellh/cli v1.0.0/go.mod h1:hNIlj7HEI86fIcpObd7a0FcrxTWetlwJDGcceTlRvqc= github.com/mitchellh/go-homedir v1.0.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= @@ -338,8 +338,8 @@ github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLA github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U= github.com/onsi/ginkgo/v2 v2.20.2 h1:7NVCeyIWROIAheY21RLS+3j2bb52W0W82tkberYytp4= github.com/onsi/ginkgo/v2 v2.20.2/go.mod h1:K9gyxPIlb+aIvnZ8bd9Ak+YP18w3APlR+5coaZoE2ag= -github.com/onsi/gomega v1.34.1 h1:EUMJIKUjM8sKjYbtxQI9A4z2o+rruxnzNvpknOXie6k= -github.com/onsi/gomega v1.34.1/go.mod h1:kU1QgUvBDLXBJq618Xvm2LUX6rSAfRaFRTcdOeDLwwY= +github.com/onsi/gomega v1.37.0 h1:CdEG8g0S133B4OswTDC/5XPSzE1OeP29QOioj2PID2Y= +github.com/onsi/gomega v1.37.0/go.mod h1:8D9+Txp43QWKhM24yyOBEdpkzN8FvJyAwecBgsU4KU0= github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc= github.com/pborman/uuid v1.2.0/go.mod h1:X/NO0urCmaxf9VXbdlT7C2Yzkj2IKimNn4k+gtPdI/k= github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic= @@ -479,8 +479,6 @@ golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= golang.org/x/exp v0.0.0-20190829153037-c13cbed26979/go.mod h1:86+5VVa7VpoJ4kLfm080zCjGlMRFzhUhsZKEZO7MGek= golang.org/x/exp v0.0.0-20191030013958-a1ab85dbe136/go.mod h1:JXzH8nQsPlswgeRAPE3MuO9GYsAcnJvJ4vnMwN/5qkY= -golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56 h1:2dVuKD2vS7b0QIHQbpyTISPd0LeHDbnYEryqj5Q1ug8= -golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56/go.mod h1:M4RDyNAINzryxdtnbRXRL/OHtkFuWGRjvuhBJpk2IlY= golang.org/x/exp/typeparams v0.0.0-20231108232855-2478ac86f678 h1:1P7xPZEwZMoBoz0Yze5Nx2/4pxj6nw9ZqHWXqP0iRgQ= golang.org/x/exp/typeparams v0.0.0-20231108232855-2478ac86f678/go.mod h1:AbB0pIl9nAr9wVwH+Z2ZpaocVmF5I4GyWCDIsVjR0bk= golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= @@ -501,8 +499,8 @@ golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY= golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.4.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= -golang.org/x/mod v0.23.0 h1:Zb7khfcRGKk+kqfxFaP5tZqCnDZMjC5VtUBs87Hr6QM= -golang.org/x/mod v0.23.0/go.mod h1:6SkKJ3Xj0I0BrPOZoBy3bdMptDDU9oJrpohJ3eWZ1fY= +golang.org/x/mod v0.26.0 h1:EGMPT//Ezu+ylkCijjPc+f4Aih7sZvaAr+O3EHBxvZg= +golang.org/x/mod v0.26.0/go.mod h1:/j6NAhSk8iQ723BGAUyoAcn7SlD7s15Dp9Nd/SfeaFQ= golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20181005035420-146acd28ed58/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= @@ -527,8 +525,8 @@ golang.org/x/net v0.0.0-20200602114024-627f9648deb9/go.mod h1:qpuaurCH72eLCgpAm/ golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= -golang.org/x/net v0.39.0 h1:ZCu7HMWDxpXpaiKdhzIfaltL9Lp31x/3fCP11bc6/fY= -golang.org/x/net v0.39.0/go.mod h1:X7NRbYVEA+ewNkCNyJ513WmMdQ3BineSwVtN2zD/d+E= +golang.org/x/net v0.42.0 h1:jzkYrhi3YQWD6MLBJcsklgQsoAcw89EcZbJw8Z614hs= +golang.org/x/net v0.42.0/go.mod h1:FF1RA5d3u7nAYA4z2TkclSCKh68eSXtiFwcWQpPXdt8= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= @@ -542,8 +540,8 @@ golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJ golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.13.0 h1:AauUjRAJ9OSnvULf/ARrrVywoJDy0YS2AwQ98I37610= -golang.org/x/sync v0.13.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= +golang.org/x/sync v0.16.0 h1:ycBJEhp9p4vXvUZNszeOq0kGTPghopOL8q0fq3vstxw= +golang.org/x/sync v0.16.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= @@ -570,17 +568,17 @@ golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.32.0 h1:s77OFDvIQeibCmezSnk/q6iAfkdiQaJi4VzroCFrN20= -golang.org/x/sys v0.32.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= -golang.org/x/term v0.31.0 h1:erwDkOK1Msy6offm1mOgvspSkslFnIGsFnxOKoufg3o= -golang.org/x/term v0.31.0/go.mod h1:R4BeIy7D95HzImkxGkTW1UQTtP54tio2RyHz7PwK0aw= +golang.org/x/sys v0.34.0 h1:H5Y5sJ2L2JRdyv7ROF1he/lPdvFsd0mJHFw2ThKHxLA= +golang.org/x/sys v0.34.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= +golang.org/x/term v0.33.0 h1:NuFncQrRcaRvVmgRkvM3j/F00gWIAlcmlB8ACEKmGIg= +golang.org/x/term v0.33.0/go.mod h1:s18+ql9tYWp1IfpV9DmCtQDDSRBUjKaw9M1eAv5UeF0= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.24.0 h1:dd5Bzh4yt5KYA8f9CJHCP4FB4D51c2c6JvN37xJJkJ0= -golang.org/x/text v0.24.0/go.mod h1:L8rBsPeo2pSS+xqN0d5u2ikmjtmoJbDBT1b7nHvFCdU= +golang.org/x/text v0.27.0 h1:4fGWRpyh641NLlecmyl4LOe6yDdfaYNrGb2zdfo4JV4= +golang.org/x/text v0.27.0/go.mod h1:1D28KMCvyooCX9hBiosv5Tz/+YLxj0j7XhWjpSUF7CU= golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/tools v0.0.0-20180221164845-07fd8470d635/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= @@ -612,8 +610,10 @@ golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtn golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= golang.org/x/tools v0.0.0-20200717024301-6ddee64345a6/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= golang.org/x/tools v0.0.0-20210101214203-2dba1e4ea05c/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= -golang.org/x/tools v0.30.0 h1:BgcpHewrV5AUp2G9MebG4XPFI1E2W41zU1SaqVA9vJY= -golang.org/x/tools v0.30.0/go.mod h1:c347cR/OJfw5TI+GfX7RUPNMdDRRbjvYTS0jPyvsVtY= +golang.org/x/tools v0.35.0 h1:mBffYraMEf7aa0sB+NuKnuCy8qI/9Bughn8dC2Gu5r0= +golang.org/x/tools v0.35.0/go.mod h1:NKdj5HkL/73byiZSJjqJgKn3ep7KjFkBOkR/Hps3VPw= +golang.org/x/tools/go/expect v0.1.1-deprecated h1:jpBZDwmgPhXsKZC6WhL20P4b/wmnpsEAGHaNy0n/rJM= +golang.org/x/tools/go/expect v0.1.1-deprecated/go.mod h1:eihoPOH+FgIqa3FpoTwguz/bVUSGBlGQU67vpBeOrBY= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=