diff --git a/.github/workflows/test-e2e.yaml b/.github/workflows/test-e2e.yaml index babbb3a39..55c82689e 100644 --- a/.github/workflows/test-e2e.yaml +++ b/.github/workflows/test-e2e.yaml @@ -43,8 +43,8 @@ jobs: uses: actions/checkout@v4 with: repository: vechain/thor-e2e-tests - # https://github.com/vechain/thor-e2e-tests/tree/209f6ea9a81a98dc2d5e42bf036d2878c5837036 - ref: 209f6ea9a81a98dc2d5e42bf036d2878c5837036 + # https://github.com/vechain/thor-e2e-tests/tree/8b72bedff11c9e8873d88b6e2dba356d43b56779 + ref: 8b72bedff11c9e8873d88b6e2dba356d43b56779 - name: Download artifact uses: actions/download-artifact@v4 diff --git a/api/accounts/accounts.go b/api/accounts/accounts.go index 54058a160..22698bdbd 100644 --- a/api/accounts/accounts.go +++ b/api/accounts/accounts.go @@ -27,11 +27,12 @@ import ( ) type Accounts struct { - repo *chain.Repository - stater *state.Stater - callGasLimit uint64 - forkConfig thor.ForkConfig - bft bft.Committer + repo *chain.Repository + stater *state.Stater + callGasLimit uint64 + forkConfig thor.ForkConfig + bft bft.Committer + enabledDeprecated bool } func New( @@ -40,6 +41,7 @@ func New( callGasLimit uint64, forkConfig thor.ForkConfig, bft bft.Committer, + enabledDeprecated bool, ) *Accounts { return &Accounts{ repo, @@ -47,6 +49,7 @@ func New( callGasLimit, forkConfig, bft, + enabledDeprecated, } } @@ -168,6 +171,9 @@ func (a *Accounts) handleGetStorage(w http.ResponseWriter, req *http.Request) er } func (a *Accounts) handleCallContract(w http.ResponseWriter, req *http.Request) error { + if !a.enabledDeprecated { + return utils.HTTPError(nil, http.StatusGone) + } callData := &CallData{} if err := utils.ParseJSON(req.Body, &callData); err != nil { return utils.BadRequest(errors.WithMessage(err, "body")) diff --git a/api/accounts/accounts_test.go b/api/accounts/accounts_test.go index 9294723eb..8630bea4b 100644 --- a/api/accounts/accounts_test.go +++ b/api/accounts/accounts_test.go @@ -103,7 +103,7 @@ var ( ) func TestAccount(t *testing.T) { - initAccountServer(t) + initAccountServer(t, true) defer ts.Close() tclient = thorclient.New(ts.URL) @@ -126,6 +126,21 @@ func TestAccount(t *testing.T) { } } +func TestDeprecated(t *testing.T) { + initAccountServer(t, false) + defer ts.Close() + + tclient = thorclient.New(ts.URL) + + body := &accounts.CallData{} + + _, statusCode, _ := tclient.RawHTTPClient().RawHTTPPost("/accounts", body) + assert.Equal(t, http.StatusGone, statusCode, "invalid address") + + _, statusCode, _ = tclient.RawHTTPClient().RawHTTPPost("/accounts/"+contractAddr.String(), body) + assert.Equal(t, http.StatusGone, statusCode, "invalid address") +} + func getAccount(t *testing.T) { _, statusCode, err := tclient.RawHTTPClient().RawHTTPGet("/accounts/" + invalidAddr) require.NoError(t, err) @@ -264,7 +279,7 @@ func getStorageWithNonExistingRevision(t *testing.T) { assert.Equal(t, "revision: leveldb: not found\n", string(res), "revision not found") } -func initAccountServer(t *testing.T) { +func initAccountServer(t *testing.T, enabledDeprecated bool) { thorChain, err := testchain.NewIntegrationTestChain() require.NoError(t, err) @@ -291,7 +306,7 @@ func initAccountServer(t *testing.T) { ) router := mux.NewRouter() - accounts.New(thorChain.Repo(), thorChain.Stater(), uint64(gasLimit), thor.NoFork, thorChain.Engine()). + accounts.New(thorChain.Repo(), thorChain.Stater(), uint64(gasLimit), thor.NoFork, thorChain.Engine(), enabledDeprecated). Mount(router, "/accounts") ts = httptest.NewServer(router) diff --git a/api/admin.go b/api/admin.go deleted file mode 100644 index afd299cfa..000000000 --- a/api/admin.go +++ /dev/null @@ -1,62 +0,0 @@ -// Copyright (c) 2024 The VeChainThor developers - -// Distributed under the GNU Lesser General Public License v3.0 software license, see the accompanying -// file LICENSE or - -package api - -import ( - "log/slog" - "net/http" - - "github.com/pkg/errors" - "github.com/vechain/thor/v2/api/utils" - "github.com/vechain/thor/v2/log" -) - -type logLevelRequest struct { - Level string `json:"level"` -} - -type logLevelResponse struct { - CurrentLevel string `json:"currentLevel"` -} - -func getLogLevelHandler(logLevel *slog.LevelVar) utils.HandlerFunc { - return func(w http.ResponseWriter, r *http.Request) error { - return utils.WriteJSON(w, logLevelResponse{ - CurrentLevel: logLevel.Level().String(), - }) - } -} - -func postLogLevelHandler(logLevel *slog.LevelVar) utils.HandlerFunc { - return func(w http.ResponseWriter, r *http.Request) error { - var req logLevelRequest - - if err := utils.ParseJSON(r.Body, &req); err != nil { - return utils.BadRequest(errors.WithMessage(err, "Invalid request body")) - } - - switch req.Level { - case "debug": - logLevel.Set(log.LevelDebug) - case "info": - logLevel.Set(log.LevelInfo) - case "warn": - logLevel.Set(log.LevelWarn) - case "error": - logLevel.Set(log.LevelError) - case "trace": - logLevel.Set(log.LevelTrace) - case "crit": - logLevel.Set(log.LevelCrit) - default: - return utils.BadRequest(errors.New("Invalid verbosity level")) - } - - return utils.WriteJSON(w, logLevelResponse{ - CurrentLevel: logLevel.Level().String(), - }) - } -} diff --git a/api/admin/admin.go b/api/admin/admin.go new file mode 100644 index 000000000..9b819c875 --- /dev/null +++ b/api/admin/admin.go @@ -0,0 +1,32 @@ +// Copyright (c) 2024 The VeChainThor developers + +// Distributed under the GNU Lesser General Public License v3.0 software license, see the accompanying +// file LICENSE or + +package admin + +import ( + "log/slog" + "net/http" + "sync/atomic" + + "github.com/gorilla/handlers" + "github.com/gorilla/mux" + "github.com/vechain/thor/v2/api/admin/apilogs" + "github.com/vechain/thor/v2/api/admin/loglevel" + + healthAPI "github.com/vechain/thor/v2/api/admin/health" +) + +func New(logLevel *slog.LevelVar, health *healthAPI.Health, apiLogsToggle *atomic.Bool) http.HandlerFunc { + router := mux.NewRouter() + subRouter := router.PathPrefix("/admin").Subrouter() + + loglevel.New(logLevel).Mount(subRouter, "/loglevel") + healthAPI.NewAPI(health).Mount(subRouter, "/health") + apilogs.New(apiLogsToggle).Mount(subRouter, "/apilogs") + + handler := handlers.CompressHandler(router) + + return handler.ServeHTTP +} diff --git a/api/admin/apilogs/api_logs.go b/api/admin/apilogs/api_logs.go new file mode 100644 index 000000000..0f815d579 --- /dev/null +++ b/api/admin/apilogs/api_logs.go @@ -0,0 +1,70 @@ +// Copyright (c) 2024 The VeChainThor developers +// +// Distributed under the GNU Lesser General Public License v3.0 software license, see the accompanying +// file LICENSE or + +package apilogs + +import ( + "net/http" + "sync" + "sync/atomic" + + "github.com/gorilla/mux" + "github.com/vechain/thor/v2/api/utils" + "github.com/vechain/thor/v2/log" +) + +type APILogs struct { + enabled *atomic.Bool + mu sync.Mutex +} + +type Status struct { + Enabled bool `json:"enabled"` +} + +func New(enabled *atomic.Bool) *APILogs { + return &APILogs{ + enabled: enabled, + } +} + +func (a *APILogs) Mount(root *mux.Router, pathPrefix string) { + sub := root.PathPrefix(pathPrefix).Subrouter() + sub.Path(""). + Methods(http.MethodGet). + Name("get-api-logs-enabled"). + HandlerFunc(utils.WrapHandlerFunc(a.areAPILogsEnabled)) + + sub.Path(""). + Methods(http.MethodPost). + Name("post-api-logs-enabled"). + HandlerFunc(utils.WrapHandlerFunc(a.setAPILogsEnabled)) +} + +func (a *APILogs) areAPILogsEnabled(w http.ResponseWriter, _ *http.Request) error { + a.mu.Lock() + defer a.mu.Unlock() + + return utils.WriteJSON(w, Status{ + Enabled: a.enabled.Load(), + }) +} + +func (a *APILogs) setAPILogsEnabled(w http.ResponseWriter, r *http.Request) error { + a.mu.Lock() + defer a.mu.Unlock() + + var req Status + if err := utils.ParseJSON(r.Body, &req); err != nil { + return utils.BadRequest(err) + } + a.enabled.Store(req.Enabled) + + log.Info("api logs updated", "pkg", "apilogs", "enabled", req.Enabled) + + return utils.WriteJSON(w, Status{ + Enabled: a.enabled.Load(), + }) +} diff --git a/api/admin/apilogs/api_logs_test.go b/api/admin/apilogs/api_logs_test.go new file mode 100644 index 000000000..95cf2c6ac --- /dev/null +++ b/api/admin/apilogs/api_logs_test.go @@ -0,0 +1,91 @@ +// Copyright (c) 2024 The VeChainThor developers +// +// Distributed under the GNU Lesser General Public License v3.0 software license, see the accompanying +// file LICENSE or + +package apilogs + +import ( + "bytes" + "encoding/json" + "net/http" + "net/http/httptest" + "sync/atomic" + "testing" + + "github.com/gorilla/mux" + "github.com/stretchr/testify/assert" +) + +type TestCase struct { + name string + method string + expectedHTTP int + startValue bool + expectedEndValue bool + requestBody bool +} + +func marshalBody(tt TestCase, t *testing.T) []byte { + var reqBody []byte + var err error + if tt.method == "POST" { + reqBody, err = json.Marshal(Status{Enabled: tt.requestBody}) + if err != nil { + t.Fatalf("could not marshal request body: %v", err) + } + } + return reqBody +} + +func TestLogLevelHandler(t *testing.T) { + tests := []TestCase{ + { + name: "Valid POST input - set logs to enabled", + method: "POST", + expectedHTTP: http.StatusOK, + startValue: false, + requestBody: true, + expectedEndValue: true, + }, + { + name: "Valid POST input - set logs to disabled", + method: "POST", + expectedHTTP: http.StatusOK, + startValue: true, + requestBody: false, + expectedEndValue: false, + }, + { + name: "GET request - get current level INFO", + method: "GET", + expectedHTTP: http.StatusOK, + startValue: true, + expectedEndValue: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + logLevel := atomic.Bool{} + logLevel.Store(tt.startValue) + + reqBodyBytes := marshalBody(tt, t) + + req, err := http.NewRequest(tt.method, "/admin/apilogs", bytes.NewBuffer(reqBodyBytes)) + if err != nil { + t.Fatal(err) + } + + rr := httptest.NewRecorder() + router := mux.NewRouter() + New(&logLevel).Mount(router, "/admin/apilogs") + router.ServeHTTP(rr, req) + + assert.Equal(t, tt.expectedHTTP, rr.Code) + responseBody := Status{} + assert.NoError(t, json.Unmarshal(rr.Body.Bytes(), &responseBody)) + assert.Equal(t, tt.expectedEndValue, responseBody.Enabled) + }) + } +} diff --git a/api/admin/health/health.go b/api/admin/health/health.go new file mode 100644 index 000000000..41522e32d --- /dev/null +++ b/api/admin/health/health.go @@ -0,0 +1,84 @@ +// Copyright (c) 2024 The VeChainThor developers + +// Distributed under the GNU Lesser General Public License v3.0 software license, see the accompanying +// file LICENSE or + +package health + +import ( + "time" + + "github.com/vechain/thor/v2/chain" + "github.com/vechain/thor/v2/comm" + "github.com/vechain/thor/v2/thor" +) + +type BlockIngestion struct { + ID *thor.Bytes32 `json:"id"` + Timestamp *time.Time `json:"timestamp"` +} + +type Status struct { + Healthy bool `json:"healthy"` + BestBlockTime *time.Time `json:"bestBlockTime"` + PeerCount int `json:"peerCount"` + IsNetworkProgressing bool `json:"isNetworkProgressing"` +} + +type Health struct { + repo *chain.Repository + p2p *comm.Communicator +} + +const ( + defaultBlockTolerance = time.Duration(2*thor.BlockInterval) * time.Second // 2 blocks tolerance + defaultMinPeerCount = 2 +) + +func New(repo *chain.Repository, p2p *comm.Communicator) *Health { + return &Health{ + repo: repo, + p2p: p2p, + } +} + +// isNetworkProgressing checks if the network is producing new blocks within the allowed interval. +func (h *Health) isNetworkProgressing(now time.Time, bestBlockTimestamp time.Time, blockTolerance time.Duration) bool { + return now.Sub(bestBlockTimestamp) <= blockTolerance +} + +// isNodeConnectedP2P checks if the node is connected to peers +func (h *Health) isNodeConnectedP2P(peerCount int, minPeerCount int) bool { + return peerCount >= minPeerCount +} + +func (h *Health) Status(blockTolerance time.Duration, minPeerCount int) (*Status, error) { + // Fetch the best block details + bestBlock := h.repo.BestBlockSummary() + bestBlockTimestamp := time.Unix(int64(bestBlock.Header.Timestamp()), 0) + + // Fetch the current connected peers + var connectedPeerCount int + if h.p2p == nil { + connectedPeerCount = minPeerCount // ignore peers in solo mode + } else { + connectedPeerCount = h.p2p.PeerCount() + } + + now := time.Now() + + // Perform the checks + networkProgressing := h.isNetworkProgressing(now, bestBlockTimestamp, blockTolerance) + nodeConnected := h.isNodeConnectedP2P(connectedPeerCount, minPeerCount) + + // Calculate overall health status + healthy := networkProgressing && nodeConnected + + // Return the current status + return &Status{ + Healthy: healthy, + BestBlockTime: &bestBlockTimestamp, + IsNetworkProgressing: networkProgressing, + PeerCount: connectedPeerCount, + }, nil +} diff --git a/api/admin/health/health_api.go b/api/admin/health/health_api.go new file mode 100644 index 000000000..3bad13f07 --- /dev/null +++ b/api/admin/health/health_api.go @@ -0,0 +1,68 @@ +// Copyright (c) 2024 The VeChainThor developers + +// Distributed under the GNU Lesser General Public License v3.0 software license, see the accompanying +// file LICENSE or + +package health + +import ( + "net/http" + "strconv" + "time" + + "github.com/gorilla/mux" + "github.com/vechain/thor/v2/api/utils" +) + +type API struct { + healthStatus *Health +} + +func NewAPI(healthStatus *Health) *API { + return &API{ + healthStatus: healthStatus, + } +} + +func (h *API) handleGetHealth(w http.ResponseWriter, r *http.Request) error { + // Parse query parameters + query := r.URL.Query() + + // Default to constants if query parameters are not provided + blockTolerance := defaultBlockTolerance + minPeerCount := defaultMinPeerCount + + // Override with query parameters if they exist + if queryBlockTolerance := query.Get("blockTolerance"); queryBlockTolerance != "" { + if parsed, err := time.ParseDuration(queryBlockTolerance); err == nil { + blockTolerance = parsed + } + } + + if queryMinPeerCount := query.Get("minPeerCount"); queryMinPeerCount != "" { + if parsed, err := strconv.Atoi(queryMinPeerCount); err == nil { + minPeerCount = parsed + } + } + + acc, err := h.healthStatus.Status(blockTolerance, minPeerCount) + if err != nil { + return err + } + + if !acc.Healthy { + w.WriteHeader(http.StatusServiceUnavailable) // Set the status to 503 + } else { + w.WriteHeader(http.StatusOK) // Set the status to 200 + } + return utils.WriteJSON(w, acc) +} + +func (h *API) Mount(root *mux.Router, pathPrefix string) { + sub := root.PathPrefix(pathPrefix).Subrouter() + + sub.Path(""). + Methods(http.MethodGet). + Name("health"). + HandlerFunc(utils.WrapHandlerFunc(h.handleGetHealth)) +} diff --git a/api/admin/health/health_api_test.go b/api/admin/health/health_api_test.go new file mode 100644 index 000000000..e50af0398 --- /dev/null +++ b/api/admin/health/health_api_test.go @@ -0,0 +1,59 @@ +// Copyright (c) 2024 The VeChainThor developers + +// Distributed under the GNU Lesser General Public License v3.0 software license, see the accompanying +// file LICENSE or + +package health + +import ( + "encoding/json" + "io" + "net/http" + "net/http/httptest" + "testing" + + "github.com/gorilla/mux" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "github.com/vechain/thor/v2/comm" + "github.com/vechain/thor/v2/test/testchain" + "github.com/vechain/thor/v2/txpool" +) + +var ts *httptest.Server + +func TestHealth(t *testing.T) { + initAPIServer(t) + + var healthStatus Status + respBody, statusCode := httpGet(t, ts.URL+"/health") + require.NoError(t, json.Unmarshal(respBody, &healthStatus)) + assert.False(t, healthStatus.Healthy) + assert.Equal(t, http.StatusServiceUnavailable, statusCode) +} + +func initAPIServer(t *testing.T) { + thorChain, err := testchain.NewIntegrationTestChain() + require.NoError(t, err) + + router := mux.NewRouter() + NewAPI( + New(thorChain.Repo(), comm.New(thorChain.Repo(), txpool.New(thorChain.Repo(), nil, txpool.Options{}))), + ).Mount(router, "/health") + + ts = httptest.NewServer(router) +} + +func httpGet(t *testing.T, url string) ([]byte, int) { + res, err := http.Get(url) //#nosec G107 + if err != nil { + t.Fatal(err) + } + defer res.Body.Close() + + r, err := io.ReadAll(res.Body) + if err != nil { + t.Fatal(err) + } + return r, res.StatusCode +} diff --git a/api/admin/health/health_test.go b/api/admin/health/health_test.go new file mode 100644 index 000000000..60f9a3dcd --- /dev/null +++ b/api/admin/health/health_test.go @@ -0,0 +1,71 @@ +// Copyright (c) 2024 The VeChainThor developers + +// Distributed under the GNU Lesser General Public License v3.0 software license, see the accompanying +// file LICENSE or + +package health + +import ( + "testing" + "time" + + "github.com/stretchr/testify/assert" +) + +func TestHealth_isNetworkProgressing(t *testing.T) { + h := &Health{} + + now := time.Now() + + tests := []struct { + name string + bestBlockTimestamp time.Time + expectedProgressing bool + }{ + { + name: "Progressing - block within timeBetweenBlocks", + bestBlockTimestamp: now.Add(-5 * time.Second), + expectedProgressing: true, + }, + { + name: "Not Progressing - block outside timeBetweenBlocks", + bestBlockTimestamp: now.Add(-25 * time.Second), + expectedProgressing: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + isProgressing := h.isNetworkProgressing(now, tt.bestBlockTimestamp, defaultBlockTolerance) + assert.Equal(t, tt.expectedProgressing, isProgressing, "isNetworkProgressing result mismatch") + }) + } +} + +func TestHealth_isNodeConnectedP2P(t *testing.T) { + h := &Health{} + + tests := []struct { + name string + peerCount int + expectedConnected bool + }{ + { + name: "Connected - more than one peer", + peerCount: 3, + expectedConnected: true, + }, + { + name: "Not Connected - one or fewer peers", + peerCount: 1, + expectedConnected: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + isConnected := h.isNodeConnectedP2P(tt.peerCount, defaultMinPeerCount) + assert.Equal(t, tt.expectedConnected, isConnected, "isNodeConnectedP2P result mismatch") + }) + } +} diff --git a/api/admin/loglevel/log_level.go b/api/admin/loglevel/log_level.go new file mode 100644 index 000000000..c91702d2d --- /dev/null +++ b/api/admin/loglevel/log_level.go @@ -0,0 +1,76 @@ +// Copyright (c) 2024 The VeChainThor developers + +// Distributed under the GNU Lesser General Public License v3.0 software license, see the accompanying +// file LICENSE or + +package loglevel + +import ( + "log/slog" + "net/http" + + "github.com/gorilla/mux" + "github.com/pkg/errors" + "github.com/vechain/thor/v2/api/utils" + "github.com/vechain/thor/v2/log" +) + +type LogLevel struct { + logLevel *slog.LevelVar +} + +func New(logLevel *slog.LevelVar) *LogLevel { + return &LogLevel{ + logLevel: logLevel, + } +} + +func (l *LogLevel) Mount(root *mux.Router, pathPrefix string) { + sub := root.PathPrefix(pathPrefix).Subrouter() + sub.Path(""). + Methods(http.MethodGet). + Name("get-log-level"). + HandlerFunc(utils.WrapHandlerFunc(l.getLogLevelHandler)) + + sub.Path(""). + Methods(http.MethodPost). + Name("post-log-level"). + HandlerFunc(utils.WrapHandlerFunc(l.postLogLevelHandler)) +} + +func (l *LogLevel) getLogLevelHandler(w http.ResponseWriter, _ *http.Request) error { + return utils.WriteJSON(w, Response{ + CurrentLevel: l.logLevel.Level().String(), + }) +} + +func (l *LogLevel) postLogLevelHandler(w http.ResponseWriter, r *http.Request) error { + var req Request + + if err := utils.ParseJSON(r.Body, &req); err != nil { + return utils.BadRequest(errors.WithMessage(err, "Invalid request body")) + } + + switch req.Level { + case "debug": + l.logLevel.Set(log.LevelDebug) + case "info": + l.logLevel.Set(log.LevelInfo) + case "warn": + l.logLevel.Set(log.LevelWarn) + case "error": + l.logLevel.Set(log.LevelError) + case "trace": + l.logLevel.Set(log.LevelTrace) + case "crit": + l.logLevel.Set(log.LevelCrit) + default: + return utils.BadRequest(errors.New("Invalid verbosity level")) + } + + log.Info("log level changed", "pkg", "loglevel", "level", l.logLevel.Level().String()) + + return utils.WriteJSON(w, Response{ + CurrentLevel: l.logLevel.Level().String(), + }) +} diff --git a/api/admin_test.go b/api/admin/loglevel/log_level_test.go similarity index 93% rename from api/admin_test.go rename to api/admin/loglevel/log_level_test.go index be2847cbf..3d1a8a960 100644 --- a/api/admin_test.go +++ b/api/admin/loglevel/log_level_test.go @@ -3,7 +3,7 @@ // Distributed under the GNU Lesser General Public License v3.0 software license, see the accompanying // file LICENSE or -package api +package loglevel import ( "bytes" @@ -14,6 +14,7 @@ import ( "strings" "testing" + "github.com/gorilla/mux" "github.com/stretchr/testify/assert" ) @@ -76,15 +77,16 @@ func TestLogLevelHandler(t *testing.T) { } rr := httptest.NewRecorder() - handler := http.HandlerFunc(HTTPHandler(&logLevel).ServeHTTP) - handler.ServeHTTP(rr, req) + router := mux.NewRouter() + New(&logLevel).Mount(router, "/admin/loglevel") + router.ServeHTTP(rr, req) if status := rr.Code; status != tt.expectedStatus { t.Errorf("handler returned wrong status code: got %v want %v", status, tt.expectedStatus) } if tt.expectedLevel != "" { - var response logLevelResponse + var response Response if err := json.NewDecoder(rr.Body).Decode(&response); err != nil { t.Fatalf("could not decode response: %v", err) } diff --git a/api/admin/loglevel/types.go b/api/admin/loglevel/types.go new file mode 100644 index 000000000..ce57187b1 --- /dev/null +++ b/api/admin/loglevel/types.go @@ -0,0 +1,14 @@ +// Copyright (c) 2024 The VeChainThor developers + +// Distributed under the GNU Lesser General Public License v3.0 software license, see the accompanying +// file LICENSE or + +package loglevel + +type Request struct { + Level string `json:"level"` +} + +type Response struct { + CurrentLevel string `json:"currentLevel"` +} diff --git a/api/admin_server.go b/api/admin_server.go index 26054e908..dca428b36 100644 --- a/api/admin_server.go +++ b/api/admin_server.go @@ -9,42 +9,32 @@ import ( "log/slog" "net" "net/http" + "sync/atomic" "time" - "github.com/gorilla/handlers" - "github.com/gorilla/mux" "github.com/pkg/errors" - "github.com/vechain/thor/v2/api/utils" + "github.com/vechain/thor/v2/api/admin" + "github.com/vechain/thor/v2/api/admin/health" + "github.com/vechain/thor/v2/chain" "github.com/vechain/thor/v2/co" + "github.com/vechain/thor/v2/comm" ) -func HTTPHandler(logLevel *slog.LevelVar) http.Handler { - router := mux.NewRouter() - sub := router.PathPrefix("/admin").Subrouter() - sub.Path("/loglevel"). - Methods(http.MethodGet). - Name("get-log-level"). - HandlerFunc(utils.WrapHandlerFunc(getLogLevelHandler(logLevel))) - - sub.Path("/loglevel"). - Methods(http.MethodPost). - Name("post-log-level"). - HandlerFunc(utils.WrapHandlerFunc(postLogLevelHandler(logLevel))) - - return handlers.CompressHandler(router) -} - -func StartAdminServer(addr string, logLevel *slog.LevelVar) (string, func(), error) { +func StartAdminServer( + addr string, + logLevel *slog.LevelVar, + repo *chain.Repository, + p2p *comm.Communicator, + apiLogs *atomic.Bool, +) (string, func(), error) { listener, err := net.Listen("tcp", addr) if err != nil { return "", nil, errors.Wrapf(err, "listen admin API addr [%v]", addr) } - router := mux.NewRouter() - router.PathPrefix("/admin").Handler(HTTPHandler(logLevel)) - handler := handlers.CompressHandler(router) + adminHandler := admin.New(logLevel, health.New(repo, p2p), apiLogs) - srv := &http.Server{Handler: handler, ReadHeaderTimeout: time.Second, ReadTimeout: 5 * time.Second} + srv := &http.Server{Handler: adminHandler, ReadHeaderTimeout: time.Second, ReadTimeout: 5 * time.Second} var goes co.Goes goes.Go(func() { srv.Serve(listener) diff --git a/api/api.go b/api/api.go index 38b412a97..c57e2a957 100644 --- a/api/api.go +++ b/api/api.go @@ -9,6 +9,7 @@ import ( "net/http" "net/http/pprof" "strings" + "sync/atomic" "github.com/gorilla/handlers" "github.com/gorilla/mux" @@ -32,6 +33,21 @@ import ( var logger = log.WithContext("pkg", "api") +type Config struct { + AllowedOrigins string + BacktraceLimit uint32 + CallGasLimit uint64 + PprofOn bool + SkipLogs bool + AllowCustomTracer bool + EnableReqLogger *atomic.Bool + EnableMetrics bool + LogsLimit uint64 + AllowedTracers []string + SoloMode bool + EnableDeprecated bool +} + // New return api router func New( repo *chain.Repository, @@ -41,19 +57,9 @@ func New( bft bft.Committer, nw node.Network, forkConfig thor.ForkConfig, - allowedOrigins string, - backtraceLimit uint32, - callGasLimit uint64, - pprofOn bool, - skipLogs bool, - allowCustomTracer bool, - enableReqLogger bool, - enableMetrics bool, - logsLimit uint64, - allowedTracers []string, - soloMode bool, + config Config, ) (http.HandlerFunc, func()) { - origins := strings.Split(strings.TrimSpace(allowedOrigins), ",") + origins := strings.Split(strings.TrimSpace(config.AllowedOrigins), ",") for i, o := range origins { origins[i] = strings.ToLower(strings.TrimSpace(o)) } @@ -71,27 +77,27 @@ func New( http.Redirect(w, req, "doc/stoplight-ui/", http.StatusTemporaryRedirect) }) - accounts.New(repo, stater, callGasLimit, forkConfig, bft). + accounts.New(repo, stater, config.CallGasLimit, forkConfig, bft, config.EnableDeprecated). Mount(router, "/accounts") - if !skipLogs { - events.New(repo, logDB, logsLimit). + if !config.SkipLogs { + events.New(repo, logDB, config.LogsLimit). Mount(router, "/logs/event") - transfers.New(repo, logDB, logsLimit). + transfers.New(repo, logDB, config.LogsLimit). Mount(router, "/logs/transfer") } blocks.New(repo, bft). Mount(router, "/blocks") transactions.New(repo, txPool). Mount(router, "/transactions") - debug.New(repo, stater, forkConfig, callGasLimit, allowCustomTracer, bft, allowedTracers, soloMode). + debug.New(repo, stater, forkConfig, config.CallGasLimit, config.AllowCustomTracer, bft, config.AllowedTracers, config.SoloMode). Mount(router, "/debug") node.New(nw). Mount(router, "/node") - subs := subscriptions.New(repo, origins, backtraceLimit, txPool) + subs := subscriptions.New(repo, origins, config.BacktraceLimit, txPool, config.EnableDeprecated) subs.Mount(router, "/subscriptions") - if pprofOn { + if config.PprofOn { router.HandleFunc("/debug/pprof/cmdline", pprof.Cmdline) router.HandleFunc("/debug/pprof/profile", pprof.Profile) router.HandleFunc("/debug/pprof/symbol", pprof.Symbol) @@ -99,7 +105,7 @@ func New( router.PathPrefix("/debug/pprof/").HandlerFunc(pprof.Index) } - if enableMetrics { + if config.EnableMetrics { router.Use(metricsMiddleware) } @@ -110,9 +116,7 @@ func New( handlers.ExposedHeaders([]string{"x-genesis-id", "x-thorest-ver"}), )(handler) - if enableReqLogger { - handler = RequestLoggerHandler(handler, logger) - } + handler = RequestLoggerHandler(handler, logger, config.EnableReqLogger) return handler.ServeHTTP, subs.Close // subscriptions handles hijacked conns, which need to be closed } diff --git a/api/blocks/blocks.go b/api/blocks/blocks.go index ff86e02e6..a8e072a02 100644 --- a/api/blocks/blocks.go +++ b/api/blocks/blocks.go @@ -6,8 +6,11 @@ package blocks import ( + "encoding/hex" + "fmt" "net/http" + "github.com/ethereum/go-ethereum/rlp" "github.com/gorilla/mux" "github.com/pkg/errors" "github.com/vechain/thor/v2/api/utils" @@ -34,9 +37,17 @@ func (b *Blocks) handleGetBlock(w http.ResponseWriter, req *http.Request) error if err != nil { return utils.BadRequest(errors.WithMessage(err, "revision")) } - expanded := req.URL.Query().Get("expanded") - if expanded != "" && expanded != "false" && expanded != "true" { - return utils.BadRequest(errors.WithMessage(errors.New("should be boolean"), "expanded")) + raw, err := utils.StringToBoolean(req.URL.Query().Get("raw"), false) + if err != nil { + return utils.BadRequest(errors.WithMessage(err, "raw")) + } + expanded, err := utils.StringToBoolean(req.URL.Query().Get("expanded"), false) + if err != nil { + return utils.BadRequest(errors.WithMessage(err, "expanded")) + } + + if raw && expanded { + return utils.BadRequest(errors.WithMessage(errors.New("Raw and Expanded are mutually exclusive"), "raw&expanded")) } summary, err := utils.GetSummary(revision, b.repo, b.bft) @@ -47,6 +58,16 @@ func (b *Blocks) handleGetBlock(w http.ResponseWriter, req *http.Request) error return err } + if raw { + rlpEncoded, err := rlp.EncodeToBytes(summary.Header) + if err != nil { + return err + } + return utils.WriteJSON(w, &JSONRawBlockSummary{ + fmt.Sprintf("0x%s", hex.EncodeToString(rlpEncoded)), + }) + } + isTrunk, err := b.isTrunk(summary.Header.ID(), summary.Header.Number()) if err != nil { return err @@ -61,7 +82,7 @@ func (b *Blocks) handleGetBlock(w http.ResponseWriter, req *http.Request) error } jSummary := buildJSONBlockSummary(summary, isTrunk, isFinalized) - if expanded == "true" { + if expanded { txs, err := b.repo.GetBlockTransactions(summary.Header.ID()) if err != nil { return err diff --git a/api/blocks/blocks_test.go b/api/blocks/blocks_test.go index dcb6c4e94..8c0439e59 100644 --- a/api/blocks/blocks_test.go +++ b/api/blocks/blocks_test.go @@ -6,6 +6,7 @@ package blocks_test import ( + "encoding/hex" "encoding/json" "math" "math/big" @@ -15,6 +16,7 @@ import ( "strings" "testing" + "github.com/ethereum/go-ethereum/rlp" "github.com/gorilla/mux" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" @@ -55,6 +57,8 @@ func TestBlock(t *testing.T) { "testGetFinalizedBlock": testGetFinalizedBlock, "testGetJustifiedBlock": testGetJustifiedBlock, "testGetBlockWithRevisionNumberTooHigh": testGetBlockWithRevisionNumberTooHigh, + "testMutuallyExclusiveQueries": testMutuallyExclusiveQueries, + "testGetRawBlock": testGetRawBlock, } { t.Run(name, tt) } @@ -67,6 +71,22 @@ func testBadQueryParams(t *testing.T) { assert.Equal(t, http.StatusBadRequest, statusCode) assert.Equal(t, "expanded: should be boolean", strings.TrimSpace(string(res))) + + badQueryParams = "?raw=1" + res, statusCode, err = tclient.RawHTTPClient().RawHTTPGet("/blocks/best" + badQueryParams) + require.NoError(t, err) + + assert.Equal(t, http.StatusBadRequest, statusCode) + assert.Equal(t, "raw: should be boolean", strings.TrimSpace(string(res))) +} + +func testMutuallyExclusiveQueries(t *testing.T) { + badQueryParams := "?expanded=true&raw=true" + res, statusCode, err := tclient.RawHTTPClient().RawHTTPGet("/blocks/best" + badQueryParams) + require.NoError(t, err) + + assert.Equal(t, http.StatusBadRequest, statusCode) + assert.Equal(t, "raw&expanded: Raw and Expanded are mutually exclusive", strings.TrimSpace(string(res))) } func testGetBestBlock(t *testing.T) { @@ -80,6 +100,41 @@ func testGetBestBlock(t *testing.T) { assert.Equal(t, http.StatusOK, statusCode) } +func testGetRawBlock(t *testing.T) { + res, statusCode, err := tclient.RawHTTPClient().RawHTTPGet("/blocks/best?raw=true") + require.NoError(t, err) + rawBlock := new(blocks.JSONRawBlockSummary) + if err := json.Unmarshal(res, &rawBlock); err != nil { + t.Fatal(err) + } + + blockBytes, err := hex.DecodeString(rawBlock.Raw[2:len(rawBlock.Raw)]) + if err != nil { + t.Fatal(err) + } + + header := block.Header{} + err = rlp.DecodeBytes(blockBytes, &header) + if err != nil { + t.Fatal(err) + } + + expHeader := blk.Header() + assert.Equal(t, expHeader.Number(), header.Number(), "Number should be equal") + assert.Equal(t, expHeader.ID(), header.ID(), "Hash should be equal") + assert.Equal(t, expHeader.ParentID(), header.ParentID(), "ParentID should be equal") + assert.Equal(t, expHeader.Timestamp(), header.Timestamp(), "Timestamp should be equal") + assert.Equal(t, expHeader.TotalScore(), header.TotalScore(), "TotalScore should be equal") + assert.Equal(t, expHeader.GasLimit(), header.GasLimit(), "GasLimit should be equal") + assert.Equal(t, expHeader.GasUsed(), header.GasUsed(), "GasUsed should be equal") + assert.Equal(t, expHeader.Beneficiary(), header.Beneficiary(), "Beneficiary should be equal") + assert.Equal(t, expHeader.TxsRoot(), header.TxsRoot(), "TxsRoot should be equal") + assert.Equal(t, expHeader.StateRoot(), header.StateRoot(), "StateRoot should be equal") + assert.Equal(t, expHeader.ReceiptsRoot(), header.ReceiptsRoot(), "ReceiptsRoot should be equal") + + assert.Equal(t, http.StatusOK, statusCode) +} + func testGetBlockByHeight(t *testing.T) { res, statusCode, err := tclient.RawHTTPClient().RawHTTPGet("/blocks/1") require.NoError(t, err) diff --git a/api/blocks/types.go b/api/blocks/types.go index 38261b2e5..989b63041 100644 --- a/api/blocks/types.go +++ b/api/blocks/types.go @@ -33,6 +33,10 @@ type JSONBlockSummary struct { IsFinalized bool `json:"isFinalized"` } +type JSONRawBlockSummary struct { + Raw string `json:"raw"` +} + type JSONCollapsedBlock struct { *JSONBlockSummary Transactions []thor.Bytes32 `json:"transactions"` diff --git a/api/debug/debug.go b/api/debug/debug.go index 5ff54f1dc..667ff2302 100644 --- a/api/debug/debug.go +++ b/api/debug/debug.go @@ -75,22 +75,8 @@ func New( } } -func (d *Debug) prepareClauseEnv(ctx context.Context, blockID thor.Bytes32, txIndex uint64, clauseIndex uint32) (*runtime.Runtime, *runtime.TransactionExecutor, thor.Bytes32, error) { - block, err := d.repo.GetBlock(blockID) - if err != nil { - if d.repo.IsNotFound(err) { - return nil, nil, thor.Bytes32{}, utils.Forbidden(errors.New("block not found")) - } - return nil, nil, thor.Bytes32{}, err - } - txs := block.Transactions() - if txIndex >= uint64(len(txs)) { - return nil, nil, thor.Bytes32{}, utils.Forbidden(errors.New("tx index out of range")) - } - txID := txs[txIndex].ID() - if clauseIndex >= uint32(len(txs[txIndex].Clauses())) { - return nil, nil, thor.Bytes32{}, utils.Forbidden(errors.New("clause index out of range")) - } +// prepareClauseEnv prepares the runtime environment for the specified clause. +func (d *Debug) prepareClauseEnv(ctx context.Context, block *block.Block, txID thor.Bytes32, clauseIndex uint32) (*runtime.Runtime, *runtime.TransactionExecutor, thor.Bytes32, error) { rt, err := consensus.New( d.repo, d.stater, @@ -99,17 +85,29 @@ func (d *Debug) prepareClauseEnv(ctx context.Context, blockID thor.Bytes32, txIn if err != nil { return nil, nil, thor.Bytes32{}, err } - for i, tx := range txs { - if uint64(i) > txIndex { - break + + var found bool + txs := block.Transactions() + for _, tx := range txs { + if txID == tx.ID() { + found = true + if clauseIndex >= uint32(len(tx.Clauses())) { + return nil, nil, thor.Bytes32{}, utils.Forbidden(errors.New("clause index out of range")) + } } + } + if !found { + return nil, nil, thor.Bytes32{}, utils.Forbidden(errors.New("transaction not found")) + } + + for _, tx := range block.Transactions() { txExec, err := rt.PrepareTransaction(tx) if err != nil { return nil, nil, thor.Bytes32{}, err } clauseCounter := uint32(0) for txExec.HasNextClause() { - if txIndex == uint64(i) && clauseIndex == clauseCounter { + if tx.ID() == txID && clauseIndex == clauseCounter { return rt, txExec, txID, nil } exec, _ := txExec.PrepareNext() @@ -127,18 +125,27 @@ func (d *Debug) prepareClauseEnv(ctx context.Context, blockID thor.Bytes32, txIn default: } } + + // no env created, that means tx was reverted at an early clause return nil, nil, thor.Bytes32{}, utils.Forbidden(errors.New("early reverted")) } // trace an existed clause -func (d *Debug) traceClause(ctx context.Context, tracer tracers.Tracer, blockID thor.Bytes32, txIndex uint64, clauseIndex uint32) (interface{}, error) { - rt, txExec, txID, err := d.prepareClauseEnv(ctx, blockID, txIndex, clauseIndex) +func (d *Debug) traceClause(ctx context.Context, tracer tracers.Tracer, block *block.Block, txID thor.Bytes32, clauseIndex uint32) (interface{}, error) { + rt, txExec, txID, err := d.prepareClauseEnv(ctx, block, txID, clauseIndex) if err != nil { return nil, err } + var txIndex uint64 = math.MaxUint64 + for i, tx := range block.Transactions() { + if tx.ID() == txID { + txIndex = uint64(i) + break + } + } tracer.SetContext(&tracers.Context{ - BlockID: blockID, + BlockID: block.Header().ID(), BlockTime: rt.Context().Time, TxID: txID, TxIndex: txIndex, @@ -178,11 +185,11 @@ func (d *Debug) handleTraceClause(w http.ResponseWriter, req *http.Request) erro return utils.Forbidden(err) } - blockID, txIndex, clauseIndex, err := d.parseTarget(opt.Target) + block, txID, clauseIndex, err := d.parseTarget(opt.Target) if err != nil { return err } - res, err := d.traceClause(req.Context(), tracer, blockID, txIndex, clauseIndex) + res, err := d.traceClause(req.Context(), tracer, block, txID, clauseIndex) if err != nil { return err } @@ -291,8 +298,8 @@ func (d *Debug) traceCall(ctx context.Context, tracer tracers.Tracer, header *bl return tracer.GetResult() } -func (d *Debug) debugStorage(ctx context.Context, contractAddress thor.Address, blockID thor.Bytes32, txIndex uint64, clauseIndex uint32, keyStart []byte, maxResult int) (*StorageRangeResult, error) { - rt, _, _, err := d.prepareClauseEnv(ctx, blockID, txIndex, clauseIndex) +func (d *Debug) debugStorage(ctx context.Context, contractAddress thor.Address, block *block.Block, txID thor.Bytes32, clauseIndex uint32, keyStart []byte, maxResult int) (*StorageRangeResult, error) { + rt, _, _, err := d.prepareClauseEnv(ctx, block, txID, clauseIndex) if err != nil { return nil, err } @@ -357,41 +364,72 @@ func (d *Debug) handleDebugStorage(w http.ResponseWriter, req *http.Request) err return utils.WriteJSON(w, res) } -func (d *Debug) parseTarget(target string) (blockID thor.Bytes32, txIndex uint64, clauseIndex uint32, err error) { +func (d *Debug) parseTarget(target string) (block *block.Block, txID thor.Bytes32, clauseIndex uint32, err error) { + // target can be `${blockID}/${txID|txIndex}/${clauseIndex}` or `${txID}/${clauseIndex}` parts := strings.Split(target, "/") - if len(parts) != 3 { - return thor.Bytes32{}, 0, 0, utils.BadRequest(errors.New("target:" + target + " unsupported")) + if len(parts) != 3 && len(parts) != 2 { + return nil, thor.Bytes32{}, 0, utils.BadRequest(errors.New("target:" + target + " unsupported")) } - blockID, err = thor.ParseBytes32(parts[0]) - if err != nil { - return thor.Bytes32{}, 0, 0, utils.BadRequest(errors.WithMessage(err, "target[0]")) - } - if len(parts[1]) == 64 || len(parts[1]) == 66 { - txID, err := thor.ParseBytes32(parts[1]) + + if len(parts) == 2 { + txID, err = thor.ParseBytes32(parts[0]) if err != nil { - return thor.Bytes32{}, 0, 0, utils.BadRequest(errors.WithMessage(err, "target[1]")) + return nil, thor.Bytes32{}, 0, utils.BadRequest(errors.WithMessage(err, "target([0]")) } - - txMeta, err := d.repo.NewChain(blockID).GetTransactionMeta(txID) + bestChain := d.repo.NewBestChain() + txMeta, err := bestChain.GetTransactionMeta(txID) if err != nil { if d.repo.IsNotFound(err) { - return thor.Bytes32{}, 0, 0, utils.Forbidden(errors.New("transaction not found")) + return nil, thor.Bytes32{}, 0, utils.Forbidden(errors.New("transaction not found")) } - return thor.Bytes32{}, 0, 0, err + return nil, thor.Bytes32{}, 0, err + } + block, err = bestChain.GetBlock(txMeta.BlockNum) + if err != nil { + return nil, thor.Bytes32{}, 0, err } - txIndex = txMeta.Index } else { - i, err := strconv.ParseUint(parts[1], 0, 0) + blockID, err := thor.ParseBytes32(parts[0]) + if err != nil { + return nil, thor.Bytes32{}, 0, utils.BadRequest(errors.WithMessage(err, "target[0]")) + } + block, err = d.repo.GetBlock(blockID) if err != nil { - return thor.Bytes32{}, 0, 0, utils.BadRequest(errors.WithMessage(err, "target[1]")) + return nil, thor.Bytes32{}, 0, err + } + if len(parts[1]) == 64 || len(parts[1]) == 66 { + txID, err = thor.ParseBytes32(parts[1]) + if err != nil { + return nil, thor.Bytes32{}, 0, utils.BadRequest(errors.WithMessage(err, "target[1]")) + } + + var found bool + for _, tx := range block.Transactions() { + if tx.ID() == txID { + found = true + break + } + } + if !found { + return nil, thor.Bytes32{}, 0, utils.Forbidden(errors.New("transaction not found")) + } + } else { + i, err := strconv.ParseUint(parts[1], 0, 0) + if err != nil { + return nil, thor.Bytes32{}, 0, utils.BadRequest(errors.WithMessage(err, "target[1]")) + } + if i >= uint64(len(block.Transactions())) { + return nil, thor.Bytes32{}, 0, utils.Forbidden(errors.New("tx index out of range")) + } + txID = block.Transactions()[i].ID() } - txIndex = i } - i, err := strconv.ParseUint(parts[2], 0, 0) + + i, err := strconv.ParseUint(parts[len(parts)-1], 0, 0) if err != nil { - return thor.Bytes32{}, 0, 0, utils.BadRequest(errors.WithMessage(err, "target[2]")) + return nil, thor.Bytes32{}, 0, utils.BadRequest(errors.WithMessage(err, fmt.Sprintf("target[%d]", len(parts)-1))) } else if i > math.MaxUint32 { - return thor.Bytes32{}, 0, 0, utils.BadRequest(errors.New("invalid target[2]")) + return nil, thor.Bytes32{}, 0, utils.BadRequest(fmt.Errorf("invalid target[%d]", len(parts)-1)) } clauseIndex = uint32(i) return diff --git a/api/debug/debug_test.go b/api/debug/debug_test.go index 0b0b2d3b3..478fe4a42 100644 --- a/api/debug/debug_test.go +++ b/api/debug/debug_test.go @@ -6,7 +6,6 @@ package debug import ( - "context" "encoding/json" "fmt" "math/big" @@ -63,6 +62,7 @@ func TestDebug(t *testing.T) { "testTraceClauseWithClauseIndexOutOfBound": testTraceClauseWithClauseIndexOutOfBound, "testTraceClauseWithCustomTracer": testTraceClauseWithCustomTracer, "testTraceClause": testTraceClause, + "testTraceClauseWithoutBlockID": testTraceClauseWithoutBlockID, } { t.Run(name, tt) } @@ -175,9 +175,11 @@ func testTraceClauseWithBadBlockID(t *testing.T) { } func testTraceClauseWithNonExistingBlockID(t *testing.T) { - _, _, _, err := debug.prepareClauseEnv(context.Background(), datagen.RandomHash(), 1, 1) - - assert.Error(t, err) + traceClauseOption := &TraceClauseOption{ + Name: "structLogger", + Target: fmt.Sprintf("%s/x/x", datagen.RandomHash()), + } + httpPostAndCheckResponseStatus(t, "/debug/tracers", traceClauseOption, 500) } func testTraceClauseWithBadTxID(t *testing.T) { @@ -264,6 +266,26 @@ func testTraceClause(t *testing.T) { assert.Equal(t, expectedExecutionResult, parsedExecutionRes) } +func testTraceClauseWithoutBlockID(t *testing.T) { + traceClauseOption := &TraceClauseOption{ + Name: "structLogger", + Target: fmt.Sprintf("%s/1", transaction.ID()), + } + expectedExecutionResult := &logger.ExecutionResult{ + Gas: 0, + Failed: false, + ReturnValue: "", + StructLogs: make([]logger.StructLogRes, 0), + } + res := httpPostAndCheckResponseStatus(t, "/debug/tracers", traceClauseOption, 200) + + var parsedExecutionRes *logger.ExecutionResult + if err := json.Unmarshal([]byte(res), &parsedExecutionRes); err != nil { + t.Fatal(err) + } + assert.Equal(t, expectedExecutionResult, parsedExecutionRes) +} + func testTraceClauseWithTxIndexOutOfBound(t *testing.T) { traceClauseOption := &TraceClauseOption{ Name: "structLogger", diff --git a/api/doc/thor.yaml b/api/doc/thor.yaml index 2a0d30b9e..99416a039 100644 --- a/api/doc/thor.yaml +++ b/api/doc/thor.yaml @@ -12,7 +12,7 @@ info: license: name: LGPL 3.0 url: https://www.gnu.org/licenses/lgpl-3.0.en.html - version: 2.1.4 + version: 2.2.0 servers: - url: / description: Current Node @@ -292,6 +292,7 @@ paths: parameters: - $ref: '#/components/parameters/RevisionInPath' - $ref: '#/components/parameters/ExpandedInQuery' + - $ref: '#/components/parameters/RawBlockInQuery' tags: - Blocks summary: Retrieve a block @@ -2139,11 +2140,12 @@ components: The unified path of the target to be traced. Currently, only the clause is supported. Format: - `blockID/(txIndex|txId)/clauseIndex` + `blockID/(txIndex|txId)/clauseIndex` or `txID/clauseIndex` + example: '0x010709463c1f0c9aa66a31182fb36d1977d99bfb6526bae0564a0eac4006c31a/0/0' nullable: false - pattern: '^0x[0-9a-fA-F]{64}\/(0x[0-9a-fA-F]{64}|\d+)\/[0-9]+$' + pattern: '^0x[0-9a-fA-F]{64}(\/(0x[0-9a-fA-F]{64}|\d+))?\/[0-9]+$' example: target: '0x010709463c1f0c9aa66a31182fb36d1977d99bfb6526bae0564a0eac4006c31a/0/0' @@ -2209,9 +2211,9 @@ components: The unified path of the transaction clause. Format: - `blockID/(txIndex|txId)/clauseIndex` + `blockID/(txIndex|txId)/clauseIndex` or `txID/clauseIndex` nullable: false - pattern: '^0x[0-9a-fA-F]{64}\/(0x[0-9a-fA-F]{64}|\d+)\/[0-9]+$' + pattern: '^0x[0-9a-fA-F]{64}(\/(0x[0-9a-fA-F]{64}|\d+))?\/[0-9]+$' StorageRange: type: object @@ -2389,6 +2391,18 @@ components: type: boolean example: false + RawBlockInQuery: + name: raw + in: query + required: false + description: | + Whether the block should be returned in RLP encoding or not. + - `true` returns `block` as an RLP encoded object + - `false` returns `block` as a structured JSON object + schema: + type: boolean + example: false + PendingInQuery: name: pending in: query diff --git a/api/events/events_test.go b/api/events/events_test.go index 1054266fe..ffc247843 100644 --- a/api/events/events_test.go +++ b/api/events/events_test.go @@ -152,7 +152,7 @@ func TestOption(t *testing.T) { } func TestZeroFrom(t *testing.T) { - thorChain := initEventServer(t, 5) + thorChain := initEventServer(t, 100) defer ts.Close() insertBlocks(t, thorChain, 5) diff --git a/api/events/types.go b/api/events/types.go index bfb032095..78d0cf712 100644 --- a/api/events/types.go +++ b/api/events/types.go @@ -179,7 +179,7 @@ func ConvertRange(chain *chain.Chain, r *Range) (*logdb.Range, error) { }, nil } - // Units are block numbers - numbers will have a max ceiling at logdb.MaxBlockNumbe + // Units are block numbers - numbers will have a max ceiling at logdb.MaxBlockNumber if r.From != nil && *r.From > logdb.MaxBlockNumber { return &emptyRange, nil } diff --git a/api/events/types_test.go b/api/events/types_test.go index 7b911b453..6d16ca4d8 100644 --- a/api/events/types_test.go +++ b/api/events/types_test.go @@ -6,7 +6,6 @@ package events import ( - "math" "testing" "github.com/ethereum/go-ethereum/common/hexutil" @@ -49,26 +48,13 @@ func testConvertRangeWithBlockRangeType(t *testing.T, chain *testchain.Chain) { assert.NoError(t, err) assert.Equal(t, uint32(*rng.From), convertedRng.From) assert.Equal(t, uint32(*rng.To), convertedRng.To) - - // ensure wild block numbers have a max ceiling of chain.head - rng = newRange(BlockRangeType, 100, 2200) - - convertedRng, err = ConvertRange(chain.Repo().NewBestChain(), rng) - require.NoError(t, err) - - bestBlock, err := chain.BestBlock() - require.NoError(t, err) - - assert.NoError(t, err) - assert.Equal(t, bestBlock.Header().Number(), convertedRng.From) - assert.Equal(t, bestBlock.Header().Number(), convertedRng.To) } func testConvertRangeWithTimeRangeTypeLessThenGenesis(t *testing.T, chain *testchain.Chain) { rng := newRange(TimeRangeType, 100, 2200) expectedEmptyRange := &logdb.Range{ - From: math.MaxUint32, - To: math.MaxUint32, + From: logdb.MaxBlockNumber, + To: logdb.MaxBlockNumber, } convRng, err := ConvertRange(chain.Repo().NewBestChain(), rng) @@ -97,8 +83,8 @@ func testConvertRangeWithFromGreaterThanGenesis(t *testing.T, chain *testchain.C rng := newRange(TimeRangeType, genesis.Timestamp()+1_000, genesis.Timestamp()+10_000) expectedEmptyRange := &logdb.Range{ - From: math.MaxUint32, - To: math.MaxUint32, + From: logdb.MaxBlockNumber, + To: logdb.MaxBlockNumber, } convRng, err := ConvertRange(chain.Repo().NewBestChain(), rng) diff --git a/api/metrics_test.go b/api/metrics_test.go index b7b1b7e0d..c76533162 100644 --- a/api/metrics_test.go +++ b/api/metrics_test.go @@ -48,7 +48,7 @@ func TestMetricsMiddleware(t *testing.T) { assert.NotNil(t, err) router := mux.NewRouter() - acc := accounts.New(thorChain.Repo(), thorChain.Stater(), math.MaxUint64, thor.NoFork, thorChain.Engine()) + acc := accounts.New(thorChain.Repo(), thorChain.Stater(), math.MaxUint64, thor.NoFork, thorChain.Engine(), true) acc.Mount(router, "/accounts") router.PathPrefix("/metrics").Handler(metrics.HTTPHandler()) router.Use(metricsMiddleware) @@ -103,7 +103,7 @@ func TestWebsocketMetrics(t *testing.T) { require.NoError(t, err) router := mux.NewRouter() - sub := subscriptions.New(thorChain.Repo(), []string{"*"}, 10, txpool.New(thorChain.Repo(), thorChain.Stater(), txpool.Options{})) + sub := subscriptions.New(thorChain.Repo(), []string{"*"}, 10, txpool.New(thorChain.Repo(), thorChain.Stater(), txpool.Options{}), true) sub.Mount(router, "/subscriptions") router.PathPrefix("/metrics").Handler(metrics.HTTPHandler()) router.Use(metricsMiddleware) diff --git a/api/node/node_test.go b/api/node/node_test.go index 3dd2e96ee..873ad29ad 100644 --- a/api/node/node_test.go +++ b/api/node/node_test.go @@ -40,7 +40,8 @@ func initCommServer(t *testing.T) { Limit: 10000, LimitPerAccount: 16, MaxLifetime: 10 * time.Minute, - })) + }), + ) router := mux.NewRouter() node.New(communicator).Mount(router, "/node") diff --git a/api/request_logger.go b/api/request_logger.go index 3d48a2d36..451059814 100644 --- a/api/request_logger.go +++ b/api/request_logger.go @@ -9,14 +9,19 @@ import ( "bytes" "io" "net/http" + "sync/atomic" "time" "github.com/vechain/thor/v2/log" ) // RequestLoggerHandler returns a http handler to ensure requests are syphoned into the writer -func RequestLoggerHandler(handler http.Handler, logger log.Logger) http.Handler { +func RequestLoggerHandler(handler http.Handler, logger log.Logger, enabled *atomic.Bool) http.Handler { fn := func(w http.ResponseWriter, r *http.Request) { + if !enabled.Load() { + handler.ServeHTTP(w, r) + return + } // Read and log the body (note: this can only be done once) // Ensure you don't disrupt the request body for handlers that need to read it var bodyBytes []byte diff --git a/api/request_logger_test.go b/api/request_logger_test.go index 6b8ddcd91..3368e6fc8 100644 --- a/api/request_logger_test.go +++ b/api/request_logger_test.go @@ -10,6 +10,7 @@ import ( "net/http" "net/http/httptest" "strings" + "sync/atomic" "testing" "github.com/stretchr/testify/assert" @@ -59,6 +60,8 @@ func (m *mockLogger) GetLoggedData() []interface{} { func TestRequestLoggerHandler(t *testing.T) { mockLog := &mockLogger{} + enabled := atomic.Bool{} + enabled.Store(true) // Define a test handler to wrap testHandler := http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { @@ -67,7 +70,7 @@ func TestRequestLoggerHandler(t *testing.T) { }) // Create the RequestLoggerHandler - loggerHandler := RequestLoggerHandler(testHandler, mockLog) + loggerHandler := RequestLoggerHandler(testHandler, mockLog, &enabled) // Create a test HTTP request reqBody := "test body" diff --git a/api/subscriptions/subscriptions.go b/api/subscriptions/subscriptions.go index 7582da5bb..715a71308 100644 --- a/api/subscriptions/subscriptions.go +++ b/api/subscriptions/subscriptions.go @@ -25,14 +25,15 @@ import ( const txQueueSize = 20 type Subscriptions struct { - backtraceLimit uint32 - repo *chain.Repository - upgrader *websocket.Upgrader - pendingTx *pendingTx - done chan struct{} - wg sync.WaitGroup - beat2Cache *messageCache[Beat2Message] - beatCache *messageCache[BeatMessage] + backtraceLimit uint32 + enabledDeprecated bool + repo *chain.Repository + upgrader *websocket.Upgrader + pendingTx *pendingTx + done chan struct{} + wg sync.WaitGroup + beat2Cache *messageCache[Beat2Message] + beatCache *messageCache[BeatMessage] } type msgReader interface { @@ -50,10 +51,11 @@ const ( pingPeriod = (pongWait * 7) / 10 ) -func New(repo *chain.Repository, allowedOrigins []string, backtraceLimit uint32, txpool *txpool.TxPool) *Subscriptions { +func New(repo *chain.Repository, allowedOrigins []string, backtraceLimit uint32, txpool *txpool.TxPool, enabledDeprecated bool) *Subscriptions { sub := &Subscriptions{ - backtraceLimit: backtraceLimit, - repo: repo, + backtraceLimit: backtraceLimit, + repo: repo, + enabledDeprecated: enabledDeprecated, upgrader: &websocket.Upgrader{ EnableCompression: true, CheckOrigin: func(r *http.Request) bool { @@ -195,6 +197,9 @@ func (s *Subscriptions) handleSubject(w http.ResponseWriter, req *http.Request) return err } case "beat": + if !s.enabledDeprecated { + return utils.HTTPError(nil, http.StatusGone) + } if reader, err = s.handleBeatReader(w, req); err != nil { return err } diff --git a/api/subscriptions/subscriptions_test.go b/api/subscriptions/subscriptions_test.go index 0c0bffe3a..8cfb55f7f 100644 --- a/api/subscriptions/subscriptions_test.go +++ b/api/subscriptions/subscriptions_test.go @@ -36,7 +36,7 @@ var ts *httptest.Server var blocks []*block.Block func TestSubscriptions(t *testing.T) { - initSubscriptionsServer(t) + initSubscriptionsServer(t, true) defer ts.Close() for name, tt := range map[string]func(*testing.T){ @@ -51,6 +51,17 @@ func TestSubscriptions(t *testing.T) { } } +func TestDeprecatedSubscriptions(t *testing.T) { + initSubscriptionsServer(t, false) + defer ts.Close() + + u := url.URL{Scheme: "ws", Host: strings.TrimPrefix(ts.URL, "http://"), Path: "/subscriptions/beat"} + + _, resp, err := websocket.DefaultDialer.Dial(u.String(), nil) + assert.Error(t, err) + assert.Equal(t, http.StatusGone, resp.StatusCode) +} + func testHandleSubjectWithBlock(t *testing.T) { genesisBlock := blocks[0] queryArg := fmt.Sprintf("pos=%s", genesisBlock.Header().ID().String()) @@ -216,7 +227,7 @@ func TestParseAddress(t *testing.T) { assert.Equal(t, expectedAddr, *result) } -func initSubscriptionsServer(t *testing.T) { +func initSubscriptionsServer(t *testing.T, enabledDeprecated bool) { thorChain, err := testchain.NewIntegrationTestChain() require.NoError(t, err) @@ -263,7 +274,7 @@ func initSubscriptionsServer(t *testing.T) { require.NoError(t, err) router := mux.NewRouter() - New(thorChain.Repo(), []string{}, 5, txPool). + New(thorChain.Repo(), []string{}, 5, txPool, enabledDeprecated). Mount(router, "/subscriptions") ts = httptest.NewServer(router) } @@ -319,7 +330,7 @@ func TestSubscriptionsBacktrace(t *testing.T) { require.NoError(t, err) router := mux.NewRouter() - New(thorChain.Repo(), []string{}, 5, txPool).Mount(router, "/subscriptions") + New(thorChain.Repo(), []string{}, 5, txPool, true).Mount(router, "/subscriptions") ts = httptest.NewServer(router) defer ts.Close() diff --git a/api/utils/http.go b/api/utils/http.go index 652c3e408..2235797de 100644 --- a/api/utils/http.go +++ b/api/utils/http.go @@ -9,6 +9,8 @@ import ( "encoding/json" "io" "net/http" + + "github.com/pkg/errors" ) type httpError struct { @@ -36,6 +38,17 @@ func BadRequest(cause error) error { } } +func StringToBoolean(boolStr string, defaultVal bool) (bool, error) { + if boolStr == "" { + return defaultVal, nil + } else if boolStr == "false" { + return false, nil + } else if boolStr == "true" { + return true, nil + } + return false, errors.New("should be boolean") +} + // Forbidden convenience method to create http forbidden error. func Forbidden(cause error) error { return &httpError{ diff --git a/chain/repository.go b/chain/repository.go index 5b69d88e2..df622bd30 100644 --- a/chain/repository.go +++ b/chain/repository.go @@ -352,7 +352,6 @@ func (r *Repository) GetBlock(id thor.Bytes32) (*block.Block, error) { return block.Compose(summary.Header, txs), nil } - func (r *Repository) getReceipt(key []byte) (*tx.Receipt, error) { result := "hit" receipt, err := r.caches.receipts.GetOrLoad(string(key), func() (interface{}, error) { diff --git a/cmd/thor/flags.go b/cmd/thor/flags.go index 4b18f22ad..2ce97516e 100644 --- a/cmd/thor/flags.go +++ b/cmd/thor/flags.go @@ -69,6 +69,10 @@ var ( Value: 1000, Usage: "limit the number of logs returned by /logs API", } + apiEnableDeprecatedFlag = cli.BoolFlag{ + Name: "api-enable-deprecated", + Usage: "enable deprecated API endpoints (POST /accounts/{address}, POST /accounts, WS /subscriptions/beat", + } enableAPILogsFlag = cli.BoolFlag{ Name: "enable-api-logs", Usage: "enables API requests logging", diff --git a/cmd/thor/main.go b/cmd/thor/main.go index ce8d1f965..bb5036472 100644 --- a/cmd/thor/main.go +++ b/cmd/thor/main.go @@ -11,7 +11,7 @@ import ( "io" "os" "path/filepath" - "strings" + "sync/atomic" "time" "github.com/ethereum/go-ethereum/accounts/keystore" @@ -80,6 +80,7 @@ func main() { apiCallGasLimitFlag, apiBacktraceLimitFlag, apiAllowCustomTracerFlag, + apiEnableDeprecatedFlag, enableAPILogsFlag, apiLogsLimitFlag, verbosityFlag, @@ -115,6 +116,7 @@ func main() { apiCallGasLimitFlag, apiBacktraceLimitFlag, apiAllowCustomTracerFlag, + apiEnableDeprecatedFlag, enableAPILogsFlag, apiLogsLimitFlag, onDemandFlag, @@ -179,16 +181,6 @@ func defaultAction(ctx *cli.Context) error { defer func() { log.Info("stopping metrics server..."); closeFunc() }() } - adminURL := "" - if ctx.Bool(enableAdminFlag.Name) { - url, closeFunc, err := api.StartAdminServer(ctx.String(adminAddrFlag.Name), logLevel) - if err != nil { - return fmt.Errorf("unable to start admin server - %w", err) - } - adminURL = url - defer func() { log.Info("stopping admin server..."); closeFunc() }() - } - gene, forkConfig, err := selectGenesis(ctx) if err != nil { return err @@ -242,6 +234,24 @@ func defaultAction(ctx *cli.Context) error { return err } + adminURL := "" + logAPIRequests := &atomic.Bool{} + logAPIRequests.Store(ctx.Bool(enableAPILogsFlag.Name)) + if ctx.Bool(enableAdminFlag.Name) { + url, closeFunc, err := api.StartAdminServer( + ctx.String(adminAddrFlag.Name), + logLevel, + repo, + p2pCommunicator.Communicator(), + logAPIRequests, + ) + if err != nil { + return fmt.Errorf("unable to start admin server - %w", err) + } + adminURL = url + defer func() { log.Info("stopping admin server..."); closeFunc() }() + } + bftEngine, err := bft.NewEngine(repo, mainDB, forkConfig, master.Address()) if err != nil { return errors.Wrap(err, "init bft engine") @@ -255,17 +265,7 @@ func defaultAction(ctx *cli.Context) error { bftEngine, p2pCommunicator.Communicator(), forkConfig, - ctx.String(apiCorsFlag.Name), - uint32(ctx.Uint64(apiBacktraceLimitFlag.Name)), - ctx.Uint64(apiCallGasLimitFlag.Name), - ctx.Bool(pprofFlag.Name), - skipLogs, - ctx.Bool(apiAllowCustomTracerFlag.Name), - ctx.Bool(enableAPILogsFlag.Name), - ctx.Bool(enableMetricsFlag.Name), - ctx.Uint64(apiLogsLimitFlag.Name), - parseTracerList(strings.TrimSpace(ctx.String(allowedTracersFlag.Name))), - false, + makeAPIConfig(ctx, logAPIRequests, false), ) defer func() { log.Info("closing API..."); apiCloser() }() @@ -298,7 +298,8 @@ func defaultAction(ctx *cli.Context) error { p2pCommunicator.Communicator(), ctx.Uint64(targetGasLimitFlag.Name), skipLogs, - forkConfig).Run(exitSignal) + forkConfig, + ).Run(exitSignal) } func soloAction(ctx *cli.Context) error { @@ -312,6 +313,12 @@ func soloAction(ctx *cli.Context) error { logLevel := initLogger(lvl, ctx.Bool(jsonLogsFlag.Name)) + onDemandBlockProduction := ctx.Bool(onDemandFlag.Name) + blockProductionInterval := ctx.Uint64(blockInterval.Name) + if blockProductionInterval == 0 { + return errors.New("block-interval cannot be zero") + } + // enable metrics as soon as possible metricsURL := "" if ctx.Bool(enableMetricsFlag.Name) { @@ -324,16 +331,6 @@ func soloAction(ctx *cli.Context) error { defer func() { log.Info("stopping metrics server..."); closeFunc() }() } - adminURL := "" - if ctx.Bool(enableAdminFlag.Name) { - url, closeFunc, err := api.StartAdminServer(ctx.String(adminAddrFlag.Name), logLevel) - if err != nil { - return fmt.Errorf("unable to start admin server - %w", err) - } - adminURL = url - defer func() { log.Info("stopping admin server..."); closeFunc() }() - } - var ( gene *genesis.Genesis forkConfig thor.ForkConfig @@ -378,6 +375,24 @@ func soloAction(ctx *cli.Context) error { return err } + adminURL := "" + logAPIRequests := &atomic.Bool{} + logAPIRequests.Store(ctx.Bool(enableAPILogsFlag.Name)) + if ctx.Bool(enableAdminFlag.Name) { + url, closeFunc, err := api.StartAdminServer( + ctx.String(adminAddrFlag.Name), + logLevel, + repo, + nil, + logAPIRequests, + ) + if err != nil { + return fmt.Errorf("unable to start admin server - %w", err) + } + adminURL = url + defer func() { log.Info("stopping admin server..."); closeFunc() }() + } + printStartupMessage1(gene, repo, nil, instanceDir, forkConfig) skipLogs := ctx.Bool(skipLogsFlag.Name) @@ -401,6 +416,7 @@ func soloAction(ctx *cli.Context) error { defer func() { log.Info("closing tx pool..."); txPool.Close() }() bftEngine := solo.NewBFTEngine(repo) + apiHandler, apiCloser := api.New( repo, state.NewStater(mainDB), @@ -409,17 +425,7 @@ func soloAction(ctx *cli.Context) error { bftEngine, &solo.Communicator{}, forkConfig, - ctx.String(apiCorsFlag.Name), - uint32(ctx.Uint64(apiBacktraceLimitFlag.Name)), - ctx.Uint64(apiCallGasLimitFlag.Name), - ctx.Bool(pprofFlag.Name), - skipLogs, - ctx.Bool(apiAllowCustomTracerFlag.Name), - ctx.Bool(enableAPILogsFlag.Name), - ctx.Bool(enableMetricsFlag.Name), - ctx.Uint64(apiLogsLimitFlag.Name), - parseTracerList(strings.TrimSpace(ctx.String(allowedTracersFlag.Name))), - true, + makeAPIConfig(ctx, logAPIRequests, true), ) defer func() { log.Info("closing API..."); apiCloser() }() @@ -449,9 +455,9 @@ func soloAction(ctx *cli.Context) error { logDB, txPool, ctx.Uint64(gasLimitFlag.Name), - ctx.Bool(onDemandFlag.Name), + onDemandBlockProduction, skipLogs, - blockInterval, + blockProductionInterval, forkConfig).Run(exitSignal) } diff --git a/cmd/thor/utils.go b/cmd/thor/utils.go index 396b153ae..3be6d4186 100644 --- a/cmd/thor/utils.go +++ b/cmd/thor/utils.go @@ -23,6 +23,7 @@ import ( "runtime" "runtime/debug" "strings" + "sync/atomic" "syscall" "time" @@ -37,6 +38,7 @@ import ( "github.com/mattn/go-isatty" "github.com/mattn/go-tty" "github.com/pkg/errors" + "github.com/vechain/thor/v2/api" "github.com/vechain/thor/v2/api/doc" "github.com/vechain/thor/v2/chain" "github.com/vechain/thor/v2/cmd/thor/node" @@ -274,6 +276,23 @@ func parseGenesisFile(filePath string) (*genesis.Genesis, thor.ForkConfig, error return customGen, forkConfig, nil } +func makeAPIConfig(ctx *cli.Context, logAPIRequests *atomic.Bool, soloMode bool) api.Config { + return api.Config{ + AllowedOrigins: ctx.String(apiCorsFlag.Name), + BacktraceLimit: uint32(ctx.Uint64(apiBacktraceLimitFlag.Name)), + CallGasLimit: ctx.Uint64(apiCallGasLimitFlag.Name), + PprofOn: ctx.Bool(pprofFlag.Name), + SkipLogs: ctx.Bool(skipLogsFlag.Name), + AllowCustomTracer: ctx.Bool(apiAllowCustomTracerFlag.Name), + EnableReqLogger: logAPIRequests, + EnableMetrics: ctx.Bool(enableMetricsFlag.Name), + LogsLimit: ctx.Uint64(apiLogsLimitFlag.Name), + AllowedTracers: parseTracerList(strings.TrimSpace(ctx.String(allowedTracersFlag.Name))), + EnableDeprecated: ctx.Bool(apiEnableDeprecatedFlag.Name), + SoloMode: soloMode, + } +} + func makeConfigDir(ctx *cli.Context) (string, error) { dir := ctx.String(configDirFlag.Name) if dir == "" { diff --git a/comm/communicator.go b/comm/communicator.go index 9d0a5a530..48419779a 100644 --- a/comm/communicator.go +++ b/comm/communicator.go @@ -72,7 +72,7 @@ func (c *Communicator) Sync(ctx context.Context, handler HandleBlockStream) { delay := initSyncInterval syncCount := 0 - shouldSynced := func() bool { + isSynced := func() bool { bestBlockTime := c.repo.BestBlockSummary().Header.Timestamp() now := uint64(time.Now().Unix()) if bestBlockTime+thor.BlockInterval >= now { @@ -115,9 +115,10 @@ func (c *Communicator) Sync(ctx context.Context, handler HandleBlockStream) { } syncCount++ - if shouldSynced() { + if isSynced() { delay = syncInterval c.onceSynced.Do(func() { + // once off - after a bootstrap the syncedCh trigger the peers.syncTxs close(c.syncedCh) }) } diff --git a/metrics/noop.go b/metrics/noop.go index b804486b6..a9e24ab2c 100644 --- a/metrics/noop.go +++ b/metrics/noop.go @@ -7,7 +7,6 @@ package metrics import ( "net/http" - "time" ) // noopMetrics implements a no operations metrics service @@ -51,5 +50,3 @@ func (n noopMeters) Set(int64) {} func (n noopMeters) Observe(int64) {} func (n *noopMetrics) ObserveWithLabels(int64, map[string]string) {} - -func (n *noopMetrics) collectDiskIO(time.Duration) {} diff --git a/metrics/prometheus.go b/metrics/prometheus.go index c1a21a345..15447f6dc 100644 --- a/metrics/prometheus.go +++ b/metrics/prometheus.go @@ -10,6 +10,7 @@ import ( "fmt" "net/http" "os" + "runtime" "strconv" "strings" "sync" @@ -131,30 +132,6 @@ func (o *prometheusMetrics) GetOrCreateGaugeVecMeter(name string, labels []strin return meter } -func (o *prometheusMetrics) newHistogramMeter(name string, buckets []int64) HistogramMeter { - var floatBuckets []float64 - for _, bucket := range buckets { - floatBuckets = append(floatBuckets, float64(bucket)) - } - - meter := prometheus.NewHistogram( - prometheus.HistogramOpts{ - Namespace: namespace, - Name: name, - Buckets: floatBuckets, - }, - ) - - err := prometheus.Register(meter) - if err != nil { - logger.Warn("unable to register metric", "err", err) - } - - return &promHistogramMeter{ - histogram: meter, - } -} - func getIOLineValue(line string) int64 { fields := strings.Fields(line) if len(fields) != 2 { @@ -194,6 +171,9 @@ func getDiskIOData() (int64, int64, error) { } func (o *prometheusMetrics) collectDiskIO(refresh time.Duration) { + if runtime.GOOS != "linux" { + return + } for { reads, writes, err := getDiskIOData() if err == nil { @@ -208,6 +188,30 @@ func (o *prometheusMetrics) collectDiskIO(refresh time.Duration) { } } +func (o *prometheusMetrics) newHistogramMeter(name string, buckets []int64) HistogramMeter { + var floatBuckets []float64 + for _, bucket := range buckets { + floatBuckets = append(floatBuckets, float64(bucket)) + } + + meter := prometheus.NewHistogram( + prometheus.HistogramOpts{ + Namespace: namespace, + Name: name, + Buckets: floatBuckets, + }, + ) + + err := prometheus.Register(meter) + if err != nil { + logger.Warn("unable to register metric", "err", err) + } + + return &promHistogramMeter{ + histogram: meter, + } +} + type promHistogramMeter struct { histogram prometheus.Histogram } diff --git a/thorclient/api_test.go b/thorclient/api_test.go index e6a0e43be..e8ae49a8a 100644 --- a/thorclient/api_test.go +++ b/thorclient/api_test.go @@ -50,7 +50,7 @@ func initAPIServer(t *testing.T) (*testchain.Chain, *httptest.Server) { router := mux.NewRouter() - accounts.New(thorChain.Repo(), thorChain.Stater(), uint64(gasLimit), thor.NoFork, thorChain.Engine()). + accounts.New(thorChain.Repo(), thorChain.Stater(), uint64(gasLimit), thor.NoFork, thorChain.Engine(), true). Mount(router, "/accounts") blocks.New(thorChain.Repo(), thorChain.Engine()).Mount(router, "/blocks") diff --git a/thorclient/httpclient/client.go b/thorclient/httpclient/client.go index 8f88783f5..ce05bf17f 100644 --- a/thorclient/httpclient/client.go +++ b/thorclient/httpclient/client.go @@ -33,9 +33,13 @@ type Client struct { // New creates a new Client with the provided URL. func New(url string) *Client { + return NewWithHTTP(url, http.DefaultClient) +} + +func NewWithHTTP(url string, c *http.Client) *Client { return &Client{ url: url, - c: &http.Client{}, + c: c, } } diff --git a/thorclient/thorclient.go b/thorclient/thorclient.go index 8458a0ae4..0b7939f51 100644 --- a/thorclient/thorclient.go +++ b/thorclient/thorclient.go @@ -11,6 +11,7 @@ package thorclient import ( "fmt" + "net/http" "github.com/ethereum/go-ethereum/common/hexutil" "github.com/ethereum/go-ethereum/common/math" @@ -44,6 +45,13 @@ func New(url string) *Client { } } +// NewWithHTTP creates a new Client using the provided HTTP URL and HTTP client. +func NewWithHTTP(url string, c *http.Client) *Client { + return &Client{ + httpConn: httpclient.NewWithHTTP(url, c), + } +} + // NewWithWS creates a new Client using the provided HTTP and WebSocket URLs. // Returns an error if the WebSocket connection fails. func NewWithWS(url string) (*Client, error) { @@ -202,7 +210,7 @@ func (c *Client) ChainTag() (byte, error) { } // SubscribeBlocks subscribes to block updates over WebSocket. -func (c *Client) SubscribeBlocks(pos string) (*common.Subscription[*blocks.JSONCollapsedBlock], error) { +func (c *Client) SubscribeBlocks(pos string) (*common.Subscription[*subscriptions.BlockMessage], error) { if c.wsConn == nil { return nil, fmt.Errorf("not a websocket typed client") } diff --git a/thorclient/wsclient/client.go b/thorclient/wsclient/client.go index 057d5aa48..9eb1519ab 100644 --- a/thorclient/wsclient/client.go +++ b/thorclient/wsclient/client.go @@ -16,7 +16,6 @@ import ( "github.com/vechain/thor/v2/thor" "github.com/gorilla/websocket" - "github.com/vechain/thor/v2/api/blocks" "github.com/vechain/thor/v2/api/subscriptions" "github.com/vechain/thor/v2/thorclient/common" ) @@ -89,7 +88,7 @@ func (c *Client) SubscribeEvents(pos string, filter *subscriptions.EventFilter) // SubscribeBlocks subscribes to block updates based on the provided query. // It returns a Subscription that streams block messages or an error if the connection fails. -func (c *Client) SubscribeBlocks(pos string) (*common.Subscription[*blocks.JSONCollapsedBlock], error) { +func (c *Client) SubscribeBlocks(pos string) (*common.Subscription[*subscriptions.BlockMessage], error) { queryValues := &url.Values{} queryValues.Add("pos", pos) conn, err := c.connect("/subscriptions/block", queryValues) @@ -97,7 +96,7 @@ func (c *Client) SubscribeBlocks(pos string) (*common.Subscription[*blocks.JSONC return nil, fmt.Errorf("unable to connect - %w", err) } - return subscribe[blocks.JSONCollapsedBlock](conn), nil + return subscribe[subscriptions.BlockMessage](conn), nil } // SubscribeTransfers subscribes to transfer events based on the provided query. diff --git a/thorclient/wsclient/client_test.go b/thorclient/wsclient/client_test.go index 483ae7233..19dd1b395 100644 --- a/thorclient/wsclient/client_test.go +++ b/thorclient/wsclient/client_test.go @@ -13,13 +13,11 @@ import ( "testing" "time" - "github.com/vechain/thor/v2/test/datagen" - "github.com/vechain/thor/v2/thor" - "github.com/gorilla/websocket" "github.com/stretchr/testify/assert" - "github.com/vechain/thor/v2/api/blocks" "github.com/vechain/thor/v2/api/subscriptions" + "github.com/vechain/thor/v2/test/datagen" + "github.com/vechain/thor/v2/thor" "github.com/vechain/thor/v2/thorclient/common" ) @@ -50,7 +48,7 @@ func TestClient_SubscribeEvents(t *testing.T) { func TestClient_SubscribeBlocks(t *testing.T) { pos := "best" - expectedBlock := &blocks.JSONCollapsedBlock{} + expectedBlock := &subscriptions.BlockMessage{} ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { assert.Equal(t, "/subscriptions/block", r.URL.Path) @@ -288,7 +286,7 @@ func TestClient_SubscribeBlocks_ServerError(t *testing.T) { func TestClient_SubscribeBlocks_ServerShutdown(t *testing.T) { pos := "best" - expectedBlock := &blocks.JSONCollapsedBlock{} + expectedBlock := &subscriptions.BlockMessage{} ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { assert.Equal(t, "/subscriptions/block", r.URL.Path) @@ -325,7 +323,7 @@ func TestClient_SubscribeBlocks_ServerShutdown(t *testing.T) { func TestClient_SubscribeBlocks_ClientShutdown(t *testing.T) { pos := "best" - expectedBlock := &blocks.JSONCollapsedBlock{} + expectedBlock := &subscriptions.BlockMessage{} ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { assert.Equal(t, "/subscriptions/block", r.URL.Path) @@ -377,7 +375,7 @@ func TestClient_SubscribeBlocks_ClientShutdown(t *testing.T) { func TestClient_SubscribeBlocks_ClientShutdown_LongBlocks(t *testing.T) { pos := "best" - expectedBlock := &blocks.JSONCollapsedBlock{} + expectedBlock := &subscriptions.BlockMessage{} ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { assert.Equal(t, "/subscriptions/block", r.URL.Path)