From 7579db4891fcb98ae4da6fb85a0fd278ed603d39 Mon Sep 17 00:00:00 2001 From: Pedro Gomes Date: Mon, 9 Dec 2024 10:55:34 +0000 Subject: [PATCH 01/10] Adding Health endpoint (#836) * Adding Health endpoint * pr comments + 503 if not healthy * refactored admin server and api + health endpoint tests * fix health condition * fix admin routing * added comments + changed from ChainSync to ChainBootstrapStatus * Adding healthcheck for solo mode * adding solo + tests * fix log_level handler funcs * refactor health package + add p2p count * remove solo methods * moving health service to api pkg * added defaults + api health query * pr comments * pr comments * pr comments * Update cmd/thor/main.go --- api/admin.go | 62 -------------- api/admin/admin.go | 28 +++++++ api/admin/health/health.go | 84 +++++++++++++++++++ api/admin/health/health_api.go | 68 +++++++++++++++ api/admin/health/health_api_test.go | 59 +++++++++++++ api/admin/health/health_test.go | 71 ++++++++++++++++ api/admin/loglevel/log_level.go | 74 ++++++++++++++++ .../loglevel/log_level_test.go} | 10 ++- api/admin/loglevel/types.go | 14 ++++ api/admin_server.go | 36 +++----- api/node/node_test.go | 3 +- cmd/thor/main.go | 63 +++++++------- comm/communicator.go | 5 +- 13 files changed, 456 insertions(+), 121 deletions(-) delete mode 100644 api/admin.go create mode 100644 api/admin/admin.go create mode 100644 api/admin/health/health.go create mode 100644 api/admin/health/health_api.go create mode 100644 api/admin/health/health_api_test.go create mode 100644 api/admin/health/health_test.go create mode 100644 api/admin/loglevel/log_level.go rename api/{admin_test.go => admin/loglevel/log_level_test.go} (93%) create mode 100644 api/admin/loglevel/types.go 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..1e16415f8 --- /dev/null +++ b/api/admin/admin.go @@ -0,0 +1,28 @@ +// 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" + + "github.com/gorilla/handlers" + "github.com/gorilla/mux" + healthAPI "github.com/vechain/thor/v2/api/admin/health" + "github.com/vechain/thor/v2/api/admin/loglevel" +) + +func New(logLevel *slog.LevelVar, health *healthAPI.Health) http.HandlerFunc { + router := mux.NewRouter() + subRouter := router.PathPrefix("/admin").Subrouter() + + loglevel.New(logLevel).Mount(subRouter, "/loglevel") + healthAPI.NewAPI(health).Mount(subRouter, "/health") + + handler := handlers.CompressHandler(router) + + return handler.ServeHTTP +} 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..d3c339ce2 --- /dev/null +++ b/api/admin/loglevel/log_level.go @@ -0,0 +1,74 @@ +// 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")) + } + + 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..f2315aeb1 100644 --- a/api/admin_server.go +++ b/api/admin_server.go @@ -11,40 +11,28 @@ import ( "net/http" "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, +) (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)) - 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/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/cmd/thor/main.go b/cmd/thor/main.go index 15531e091..ba19b0c11 100644 --- a/cmd/thor/main.go +++ b/cmd/thor/main.go @@ -180,16 +180,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 @@ -243,6 +233,21 @@ func defaultAction(ctx *cli.Context) error { return err } + adminURL := "" + if ctx.Bool(enableAdminFlag.Name) { + url, closeFunc, err := api.StartAdminServer( + ctx.String(adminAddrFlag.Name), + logLevel, + repo, + p2pCommunicator.Communicator(), + ) + 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") @@ -287,7 +292,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 { @@ -301,6 +307,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) { @@ -313,16 +325,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 @@ -367,6 +369,16 @@ func soloAction(ctx *cli.Context) error { return err } + adminURL := "" + if ctx.Bool(enableAdminFlag.Name) { + url, closeFunc, err := api.StartAdminServer(ctx.String(adminAddrFlag.Name), logLevel, repo, nil) + 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) @@ -412,11 +424,6 @@ func soloAction(ctx *cli.Context) error { srvCloser() }() - blockInterval := ctx.Uint64(blockInterval.Name) - if blockInterval == 0 { - return errors.New("block-interval cannot be zero") - } - printStartupMessage2(gene, apiURL, "", metricsURL, adminURL) optimizer := optimizer.New(mainDB, repo, !ctx.Bool(disablePrunerFlag.Name)) @@ -427,9 +434,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/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) }) } From de248a60128e278d9a8209ac0982f339483a4d46 Mon Sep 17 00:00:00 2001 From: Darren Kelly <107671032+darrenvechain@users.noreply.github.com> Date: Mon, 9 Dec 2024 11:25:30 +0000 Subject: [PATCH 02/10] Darren/admin api log toggler (#877) * Adding Health endpoint * pr comments + 503 if not healthy * refactored admin server and api + health endpoint tests * fix health condition * fix admin routing * added comments + changed from ChainSync to ChainBootstrapStatus * Adding healthcheck for solo mode * adding solo + tests * fix log_level handler funcs * feat(admin): toggle api logs via admin API * feat(admin): add license headers * refactor health package + add p2p count * remove solo methods * moving health service to api pkg * added defaults + api health query * pr comments * pr comments --------- Co-authored-by: otherview --- api/admin/admin.go | 8 ++- api/admin/apilogs/api_logs.go | 70 +++++++++++++++++++++++ api/admin/apilogs/api_logs_test.go | 91 ++++++++++++++++++++++++++++++ api/admin/loglevel/log_level.go | 2 + api/admin_server.go | 4 +- api/api.go | 7 +-- api/request_logger.go | 7 ++- api/request_logger_test.go | 5 +- cmd/thor/main.go | 23 +++++++- cmd/thor/utils.go | 5 +- 10 files changed, 208 insertions(+), 14 deletions(-) create mode 100644 api/admin/apilogs/api_logs.go create mode 100644 api/admin/apilogs/api_logs_test.go diff --git a/api/admin/admin.go b/api/admin/admin.go index 1e16415f8..9b819c875 100644 --- a/api/admin/admin.go +++ b/api/admin/admin.go @@ -8,19 +8,23 @@ package admin import ( "log/slog" "net/http" + "sync/atomic" "github.com/gorilla/handlers" "github.com/gorilla/mux" - healthAPI "github.com/vechain/thor/v2/api/admin/health" + "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) http.HandlerFunc { +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) 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/loglevel/log_level.go b/api/admin/loglevel/log_level.go index d3c339ce2..c91702d2d 100644 --- a/api/admin/loglevel/log_level.go +++ b/api/admin/loglevel/log_level.go @@ -68,6 +68,8 @@ func (l *LogLevel) postLogLevelHandler(w http.ResponseWriter, r *http.Request) e 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_server.go b/api/admin_server.go index f2315aeb1..dca428b36 100644 --- a/api/admin_server.go +++ b/api/admin_server.go @@ -9,6 +9,7 @@ import ( "log/slog" "net" "net/http" + "sync/atomic" "time" "github.com/pkg/errors" @@ -24,13 +25,14 @@ func StartAdminServer( 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) } - adminHandler := admin.New(logLevel, health.New(repo, p2p)) + adminHandler := admin.New(logLevel, health.New(repo, p2p), apiLogs) srv := &http.Server{Handler: adminHandler, ReadHeaderTimeout: time.Second, ReadTimeout: 5 * time.Second} var goes co.Goes diff --git a/api/api.go b/api/api.go index 0385929ec..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" @@ -39,7 +40,7 @@ type Config struct { PprofOn bool SkipLogs bool AllowCustomTracer bool - EnableReqLogger bool + EnableReqLogger *atomic.Bool EnableMetrics bool LogsLimit uint64 AllowedTracers []string @@ -115,9 +116,7 @@ func New( handlers.ExposedHeaders([]string{"x-genesis-id", "x-thorest-ver"}), )(handler) - if config.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/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/cmd/thor/main.go b/cmd/thor/main.go index ba19b0c11..2cb638f2a 100644 --- a/cmd/thor/main.go +++ b/cmd/thor/main.go @@ -11,6 +11,7 @@ import ( "io" "os" "path/filepath" + "sync/atomic" "time" "github.com/ethereum/go-ethereum/accounts/keystore" @@ -234,12 +235,15 @@ func defaultAction(ctx *cli.Context) error { } 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) @@ -261,7 +265,7 @@ func defaultAction(ctx *cli.Context) error { bftEngine, p2pCommunicator.Communicator(), forkConfig, - makeAPIConfig(ctx, false), + makeAPIConfig(ctx, logAPIRequests, false), ) defer func() { log.Info("closing API..."); apiCloser() }() @@ -370,8 +374,16 @@ func soloAction(ctx *cli.Context) error { } 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) + 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) } @@ -411,7 +423,7 @@ func soloAction(ctx *cli.Context) error { bftEngine, &solo.Communicator{}, forkConfig, - makeAPIConfig(ctx, true), + makeAPIConfig(ctx, logAPIRequests, true), ) defer func() { log.Info("closing API..."); apiCloser() }() @@ -424,6 +436,11 @@ func soloAction(ctx *cli.Context) error { srvCloser() }() + blockInterval := ctx.Uint64(blockInterval.Name) + if blockInterval == 0 { + return errors.New("block-interval cannot be zero") + } + printStartupMessage2(gene, apiURL, "", metricsURL, adminURL) optimizer := optimizer.New(mainDB, repo, !ctx.Bool(disablePrunerFlag.Name)) diff --git a/cmd/thor/utils.go b/cmd/thor/utils.go index 6877a45ee..59c5ec284 100644 --- a/cmd/thor/utils.go +++ b/cmd/thor/utils.go @@ -23,6 +23,7 @@ import ( "runtime" "runtime/debug" "strings" + "sync/atomic" "syscall" "time" @@ -275,7 +276,7 @@ func parseGenesisFile(filePath string) (*genesis.Genesis, thor.ForkConfig, error return customGen, forkConfig, nil } -func makeAPIConfig(ctx *cli.Context, soloMode bool) api.Config { +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)), @@ -283,7 +284,7 @@ func makeAPIConfig(ctx *cli.Context, soloMode bool) api.Config { PprofOn: ctx.Bool(pprofFlag.Name), SkipLogs: ctx.Bool(skipLogsFlag.Name), AllowCustomTracer: ctx.Bool(apiAllowCustomTracerFlag.Name), - EnableReqLogger: ctx.Bool(enableAPILogsFlag.Name), + EnableReqLogger: logAPIRequests, EnableMetrics: ctx.Bool(enableMetricsFlag.Name), LogsLimit: ctx.Uint64(apiLogsLimitFlag.Name), AllowedTracers: parseTracerList(strings.TrimSpace(ctx.String(allowedTracersFlag.Name))), From d9a11a8590a91556c0459724527b11c6d189b290 Mon Sep 17 00:00:00 2001 From: Darren Kelly <107671032+darrenvechain@users.noreply.github.com> Date: Mon, 9 Dec 2024 11:33:50 +0000 Subject: [PATCH 03/10] Darren/chore/backport metrics (#909) * chore(muxdb): backport muxdb cache metrics * chore(muxdb): backport muxdb cache metrics * chore(metrics): backport disk IO * chore(metrics): fix lint * chore(chain): add repo cache metrics * fix(chain): fix cache return value * refactor(chain): cache hit miss --- chain/metric.go | 12 +++++++ chain/repository.go | 24 +++++++++---- metrics/noop.go | 4 ++- metrics/prometheus.go | 65 ++++++++++++++++++++++++++++++++++ muxdb/internal/trie/cache.go | 20 ++++++----- muxdb/internal/trie/metrics.go | 12 +++++++ 6 files changed, 121 insertions(+), 16 deletions(-) create mode 100644 chain/metric.go create mode 100644 muxdb/internal/trie/metrics.go diff --git a/chain/metric.go b/chain/metric.go new file mode 100644 index 000000000..8c9a764d4 --- /dev/null +++ b/chain/metric.go @@ -0,0 +1,12 @@ +// 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 chain + +import "github.com/vechain/thor/v2/metrics" + +var ( + metricCacheHitMiss = metrics.LazyLoadCounterVec("repo_cache_hit_miss_count", []string{"type", "event"}) +) diff --git a/chain/repository.go b/chain/repository.go index 2460d6a3c..81b1f6af4 100644 --- a/chain/repository.go +++ b/chain/repository.go @@ -322,23 +322,29 @@ func (r *Repository) GetMaxBlockNum() (uint32, error) { // GetBlockSummary get block summary by block id. func (r *Repository) GetBlockSummary(id thor.Bytes32) (summary *BlockSummary, err error) { - var cached interface{} - if cached, err = r.caches.summaries.GetOrLoad(id, func() (interface{}, error) { + var blk interface{} + result := "hit" + if blk, err = r.caches.summaries.GetOrLoad(id, func() (interface{}, error) { + result = "miss" return loadBlockSummary(r.data, id) }); err != nil { return } - return cached.(*BlockSummary), nil + metricCacheHitMiss().AddWithLabel(1, map[string]string{"type": "blocks", "event": result}) + return blk.(*BlockSummary), nil } func (r *Repository) getTransaction(key txKey) (*tx.Transaction, error) { - cached, err := r.caches.txs.GetOrLoad(key, func() (interface{}, error) { + result := "hit" + trx, err := r.caches.txs.GetOrLoad(key, func() (interface{}, error) { + result = "miss" return loadTransaction(r.data, key) }) if err != nil { return nil, err } - return cached.(*tx.Transaction), nil + metricCacheHitMiss().AddWithLabel(1, map[string]string{"type": "transaction", "event": result}) + return trx.(*tx.Transaction), nil } // GetBlockTransactions get all transactions of the block for given block id. @@ -377,13 +383,17 @@ func (r *Repository) GetBlock(id thor.Bytes32) (*block.Block, error) { } func (r *Repository) getReceipt(key txKey) (*tx.Receipt, error) { - cached, err := r.caches.receipts.GetOrLoad(key, func() (interface{}, error) { + result := "hit" + receipt, err := r.caches.receipts.GetOrLoad(key, func() (interface{}, error) { + result = "miss" return loadReceipt(r.data, key) }) if err != nil { return nil, err } - return cached.(*tx.Receipt), nil + metricCacheHitMiss().AddWithLabel(1, map[string]string{"type": "receipt", "event": result}) + + return receipt.(*tx.Receipt), nil } // GetBlockReceipts get all tx receipts of the block for given block id. diff --git a/metrics/noop.go b/metrics/noop.go index 6eb909ff9..a9e24ab2c 100644 --- a/metrics/noop.go +++ b/metrics/noop.go @@ -5,7 +5,9 @@ package metrics -import "net/http" +import ( + "net/http" +) // noopMetrics implements a no operations metrics service type noopMetrics struct{} diff --git a/metrics/prometheus.go b/metrics/prometheus.go index 50745752c..15447f6dc 100644 --- a/metrics/prometheus.go +++ b/metrics/prometheus.go @@ -6,8 +6,15 @@ package metrics import ( + "bufio" + "fmt" "net/http" + "os" + "runtime" + "strconv" + "strings" "sync" + "time" "github.com/prometheus/client_golang/prometheus" "github.com/prometheus/client_golang/prometheus/promhttp" @@ -24,6 +31,8 @@ func InitializePrometheusMetrics() { // don't allow for reset if _, ok := metrics.(*prometheusMetrics); !ok { metrics = newPrometheusMetrics() + // collection disk io metrics every 5 seconds + go metrics.(*prometheusMetrics).collectDiskIO(5 * time.Second) } } @@ -123,6 +132,62 @@ func (o *prometheusMetrics) GetOrCreateGaugeVecMeter(name string, labels []strin return meter } +func getIOLineValue(line string) int64 { + fields := strings.Fields(line) + if len(fields) != 2 { + logger.Warn("this io file line is malformed", "err", line) + return 0 + } + value, err := strconv.ParseInt(fields[1], 10, 64) + if err != nil { + logger.Warn("unable to parse int", "err", err) + return 0 + } + + return value +} + +func getDiskIOData() (int64, int64, error) { + pid := os.Getpid() + ioFilePath := fmt.Sprintf("/proc/%d/io", pid) + file, err := os.Open(ioFilePath) + if err != nil { + return 0, 0, err + } + + // Parse the file line by line + scanner := bufio.NewScanner(file) + var reads, writes int64 + for scanner.Scan() { + line := scanner.Text() + if strings.HasPrefix(line, "syscr") { + reads = getIOLineValue(line) + } else if strings.HasPrefix(line, "syscw") { + writes = getIOLineValue(line) + } + } + + return reads, writes, nil +} + +func (o *prometheusMetrics) collectDiskIO(refresh time.Duration) { + if runtime.GOOS != "linux" { + return + } + for { + reads, writes, err := getDiskIOData() + if err == nil { + readsMeter := o.GetOrCreateGaugeMeter("disk_reads") + readsMeter.Set(reads) + + writesMeter := o.GetOrCreateGaugeMeter("disk_writes") + writesMeter.Set(writes) + } + + time.Sleep(refresh) + } +} + func (o *prometheusMetrics) newHistogramMeter(name string, buckets []int64) HistogramMeter { var floatBuckets []float64 for _, bucket := range buckets { diff --git a/muxdb/internal/trie/cache.go b/muxdb/internal/trie/cache.go index cc7bca300..d7d78aaae 100644 --- a/muxdb/internal/trie/cache.go +++ b/muxdb/internal/trie/cache.go @@ -45,12 +45,16 @@ func (c *Cache) log() { last := atomic.SwapInt64(&c.lastLogTime, now) if now-last > int64(time.Second*20) { - log1, ok1 := c.nodeStats.ShouldLog("node cache stats") - log2, ok2 := c.rootStats.ShouldLog("root cache stats") - - if ok1 || ok2 { - log1() - log2() + logNode, hitNode, missNode, okNode := c.nodeStats.shouldLog("node cache stats") + logRoot, hitRoot, missRoot, okRoot := c.rootStats.shouldLog("root cache stats") + + if okNode || okRoot { + logNode() + metricCacheHitMissGaugeVec().SetWithLabel(hitNode, map[string]string{"type": "node", "event": "hit"}) + metricCacheHitMissGaugeVec().SetWithLabel(missNode, map[string]string{"type": "node", "event": "miss"}) + logRoot() + metricCacheHitMissGaugeVec().SetWithLabel(hitRoot, map[string]string{"type": "root", "event": "hit"}) + metricCacheHitMissGaugeVec().SetWithLabel(missRoot, map[string]string{"type": "root", "event": "miss"}) } } else { atomic.CompareAndSwapInt64(&c.lastLogTime, now, last) @@ -189,7 +193,7 @@ type cacheStats struct { func (cs *cacheStats) Hit() int64 { return atomic.AddInt64(&cs.hit, 1) } func (cs *cacheStats) Miss() int64 { return atomic.AddInt64(&cs.miss, 1) } -func (cs *cacheStats) ShouldLog(msg string) (func(), bool) { +func (cs *cacheStats) shouldLog(msg string) (func(), int64, int64, bool) { hit := atomic.LoadInt64(&cs.hit) miss := atomic.LoadInt64(&cs.miss) lookups := hit + miss @@ -209,5 +213,5 @@ func (cs *cacheStats) ShouldLog(msg string) (func(), bool) { "hitrate", str, ) atomic.StoreInt32(&cs.flag, flag) - }, atomic.LoadInt32(&cs.flag) != flag + }, hit, miss, atomic.LoadInt32(&cs.flag) != flag } diff --git a/muxdb/internal/trie/metrics.go b/muxdb/internal/trie/metrics.go new file mode 100644 index 000000000..6db862df9 --- /dev/null +++ b/muxdb/internal/trie/metrics.go @@ -0,0 +1,12 @@ +// 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 trie + +import ( + "github.com/vechain/thor/v2/metrics" +) + +var metricCacheHitMissGaugeVec = metrics.LazyLoadGaugeVec("cache_hit_miss_count", []string{"type", "event"}) From b0a3d7362e30834b525f07b316b6644d8e8199ae Mon Sep 17 00:00:00 2001 From: Darren Kelly <107671032+darrenvechain@users.noreply.github.com> Date: Mon, 9 Dec 2024 12:49:42 +0000 Subject: [PATCH 04/10] chore(thor): update version (#912) * chore(thor): update version * chore(openapi): version --- api/doc/thor.yaml | 2 +- cmd/thor/VERSION | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/api/doc/thor.yaml b/api/doc/thor.yaml index dcf0ae6b9..d8f6c45e8 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.1.5 servers: - url: / description: Current Node diff --git a/cmd/thor/VERSION b/cmd/thor/VERSION index 7d2ed7c70..cd57a8b95 100644 --- a/cmd/thor/VERSION +++ b/cmd/thor/VERSION @@ -1 +1 @@ -2.1.4 +2.1.5 From c74bbf0159b08c048181beac82412e3af02af084 Mon Sep 17 00:00:00 2001 From: Delweng Date: Mon, 9 Dec 2024 21:48:13 +0800 Subject: [PATCH 05/10] feat(api/debug): support debug trace without blockId (#905) * api/debug: support debug with txhash Signed-off-by: jsvisa api/debug: blockId should use tx's instead Signed-off-by: jsvisa fix tests Signed-off-by: jsvisa * debug: add test Signed-off-by: jsvisa * improve parseTarget Signed-off-by: jsvisa * update doc Signed-off-by: jsvisa * fix tests Signed-off-by: jsvisa --------- Signed-off-by: jsvisa Co-authored-by: tony --- api/debug/debug.go | 133 +++++++++++++++++++++++++--------------- api/debug/debug_test.go | 30 +++++++-- api/doc/thor.yaml | 9 +-- 3 files changed, 116 insertions(+), 56 deletions(-) diff --git a/api/debug/debug.go b/api/debug/debug.go index 5ff54f1dc..497518982 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,71 @@ 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) + txMeta, err := d.repo.NewBestChain().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 = d.repo.GetBlock(txMeta.BlockID) + 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 1275a9030..d56718143 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" @@ -62,6 +61,7 @@ func TestDebug(t *testing.T) { "testTraceClauseWithClauseIndexOutOfBound": testTraceClauseWithClauseIndexOutOfBound, "testTraceClauseWithCustomTracer": testTraceClauseWithCustomTracer, "testTraceClause": testTraceClause, + "testTraceClauseWithoutBlockID": testTraceClauseWithoutBlockID, } { t.Run(name, tt) } @@ -176,9 +176,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) { @@ -265,6 +267,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 d8f6c45e8..702645411 100644 --- a/api/doc/thor.yaml +++ b/api/doc/thor.yaml @@ -2104,11 +2104,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' @@ -2174,9 +2175,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 From ea748996aae623bd2bc8faa630e108b9caedfd2c Mon Sep 17 00:00:00 2001 From: Miguel Angel Rojo Date: Tue, 10 Dec 2024 09:30:54 +0000 Subject: [PATCH 06/10] fix: Get Devnet ID after the Thor flags are set (#915) * first commit * using bytes32 method --- cmd/thor/utils.go | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/cmd/thor/utils.go b/cmd/thor/utils.go index 59c5ec284..d53935c10 100644 --- a/cmd/thor/utils.go +++ b/cmd/thor/utils.go @@ -57,7 +57,7 @@ import ( "gopkg.in/urfave/cli.v1" ) -var devNetGenesisID = genesis.NewDevnet().ID() +var devNetGenesisID thor.Bytes32 func initLogger(lvl int, jsonLogs bool) *slog.LevelVar { logLevel := log.FromLegacyLevel(lvl) @@ -632,6 +632,13 @@ func printStartupMessage1( ) } +func getOrCreateDevnetID() thor.Bytes32 { + if devNetGenesisID.IsZero() { + devNetGenesisID = genesis.NewDevnet().ID() + } + return devNetGenesisID +} + func printStartupMessage2( gene *genesis.Genesis, apiURL string, @@ -670,7 +677,7 @@ func printStartupMessage2( }(), func() string { // print default dev net's dev accounts info - if gene.ID() == devNetGenesisID { + if gene.ID() == getOrCreateDevnetID() { return ` ┌──────────────────┬───────────────────────────────────────────────────────────────────────────────┐ │ Mnemonic Words │ denial kitchen pet squirrel other broom bar gas better priority spoil cross │ From 9d359f702b32712cf8acd4a4fc53393419509c35 Mon Sep 17 00:00:00 2001 From: Miguel Angel Rojo Date: Tue, 10 Dec 2024 14:02:39 +0000 Subject: [PATCH 07/10] fixing attestations (#917) --- .github/workflows/publish-docker-images.yaml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/publish-docker-images.yaml b/.github/workflows/publish-docker-images.yaml index fd97eb27a..63cb381a6 100644 --- a/.github/workflows/publish-docker-images.yaml +++ b/.github/workflows/publish-docker-images.yaml @@ -83,7 +83,8 @@ jobs: platforms: ${{ github.event_name != 'pull_request' && 'linux/amd64,linux/arm64' || 'linux/amd64' }} push: ${{ github.event_name != 'pull_request' }} load: ${{ github.event_name == 'pull_request' }} - provenance: false + provenance: ${{ github.event_name != 'pull_request' }} + sbom: ${{ github.event_name != 'pull_request' }} tags: ${{ steps.meta.outputs.tags }} labels: ${{ steps.meta.outputs.labels }} From 5fe421e89a1dea12597115b527e06a0dabde2eb4 Mon Sep 17 00:00:00 2001 From: leszek-vechain <158060059+leszek-vechain@users.noreply.github.com> Date: Wed, 11 Dec 2024 16:12:14 +0000 Subject: [PATCH 08/10] chore(e2e): (#918) - update test commit sha --- .github/workflows/test-e2e.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/test-e2e.yaml b/.github/workflows/test-e2e.yaml index 55c82689e..c70e0f62c 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/8b72bedff11c9e8873d88b6e2dba356d43b56779 - ref: 8b72bedff11c9e8873d88b6e2dba356d43b56779 + # https://github.com/vechain/thor-e2e-tests/tree/956b34bcf5b0b072cd7c8fd2e546a9beb66a866a + ref: 956b34bcf5b0b072cd7c8fd2e546a9beb66a866a - name: Download artifact uses: actions/download-artifact@v4 From dcea741d1a907ee5a3ca90bb8b26b86cb2ce0781 Mon Sep 17 00:00:00 2001 From: Pedro Gomes Date: Thu, 12 Dec 2024 14:26:59 +0000 Subject: [PATCH 09/10] Fixes to thorclient (#920) --- thorclient/api_test.go | 238 ++++++++++++++------------- thorclient/httpclient/client.go | 2 +- thorclient/httpclient/client_test.go | 4 +- thorclient/thorclient.go | 18 +- 4 files changed, 146 insertions(+), 116 deletions(-) diff --git a/thorclient/api_test.go b/thorclient/api_test.go index e8ae49a8a..df72b1296 100644 --- a/thorclient/api_test.go +++ b/thorclient/api_test.go @@ -14,6 +14,7 @@ import ( "testing" "time" + "github.com/ethereum/go-ethereum/common/math" "github.com/ethereum/go-ethereum/crypto" "github.com/gorilla/mux" "github.com/stretchr/testify/require" @@ -22,6 +23,7 @@ import ( "github.com/vechain/thor/v2/api/debug" "github.com/vechain/thor/v2/api/events" "github.com/vechain/thor/v2/api/node" + "github.com/vechain/thor/v2/api/transactions" "github.com/vechain/thor/v2/comm" "github.com/vechain/thor/v2/genesis" "github.com/vechain/thor/v2/logdb" @@ -41,6 +43,10 @@ const ( logDBLimit = 1_000 ) +var ( + preMintedTx01 *tx.Transaction +) + func initAPIServer(t *testing.T) (*testchain.Chain, *httptest.Server) { thorChain, err := testchain.NewIntegrationTestChain() require.NoError(t, err) @@ -53,6 +59,9 @@ func initAPIServer(t *testing.T) (*testchain.Chain, *httptest.Server) { accounts.New(thorChain.Repo(), thorChain.Stater(), uint64(gasLimit), thor.NoFork, thorChain.Engine(), true). Mount(router, "/accounts") + mempool := txpool.New(thorChain.Repo(), thorChain.Stater(), txpool.Options{Limit: 10000, LimitPerAccount: 16, MaxLifetime: 10 * time.Minute}) + transactions.New(thorChain.Repo(), mempool).Mount(router, "/transactions") + blocks.New(thorChain.Repo(), thorChain.Engine()).Mount(router, "/blocks") debug.New(thorChain.Repo(), thorChain.Stater(), thorChain.GetForkConfig(), gasLimit, true, thorChain.Engine(), []string{"all"}, false). @@ -109,6 +118,7 @@ func mintTransactions(t *testing.T, thorChain *testchain.Chain) { transaction = transaction.WithSignature(sig) require.NoError(t, thorChain.MintTransactions(genesis.DevAccounts()[0], transaction, noClausesTx)) + preMintedTx01 = transaction } func TestAPIs(t *testing.T) { @@ -116,11 +126,12 @@ func TestAPIs(t *testing.T) { defer ts.Close() for name, tt := range map[string]func(*testing.T, *testchain.Chain, *httptest.Server){ - "testAccountEndpoint": testAccountEndpoint, - "testBlocksEndpoint": testBlocksEndpoint, - "testDebugEndpoint": testDebugEndpoint, - "testEventsEndpoint": testEventsEndpoint, - "testNodeEndpoint": testNodeEndpoint, + "testAccountEndpoint": testAccountEndpoint, + "testTransactionsEndpoint": testTransactionsEndpoint, + "testBlocksEndpoint": testBlocksEndpoint, + "testDebugEndpoint": testDebugEndpoint, + "testEventsEndpoint": testEventsEndpoint, + "testNodeEndpoint": testNodeEndpoint, } { t.Run(name, func(t *testing.T) { tt(t, thorChain, ts) @@ -130,116 +141,136 @@ func TestAPIs(t *testing.T) { func testAccountEndpoint(t *testing.T, _ *testchain.Chain, ts *httptest.Server) { // Example storage key - storageKey := "0x0000000000000000000000000000000000000000000000000000000000000000" + storageKey := thor.MustParseBytes32("0x0000000000000000000000000000000000000000000000000000000000000000") // Example addresses - address1 := "0x0123456789abcdef0123456789abcdef01234567" - address2 := "0xabcdef0123456789abcdef0123456789abcdef01" + address1 := thor.MustParseAddress("0x0123456789abcdef0123456789abcdef01234567") + address2 := thor.MustParseAddress("0xabcdef0123456789abcdef0123456789abcdef01") // 1. Test GET /accounts/{address} t.Run("GetAccount", func(t *testing.T) { - resp, err := ts.Client().Get(ts.URL + "/accounts/" + address1) + c := New(ts.URL) + _, err := c.Account(&address1) require.NoError(t, err) - defer resp.Body.Close() - require.Equal(t, 200, resp.StatusCode) - // Optionally, you can unmarshal and validate the response body here + // TODO validate the response body here }) // 2. Test GET /accounts/{address}/code t.Run("GetCode", func(t *testing.T) { - resp, err := ts.Client().Get(ts.URL + "/accounts/" + address1 + "/code") + c := New(ts.URL) + _, err := c.AccountCode(&address1) require.NoError(t, err) - defer resp.Body.Close() - require.Equal(t, 200, resp.StatusCode) - // Optionally, you can unmarshal and validate the response body here + // TODO validate the response body here }) // 3. Test GET /accounts/{address}/storage/{key} t.Run("GetStorage", func(t *testing.T) { - resp, err := ts.Client().Get(ts.URL + "/accounts/" + address1 + "/storage/" + storageKey) + c := New(ts.URL) + _, err := c.AccountStorage(&address1, &storageKey) require.NoError(t, err) - defer resp.Body.Close() - require.Equal(t, 200, resp.StatusCode) - // Optionally, you can unmarshal and validate the response body here + // TODO validate the response body here }) // 4. Test POST /accounts/* t.Run("InspectClauses", func(t *testing.T) { + c := New(ts.URL) // Define the payload for the batch call - payload := `{ - "clauses": [ - { - "to": "` + address1 + `", - "value": "0x0", - "data": "0x" + value := math.HexOrDecimal256(*big.NewInt(1)) + payload := &accounts.BatchCallData{ + Clauses: accounts.Clauses{ + accounts.Clause{ + To: &address1, + Value: nil, + Data: "0x", + }, + accounts.Clause{ + To: &address2, + Value: &value, + Data: "0x", + }, }, - { - "to": "` + address2 + `", - "value": "0x1", - "data": "0x" - } - ], - "gas": 1000000, - "gasPrice": "0x0", - "caller": "` + address1 + `" - }` - req, err := http.NewRequest("POST", ts.URL+"/accounts/*", strings.NewReader(payload)) + Gas: 1000000, + GasPrice: &value, + Caller: &address1, + } + _, err := c.InspectClauses(payload) require.NoError(t, err) - req.Header.Set("Content-Type", "application/json") // Simulate sending request with revision query parameter - query := req.URL.Query() - query.Add("revision", "best") // Add any revision parameter as expected - req.URL.RawQuery = query.Encode() + _, err = c.InspectClauses(payload, Revision("best")) + require.NoError(t, err) + }) +} - // Perform the request - resp, err := ts.Client().Do(req) +func testTransactionsEndpoint(t *testing.T, thorChain *testchain.Chain, ts *httptest.Server) { + c := New(ts.URL) + + // 1. Test retrieving a pre-mined transaction by ID + t.Run("GetTransaction", func(t *testing.T) { + id := preMintedTx01.ID() + trx, err := c.Transaction(&id) require.NoError(t, err) - defer resp.Body.Close() + require.NotNil(t, trx) + require.Equal(t, id.String(), trx.ID.String()) + }) - // Ensure the response code is 200 OK - require.Equal(t, 200, resp.StatusCode) + // 2. Test sending a new transaction + t.Run("SendTransaction", func(t *testing.T) { + toAddr := thor.MustParseAddress("0x0123456789abcdef0123456789abcdef01234567") + clause := tx.NewClause(&toAddr).WithValue(big.NewInt(10000)) + trx := new(tx.Builder). + ChainTag(thorChain.Repo().ChainTag()). + Expiration(10). + Gas(21000). + Clause(clause). + Build() + + trx = tx.MustSign(trx, genesis.DevAccounts()[0].PrivateKey) + sendResult, err := c.SendTransaction(trx) + require.NoError(t, err) + require.NotNil(t, sendResult) + require.Equal(t, trx.ID().String(), sendResult.ID.String()) // Ensure transaction was successful + }) + + // 3. Test retrieving the transaction receipt + t.Run("GetTransactionReceipt", func(t *testing.T) { + txID := preMintedTx01.ID() + receipt, err := c.TransactionReceipt(&txID) + require.NoError(t, err) + require.NotNil(t, receipt) + require.Equal(t, txID.String(), receipt.Meta.TxID.String()) + }) + + // 4. Test inspecting clauses of a transaction + t.Run("InspectClauses", func(t *testing.T) { + clause := tx.NewClause(nil).WithValue(big.NewInt(10000)).WithData([]byte("0x")) + batchCallData := convertToBatchCallData(preMintedTx01, nil) + batchCallData.Clauses = append(batchCallData.Clauses, convertClauseAccounts(clause)) + + callResults, err := c.InspectClauses(batchCallData) + require.NoError(t, err) + require.NotNil(t, callResults) + require.Greater(t, len(callResults), 0) }) } func testBlocksEndpoint(t *testing.T, _ *testchain.Chain, ts *httptest.Server) { + c := New(ts.URL) // Example revision (this could be a block number or block ID) - revision := "best" // You can adjust this to a real block number or ID for integration testing + revision := "best" // 1. Test GET /blocks/{revision} t.Run("GetBlock", func(t *testing.T) { - // Send request to get block information by revision - resp, err := ts.Client().Get(ts.URL + "/blocks/" + revision) + _, err := c.Block(revision) require.NoError(t, err) - defer resp.Body.Close() - - // Ensure the response code is 200 OK - require.Equal(t, 200, resp.StatusCode) - // Optionally, you can unmarshal and validate the response body here + // TODO validate the response body here }) // 2. Test GET /blocks/{revision}?expanded=true t.Run("GetBlockExpanded", func(t *testing.T) { - // Send request to get expanded block information (includes transactions and receipts) - resp, err := ts.Client().Get(ts.URL + "/blocks/" + revision + "?expanded=true") + _, err := c.ExpandedBlock(revision) require.NoError(t, err) - defer resp.Body.Close() - - // Ensure the response code is 200 OK - require.Equal(t, 200, resp.StatusCode) - // Optionally, you can unmarshal and validate the response body here - }) - - // 3. Test GET /blocks/{revision}?expanded=invalid (should return bad request) - t.Run("GetBlockInvalidExpanded", func(t *testing.T) { - // Send request with an invalid 'expanded' parameter - resp, err := ts.Client().Get(ts.URL + "/blocks/" + revision + "?expanded=invalid") - require.NoError(t, err) - defer resp.Body.Close() - - // Ensure the response code is 400 Bad Request - require.Equal(t, 400, resp.StatusCode) - // Optionally, you can unmarshal and validate the response body here + // TODO validate the response body here }) } @@ -325,57 +356,44 @@ func testDebugEndpoint(t *testing.T, thorChain *testchain.Chain, ts *httptest.Se } func testEventsEndpoint(t *testing.T, _ *testchain.Chain, ts *httptest.Server) { + c := New(ts.URL) + // Example address and topic for filtering events - address := "0x0123456789abcdef0123456789abcdef01234567" - topic := thor.BytesToBytes32([]byte("topic")).String() + address := thor.MustParseAddress("0x0123456789abcdef0123456789abcdef01234567") + topic := thor.BytesToBytes32([]byte("topic")) // 1. Test POST /events (Filter events) t.Run("FilterEvents", func(t *testing.T) { // Define the payload for filtering events - payload := `{ - "criteriaSet": [ - { - "address": "` + address + `", - "topic0": "` + topic + `" - } - ], - "options": { - "limit": 10, - "offset": 0 - } - }` - - req, err := http.NewRequest("POST", ts.URL+"/logs/event", strings.NewReader(payload)) - require.NoError(t, err) - req.Header.Set("Content-Type", "application/json") + payload := &events.EventFilter{ + CriteriaSet: []*events.EventCriteria{ + &events.EventCriteria{ + Address: &address, + TopicSet: events.TopicSet{ + Topic0: &topic, + }, + }, + }, + Range: nil, + Options: &logdb.Options{ + Offset: 0, + Limit: 10, + }, + Order: "", + } - // Perform the request - resp, err := ts.Client().Do(req) + _, err := c.FilterEvents(payload) require.NoError(t, err) - defer resp.Body.Close() - // Ensure the response code is 200 OK - require.Equal(t, 200, resp.StatusCode) - // Optionally, you can unmarshal and validate the response body here - // body, err := ioutil.ReadAll(resp.Body) - // require.NoError(t, err) - // fmt.Println(string(body)) + //TODO validate the response body here }) } func testNodeEndpoint(t *testing.T, _ *testchain.Chain, ts *httptest.Server) { + c := New(ts.URL) // 1. Test GET /node/network/peers t.Run("GetPeersStats", func(t *testing.T) { - // Send request to get peers statistics - resp, err := ts.Client().Get(ts.URL + "/node/network/peers") + _, err := c.Peers() require.NoError(t, err) - defer resp.Body.Close() - - // Ensure the response code is 200 OK - require.Equal(t, 200, resp.StatusCode) - // Optionally, you can unmarshal and validate the response body here - // body, err := ioutil.ReadAll(resp.Body) - // require.NoError(t, err) - // fmt.Println(string(body)) }) } diff --git a/thorclient/httpclient/client.go b/thorclient/httpclient/client.go index ce05bf17f..c2082a692 100644 --- a/thorclient/httpclient/client.go +++ b/thorclient/httpclient/client.go @@ -104,7 +104,7 @@ func (c *Client) GetAccountCode(addr *thor.Address, revision string) (*accounts. // GetAccountStorage retrieves the storage value for the given address and key at the specified revision. func (c *Client) GetAccountStorage(addr *thor.Address, key *thor.Bytes32, revision string) (*accounts.GetStorageResult, error) { - url := c.url + "/accounts/" + addr.String() + "/key/" + key.String() + url := c.url + "/accounts/" + addr.String() + "/storage/" + key.String() if revision != "" { url += "?revision=" + revision } diff --git a/thorclient/httpclient/client_test.go b/thorclient/httpclient/client_test.go index 7e807ba55..834cf2e1c 100644 --- a/thorclient/httpclient/client_test.go +++ b/thorclient/httpclient/client_test.go @@ -197,7 +197,7 @@ func TestClient_GetStorage(t *testing.T) { expectedStorageRsp := &accounts.GetStorageResult{Value: hexutil.Encode([]byte{0x01, 0x03})} ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - assert.Equal(t, "/accounts/"+addr.String()+"/key/"+key.String(), r.URL.Path) + assert.Equal(t, "/accounts/"+addr.String()+"/storage/"+key.String(), r.URL.Path) marshal, err := json.Marshal(expectedStorageRsp) require.NoError(t, err) @@ -448,7 +448,7 @@ func TestClient_Errors(t *testing.T) { }, { name: "GetAccountStorage", - path: "/accounts/" + addr.String() + "/key/" + thor.Bytes32{}.String(), + path: "/accounts/" + addr.String() + "/storage/" + thor.Bytes32{}.String(), function: func(client *Client) (*accounts.GetStorageResult, error) { return client.GetAccountStorage(&addr, &thor.Bytes32{}, tccommon.BestRevision) }, diff --git a/thorclient/thorclient.go b/thorclient/thorclient.go index 0b7939f51..d2e035145 100644 --- a/thorclient/thorclient.go +++ b/thorclient/thorclient.go @@ -87,6 +87,18 @@ func applyOptions(opts []Option) *getOptions { return options } +// applyHeadOptions applies the given functional options to the default options. +func applyHeadOptions(opts []Option) *getOptions { + options := &getOptions{ + revision: "", + pending: false, + } + for _, o := range opts { + o(options) + } + return options +} + // Revision returns an Option to specify the revision for requests. func Revision(revision string) Option { return func(o *getOptions) { @@ -144,19 +156,19 @@ func (c *Client) AccountStorage(addr *thor.Address, key *thor.Bytes32, opts ...O // Transaction retrieves a transaction by its ID. func (c *Client) Transaction(id *thor.Bytes32, opts ...Option) (*transactions.Transaction, error) { - options := applyOptions(opts) + options := applyHeadOptions(opts) return c.httpConn.GetTransaction(id, options.revision, options.pending) } // RawTransaction retrieves the raw transaction data by its ID. func (c *Client) RawTransaction(id *thor.Bytes32, opts ...Option) (*transactions.RawTransaction, error) { - options := applyOptions(opts) + options := applyHeadOptions(opts) return c.httpConn.GetRawTransaction(id, options.revision, options.pending) } // TransactionReceipt retrieves the receipt for a transaction by its ID. func (c *Client) TransactionReceipt(id *thor.Bytes32, opts ...Option) (*transactions.Receipt, error) { - options := applyOptions(opts) + options := applyHeadOptions(opts) return c.httpConn.GetTransactionReceipt(id, options.revision) } From 1a8ab07dadbe2a26fd8189a899b5cd9f13faa531 Mon Sep 17 00:00:00 2001 From: Darren Kelly <107671032+darrenvechain@users.noreply.github.com> Date: Tue, 17 Dec 2024 13:10:25 +0000 Subject: [PATCH 10/10] chore(dockerhub): update description on release (#921) --- .github/workflows/publish-docker-images.yaml | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/.github/workflows/publish-docker-images.yaml b/.github/workflows/publish-docker-images.yaml index 63cb381a6..b75138035 100644 --- a/.github/workflows/publish-docker-images.yaml +++ b/.github/workflows/publish-docker-images.yaml @@ -88,6 +88,15 @@ jobs: tags: ${{ steps.meta.outputs.tags }} labels: ${{ steps.meta.outputs.labels }} + - name: Update Docker Hub + # official documentation docker: https://docs.docker.com/build/ci/github-actions/update-dockerhub-desc/ + if: ${{ inputs.environment == 'docker-publish' && github.event_name != 'pull_request' }} + uses: peter-evans/dockerhub-description@v4 + with: + username: ${{ secrets.DOCKERHUB_USERNAME }} + password: ${{ secrets.DOCKERHUB_TOKEN }} + repository: ${{ github.repository }} + - name: Scan for vulnerabilities uses: crazy-max/ghaction-container-scan@v3 if: ${{ github.event_name == 'pull_request' || github.ref_name == 'master' }}