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 {