diff --git a/.github/workflows/test-e2e.yaml b/.github/workflows/test-e2e.yaml index 76b885396..44bd68eff 100644 --- a/.github/workflows/test-e2e.yaml +++ b/.github/workflows/test-e2e.yaml @@ -44,8 +44,8 @@ jobs: uses: actions/checkout@v4 with: repository: vechain/thor-e2e-tests - # https://github.com/vechain/thor-e2e-tests/tree/00bd3f1b949b05da94e82686e0089a11a136c34c - ref: 87aeb1747995e8b235a796303b0fc08ab16262c6 + # https://github.com/vechain/thor-e2e-tests/tree/d86b30b6409b27e841eace0f7c5b6e75c0a4e25e + ref: d86b30b6409b27e841eace0f7c5b6e75c0a4e25e - name: Download artifact uses: actions/download-artifact@v4 diff --git a/README.md b/README.md index 65313e520..50ee4e85c 100644 --- a/README.md +++ b/README.md @@ -33,11 +33,11 @@ ## Getting Started -VechainThor is the layer 1 blockchain, highly compatible with EVM*, which powers the vechain ecosystem. -VechainThor is a public blockchain that is designed for the mass adoption of blockchain technology by enterprise users +VeChainThor is the layer 1 blockchain, highly compatible with EVM*, which powers the VeChain ecosystem. +VeChainThor is a public blockchain that is designed for the mass adoption of blockchain technology by enterprise users of all sizes and is intended to serve as a foundation for a sustainable and scalable enterprise blockchain ecosystem. -> VechainThor is currently up-to-date with the EVM's `paris` hard fork, +> VeChainThor is currently up-to-date with the EVM's `paris` hard fork, > set [evmVersion](https://docs.soliditylang.org/en/latest/using-the-compiler.html#setting-the-evm-version-to-target) > to `paris` if you are using solidity compiler version `0.8.20` or above. ___ @@ -47,17 +47,17 @@ ___ - [Build](./docs/build.md) - How to build the `thor` binary. - [Usage](./docs/usage.md) - How to run thor with different configurations. - [Hosting a Node](./docs/hosting-a-node.md) - Considerations and requirements for hosting a node. -- [Core Concepts](https://docs.vechain.org/core-concepts) - Core concepts of the VechainThor blockchain. -- [API Reference](https://mainnet.vechain.org) - The API reference for the VechainThor blockchain. +- [Core Concepts](https://docs.vechain.org/core-concepts) - Core concepts of the VeChainThor blockchain. +- [API Reference](https://mainnet.vechain.org) - The API reference for the VeChainThor blockchain. --- ## Community -The VechainThor community can be found on [Discourse](https://vechain.discourse.group/) where you can ask questions, +The VeChainThor community can be found on [Discourse](https://vechain.discourse.group/) where you can ask questions, voice ideas, and share your projects with other people. -The Vechain Improvement Proposals (VIPs) repository can be found [here](https://github.com/vechain/VIPs). +The VeChain Improvement Proposals (VIPs) repository can be found [here](https://github.com/vechain/VIPs). To chat with other community members you can join: @@ -67,16 +67,16 @@ To chat with other community members you can join:

-Do note that our [Code of Conduct](./docs/CODE_OF_CONDUCT.md) applies to all vechain community channels. Users are +Do note that our [Code of Conduct](./docs/CODE_OF_CONDUCT.md) applies to all VeChain community channels. Users are **highly encouraged** to read and adhere to them to avoid repercussions. --- ## Contributing -Contributions to VechainThor are welcome and highly appreciated. However, before you jump right into it, we would like +Contributions to VeChainThor are welcome and highly appreciated. However, before you jump right into it, we would like you to review our [Contribution Guidelines](./docs/CONTRIBUTING.md) to make sure you have a smooth experience -contributing to VechainThor. +contributing to VeChainThor. --- @@ -102,6 +102,6 @@ A special shout out to following projects: ## License -Vechain Thor is licensed under the [GNU Lesser General Public License v3.0](https://www.gnu.org/licenses/lgpl-3.0.html), +VeChainThor is licensed under the [GNU Lesser General Public License v3.0](https://www.gnu.org/licenses/lgpl-3.0.html), also included in *LICENSE* file in repository. diff --git a/admin/admin.go b/admin/admin.go new file mode 100644 index 000000000..17e873293 --- /dev/null +++ b/admin/admin.go @@ -0,0 +1,103 @@ +// 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 ( + "encoding/json" + "log/slog" + "net/http" + + "github.com/gorilla/handlers" + "github.com/gorilla/mux" + "github.com/vechain/thor/v2/log" +) + +type logLevelRequest struct { + Level string `json:"level"` +} + +type logLevelResponse struct { + CurrentLevel string `json:"currentLevel"` +} + +type errorResponse struct { + ErrorCode int `json:"errorCode"` + ErrorMessage string `json:"errorMessage"` +} + +func writeError(w http.ResponseWriter, errCode int, errMsg string) { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(errCode) + json.NewEncoder(w).Encode(errorResponse{ + ErrorCode: errCode, + ErrorMessage: errMsg, + }) +} + +func getLogLevelHandler(logLevel *slog.LevelVar) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + response := logLevelResponse{ + CurrentLevel: logLevel.Level().String(), + } + if err := json.NewEncoder(w).Encode(response); err != nil { + writeError(w, http.StatusInternalServerError, "Failed to encode response") + } + } +} + +func postLogLevelHandler(logLevel *slog.LevelVar) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + var req logLevelRequest + if err := json.NewDecoder(r.Body).Decode(&req); err != nil { + writeError(w, http.StatusBadRequest, "Invalid request body") + return + } + + 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: + writeError(w, http.StatusBadRequest, "Invalid verbosity level") + return + } + + w.Header().Set("Content-Type", "application/json") + response := logLevelResponse{ + CurrentLevel: logLevel.Level().String(), + } + w.WriteHeader(http.StatusOK) + json.NewEncoder(w).Encode(response) + } +} + +func logLevelHandler(logLevel *slog.LevelVar) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + switch r.Method { + case http.MethodGet: + getLogLevelHandler(logLevel).ServeHTTP(w, r) + case http.MethodPost: + postLogLevelHandler(logLevel).ServeHTTP(w, r) + default: + writeError(w, http.StatusMethodNotAllowed, "method not allowed") + } + } +} + +func HTTPHandler(logLevel *slog.LevelVar) http.Handler { + router := mux.NewRouter() + router.HandleFunc("/admin/loglevel", logLevelHandler(logLevel)) + return handlers.CompressHandler(router) +} diff --git a/api/api.go b/api/api.go index c63573764..acbab3201 100644 --- a/api/api.go +++ b/api/api.go @@ -50,6 +50,7 @@ func New( enableReqLogger bool, enableMetrics bool, logsLimit uint64, + allowedTracers map[string]interface{}, ) (http.HandlerFunc, func()) { origins := strings.Split(strings.TrimSpace(allowedOrigins), ",") for i, o := range origins { @@ -82,7 +83,7 @@ func New( Mount(router, "/blocks") transactions.New(repo, txPool). Mount(router, "/transactions") - debug.New(repo, stater, forkConfig, callGasLimit, allowCustomTracer, bft). + debug.New(repo, stater, forkConfig, callGasLimit, allowCustomTracer, bft, allowedTracers). Mount(router, "/debug") node.New(nw). Mount(router, "/node") diff --git a/api/debug/debug.go b/api/debug/debug.go index 182367cdc..cca91a616 100644 --- a/api/debug/debug.go +++ b/api/debug/debug.go @@ -8,6 +8,7 @@ package debug import ( "context" "encoding/json" + "fmt" "math" "math/big" "net/http" @@ -29,7 +30,6 @@ import ( "github.com/vechain/thor/v2/state" "github.com/vechain/thor/v2/thor" "github.com/vechain/thor/v2/tracers" - "github.com/vechain/thor/v2/tracers/logger" "github.com/vechain/thor/v2/trie" "github.com/vechain/thor/v2/tx" "github.com/vechain/thor/v2/vm" @@ -46,16 +46,18 @@ type Debug struct { forkConfig thor.ForkConfig callGasLimit uint64 allowCustomTracer bool + allowedTracers map[string]interface{} bft bft.Committer } -func New(repo *chain.Repository, stater *state.Stater, forkConfig thor.ForkConfig, callGaslimit uint64, allowCustomTracer bool, bft bft.Committer) *Debug { +func New(repo *chain.Repository, stater *state.Stater, forkConfig thor.ForkConfig, callGaslimit uint64, allowCustomTracer bool, bft bft.Committer, allowedTracers map[string]interface{}) *Debug { return &Debug{ repo, stater, forkConfig, callGaslimit, allowCustomTracer, + allowedTracers, bft, } } @@ -211,9 +213,17 @@ func (d *Debug) handleTraceCall(w http.ResponseWriter, req *http.Request) error } func (d *Debug) createTracer(name string, config json.RawMessage) (tracers.Tracer, error) { - if name == "" { - return logger.NewStructLogger(config) + if strings.TrimSpace(name) == "" { + return nil, fmt.Errorf("tracer name must be defined") } + _, noTracers := d.allowedTracers["none"] + _, allTracers := d.allowedTracers["all"] + + // fail if the requested tracer is not allowed OR if the "all" tracers code isn't active + if _, ok := d.allowedTracers[name]; noTracers || (!ok && !allTracers) { + return nil, fmt.Errorf("creating tracer is not allowed: %s", name) + } + return tracers.DefaultDirectory.New(name, config, d.allowCustomTracer) } diff --git a/api/debug/debug_test.go b/api/debug/debug_test.go index 7075e728a..467fc95fb 100644 --- a/api/debug/debug_test.go +++ b/api/debug/debug_test.go @@ -52,6 +52,8 @@ func TestDebug(t *testing.T) { // /tracers endpoint for name, tt := range map[string]func(*testing.T){ + "testTraceClauseWithEmptyTracerName": testTraceClauseWithEmptyTracerName, + "testTraceClauseWithInvalidTracerName": testTraceClauseWithInvalidTracerName, "testTraceClauseWithEmptyTracerTarget": testTraceClauseWithEmptyTracerTarget, "testTraceClauseWithBadBlockId": testTraceClauseWithBadBlockId, "testTraceClauseWithNonExistingBlockId": testTraceClauseWithNonExistingBlockId, @@ -156,13 +158,27 @@ func TestStorageRangeMaxResult(t *testing.T) { assert.Equal(t, 10, len(storageRangeRes.Storage)) } +func testTraceClauseWithEmptyTracerName(t *testing.T) { + res := httpPostAndCheckResponseStatus(t, ts.URL+"/debug/tracers", &TraceClauseOption{}, 403) + assert.Equal(t, "tracer name must be defined", strings.TrimSpace(res)) + + res = httpPostAndCheckResponseStatus(t, ts.URL+"/debug/tracers", &TraceClauseOption{Name: " "}, 403) + assert.Equal(t, "tracer name must be defined", strings.TrimSpace(res)) +} + +func testTraceClauseWithInvalidTracerName(t *testing.T) { + res := httpPostAndCheckResponseStatus(t, ts.URL+"/debug/tracers", &TraceClauseOption{Name: "non-existent"}, 403) + assert.Contains(t, res, "unable to create custom tracer") +} + func testTraceClauseWithEmptyTracerTarget(t *testing.T) { - res := httpPostAndCheckResponseStatus(t, ts.URL+"/debug/tracers", &TraceClauseOption{}, 400) + res := httpPostAndCheckResponseStatus(t, ts.URL+"/debug/tracers", &TraceClauseOption{Name: "logger"}, 400) assert.Equal(t, "target: unsupported", strings.TrimSpace(res)) } func testTraceClauseWithBadBlockId(t *testing.T) { traceClauseOption := &TraceClauseOption{ + Name: "logger", Target: "badBlockId/x/x", } res := httpPostAndCheckResponseStatus(t, ts.URL+"/debug/tracers", traceClauseOption, 400) @@ -177,6 +193,7 @@ func testTraceClauseWithNonExistingBlockId(t *testing.T) { func testTraceClauseWithBadTxId(t *testing.T) { traceClauseOption := &TraceClauseOption{ + Name: "logger", Target: fmt.Sprintf("%s/badTxId/x", blk.Header().ID()), } res := httpPostAndCheckResponseStatus(t, ts.URL+"/debug/tracers", traceClauseOption, 400) @@ -186,6 +203,7 @@ func testTraceClauseWithBadTxId(t *testing.T) { func testTraceClauseWithNonExistingTx(t *testing.T) { nonExistingTxId := "0x4500ade0d72115abfc77571aef752df45ba5e87ca81fbd67fbfc46d455b17f91" traceClauseOption := &TraceClauseOption{ + Name: "logger", Target: fmt.Sprintf("%s/%s/x", blk.Header().ID(), nonExistingTxId), } res := httpPostAndCheckResponseStatus(t, ts.URL+"/debug/tracers", traceClauseOption, 403) @@ -195,6 +213,7 @@ func testTraceClauseWithNonExistingTx(t *testing.T) { func testTraceClauseWithBadClauseIndex(t *testing.T) { // Clause index is not a number traceClauseOption := &TraceClauseOption{ + Name: "logger", Target: fmt.Sprintf("%s/%s/x", blk.Header().ID(), transaction.ID()), } res := httpPostAndCheckResponseStatus(t, ts.URL+"/debug/tracers", traceClauseOption, 400) @@ -202,6 +221,7 @@ func testTraceClauseWithBadClauseIndex(t *testing.T) { // Clause index is out of range traceClauseOption = &TraceClauseOption{ + Name: "logger", Target: fmt.Sprintf("%s/%s/%d", blk.Header().ID(), transaction.ID(), uint64(math.MaxUint64)), } res = httpPostAndCheckResponseStatus(t, ts.URL+"/debug/tracers", traceClauseOption, 400) @@ -237,6 +257,7 @@ func testTraceClauseWithCustomTracer(t *testing.T) { func testTraceClause(t *testing.T) { traceClauseOption := &TraceClauseOption{ + Name: "logger", Target: fmt.Sprintf("%s/%s/1", blk.Header().ID(), transaction.ID()), } expectedExecutionResult := &logger.ExecutionResult{ @@ -256,6 +277,7 @@ func testTraceClause(t *testing.T) { func testTraceClauseWithTxIndexOutOfBound(t *testing.T) { traceClauseOption := &TraceClauseOption{ + Name: "logger", Target: fmt.Sprintf("%s/10/1", blk.Header().ID()), } @@ -266,6 +288,7 @@ func testTraceClauseWithTxIndexOutOfBound(t *testing.T) { func testTraceClauseWithClauseIndexOutOfBound(t *testing.T) { traceClauseOption := &TraceClauseOption{ + Name: "logger", Target: fmt.Sprintf("%s/%s/10", blk.Header().ID(), transaction.ID()), } @@ -280,7 +303,7 @@ func testHandleTraceCallWithMalformedBodyRequest(t *testing.T) { } func testHandleTraceCallWithEmptyTraceCallOption(t *testing.T) { - traceCallOption := &TraceCallOption{} + traceCallOption := &TraceCallOption{Name: "logger"} expectedExecutionResult := &logger.ExecutionResult{ Gas: 0, Failed: false, @@ -298,7 +321,7 @@ func testHandleTraceCallWithEmptyTraceCallOption(t *testing.T) { } func testTraceCallNextBlock(t *testing.T) { - traceCallOption := &TraceCallOption{} + traceCallOption := &TraceCallOption{Name: "logger"} httpPostAndCheckResponseStatus(t, ts.URL+"/debug/tracers/call?revision=next", traceCallOption, 200) } @@ -306,6 +329,7 @@ func testHandleTraceCall(t *testing.T) { addr := randAddress() provedWork := math.HexOrDecimal256(*big.NewInt(1000)) traceCallOption := &TraceCallOption{ + Name: "logger", To: &addr, Value: &math.HexOrDecimal256{}, Data: "0x00", @@ -349,7 +373,7 @@ func testHandleTraceCallWithValidRevisions(t *testing.T) { StructLogs: make([]logger.StructLogRes, 0), } - res := httpPostAndCheckResponseStatus(t, ts.URL+"/debug/tracers/call?revision="+revision, &TraceCallOption{}, 200) + res := httpPostAndCheckResponseStatus(t, ts.URL+"/debug/tracers/call?revision="+revision, &TraceCallOption{Name: "logger"}, 200) var parsedExecutionRes *logger.ExecutionResult if err := json.Unmarshal([]byte(res), &parsedExecutionRes); err != nil { @@ -391,6 +415,7 @@ func testHandleTraceCallWithMalfomredRevision(t *testing.T) { func testHandleTraceCallWithInsufficientGas(t *testing.T) { addr := randAddress() traceCallOption := &TraceCallOption{ + Name: "logger", To: &addr, Value: &math.HexOrDecimal256{}, Data: "0x00", @@ -410,6 +435,7 @@ func testHandleTraceCallWithInsufficientGas(t *testing.T) { func testHandleTraceCallWithBadBlockRef(t *testing.T) { addr := randAddress() traceCallOption := &TraceCallOption{ + Name: "logger", To: &addr, Value: &math.HexOrDecimal256{}, Data: "0x00", @@ -429,6 +455,7 @@ func testHandleTraceCallWithBadBlockRef(t *testing.T) { func testHandleTraceCallWithInvalidLengthBlockRef(t *testing.T) { addr := randAddress() traceCallOption := &TraceCallOption{ + Name: "logger", To: &addr, Value: &math.HexOrDecimal256{}, Data: "0x00", @@ -569,7 +596,8 @@ func initDebugServer(t *testing.T) { forkConfig := thor.GetForkConfig(b.Header().ID()) router := mux.NewRouter() - debug = New(repo, stater, forkConfig, 21000, true, solo.NewBFTEngine(repo)) + allTracersEnabled := map[string]interface{}{"all": new(interface{})} + debug = New(repo, stater, forkConfig, 21000, true, solo.NewBFTEngine(repo), allTracersEnabled) debug.Mount(router, "/debug") ts = httptest.NewServer(router) } diff --git a/api/doc/thor.yaml b/api/doc/thor.yaml index b629ff7b4..85fc66043 100644 --- a/api/doc/thor.yaml +++ b/api/doc/thor.yaml @@ -2117,7 +2117,7 @@ components: name: type: string enum: - - "" + - logger - 4byte - call - noop diff --git a/cmd/disco/utils.go b/cmd/disco/utils.go index 53a2a2e5f..c73d357e4 100644 --- a/cmd/disco/utils.go +++ b/cmd/disco/utils.go @@ -9,6 +9,7 @@ import ( "crypto/ecdsa" "fmt" "io" + "log/slog" "os" "os/user" "path/filepath" @@ -19,15 +20,19 @@ import ( "github.com/vechain/thor/v2/log" ) -func initLogger(lvl int) { +func initLogger(lvl int) *slog.LevelVar { logLevel := log.FromLegacyLevel(lvl) + var level slog.LevelVar + level.Set(logLevel) output := io.Writer(os.Stdout) useColor := (isatty.IsTerminal(os.Stderr.Fd()) || isatty.IsCygwinTerminal(os.Stderr.Fd())) && os.Getenv("TERM") != "dumb" - handler := log.NewTerminalHandlerWithLevel(output, logLevel, useColor) + handler := log.NewTerminalHandlerWithLevel(output, &level, useColor) log.SetDefault(log.NewLogger(handler)) ethlog.Root().SetHandler(ðLogger{ logger: log.WithContext("pkg", "geth"), }) + + return &level } type ethLogger struct { diff --git a/cmd/thor/flags.go b/cmd/thor/flags.go index 34eaa3325..382bac66d 100644 --- a/cmd/thor/flags.go +++ b/cmd/thor/flags.go @@ -151,6 +151,27 @@ var ( Usage: "metrics service listening address", } + enableAdminFlag = cli.BoolFlag{ + Name: "enable-admin", + Usage: "enables admin service", + } + adminAddrFlag = cli.StringFlag{ + Name: "admin-addr", + Value: "localhost:2113", + Usage: "admin service listening address", + } + txPoolLimitPerAccountFlag = cli.Uint64Flag{ + Name: "txpool-limit-per-account", + Value: 16, + Usage: "set tx limit per account in pool", + } + + allowedTracersFlag = cli.StringFlag{ + Name: "api-allowed-tracers", + Value: "none", + Usage: "define allowed API tracers", + } + // solo mode only flags onDemandFlag = cli.BoolFlag{ Name: "on-demand", @@ -175,11 +196,6 @@ var ( Value: 10000, Usage: "set tx limit in pool", } - txPoolLimitPerAccountFlag = cli.Uint64Flag{ - Name: "txpool-limit-per-account", - Value: 16, - Usage: "set tx limit per account in pool", - } genesisFlag = cli.StringFlag{ Name: "genesis", Usage: "path to genesis file, if not set, the default devnet genesis will be used", diff --git a/cmd/thor/main.go b/cmd/thor/main.go index 8c2f00d19..a66dbc563 100644 --- a/cmd/thor/main.go +++ b/cmd/thor/main.go @@ -11,6 +11,7 @@ import ( "io" "os" "path/filepath" + "strings" "time" "github.com/ethereum/go-ethereum/accounts/keystore" @@ -35,6 +36,7 @@ import ( // Force-load the tracer engines to trigger registration _ "github.com/vechain/thor/v2/tracers/js" + _ "github.com/vechain/thor/v2/tracers/logger" _ "github.com/vechain/thor/v2/tracers/native" ) @@ -93,6 +95,10 @@ func main() { disablePrunerFlag, enableMetricsFlag, metricsAddrFlag, + adminAddrFlag, + enableAdminFlag, + txPoolLimitPerAccountFlag, + allowedTracersFlag, }, Action: defaultAction, Commands: []cli.Command{ @@ -125,6 +131,9 @@ func main() { disablePrunerFlag, enableMetricsFlag, metricsAddrFlag, + adminAddrFlag, + enableAdminFlag, + allowedTracersFlag, }, Action: soloAction, }, @@ -156,7 +165,7 @@ func defaultAction(ctx *cli.Context) error { if err != nil { return errors.Wrap(err, "parse verbosity flag") } - initLogger(lvl, ctx.Bool(jsonLogsFlag.Name)) + logLevel := initLogger(lvl, ctx.Bool(jsonLogsFlag.Name)) // enable metrics as soon as possible metricsURL := "" @@ -170,6 +179,16 @@ func defaultAction(ctx *cli.Context) error { defer func() { log.Info("stopping metrics server..."); close() }() } + adminURL := "" + if ctx.Bool(enableAdminFlag.Name) { + url, close, err := 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..."); close() }() + } + gene, forkConfig, err := selectGenesis(ctx) if err != nil { return err @@ -212,6 +231,10 @@ func defaultAction(ctx *cli.Context) error { } txpoolOpt := defaultTxPoolOptions + txpoolOpt.LimitPerAccount, err = readIntFromUInt64Flag(ctx.Uint64(txPoolLimitPerAccountFlag.Name)) + if err != nil { + return errors.Wrap(err, "parse txpool-limit-per-account flag") + } txPool := txpool.New(repo, state.NewStater(mainDB), txpoolOpt) defer func() { log.Info("closing tx pool..."); txPool.Close() }() @@ -242,6 +265,7 @@ func defaultAction(ctx *cli.Context) error { ctx.Bool(enableAPILogsFlag.Name), ctx.Bool(enableMetricsFlag.Name), ctx.Uint64(apiLogsLimitFlag.Name), + parseTracerList(strings.TrimSpace(ctx.String(allowedTracersFlag.Name))), ) defer func() { log.Info("closing API..."); apiCloser() }() @@ -251,7 +275,7 @@ func defaultAction(ctx *cli.Context) error { } defer func() { log.Info("stopping API server..."); srvCloser() }() - printStartupMessage2(gene, apiURL, p2pCommunicator.Enode(), metricsURL) + printStartupMessage2(gene, apiURL, p2pCommunicator.Enode(), metricsURL, adminURL) if err := p2pCommunicator.Start(); err != nil { return err @@ -283,7 +307,8 @@ func soloAction(ctx *cli.Context) error { if err != nil { return errors.Wrap(err, "parse verbosity flag") } - initLogger(lvl, ctx.Bool(jsonLogsFlag.Name)) + + logLevel := initLogger(lvl, ctx.Bool(jsonLogsFlag.Name)) // enable metrics as soon as possible metricsURL := "" @@ -297,6 +322,16 @@ func soloAction(ctx *cli.Context) error { defer func() { log.Info("stopping metrics server..."); close() }() } + adminURL := "" + if ctx.Bool(enableAdminFlag.Name) { + url, close, err := 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..."); close() }() + } + var ( gene *genesis.Genesis forkConfig thor.ForkConfig @@ -380,6 +415,7 @@ func soloAction(ctx *cli.Context) error { ctx.Bool(enableAPILogsFlag.Name), ctx.Bool(enableMetricsFlag.Name), ctx.Uint64(apiLogsLimitFlag.Name), + parseTracerList(strings.TrimSpace(ctx.String(allowedTracersFlag.Name))), ) defer func() { log.Info("closing API..."); apiCloser() }() @@ -397,7 +433,7 @@ func soloAction(ctx *cli.Context) error { return errors.New("block-interval cannot be zero") } - printSoloStartupMessage(gene, repo, instanceDir, apiURL, forkConfig, metricsURL) + printSoloStartupMessage(gene, repo, instanceDir, apiURL, forkConfig, metricsURL, adminURL) optimizer := optimizer.New(mainDB, repo, !ctx.Bool(disablePrunerFlag.Name)) defer func() { log.Info("stopping optimizer..."); optimizer.Stop() }() diff --git a/cmd/thor/utils.go b/cmd/thor/utils.go index 620e9c16a..88704dd1d 100644 --- a/cmd/thor/utils.go +++ b/cmd/thor/utils.go @@ -39,6 +39,7 @@ import ( "github.com/mattn/go-isatty" "github.com/mattn/go-tty" "github.com/pkg/errors" + "github.com/vechain/thor/v2/admin" "github.com/vechain/thor/v2/api/doc" "github.com/vechain/thor/v2/chain" "github.com/vechain/thor/v2/cmd/thor/node" @@ -60,21 +61,25 @@ import ( var devNetGenesisID = genesis.NewDevnet().ID() -func initLogger(lvl int, jsonLogs bool) { +func initLogger(lvl int, jsonLogs bool) *slog.LevelVar { logLevel := log.FromLegacyLevel(lvl) output := io.Writer(os.Stdout) + var level slog.LevelVar + level.Set(logLevel) var handler slog.Handler if jsonLogs { - handler = log.JSONHandlerWithLevel(output, logLevel) + handler = log.JSONHandlerWithLevel(output, &level) } else { useColor := (isatty.IsTerminal(os.Stderr.Fd()) || isatty.IsCygwinTerminal(os.Stderr.Fd())) && os.Getenv("TERM") != "dumb" - handler = log.NewTerminalHandlerWithLevel(output, logLevel, useColor) + handler = log.NewTerminalHandlerWithLevel(output, &level, useColor) } log.SetDefault(log.NewLogger(handler)) ethlog.Root().SetHandler(ethlog.LvlFilterHandler(ethlog.LvlWarn, ðLogger{ logger: log.WithContext("pkg", "geth"), })) + + return &level } type ethLogger struct { @@ -587,6 +592,27 @@ func startMetricsServer(addr string) (string, func(), error) { }, nil } +func startAdminServer(addr string, logLevel *slog.LevelVar) (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(admin.HTTPHandler(logLevel)) + handler := handlers.CompressHandler(router) + + srv := &http.Server{Handler: handler, ReadHeaderTimeout: time.Second, ReadTimeout: 5 * time.Second} + var goes co.Goes + goes.Go(func() { + srv.Serve(listener) + }) + return "http://" + listener.Addr().String() + "/admin", func() { + srv.Close() + goes.Wait() + }, nil +} + func printStartupMessage1( gene *genesis.Genesis, repo *chain.Repository, @@ -638,8 +664,9 @@ func printStartupMessage2( apiURL string, nodeID string, metricsURL string, + adminURL string, ) { - fmt.Printf(`%v API portal [ %v ]%v%v`, + fmt.Printf(`%v API portal [ %v ]%v%v%v`, func() string { // node ID if nodeID == "" { return "" @@ -659,6 +686,15 @@ func printStartupMessage2( metricsURL) } }(), + func() string { // admin URL + if adminURL == "" { + return "" + } else { + return fmt.Sprintf(` + Admin [ %v ]`, + adminURL) + } + }(), func() string { // print default dev net's dev accounts info if gene.ID() == devNetGenesisID { @@ -681,6 +717,7 @@ func printSoloStartupMessage( apiURL string, forkConfig thor.ForkConfig, metricsURL string, + adminURL string, ) { bestBlock := repo.BestBlockSummary() @@ -691,6 +728,7 @@ func printSoloStartupMessage( Data dir [ %v ] API portal [ %v ] Metrics [ %v ] + Admin [ %v ] `, common.MakeName("Thor solo", fullVersion()), gene.ID(), gene.Name(), @@ -704,6 +742,12 @@ func printSoloStartupMessage( } return metricsURL }(), + func() string { + if adminURL == "" { + return "Disabled" + } + return adminURL + }(), ) if gene.ID() == devNetGenesisID { @@ -754,3 +798,16 @@ func readIntFromUInt64Flag(val uint64) (int, error) { return i, nil } + +func parseTracerList(list string) map[string]interface{} { + inputs := strings.Split(list, ",") + tracerMap := map[string]interface{}{} + for _, i := range inputs { + if i == "" { + continue + } + tracerMap[i] = new(interface{}) + } + + return tracerMap +} diff --git a/docs/hosting-a-node.md b/docs/hosting-a-node.md index ac9e8fa73..8defd58dc 100644 --- a/docs/hosting-a-node.md +++ b/docs/hosting-a-node.md @@ -108,4 +108,22 @@ By default, a [prometheus](https://prometheus.io/docs/introduction/overview/) se curl localhost:2112/metrics ``` -Instrumentation is in a beta phase at this stage. You can read more about the metric types [here](https://prometheus.io/docs/concepts/metric_types/). \ No newline at end of file +Instrumentation is in a beta phase at this stage. You can read more about the metric types [here](https://prometheus.io/docs/concepts/metric_types/). + +### Admin + +Admin is used to allow privileged actions to the node by the administrator. Currently it supports changing the logger's verbosity at runtime. + +Admin is not enabled in nodes by default. It's possible to enable it by setting `--enable-admin`. Once enabled, an Admin server is available at `localhost:2113/admin` with the following capabilities: + +Retrieve the current log level via a GET request to /admin/loglevel. + +```shell +curl http://localhost:2113/admin/loglevel +``` + +Change the log level via a POST request to /admin/loglevel. + +```shell +curl -X POST -H "Content-Type: application/json" -d '{"level": "trace"}' http://localhost:2113/admin/loglevel +``` \ No newline at end of file diff --git a/docs/usage.md b/docs/usage.md index 552bc62b9..7358e3f0e 100644 --- a/docs/usage.md +++ b/docs/usage.md @@ -168,6 +168,7 @@ bin/thor -h | `--api-call-gas-limit` | Limit contract call gas (default: 50000000) | | `--api-backtrace-limit` | Limit the distance between 'position' and best block for subscriptions APIs (default: 1000) | | `--api-allow-custom-tracer` | Allow custom JS tracer to be used for the tracer API | +| `--api-allowed-tracers` | Comma-separated list of allowed tracers (default: "none") | | `--enable-api-logs` | Enables API requests logging | | `--api-logs-limit` | Limit the number of logs returned by /logs API (default: 1000) | | `--verbosity` | Log verbosity (0-9) (default: 3) | @@ -182,6 +183,9 @@ bin/thor -h | `--disable-pruner` | Disable state pruner to keep all history | | `--enable-metrics` | Enables the metrics server | | `--metrics-addr` | Metrics service listening address | +| `--enable-admin` | Enables the admin server | +| `--admin-addr` | Admin service listening address | +| `--txpool-limit-per-account`| Transaction pool size limit per account | | `--help, -h` | Show help | | `--version, -v` | Print the version | @@ -195,7 +199,7 @@ bin/thor -h | `--persist` | Save blockchain data to disk(default to memory) | | `--gas-limit` | Gas limit for each block | | `--txpool-limit` | Transaction pool size limit | -| `--txpool-limit-per-account` | Transaction pool size limit per account | + #### Discovery Node Flags diff --git a/log/handler.go b/log/handler.go index 17ad6bc89..2cc150fa4 100644 --- a/log/handler.go +++ b/log/handler.go @@ -55,7 +55,7 @@ func (h *discardHandler) WithAttrs(attrs []slog.Attr) slog.Handler { type TerminalHandler struct { mu sync.Mutex wr io.Writer - lvl slog.Level + lvl *slog.LevelVar useColor bool attrs []slog.Attr // fieldPadding is a map with maximum field value lengths seen until now @@ -75,12 +75,14 @@ type TerminalHandler struct { // // [DBUG] [May 16 20:58:45] remove route ns=haproxy addr=127.0.0.1:50002 func NewTerminalHandler(wr io.Writer, useColor bool) *TerminalHandler { - return NewTerminalHandlerWithLevel(wr, levelMaxVerbosity, useColor) + var level slog.LevelVar + level.Set(levelMaxVerbosity) + return NewTerminalHandlerWithLevel(wr, &level, useColor) } // NewTerminalHandlerWithLevel returns the same handler as NewTerminalHandler but only outputs // records which are less than or equal to the specified verbosity level. -func NewTerminalHandlerWithLevel(wr io.Writer, lvl slog.Level, useColor bool) *TerminalHandler { +func NewTerminalHandlerWithLevel(wr io.Writer, lvl *slog.LevelVar, useColor bool) *TerminalHandler { return &TerminalHandler{ wr: wr, lvl: lvl, @@ -99,7 +101,7 @@ func (h *TerminalHandler) Handle(_ context.Context, r slog.Record) error { } func (h *TerminalHandler) Enabled(_ context.Context, level slog.Level) bool { - return level >= h.lvl + return level.Level() >= h.lvl.Level() } func (h *TerminalHandler) WithGroup(name string) slog.Handler { @@ -123,20 +125,22 @@ func (t *TerminalHandler) ResetFieldPadding() { t.mu.Unlock() } -type leveler struct{ minLevel slog.Level } +type leveler struct{ minLevel *slog.LevelVar } func (l *leveler) Level() slog.Level { - return l.minLevel + return l.minLevel.Level() } // JSONHandler returns a handler which prints records in JSON format. func JSONHandler(wr io.Writer) slog.Handler { - return JSONHandlerWithLevel(wr, levelMaxVerbosity) + var level slog.LevelVar + level.Set(levelMaxVerbosity) + return JSONHandlerWithLevel(wr, &level) } // JSONHandlerWithLevel returns a handler which prints records in JSON format that are less than or equal to // the specified verbosity level. -func JSONHandlerWithLevel(wr io.Writer, level slog.Level) slog.Handler { +func JSONHandlerWithLevel(wr io.Writer, level *slog.LevelVar) slog.Handler { return slog.NewJSONHandler(wr, &slog.HandlerOptions{ ReplaceAttr: builtinReplaceJSON, Level: &leveler{level}, @@ -155,7 +159,7 @@ func LogfmtHandler(wr io.Writer) slog.Handler { // LogfmtHandlerWithLevel returns the same handler as LogfmtHandler but it only outputs // records which are less than or equal to the specified verbosity level. -func LogfmtHandlerWithLevel(wr io.Writer, level slog.Level) slog.Handler { +func LogfmtHandlerWithLevel(wr io.Writer, level *slog.LevelVar) slog.Handler { return slog.NewTextHandler(wr, &slog.HandlerOptions{ ReplaceAttr: builtinReplaceLogfmt, Level: &leveler{level}, diff --git a/log/logger_test.go b/log/logger_test.go index 5310362dc..4d82c710e 100644 --- a/log/logger_test.go +++ b/log/logger_test.go @@ -33,7 +33,9 @@ import ( func TestTerminalHandlerWithAttrs(t *testing.T) { out := new(bytes.Buffer) - handler := NewTerminalHandlerWithLevel(out, LevelTrace, false).WithAttrs([]slog.Attr{slog.String("baz", "bat")}) + var level slog.LevelVar + level.Set(LevelTrace) + handler := NewTerminalHandlerWithLevel(out, &level, false).WithAttrs([]slog.Attr{slog.String("baz", "bat")}) logger := NewLogger(handler) logger.Trace("a message", "foo", "bar") have := out.String() @@ -57,7 +59,11 @@ func TestJSONHandler(t *testing.T) { } out.Reset() - handler = JSONHandlerWithLevel(out, slog.LevelInfo) + + var level slog.LevelVar + level.Set(LevelInfo) + + handler = JSONHandlerWithLevel(out, &level) logger = slog.New(handler) logger.Debug("hi there") if len(out.String()) != 0 { @@ -128,7 +134,9 @@ func TestLoggerOutput(t *testing.T) { ) out := new(bytes.Buffer) - handler := NewTerminalHandlerWithLevel(out, LevelInfo, false) + var level slog.LevelVar + level.Set(LevelInfo) + handler := NewTerminalHandlerWithLevel(out, &level, false) NewLogger(handler).Info("This is a message", "foo", int16(123), "bytes", bb, diff --git a/tracers/logger/logger.go b/tracers/logger/logger.go index 936dbf192..ff3735a6e 100644 --- a/tracers/logger/logger.go +++ b/tracers/logger/logger.go @@ -34,6 +34,10 @@ import ( "github.com/vechain/thor/v2/vm" ) +func init() { + tracers.DefaultDirectory.Register("logger", NewStructLogger, false) +} + // Storage represents a contract's storage. type Storage map[common.Hash]common.Hash @@ -119,7 +123,7 @@ type StructLogger struct { } // NewStructLogger returns a new logger -func NewStructLogger(cfg json.RawMessage) (*StructLogger, error) { +func NewStructLogger(cfg json.RawMessage) (tracers.Tracer, error) { var config Config if cfg != nil { if err := json.Unmarshal(cfg, &config); err != nil { diff --git a/tracers/logger/logger_test.go b/tracers/logger/logger_test.go index 0cc8dc0ec..ebf4b8772 100644 --- a/tracers/logger/logger_test.go +++ b/tracers/logger/logger_test.go @@ -58,11 +58,12 @@ func (*dummyStatedb) SetState(_ common.Address, _ common.Hash, _ common.Hash) {} func TestStoreCapture(t *testing.T) { var ( - logger, _ = NewStructLogger(nil) - env = vm.NewEVM(vm.Context{}, &dummyStatedb{}, &vm.ChainConfig{ChainConfig: *params.TestChainConfig}, vm.Config{Tracer: logger}) + unCastedLogger, _ = NewStructLogger(nil) + env = vm.NewEVM(vm.Context{}, &dummyStatedb{}, &vm.ChainConfig{ChainConfig: *params.TestChainConfig}, vm.Config{Tracer: unCastedLogger.(*StructLogger)}) contract = vm.NewContract(&dummyContractRef{}, &dummyContractRef{}, new(big.Int), 100000) ) + logger := unCastedLogger.(*StructLogger) contract.Code = []byte{byte(vm.PUSH1), 0x1, byte(vm.PUSH1), 0x0, byte(vm.SSTORE)} var index common.Hash logger.CaptureStart(env, common.Address{}, contract.Address(), false, nil, 0, nil) @@ -123,7 +124,8 @@ func TestFormatLogs(t *testing.T) { } func TestCaptureStart(t *testing.T) { - logger, _ := NewStructLogger(nil) + unCastedLogger, _ := NewStructLogger(nil) + logger := unCastedLogger.(*StructLogger) env := &vm.EVM{} logger.CaptureStart(env, common.Address{}, common.Address{}, false, nil, 0, nil) diff --git a/tracers/tracers.go b/tracers/tracers.go index 0872efe20..4f5c7122a 100644 --- a/tracers/tracers.go +++ b/tracers/tracers.go @@ -95,7 +95,7 @@ func (d *directory) New(name string, cfg json.RawMessage, allowCustom bool) (Tra // Assume JS code tracer, err := d.jsEval(name, cfg) if err != nil { - return nil, errors.Wrap(err, "create custom tracer") + return nil, errors.Wrap(err, "unable to create custom tracer") } return tracer, nil } else {