diff --git a/.golangci.yml b/.golangci.yml index 11da0f469..c3c08b0ca 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -11,3 +11,6 @@ linters-settings: lll: line-length: 100 tab-width: 4 + gomnd: + ignored-functions: + - "^make" diff --git a/CHANGELOG.md b/CHANGELOG.md index a00082c11..2d913d275 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,9 +10,21 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added -- Added verification to ensure CARTESI_BLOCKCHAIN_ID matches the id returned from the Ethereum node -- Added support for CARTESI_AUTH_PRIVATE_KEY and CARTESI_AUTH_PRIVATE_KEY_FILE - +- Added verification to ensure `CARTESI_BLOCKCHAIN_ID` matches the id returned from the Ethereum node +- Added support for `CARTESI_AUTH_PRIVATE_KEY` and `CARTESI_AUTH_PRIVATE_KEY_FILE` +- Added `CARTESI_AUTH_KIND` environment variable to select the blockchain authetication method +- Added structured logging with slog. Colored logs can now be enabled with `CARTESI_LOG_PRETTY` environment variable. + +### Changed + +- Changed `CARTESI_BLOCKCHAIN_ID` type from int to uint64 +- Changed `CARTESI_CONTRACTS_APPLICATION_DEPLOYMENT_BLOCK_NUMBER` type from string to int64. +- Changed `CARTESI_LOG_LEVEL` option `warning` to `warn` + +### Removed + +- Removed `CARTESI_EXPERIMENTAL_DISABLE_CONFIG_LOG` and `CARTESI_LOG_TIMESTAMP` environment variables + ## [1.3.1] 2024-03-13 ### Added @@ -20,10 +32,6 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Added `CARTESI_EXPERIMENTAL_SERVER_MANAGER_BYPASS_LOG` env var to allow `server-manager` output to bypass all log configuration - Added `CARTESI_EXPERIMENTAL_DISABLE_CONFIG_LOG` env var to disable log entries related to the node's configuration -## Changed - -- Changed CARTESI_BLOCKCHAIN_ID type from int to uint64 - ## [1.3.0] 2024-02-09 ### Added diff --git a/build/Dockerfile b/build/Dockerfile index 188622fc1..85d3f2f04 100644 --- a/build/Dockerfile +++ b/build/Dockerfile @@ -178,7 +178,6 @@ ENV PATH=$PATH:/usr/local/go/bin # Copy gen-devnet script and dependencies # This should be simplified in the future when `COPY --parents` is made available -COPY internal/config internal/config COPY internal/services internal/services COPY pkg/addresses pkg/addresses COPY pkg/contracts pkg/contracts @@ -226,7 +225,6 @@ HEALTHCHECK --interval=1s --timeout=1s --retries=5 \ # Start Anvil. CMD anvil --block-time 1 --load-state $ANVIL_STATE_PATH - #################################################################################################### # TARGET: rollups-node # diff --git a/build/compose-devnet.yaml b/build/compose-devnet.yaml index 695874c95..7f72571e8 100644 --- a/build/compose-devnet.yaml +++ b/build/compose-devnet.yaml @@ -26,4 +26,5 @@ services: CARTESI_CONTRACTS_INPUT_BOX_ADDRESS: "0x59b22D57D4f067708AB0c00552767405926dc768" CARTESI_CONTRACTS_INPUT_BOX_DEPLOYMENT_BLOCK_NUMBER: "20" CARTESI_EPOCH_DURATION: "120" + CARTESI_AUTH_KIND: "mnemonic" CARTESI_AUTH_MNEMONIC: "test test test test test test test test test test test junk" diff --git a/build/compose-node.yaml b/build/compose-node.yaml index b05be99ba..b157b4482 100644 --- a/build/compose-node.yaml +++ b/build/compose-node.yaml @@ -12,7 +12,7 @@ services: - "10000:10000" # Supervisor environment: CARTESI_LOG_LEVEL: "info" - CARTESI_LOG_TIMESTAMP: "false" + CARTESI_LOG_PRETTY: "true" CARTESI_FEATURE_HOST_MODE: "false" CARTESI_FEATURE_DISABLE_CLAIMER: "false" CARTESI_HTTP_ADDRESS: "0.0.0.0" diff --git a/cmd/cartesi-rollups-cli/main.go b/cmd/cartesi-rollups-cli/main.go index 94d406e4b..78d597e26 100644 --- a/cmd/cartesi-rollups-cli/main.go +++ b/cmd/cartesi-rollups-cli/main.go @@ -5,12 +5,21 @@ package main import ( + "log/slog" "os" "github.com/cartesi/rollups-node/cmd/cartesi-rollups-cli/root" + "github.com/lmittmann/tint" ) func main() { + opts := &tint.Options{ + Level: slog.LevelInfo, + } + handler := tint.NewHandler(os.Stdout, opts) + logger := slog.New(handler) + slog.SetDefault(logger) + err := root.Cmd.Execute() if err != nil { os.Exit(1) diff --git a/cmd/cartesi-rollups-cli/root/deps/deps.go b/cmd/cartesi-rollups-cli/root/deps/deps.go index 94b0d92b5..83f544aec 100644 --- a/cmd/cartesi-rollups-cli/root/deps/deps.go +++ b/cmd/cartesi-rollups-cli/root/deps/deps.go @@ -5,10 +5,10 @@ package deps import ( "context" + "log/slog" "os/signal" "syscall" - "github.com/cartesi/rollups-node/internal/config" "github.com/cartesi/rollups-node/internal/deps" "github.com/spf13/cobra" ) @@ -48,18 +48,16 @@ func init() { } func run(cmd *cobra.Command, args []string) { - ctx, cancel := signal.NotifyContext(cmd.Context(), syscall.SIGINT, syscall.SIGTERM) defer cancel() depsContainers, err := deps.Run(ctx, *depsConfig) cobra.CheckErr(err) - config.InfoLogger.Println("all deps are up") + slog.Info("All dependencies are up") <-ctx.Done() err = deps.Terminate(context.Background(), depsContainers) cobra.CheckErr(err) - } diff --git a/cmd/cartesi-rollups-cli/root/execute/execute.go b/cmd/cartesi-rollups-cli/root/execute/execute.go index 4ae14c823..c5505ec61 100644 --- a/cmd/cartesi-rollups-cli/root/execute/execute.go +++ b/cmd/cartesi-rollups-cli/root/execute/execute.go @@ -4,10 +4,10 @@ package execute import ( + "log/slog" "os" "github.com/Khan/genqlient/graphql" - "github.com/cartesi/rollups-node/internal/config" "github.com/cartesi/rollups-node/pkg/addresses" "github.com/cartesi/rollups-node/pkg/ethutil" "github.com/cartesi/rollups-node/pkg/readerclient" @@ -70,13 +70,13 @@ func run(cmd *cobra.Command, args []string) { cobra.CheckErr(err) if resp.Proof == nil { - config.InfoLogger.Printf("The voucher has no associated proof yet.\n") + slog.Warn("The voucher has no associated proof yet") os.Exit(0) } client, err := ethclient.DialContext(ctx, ethEndpoint) cobra.CheckErr(err) - config.InfoLogger.Printf("connected to %v\n", ethEndpoint) + slog.Info("Connected", "eth-endpoint", ethEndpoint) signer, err := ethutil.NewMnemonicSigner(ctx, client, mnemonic, account) cobra.CheckErr(err) @@ -91,9 +91,10 @@ func run(cmd *cobra.Command, args []string) { proof := readerclient.ConvertToContractProof(resp.Proof) - config.InfoLogger.Printf("executing voucher %d from input %d\n", - voucherIndex, - inputIndex, + slog.Info("Executing voucher", + "voucher-index", voucherIndex, + "input-index", inputIndex, + "application-address", book.CartesiDApp, ) txHash, err := ethutil.ExecuteVoucher( ctx, @@ -106,5 +107,5 @@ func run(cmd *cobra.Command, args []string) { ) cobra.CheckErr(err) - config.InfoLogger.Printf("The voucher was executed! (tx=%v)\n", txHash) + slog.Info("Voucher executed", "tx-hash", txHash) } diff --git a/cmd/cartesi-rollups-cli/root/savesnapshot/savesnapshot.go b/cmd/cartesi-rollups-cli/root/savesnapshot/savesnapshot.go index 0a92d5785..e9f400ea0 100644 --- a/cmd/cartesi-rollups-cli/root/savesnapshot/savesnapshot.go +++ b/cmd/cartesi-rollups-cli/root/savesnapshot/savesnapshot.go @@ -37,8 +37,6 @@ func init() { } func run(cmd *cobra.Command, args []string) { - err := machine.Save(sourceDockerImage, destDir, tempContainerName) - cobra.CheckErr(err) } diff --git a/cmd/cartesi-rollups-cli/root/send/send.go b/cmd/cartesi-rollups-cli/root/send/send.go index 22e3a1767..d44554eb2 100644 --- a/cmd/cartesi-rollups-cli/root/send/send.go +++ b/cmd/cartesi-rollups-cli/root/send/send.go @@ -4,7 +4,8 @@ package send import ( - "github.com/cartesi/rollups-node/internal/config" + "log/slog" + "github.com/cartesi/rollups-node/pkg/addresses" "github.com/cartesi/rollups-node/pkg/ethutil" "github.com/ethereum/go-ethereum/common/hexutil" @@ -56,7 +57,7 @@ func run(cmd *cobra.Command, args []string) { ctx := cmd.Context() client, err := ethclient.DialContext(ctx, ethEndpoint) cobra.CheckErr(err) - config.InfoLogger.Printf("connected to %v", ethEndpoint) + slog.Info("Connected", "eth-endpoint", ethEndpoint) signer, err := ethutil.NewMnemonicSigner(ctx, client, mnemonic, account) cobra.CheckErr(err) @@ -69,9 +70,9 @@ func run(cmd *cobra.Command, args []string) { book = addresses.GetTestBook() } - config.InfoLogger.Printf("sending input to %x", book.CartesiDApp) + slog.Info("Sending input", "application-address", book.CartesiDApp) inputIndex, err := ethutil.AddInput(ctx, client, book, signer, payload) cobra.CheckErr(err) - config.InfoLogger.Printf("added input with index %v", inputIndex) + slog.Info("Input added", "input-index", inputIndex) } diff --git a/cmd/cartesi-rollups-cli/root/validate/validate.go b/cmd/cartesi-rollups-cli/root/validate/validate.go index e195abac4..fcca2fc79 100644 --- a/cmd/cartesi-rollups-cli/root/validate/validate.go +++ b/cmd/cartesi-rollups-cli/root/validate/validate.go @@ -4,10 +4,10 @@ package validate import ( + "log/slog" "os" "github.com/Khan/genqlient/graphql" - "github.com/cartesi/rollups-node/internal/config" "github.com/cartesi/rollups-node/pkg/addresses" "github.com/cartesi/rollups-node/pkg/ethutil" "github.com/cartesi/rollups-node/pkg/readerclient" @@ -62,13 +62,13 @@ func run(cmd *cobra.Command, args []string) { cobra.CheckErr(err) if resp.Proof == nil { - config.InfoLogger.Printf("The notice has no associated proof yet.\n") + slog.Warn("The notice has no associated proof yet") os.Exit(0) } client, err := ethclient.DialContext(ctx, ethEndpoint) cobra.CheckErr(err) - config.InfoLogger.Printf("connected to %v\n", ethEndpoint) + slog.Info("Connected", "eth-endpoint", ethEndpoint) var book *addresses.Book if addressBookFile != "" { @@ -80,13 +80,13 @@ func run(cmd *cobra.Command, args []string) { proof := readerclient.ConvertToContractProof(resp.Proof) - config.InfoLogger.Printf("validating notice %d from input %d with address %x\n", - noticeIndex, - inputIndex, - book.CartesiDApp, + slog.Info("Validating notice", + "notice-index", noticeIndex, + "input-index", inputIndex, + "application-address", book.CartesiDApp, ) err = ethutil.ValidateNotice(ctx, client, book, resp.Payload, proof) cobra.CheckErr(err) - config.InfoLogger.Printf("The notice is valid!\n") + slog.Info("Notice validated") } diff --git a/cmd/cartesi-rollups-node/handlers.go b/cmd/cartesi-rollups-node/handlers.go deleted file mode 100644 index 329e76935..000000000 --- a/cmd/cartesi-rollups-node/handlers.go +++ /dev/null @@ -1,68 +0,0 @@ -// (c) Cartesi and individual authors (see AUTHORS) -// SPDX-License-Identifier: Apache-2.0 (see LICENSE) - -package main - -import ( - "fmt" - "net/http" - "net/http/httputil" - "net/url" - - "github.com/cartesi/rollups-node/internal/config" -) - -func newHttpServiceHandler() http.Handler { - handler := http.NewServeMux() - handler.Handle("/healthz", http.HandlerFunc(healthcheckHandler)) - - graphqlProxy, err := newReverseProxy(getPort(portOffsetGraphQLServer)) - if err != nil { - config.ErrorLogger.Fatal(err) - } - handler.Handle("/graphql", graphqlProxy) - - dispatcherProxy, err := newReverseProxy(getPort(portOffsetDispatcher)) - if err != nil { - config.ErrorLogger.Fatal(err) - } - handler.Handle("/metrics", dispatcherProxy) - - inspectProxy, err := newReverseProxy(getPort(portOffsetInspectServer)) - if err != nil { - config.ErrorLogger.Fatal(err) - } - handler.Handle("/inspect", inspectProxy) - handler.Handle("/inspect/", inspectProxy) - - if config.GetCartesiFeatureHostMode() { - hostProxy, err := newReverseProxy(getPort(portOffsetHostRunnerRollups)) - if err != nil { - config.ErrorLogger.Fatal(err) - } - handler.Handle("/rollup/", http.StripPrefix("/rollup", hostProxy)) - } - return handler -} - -func healthcheckHandler(w http.ResponseWriter, r *http.Request) { - config.DebugLogger.Println("received healthcheck request") - w.WriteHeader(http.StatusOK) -} - -func newReverseProxy(port int) (*httputil.ReverseProxy, error) { - urlStr := fmt.Sprintf( - "http://%v:%v/", - config.GetCartesiHttpAddress(), - port, - ) - - url, err := url.Parse(urlStr) - if err != nil { - return nil, err - } - - proxy := httputil.NewSingleHostReverseProxy(url) - proxy.ErrorLog = config.ErrorLogger - return proxy, nil -} diff --git a/cmd/cartesi-rollups-node/main.go b/cmd/cartesi-rollups-node/main.go index c8ef42e36..f172dbe3f 100644 --- a/cmd/cartesi-rollups-node/main.go +++ b/cmd/cartesi-rollups-node/main.go @@ -5,73 +5,58 @@ package main import ( "context" + "log/slog" + "os" "os/signal" "syscall" "time" - "github.com/cartesi/rollups-node/internal/config" - "github.com/cartesi/rollups-node/internal/services" + "github.com/cartesi/rollups-node/internal/node" + "github.com/cartesi/rollups-node/internal/node/config" + "github.com/lmittmann/tint" + "github.com/mattn/go-isatty" ) func main() { startTime := time.Now() - var s []services.Service ctx, stop := signal.NotifyContext(context.Background(), syscall.SIGINT, syscall.SIGTERM) defer stop() - if err := validateChainId( - ctx, - config.GetCartesiBlockchainId(), - config.GetCartesiBlockchainHttpEndpoint(), - ); err != nil { - config.ErrorLogger.Fatal(err) - } + config := config.FromEnv() - sunodoValidatorEnabled := config.GetCartesiExperimentalSunodoValidatorEnabled() - if !sunodoValidatorEnabled { - // add Redis first - s = append(s, newRedis()) + // setup log + opts := &tint.Options{ + Level: config.LogLevel, + AddSource: config.LogLevel == slog.LevelDebug, + NoColor: !config.LogPretty || !isatty.IsTerminal(os.Stdout.Fd()), } - - // add services without dependencies - s = append(s, newGraphQLServer()) - s = append(s, newIndexer()) - s = append(s, newStateServer()) - - // start either the server manager or host runner - if config.GetCartesiFeatureHostMode() { - s = append(s, newHostRunner()) - } else { - s = append(s, newServerManager()) - } - - // enable claimer if reader mode and sunodo validator mode are disabled - if !config.GetCartesiFeatureDisableClaimer() && !sunodoValidatorEnabled { - s = append(s, newAuthorityClaimer()) + handler := tint.NewHandler(os.Stdout, opts) + logger := slog.New(handler) + slog.SetDefault(logger) + slog.Info("Starting the Cartesi Rollups Node", "config", config) + + // create the node supervisor + supervisor, err := node.Setup(ctx, config) + if err != nil { + slog.Error("Node exited with an error", "error", err) + os.Exit(1) } - // add services with dependencies - s = append(s, newAdvanceRunner()) // Depends on the server-manager/host-runner - s = append(s, newDispatcher()) // Depends on the state server - s = append(s, newInspectServer()) // Depends on the server-manager/host-runner - - s = append(s, newHttpService()) - - ready := make(chan struct{}, 1) // logs startup time + ready := make(chan struct{}, 1) go func() { select { case <-ready: duration := time.Since(startTime) - config.InfoLogger.Printf("rollups-node: ready after %s", duration) + slog.Info("Node is ready", "after", duration) case <-ctx.Done(): } }() // start supervisor - supervisor := newSupervisorService(s) if err := supervisor.Start(ctx, ready); err != nil { - config.ErrorLogger.Print(err) + slog.Error("Node exited with an error", "error", err) + os.Exit(1) } } diff --git a/cmd/cartesi-rollups-node/services.go b/cmd/cartesi-rollups-node/services.go deleted file mode 100644 index 9464310cb..000000000 --- a/cmd/cartesi-rollups-node/services.go +++ /dev/null @@ -1,343 +0,0 @@ -// (c) Cartesi and individual authors (see AUTHORS) -// SPDX-License-Identifier: Apache-2.0 (see LICENSE) - -package main - -import ( - "fmt" - "os" - - "github.com/cartesi/rollups-node/internal/config" - "github.com/cartesi/rollups-node/internal/services" -) - -// We use an enum to define the ports of each service and avoid conflicts. -type portOffset = int - -const ( - portOffsetProxy = iota - portOffsetAdvanceRunner - portOffsetAuthorityClaimer - portOffsetDispatcher - portOffsetGraphQLServer - portOffsetGraphQLHealthcheck - portOffsetHostRunnerHealthcheck - portOffsetHostRunnerRollups - portOffsetIndexer - portOffsetInspectServer - portOffsetInspectHealthcheck - portOffsetRedis - portOffsetServerManager - portOffsetStateServer -) - -const ( - localhost = "127.0.0.1" - serverManagerSessionId = "default_session_id" -) - -// Get the port of the given service. -func getPort(offset portOffset) int { - return config.GetCartesiHttpPort() + int(offset) -} - -// Get the redis endpoint based on whether the experimental sunodo validator mode is enabled. -func getRedisEndpoint() string { - if config.GetCartesiExperimentalSunodoValidatorEnabled() { - return config.GetCartesiExperimentalSunodoValidatorRedisEndpoint() - } else { - return fmt.Sprintf("redis://%v:%v", localhost, getPort(portOffsetRedis)) - } -} - -// Create the RUST_LOG variable using the config log level. -// If the log level is set to debug, set tracing log for the given rust module. -func getRustLog(rustModule string) string { - switch config.GetCartesiLogLevel() { - case config.LogLevelDebug: - return fmt.Sprintf("RUST_LOG=info,%v=trace", rustModule) - case config.LogLevelInfo: - return "RUST_LOG=info" - case config.LogLevelWarning: - return "RUST_LOG=warn" - case config.LogLevelError: - return "RUST_LOG=error" - default: - panic("impossible") - } -} - -func newAdvanceRunner() services.CommandService { - var s services.CommandService - s.Name = "advance-runner" - s.HealthcheckPort = getPort(portOffsetAdvanceRunner) - s.Path = "cartesi-rollups-advance-runner" - s.Env = append(s.Env, "LOG_ENABLE_TIMESTAMP=false") - s.Env = append(s.Env, "LOG_ENABLE_COLOR=false") - s.Env = append(s.Env, getRustLog("advance_runner")) - s.Env = append(s.Env, - fmt.Sprintf("SERVER_MANAGER_ENDPOINT=http://%v:%v", - localhost, - getPort(portOffsetServerManager))) - s.Env = append(s.Env, - fmt.Sprintf("SESSION_ID=%v", serverManagerSessionId)) - s.Env = append(s.Env, - fmt.Sprintf("REDIS_ENDPOINT=%v", getRedisEndpoint())) - s.Env = append(s.Env, - fmt.Sprintf("CHAIN_ID=%v", config.GetCartesiBlockchainId())) - s.Env = append(s.Env, - fmt.Sprintf("DAPP_CONTRACT_ADDRESS=%v", config.GetCartesiContractsApplicationAddress())) - s.Env = append(s.Env, - fmt.Sprintf("PROVIDER_HTTP_ENDPOINT=%v", config.GetCartesiBlockchainHttpEndpoint())) - s.Env = append(s.Env, - fmt.Sprintf("ADVANCE_RUNNER_HEALTHCHECK_PORT=%v", getPort(portOffsetAdvanceRunner))) - s.Env = append(s.Env, - fmt.Sprintf("READER_MODE=%v", config.GetCartesiFeatureDisableClaimer())) - if config.GetCartesiFeatureHostMode() || config.GetCartesiFeatureDisableMachineHashCheck() { - s.Env = append(s.Env, "SNAPSHOT_VALIDATION_ENABLED=false") - } - if !config.GetCartesiFeatureHostMode() { - s.Env = append(s.Env, - fmt.Sprintf("MACHINE_SNAPSHOT_PATH=%v", config.GetCartesiSnapshotDir())) - } - s.Env = append(s.Env, os.Environ()...) - return s -} - -func newAuthorityClaimer() services.CommandService { - var s services.CommandService - s.Name = "authority-claimer" - s.HealthcheckPort = getPort(portOffsetAuthorityClaimer) - s.Path = "cartesi-rollups-authority-claimer" - s.Env = append(s.Env, "LOG_ENABLE_TIMESTAMP=false") - s.Env = append(s.Env, "LOG_ENABLE_COLOR=false") - s.Env = append(s.Env, getRustLog("authority_claimer")) - s.Env = append(s.Env, - fmt.Sprintf("TX_PROVIDER_HTTP_ENDPOINT=%v", config.GetCartesiBlockchainHttpEndpoint())) - s.Env = append(s.Env, - fmt.Sprintf("TX_CHAIN_ID=%v", config.GetCartesiBlockchainId())) - s.Env = append(s.Env, - fmt.Sprintf("TX_CHAIN_IS_LEGACY=%v", config.GetCartesiBlockchainIsLegacy())) - s.Env = append(s.Env, - fmt.Sprintf("TX_DEFAULT_CONFIRMATIONS=%v", config.GetCartesiBlockchainFinalityOffset())) - s.Env = append(s.Env, - fmt.Sprintf("REDIS_ENDPOINT=%v", getRedisEndpoint())) - s.Env = append(s.Env, - fmt.Sprintf("HISTORY_ADDRESS=%v", config.GetCartesiContractsHistoryAddress())) - s.Env = append(s.Env, - fmt.Sprintf("AUTHORITY_ADDRESS=%v", config.GetCartesiContractsAuthorityAddress())) - s.Env = append(s.Env, - fmt.Sprintf("INPUT_BOX_ADDRESS=%v", config.GetCartesiContractsInputBoxAddress())) - s.Env = append(s.Env, - fmt.Sprintf("GENESIS_BLOCK=%v", config.GetCartesiContractsInputBoxDeploymentBlockNumber())) - s.Env = append(s.Env, - fmt.Sprintf("AUTHORITY_CLAIMER_HTTP_SERVER_PORT=%v", getPort(portOffsetAuthorityClaimer))) - switch auth := config.GetAuth().(type) { - case config.AuthPrivateKey: - s.Env = append(s.Env, - fmt.Sprintf("TX_SIGNING_PRIVATE_KEY=%v", auth.PrivateKey)) - case config.AuthMnemonic: - s.Env = append(s.Env, - fmt.Sprintf("TX_SIGNING_MNEMONIC=%v", auth.Mnemonic)) - s.Env = append(s.Env, - fmt.Sprintf("TX_SIGNING_MNEMONIC_ACCOUNT_INDEX=%v", auth.AccountIndex)) - case config.AuthAWS: - s.Env = append(s.Env, - fmt.Sprintf("TX_SIGNING_AWS_KMS_KEY_ID=%v", auth.KeyID)) - s.Env = append(s.Env, - fmt.Sprintf("TX_SIGNING_AWS_KMS_REGION=%v", auth.Region)) - default: - panic("invalid auth config") - } - s.Env = append(s.Env, os.Environ()...) - return s -} - -func newDispatcher() services.CommandService { - var s services.CommandService - s.Name = "dispatcher" - s.HealthcheckPort = getPort(portOffsetDispatcher) - s.Path = "cartesi-rollups-dispatcher" - s.Env = append(s.Env, "LOG_ENABLE_TIMESTAMP=false") - s.Env = append(s.Env, "LOG_ENABLE_COLOR=false") - s.Env = append(s.Env, getRustLog("dispatcher")) - s.Env = append(s.Env, - fmt.Sprintf("SC_GRPC_ENDPOINT=http://%v:%v", localhost, getPort(portOffsetStateServer))) - s.Env = append(s.Env, - fmt.Sprintf("SC_DEFAULT_CONFIRMATIONS=%v", config.GetCartesiBlockchainFinalityOffset())) - s.Env = append(s.Env, - fmt.Sprintf("REDIS_ENDPOINT=%v", getRedisEndpoint())) - s.Env = append(s.Env, - fmt.Sprintf("DAPP_ADDRESS=%v", config.GetCartesiContractsApplicationAddress())) - s.Env = append(s.Env, - fmt.Sprintf("DAPP_DEPLOYMENT_BLOCK_NUMBER=%v", - config.GetCartesiContractsApplicationDeploymentBlockNumber())) - s.Env = append(s.Env, - fmt.Sprintf("HISTORY_ADDRESS=%v", config.GetCartesiContractsHistoryAddress())) - s.Env = append(s.Env, - fmt.Sprintf("AUTHORITY_ADDRESS=%v", config.GetCartesiContractsAuthorityAddress())) - s.Env = append(s.Env, - fmt.Sprintf("INPUT_BOX_ADDRESS=%v", config.GetCartesiContractsInputBoxAddress())) - s.Env = append(s.Env, - fmt.Sprintf("RD_EPOCH_DURATION=%v", int(config.GetCartesiEpochDuration().Seconds()))) - s.Env = append(s.Env, - fmt.Sprintf("CHAIN_ID=%v", config.GetCartesiBlockchainId())) - s.Env = append(s.Env, - fmt.Sprintf("DISPATCHER_HTTP_SERVER_PORT=%v", getPort(portOffsetDispatcher))) - s.Env = append(s.Env, os.Environ()...) - return s -} - -func newGraphQLServer() services.CommandService { - var s services.CommandService - s.Name = "graphql-server" - s.HealthcheckPort = getPort(portOffsetGraphQLHealthcheck) - s.Path = "cartesi-rollups-graphql-server" - s.Env = append(s.Env, "LOG_ENABLE_TIMESTAMP=false") - s.Env = append(s.Env, "LOG_ENABLE_COLOR=false") - s.Env = append(s.Env, getRustLog("graphql_server")) - s.Env = append(s.Env, - fmt.Sprintf("POSTGRES_ENDPOINT=%v", config.GetCartesiPostgresEndpoint())) - s.Env = append(s.Env, fmt.Sprintf("GRAPHQL_HOST=%v", localhost)) - s.Env = append(s.Env, - fmt.Sprintf("GRAPHQL_PORT=%v", getPort(portOffsetGraphQLServer))) - s.Env = append(s.Env, - fmt.Sprintf("GRAPHQL_HEALTHCHECK_PORT=%v", getPort(portOffsetGraphQLHealthcheck))) - s.Env = append(s.Env, os.Environ()...) - return s -} - -func newHostRunner() services.CommandService { - var s services.CommandService - s.Name = "host-runner" - s.HealthcheckPort = getPort(portOffsetHostRunnerHealthcheck) - s.Path = "cartesi-rollups-host-runner" - s.Env = append(s.Env, "LOG_ENABLE_TIMESTAMP=false") - s.Env = append(s.Env, "LOG_ENABLE_COLOR=false") - s.Env = append(s.Env, getRustLog("host_runner")) - s.Env = append(s.Env, fmt.Sprintf("GRPC_SERVER_MANAGER_ADDRESS=%v", localhost)) - s.Env = append(s.Env, - fmt.Sprintf("GRPC_SERVER_MANAGER_PORT=%v", getPort(portOffsetServerManager))) - s.Env = append(s.Env, fmt.Sprintf("HTTP_ROLLUP_SERVER_ADDRESS=%v", localhost)) - s.Env = append(s.Env, - fmt.Sprintf("HTTP_ROLLUP_SERVER_PORT=%v", getPort(portOffsetHostRunnerRollups))) - s.Env = append(s.Env, - fmt.Sprintf("HOST_RUNNER_HEALTHCHECK_PORT=%v", getPort(portOffsetHostRunnerHealthcheck))) - s.Env = append(s.Env, os.Environ()...) - return s -} - -func newIndexer() services.CommandService { - var s services.CommandService - s.Name = "indexer" - s.HealthcheckPort = getPort(portOffsetIndexer) - s.Path = "cartesi-rollups-indexer" - s.Env = append(s.Env, "LOG_ENABLE_TIMESTAMP=false") - s.Env = append(s.Env, "LOG_ENABLE_COLOR=false") - s.Env = append(s.Env, getRustLog("indexer")) - s.Env = append(s.Env, - fmt.Sprintf("POSTGRES_ENDPOINT=%v", config.GetCartesiPostgresEndpoint())) - s.Env = append(s.Env, - fmt.Sprintf("CHAIN_ID=%v", config.GetCartesiBlockchainId())) - s.Env = append(s.Env, - fmt.Sprintf("DAPP_CONTRACT_ADDRESS=%v", config.GetCartesiContractsApplicationAddress())) - s.Env = append(s.Env, - fmt.Sprintf("REDIS_ENDPOINT=%v", getRedisEndpoint())) - s.Env = append(s.Env, - fmt.Sprintf("INDEXER_HEALTHCHECK_PORT=%v", getPort(portOffsetIndexer))) - s.Env = append(s.Env, os.Environ()...) - return s -} - -func newInspectServer() services.CommandService { - var s services.CommandService - s.Name = "inspect-server" - s.HealthcheckPort = getPort(portOffsetInspectHealthcheck) - s.Path = "cartesi-rollups-inspect-server" - s.Env = append(s.Env, "LOG_ENABLE_TIMESTAMP=false") - s.Env = append(s.Env, "LOG_ENABLE_COLOR=false") - s.Env = append(s.Env, getRustLog("inspect_server")) - s.Env = append(s.Env, - fmt.Sprintf("INSPECT_SERVER_ADDRESS=%v:%v", localhost, getPort(portOffsetInspectServer))) - s.Env = append(s.Env, - fmt.Sprintf("SERVER_MANAGER_ADDRESS=%v:%v", localhost, getPort(portOffsetServerManager))) - s.Env = append(s.Env, - fmt.Sprintf("SESSION_ID=%v", serverManagerSessionId)) - s.Env = append(s.Env, - fmt.Sprintf("INSPECT_SERVER_HEALTHCHECK_PORT=%v", getPort(portOffsetInspectHealthcheck))) - s.Env = append(s.Env, os.Environ()...) - return s -} - -func newRedis() services.CommandService { - var s services.CommandService - s.Name = "redis" - s.HealthcheckPort = getPort(portOffsetRedis) - s.Path = "redis-server" - s.Args = append(s.Args, "--port", fmt.Sprint(getPort(portOffsetRedis))) - // Disable persistence with --save and --appendonly config - s.Args = append(s.Args, "--save", "") - s.Args = append(s.Args, "--appendonly", "no") - s.Env = append(s.Env, os.Environ()...) - return s -} - -func newServerManager() services.ServerManager { - var s services.ServerManager - s.Name = "server-manager" - s.HealthcheckPort = getPort(portOffsetServerManager) - s.Path = "server-manager" - s.Args = append(s.Args, - fmt.Sprintf("--manager-address=%v:%v", localhost, getPort(portOffsetServerManager))) - s.Env = append(s.Env, "REMOTE_CARTESI_MACHINE_LOG_LEVEL=info") - if config.GetCartesiLogLevel() == config.LogLevelDebug { - s.Env = append(s.Env, "SERVER_MANAGER_LOG_LEVEL=info") - } else { - s.Env = append(s.Env, "SERVER_MANAGER_LOG_LEVEL=warning") - } - s.Env = append(s.Env, os.Environ()...) - return s -} - -func newStateServer() services.CommandService { - var s services.CommandService - s.Name = "state-server" - s.HealthcheckPort = getPort(portOffsetStateServer) - s.Path = "cartesi-rollups-state-server" - s.Env = append(s.Env, "LOG_ENABLE_TIMESTAMP=false") - s.Env = append(s.Env, "LOG_ENABLE_COLOR=false") - s.Env = append(s.Env, getRustLog("state_server")) - s.Env = append(s.Env, "SF_CONCURRENT_EVENTS_FETCH=1") - s.Env = append(s.Env, - fmt.Sprintf("SF_GENESIS_BLOCK=%v", - config.GetCartesiContractsInputBoxDeploymentBlockNumber())) - s.Env = append(s.Env, - fmt.Sprintf("SF_SAFETY_MARGIN=%v", config.GetCartesiBlockchainFinalityOffset())) - s.Env = append(s.Env, - fmt.Sprintf("BH_WS_ENDPOINT=%v", config.GetCartesiBlockchainWsEndpoint())) - s.Env = append(s.Env, - fmt.Sprintf("BH_HTTP_ENDPOINT=%v", config.GetCartesiBlockchainHttpEndpoint())) - s.Env = append(s.Env, - fmt.Sprintf("BLOCKCHAIN_BLOCK_TIMEOUT=%v", config.GetCartesiBlockchainBlockTimeout())) - s.Env = append(s.Env, - fmt.Sprintf("SS_SERVER_ADDRESS=%v:%v", localhost, getPort(portOffsetStateServer))) - s.Env = append(s.Env, os.Environ()...) - return s -} - -func newSupervisorService(s []services.Service) services.SupervisorService { - return services.SupervisorService{ - Name: "rollups-node", - Services: s, - } -} - -func newHttpService() services.HttpService { - addr := fmt.Sprintf("%v:%v", config.GetCartesiHttpAddress(), getPort(portOffsetProxy)) - handler := newHttpServiceHandler() - return services.HttpService{ - Name: "http", - Address: addr, - Handler: handler, - } -} diff --git a/docs/config.md b/docs/config.md index d4027c0a1..d51839d24 100644 --- a/docs/config.md +++ b/docs/config.md @@ -12,6 +12,7 @@ The node is configurable through environment variables. This file documents the configuration options. + ## `CARTESI_AUTH_AWS_KMS_KEY_ID` If set, the node will use the AWS KMS service with this key ID to sign transactions. @@ -28,12 +29,17 @@ Must be set alongside `CARTESI_AUTH_AWS_KMS_KEY_ID`. * **Type:** `string` +## `CARTESI_AUTH_KIND` + +One of "private_key", "private_key_file", "mnemonic", "mnemonic_file", "aws". + +* **Type:** `AuthKind` +* **Default:** `"mnemonic"` + ## `CARTESI_AUTH_MNEMONIC` The node will use the private key generated from this mnemonic to sign transactions. -Overrides `CARTESI_AUTH_MNEMONIC_FILE` and `CARTESI_AUTH_AWS_KMS_*`. - * **Type:** `string` ## `CARTESI_AUTH_MNEMONIC_ACCOUNT_INDEX` @@ -49,24 +55,18 @@ the node will use this account index to generate the private key. The node will use the private key generated from the mnemonic contained in this file to sign transactions. -Overrides `CARTESI_AUTH_AWS_KMS_*`. - * **Type:** `string` ## `CARTESI_AUTH_PRIVATE_KEY` The node will use this private key to sign transactions. -Overrides `CARTESI_AUTH_PRIVATE_KEY_FILE`, `CARTESI_AUTH_MNEMONIC`, `CARTESI_AUTH_MNEMONIC_FILE` and `CARTESI_AUTH_AWS_KMS_*`. - * **Type:** `string` ## `CARTESI_AUTH_PRIVATE_KEY_FILE` The node will use the private key contained in this file to sign transactions. -Overrides `CARTESI_AUTH_MNEMONIC`, `CARTESI_AUTH_MNEMONIC_FILE` and `CARTESI_AUTH_AWS_KMS_*`. - * **Type:** `string` ## `CARTESI_BLOCKCHAIN_BLOCK_TIMEOUT` @@ -127,7 +127,7 @@ Address of the DApp's contract. Block in which the DApp's contract was deployed. -* **Type:** `string` +* **Type:** `int64` ## `CARTESI_CONTRACTS_AUTHORITY_ADDRESS` @@ -147,13 +147,6 @@ Address of the InputBox contract. * **Type:** `string` -## `CARTESI_EXPERIMENTAL_DISABLE_CONFIG_LOG` - -Disables all log entries related to the node's configuration - -* **Type:** `bool` -* **Default:** `"false"` - ## `CARTESI_EXPERIMENTAL_SERVER_MANAGER_BYPASS_LOG` When enabled, prints server-manager output to stdout and stderr directly. @@ -217,14 +210,14 @@ The node will also use the 20 ports after this one for internal services. ## `CARTESI_LOG_LEVEL` -One of "debug", "info", "warning", "error". +One of "debug", "info", "warn", "error". * **Type:** `LogLevel` * **Default:** `"info"` -## `CARTESI_LOG_TIMESTAMP` +## `CARTESI_LOG_PRETTY` -If set to true, the node will print the timestamp when logging. +If set to true, the node will add colors to its log output. * **Type:** `bool` * **Default:** `"false"` @@ -257,4 +250,3 @@ At the end of each epoch, the node will send claims to the blockchain. Path to the directory with the cartesi-machine snapshot that will be loaded by the node. * **Type:** `string` - diff --git a/go.mod b/go.mod index d9942c8a9..963d0b3d0 100644 --- a/go.mod +++ b/go.mod @@ -16,6 +16,8 @@ require github.com/BurntSushi/toml v1.3.2 require ( github.com/Khan/genqlient v0.6.0 github.com/deepmap/oapi-codegen/v2 v2.1.0 + github.com/lmittmann/tint v1.0.4 + github.com/mattn/go-isatty v0.0.20 github.com/oapi-codegen/runtime v1.1.1 golang.org/x/sync v0.6.0 golang.org/x/text v0.14.0 diff --git a/go.sum b/go.sum index 35d0e1812..34b30d8b0 100644 --- a/go.sum +++ b/go.sum @@ -190,6 +190,8 @@ github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0 github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= github.com/leanovate/gopter v0.2.9 h1:fQjYxZaynp97ozCzfOyOuAGOU4aU/z37zf/tOujFk7c= github.com/leanovate/gopter v0.2.9/go.mod h1:U2L/78B+KVFIx2VmW6onHJQzXtFb+p5y3y2Sh+Jxxv8= +github.com/lmittmann/tint v1.0.4 h1:LeYihpJ9hyGvE0w+K2okPTGUdVLfng1+nDNVR4vWISc= +github.com/lmittmann/tint v1.0.4/go.mod h1:HIS3gSy7qNwGCj+5oRjAutErFBl4BzdQP6cJZ0NfMwE= github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0/go.mod h1:zJYVVT2jmtg6P3p1VtQj7WsuWi/y4VnjVBn7F8KPB3I= github.com/lufia/plan9stats v0.0.0-20231016141302-07b5767bb0ed h1:036IscGBfJsFIgJQzlui7nK1Ncm0tp2ktmPj8xO4N/0= github.com/lufia/plan9stats v0.0.0-20231016141302-07b5767bb0ed/go.mod h1:ilwx/Dta8jXAgpFYFvSWEMwxmbWXyiUHkd5FwyKhb5k= @@ -369,6 +371,7 @@ golang.org/x/sys v0.0.0-20201204225414-ed752295db88/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.16.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= diff --git a/internal/config/auth.go b/internal/config/auth.go deleted file mode 100644 index 5e03875a1..000000000 --- a/internal/config/auth.go +++ /dev/null @@ -1,24 +0,0 @@ -// (c) Cartesi and individual authors (see AUTHORS) -// SPDX-License-Identifier: Apache-2.0 (see LICENSE) - -package config - -// Auth objects are used to sign transactions. -type Auth any - -// Allows signing through private keys. -type AuthPrivateKey struct { - PrivateKey string -} - -// Allows signing through mnemonics. -type AuthMnemonic struct { - Mnemonic string - AccountIndex int -} - -// Allows signing through AWS services. -type AuthAWS struct { - KeyID string - Region string -} diff --git a/internal/config/config.go b/internal/config/config.go deleted file mode 100644 index c0ecbdfc5..000000000 --- a/internal/config/config.go +++ /dev/null @@ -1,188 +0,0 @@ -// (c) Cartesi and individual authors (see AUTHORS) -// SPDX-License-Identifier: Apache-2.0 (see LICENSE) - -// The config package handles all of the node's configurations, -// which are mostly obtained through environment variables. -// -// The get.go file contains most of the getter functions for the configuration values. -// Its code is automatically generated by the generate/main.go script, -// given the environment variable configured in the generate/Config.toml file. -// -// Custom getters are defined in the config.go file. -package config - -import ( - "fmt" - "io" - "log" - "os" - "strconv" - "sync" - "time" -) - -//go:generate go run ./generate - -// ------------------------------------------------------------------------------------------------ -// Parsing functions -// ------------------------------------------------------------------------------------------------ - -func toInt64FromString(s string) (int64, error) { - return strconv.ParseInt(s, 10, 64) -} - -func toUint64FromString(s string) (uint64, error) { - value, err := strconv.ParseUint(s, 10, 64) - return value, err -} - -func toStringFromString(s string) (string, error) { - return s, nil -} - -func toDurationFromSeconds(s string) (time.Duration, error) { - return time.ParseDuration(s + "s") -} - -// Aliases to be used by the generated functions. -var ( - toBool = strconv.ParseBool - toInt = strconv.Atoi - toUint64 = toUint64FromString - toInt64 = toInt64FromString - toString = toStringFromString - toDuration = toDurationFromSeconds - toLogLevel = toLogLevelFromString -) - -// ------------------------------------------------------------------------------------------------ -// Custom GETs -// ------------------------------------------------------------------------------------------------ - -func GetAuth() Auth { - // if private key is coming from an environment variable - if privateKey, ok := getCartesiAuthPrivateKey(); ok { - return AuthPrivateKey{PrivateKey: privateKey} - } - - // getting the (optional) account index - index, _ := getCartesiAuthMnemonicAccountIndex() - - // if the mnemonic is coming from an environment variable - if mnemonic, ok := getCartesiAuthMnemonic(); ok { - return AuthMnemonic{Mnemonic: mnemonic, AccountIndex: index} - } - - // if the mnemonic is coming from a file - if file, ok := getCartesiAuthMnemonicFile(); ok { - mnemonic, err := os.ReadFile(file) - if err != nil { - fail("mnemonic file error: %s", err) - } - return AuthMnemonic{Mnemonic: string(mnemonic), AccountIndex: index} - } - - // if we are not using mnemonics, but AWS authentication - keyID, ok1 := getCartesiAuthAwsKmsKeyId() - region, ok2 := getCartesiAuthAwsKmsRegion() - if !ok1 || !ok2 { - fail("missing auth environment variables") - } - return AuthAWS{KeyID: keyID, Region: region} -} - -// ------------------------------------------------------------------------------------------------ -// Get Helpers -// ------------------------------------------------------------------------------------------------ - -// Cache of environment variable values -var cache struct { - sync.Mutex - values map[string]string -} - -var configLogger = log.New(os.Stdout, "CONFIG ", log.LstdFlags) - -func init() { - cache.values = make(map[string]string) - if GetCartesiExperimentalDisableConfigLog() { - configLogger.SetOutput(io.Discard) - } -} - -// Reads the value of an environment variable (loads from a cached value when possible). -// It returns the value read and true if the variable was set, -// otherwise it returns the empty string and false. -func read(name string, redact bool) (string, bool) { - cache.Lock() - defer cache.Unlock() - if s, ok := cache.values[name]; ok { - return s, true - } else if s, ok := os.LookupEnv(name); ok { - if !redact { - configLogger.Printf("read %s environment variable: %v", name, s) - } - cache.values[name] = s - return s, true - } else { - return "", false - } -} - -// Parses a string using a given function. -// It fails on a parsing error, otherwise returns the parsed value. -func parse[T any](s string, f func(string) (T, error)) T { - v, err := f(s) - if err != nil { - fail("parsing error: %s", err) - } - return v -} - -// Returns a zero value and false or the value of an environment variable and true. -// -// If the variable could not be read from the environment and it has a default value, -// then this function will set the cache with the default value and return it parsed. -func getOptional[T any]( - name string, - default_ string, - hasDefault bool, - redact bool, - parser func(string) (T, error)) (T, bool) { - - if s, ok := read(name, redact); ok { - v := parse(s, parser) - return v, true - } - - if hasDefault { - cache.Lock() - defer cache.Unlock() - cache.values[name] = default_ - v := parse(default_, parser) - return v, true - } - - var zeroValue T - return zeroValue, false -} - -// Same as getOptional, but fails instead of returning a zero value and false. -func get[T any]( - name string, - defaultValue string, - hasDefault bool, - redact bool, - parser func(string) (T, error)) T { - v, ok := getOptional(name, defaultValue, hasDefault, redact, parser) - if !ok { - fail("missing required %s env var", name) - } - - return v -} - -func fail(s string, v ...any) { - fmt.Fprintf(os.Stderr, s+"\n", v...) - os.Exit(1) -} diff --git a/internal/config/config_test.go b/internal/config/config_test.go deleted file mode 100644 index 6ad1d26f5..000000000 --- a/internal/config/config_test.go +++ /dev/null @@ -1,184 +0,0 @@ -// (c) Cartesi and individual authors (see AUTHORS) -// SPDX-License-Identifier: Apache-2.0 (see LICENSE) - -package config - -import ( - "bytes" - "log" - "os" - "os/exec" - "strings" - "testing" - - "github.com/stretchr/testify/require" - "github.com/stretchr/testify/suite" -) - -func TestEnv(t *testing.T) { - suite.Run(t, new(EnvSuite)) -} - -type EnvSuite struct { - suite.Suite -} - -func (suite *EnvSuite) TearDownTest() { - os.Unsetenv(FOO) - os.Unsetenv(BAR) - os.Unsetenv(BAZ) - cache.values = make(map[string]string) - logInit() -} - -// ------------------------------------------------------------------------------------------------ - -func (suite *EnvSuite) TestRead() { - require := suite.Require() - - // test specific setup - cache.values = make(map[string]string) - cacheLen := len(cache.values) - require.Equal(cacheLen, len(cache.values)) - - // mocking the logger - var buffer bytes.Buffer - configLogger = log.New(&buffer, "", 0) - - name, value := FOO, foo - - { // not initialized - s, ok := read(name, false) - require.Equal("", s) - require.False(ok) - require.Equal(cacheLen, len(cache.values)) - require.Zero(len(getMockedLog(buffer))) - } - { // initialized - os.Setenv(name, value) - s, ok := read(name, false) - require.True(ok) - require.Equal(value, s) - require.Equal(cacheLen+1, len(cache.values)) - suite.T().Log(buffer.String()) - require.Equal(1, len(getMockedLog(buffer))) - } - { // cached - os.Setenv(name, "another foo") - s, ok := read(name, false) - require.True(ok) - require.Equal(value, s) - require.Equal(cacheLen+1, len(cache.values)) - require.Equal(1, len(getMockedLog(buffer))) - } - { // redacted - os.Setenv(BAR, bar) - s, ok := read(BAR, true) - require.True(ok) - require.Equal(bar, s) - require.Equal(cacheLen+2, len(cache.values)) - require.Equal(1, len(getMockedLog(buffer))) - } - { // empty string - os.Setenv(BAZ, "") - s, ok := read(BAZ, false) - require.True(ok) - require.Equal("", s) - require.Equal(cacheLen+3, len(cache.values)) - require.Equal(2, len(getMockedLog(buffer))) - } -} - -func (suite *EnvSuite) TestGetOptional() { - require := suite.Require() - { // not set | not cached | no default - _, ok := getOptional[int](FOO, "", false, true, toInt) - require.False(ok) - } - { // not set | not cached | has default - v, ok := getOptional[int](FOO, "10", true, true, toInt) - require.True(ok) - require.Equal(10, v) - } - { // not set | cached | no default - v, ok := getOptional[int](FOO, "", false, true, toInt) - require.True(ok) - require.Equal(10, v) - } - { // set | cached | has default - os.Setenv(FOO, foo) - v, ok := getOptional[int](FOO, "20", true, true, toInt) - require.True(ok) - require.Equal(10, v) - } - { // set | not cached | no default - os.Setenv(BAR, bar) - v, ok := getOptional[string](BAR, "", false, true, toString) - require.True(ok) - require.Equal(bar, v) - } -} - -func (suite *EnvSuite) TestGet() { - os.Setenv(FOO, foo) - v := get[string](FOO, "", false, true, toString) - require.Equal(suite.T(), foo, v) -} - -func (suite *EnvSuite) TestLogInit() { - require.Equal(suite.T(), 2, len(cache.values)) -} - -// ------------------------------------------------------------------------------------------------ -// Individual Tests -// ------------------------------------------------------------------------------------------------ - -func TestParse(t *testing.T) { - v := parse("true", toBool) - require.True(t, v) -} - -func TestParseFail(t *testing.T) { - requireExit(t, "TestParseFail", func() { - parse("not int", toInt) - }) -} - -func TestGetFail(t *testing.T) { - os.Unsetenv(FOO) - requireExit(t, "TestGetFail", func() { - get[string](FOO, "", false, true, toString) - }) -} - -// ------------------------------------------------------------------------------------------------ -// Auxiliary -// ------------------------------------------------------------------------------------------------ - -var ( - FOO = "FOO" - BAR = "BAR" - BAZ = "BAZ" - - foo = "foo" - bar = "bar" -) - -// For testing code that terminates with os.Exit(1). -func requireExit(t *testing.T, name string, test func()) { - if os.Getenv("IS_TEST") == "1" { - test() - return - } - cmd := exec.Command(os.Args[0], "-test.run="+name) - cmd.Env = append(os.Environ(), "IS_TEST=1") - err := cmd.Run() - if e, ok := err.(*exec.ExitError); ok && !e.Success() { - return - } - t.Fatalf("ran with err %v, want exit(1)", err) -} - -func getMockedLog(buffer bytes.Buffer) []string { - return strings.Split(buffer.String(), "\n")[1:] -} diff --git a/internal/config/generate/helpers.go b/internal/config/generate/helpers.go deleted file mode 100644 index 0e476090b..000000000 --- a/internal/config/generate/helpers.go +++ /dev/null @@ -1,131 +0,0 @@ -// (c) Cartesi and individual authors (see AUTHORS) -// SPDX-License-Identifier: Apache-2.0 (see LICENSE) - -package main - -import ( - "fmt" - "go/format" - "os" - "sort" - "strings" - - "github.com/BurntSushi/toml" -) - -func readTOML(name string) string { - bytes, err := os.ReadFile(name) - if err != nil { - panic(err) - } - return string(bytes) -} - -type configTOML = map[string](map[string]*Env) - -func decodeTOML(data string) configTOML { - var config configTOML - _, err := toml.Decode(data, &config) - if err != nil { - panic(err) - } - return config -} - -// Creates sorted lists of environment variables from the config -// to make the generated files deterministic. -func sortConfig(config configTOML) []Env { - var topics []string - mapping := make(map[string]([]string)) // topic names to env names - - for name, topic := range config { - var envs []string - for name, env := range topic { - env.Name = name // initializes the environment variable's name - envs = append(envs, name) - } - sort.Strings(envs) - - topics = append(topics, name) - mapping[name] = envs - } - sort.Strings(topics) - - var envs []Env - for _, topic := range topics { - for _, name := range mapping[topic] { - envs = append(envs, *config[topic][name]) - } - } - - return envs -} - -func addLine(builder *strings.Builder, s string, a ...any) { - builder.WriteString(fmt.Sprintf(s, a...)) - builder.WriteString("\n") -} - -func addCodeHeader(builder *strings.Builder) { - addLine(builder, `// Code generated by internal/config/generate.`) - addLine(builder, `// DO NOT EDIT.`) - addLine(builder, "") - - addLine(builder, `// (c) Cartesi and individual authors (see AUTHORS)`) - addLine(builder, `// SPDX-License-Identifier: Apache-2.0 (see LICENSE)`) - addLine(builder, "") - - addLine(builder, `package config`) - addLine(builder, `import (`) - addLine(builder, `"time"`) - addLine(builder, `)`) - addLine(builder, "") - - // adding aliases for the functions - addLine(builder, `type (`) - addLine(builder, `Duration = time.Duration`) - addLine(builder, `)`) - addLine(builder, "") -} - -func addDocHeader(builder *strings.Builder) { - addLine(builder, "") - addLine(builder, "") - addLine(builder, "") - - addLine(builder, "# Node Configuration") - addLine(builder, "") - - addLine(builder, "The node is configurable through environment variables.") - addLine(builder, "(There is no other way to configure it.)") - addLine(builder, "") - - addLine(builder, "This file documents the configuration options.") - addLine(builder, "") -} - -func formatCode(s string) []byte { - bytes, err := format.Source([]byte(s)) - if err != nil { - panic(err) - } - return bytes -} - -func writeToFile(name string, bytes []byte) { - // creating the file - codeFile, err := os.Create(name) - if err != nil { - panic(err) - } - defer codeFile.Close() - - // writing to the file - _, err = codeFile.Write(bytes) - if err != nil { - panic(err) - } -} diff --git a/internal/config/generate/main.go b/internal/config/generate/main.go deleted file mode 100644 index ff5346fce..000000000 --- a/internal/config/generate/main.go +++ /dev/null @@ -1,144 +0,0 @@ -// (c) Cartesi and individual authors (see AUTHORS) -// SPDX-License-Identifier: Apache-2.0 (see LICENSE) - -package main - -import ( - "fmt" - "strings" - "unicode" - - "golang.org/x/text/cases" - "golang.org/x/text/language" -) - -// This script will read the Config.toml file and create: -// -// - a formatted get.go file, with get functions for each environment variable; -// -// - a config.md file with documentation for the environment variables. -// -// Each table entry in the toml file translates into an environment variable. -// In Go, this becomes a map[string](map[string]Env), -// with the keys of the outter map being topic names, -// and the keys of the inner map being variable names. -func main() { - data := readTOML("generate/Config.toml") - config := decodeTOML(data) - envs := sortConfig(config) - - var code strings.Builder - var doc strings.Builder - - addCodeHeader(&code) - addDocHeader(&doc) - - addLine(&doc, "") - for _, env := range envs { - env.validate() - addLine(&code, env.toFunction()) - addLine(&doc, env.toDoc()) - } - - writeToFile("get.go", formatCode(code.String())) - writeToFile("../../docs/config.md", []byte(doc.String())) -} - -// ------------------------------------------------------------------------------------------------ -// Env -// ------------------------------------------------------------------------------------------------ - -// An entry in the toml's top level table representing an environment variable. -type Env struct { - // Name of the environment variable. - Name string - // The default value for the variable. - // This field is optional. - Default *string `toml:"default"` - // The Go type for the environment variable. - // This field is required. - GoType string `toml:"go-type"` - // If true, the generated get function will be exported by the config module. - // This field is optional. - // By default, this field is true. - Export *bool `toml:"export"` - // If true, the generated get function will not log into the console. - // This field is optional. - // By default, this field is false. - Redact *bool `toml:"redact"` - // A brief description of the environment variable. - // This field is required. - Description string `toml:"description"` -} - -// Validates whether the fields of the environment variables were initialized correctly -// and sets defaults for optional fields. -func (e *Env) validate() { - if e.GoType == "" { - panic("missing go-type for " + e.Name) - } - if e.Export == nil { - export := true - e.Export = &export - } - if e.Redact == nil { - redact := false - e.Redact = &redact - } - if e.Description == "" { - panic("missing description for " + e.Name) - } -} - -// Generates the get function for the environment variable. -func (e Env) toFunction() string { - name := toFunctionName(e.Name) - typ := e.GoType - get := "get" - vars := "v" - - var defaultValue string - hasDefault := e.Default != nil - if hasDefault { - defaultValue = *e.Default - } - - to_ := []rune(e.GoType) - to_[0] = unicode.ToUpper(to_[0]) - to := "to" + string(to_) - - args := fmt.Sprintf(`"%s", "%s", %t, %t, %s`, e.Name, defaultValue, hasDefault, *e.Redact, to) - - if *e.Export { - name = "Get" + name - } else { - name = "get" + name - typ = fmt.Sprintf("(%s, bool)", typ) - get += "Optional" - vars += ", ok" - } - - body := fmt.Sprintf("%s := %s(%s)\n", vars, get, args) - body += "return " + vars - return fmt.Sprintf("func %s() %s { %s }\n", name, typ, body) -} - -// Generates the documentation entry for the environment variable. -func (e Env) toDoc() string { - s := fmt.Sprintf("## `%s`\n\n%s\n\n", e.Name, e.Description) - s = fmt.Sprintf("%s* **Type:** `%s`\n", s, e.GoType) - if e.Default != nil { - s = fmt.Sprintf("%s* **Default:** `\"%s\"`\n", s, *e.Default) - } - return s -} - -// Splits the string by "_" and joins each substring with the first letter in upper case. -func toFunctionName(env string) string { - caser := cases.Title(language.English) - words := strings.Split(env, "_") - for i, word := range words { - words[i] = caser.String(word) - } - return strings.Join(words, "") -} diff --git a/internal/config/get.go b/internal/config/get.go deleted file mode 100644 index 04be9cbc9..000000000 --- a/internal/config/get.go +++ /dev/null @@ -1,180 +0,0 @@ -// Code generated by internal/config/generate. -// DO NOT EDIT. - -// (c) Cartesi and individual authors (see AUTHORS) -// SPDX-License-Identifier: Apache-2.0 (see LICENSE) - -package config - -import ( - "time" -) - -type ( - Duration = time.Duration -) - -func getCartesiAuthAwsKmsKeyId() (string, bool) { - v, ok := getOptional("CARTESI_AUTH_AWS_KMS_KEY_ID", "", false, true, toString) - return v, ok -} - -func getCartesiAuthAwsKmsRegion() (string, bool) { - v, ok := getOptional("CARTESI_AUTH_AWS_KMS_REGION", "", false, true, toString) - return v, ok -} - -func getCartesiAuthMnemonic() (string, bool) { - v, ok := getOptional("CARTESI_AUTH_MNEMONIC", "", false, true, toString) - return v, ok -} - -func getCartesiAuthMnemonicAccountIndex() (int, bool) { - v, ok := getOptional("CARTESI_AUTH_MNEMONIC_ACCOUNT_INDEX", "0", true, true, toInt) - return v, ok -} - -func getCartesiAuthMnemonicFile() (string, bool) { - v, ok := getOptional("CARTESI_AUTH_MNEMONIC_FILE", "", false, true, toString) - return v, ok -} - -func getCartesiAuthPrivateKey() (string, bool) { - v, ok := getOptional("CARTESI_AUTH_PRIVATE_KEY", "", false, true, toString) - return v, ok -} - -func getCartesiAuthPrivateKeyFile() (string, bool) { - v, ok := getOptional("CARTESI_AUTH_PRIVATE_KEY_FILE", "", false, true, toString) - return v, ok -} - -func GetCartesiBlockchainBlockTimeout() int { - v := get("CARTESI_BLOCKCHAIN_BLOCK_TIMEOUT", "60", true, false, toInt) - return v -} - -func GetCartesiBlockchainFinalityOffset() int { - v := get("CARTESI_BLOCKCHAIN_FINALITY_OFFSET", "10", true, false, toInt) - return v -} - -func GetCartesiBlockchainHttpEndpoint() string { - v := get("CARTESI_BLOCKCHAIN_HTTP_ENDPOINT", "", false, false, toString) - return v -} - -func GetCartesiBlockchainId() uint64 { - v := get("CARTESI_BLOCKCHAIN_ID", "", false, false, toUint64) - return v -} - -func GetCartesiBlockchainIsLegacy() bool { - v := get("CARTESI_BLOCKCHAIN_IS_LEGACY", "false", true, false, toBool) - return v -} - -func GetCartesiBlockchainWsEndpoint() string { - v := get("CARTESI_BLOCKCHAIN_WS_ENDPOINT", "", false, false, toString) - return v -} - -func GetCartesiContractsInputBoxDeploymentBlockNumber() int64 { - v := get("CARTESI_CONTRACTS_INPUT_BOX_DEPLOYMENT_BLOCK_NUMBER", "", false, false, toInt64) - return v -} - -func GetCartesiContractsApplicationAddress() string { - v := get("CARTESI_CONTRACTS_APPLICATION_ADDRESS", "", false, false, toString) - return v -} - -func GetCartesiContractsApplicationDeploymentBlockNumber() string { - v := get("CARTESI_CONTRACTS_APPLICATION_DEPLOYMENT_BLOCK_NUMBER", "", false, false, toString) - return v -} - -func GetCartesiContractsAuthorityAddress() string { - v := get("CARTESI_CONTRACTS_AUTHORITY_ADDRESS", "", false, false, toString) - return v -} - -func GetCartesiContractsHistoryAddress() string { - v := get("CARTESI_CONTRACTS_HISTORY_ADDRESS", "", false, false, toString) - return v -} - -func GetCartesiContractsInputBoxAddress() string { - v := get("CARTESI_CONTRACTS_INPUT_BOX_ADDRESS", "", false, false, toString) - return v -} - -func GetCartesiExperimentalDisableConfigLog() bool { - v := get("CARTESI_EXPERIMENTAL_DISABLE_CONFIG_LOG", "false", true, true, toBool) - return v -} - -func GetCartesiExperimentalServerManagerBypassLog() bool { - v := get("CARTESI_EXPERIMENTAL_SERVER_MANAGER_BYPASS_LOG", "false", true, false, toBool) - return v -} - -func GetCartesiExperimentalSunodoValidatorEnabled() bool { - v := get("CARTESI_EXPERIMENTAL_SUNODO_VALIDATOR_ENABLED", "false", true, false, toBool) - return v -} - -func GetCartesiExperimentalSunodoValidatorRedisEndpoint() string { - v := get("CARTESI_EXPERIMENTAL_SUNODO_VALIDATOR_REDIS_ENDPOINT", "", false, false, toString) - return v -} - -func GetCartesiFeatureDisableClaimer() bool { - v := get("CARTESI_FEATURE_DISABLE_CLAIMER", "false", true, false, toBool) - return v -} - -func GetCartesiFeatureDisableMachineHashCheck() bool { - v := get("CARTESI_FEATURE_DISABLE_MACHINE_HASH_CHECK", "false", true, false, toBool) - return v -} - -func GetCartesiFeatureHostMode() bool { - v := get("CARTESI_FEATURE_HOST_MODE", "false", true, false, toBool) - return v -} - -func GetCartesiHttpAddress() string { - v := get("CARTESI_HTTP_ADDRESS", "127.0.0.1", true, false, toString) - return v -} - -func GetCartesiHttpPort() int { - v := get("CARTESI_HTTP_PORT", "10000", true, false, toInt) - return v -} - -func GetCartesiLogLevel() LogLevel { - v := get("CARTESI_LOG_LEVEL", "info", true, false, toLogLevel) - return v -} - -func GetCartesiLogTimestamp() bool { - v := get("CARTESI_LOG_TIMESTAMP", "false", true, false, toBool) - return v -} - -func GetCartesiPostgresEndpoint() string { - v := get("CARTESI_POSTGRES_ENDPOINT", "", true, true, toString) - return v -} - -func GetCartesiEpochDuration() Duration { - v := get("CARTESI_EPOCH_DURATION", "86400", true, false, toDuration) - return v -} - -func GetCartesiSnapshotDir() string { - v := get("CARTESI_SNAPSHOT_DIR", "", false, false, toString) - return v -} diff --git a/internal/config/log.go b/internal/config/log.go deleted file mode 100644 index 57a3882f1..000000000 --- a/internal/config/log.go +++ /dev/null @@ -1,77 +0,0 @@ -// (c) Cartesi and individual authors (see AUTHORS) -// SPDX-License-Identifier: Apache-2.0 (see LICENSE) - -package config - -import ( - "fmt" - "io" - "log" - "os" -) - -var ( - ErrorLogger *log.Logger - WarningLogger *log.Logger - InfoLogger *log.Logger - DebugLogger *log.Logger -) - -func init() { - logInit() -} - -func logInit() { - var flags int - if GetCartesiLogTimestamp() { - flags = log.LstdFlags - } - - ErrorLogger = log.New(os.Stderr, "ERROR ", flags) - WarningLogger = log.New(os.Stderr, "WARN ", flags) - InfoLogger = log.New(os.Stdout, "INFO ", flags) - DebugLogger = log.New(os.Stdout, "DEBUG ", flags) - - switch GetCartesiLogLevel() { - case LogLevelError: - WarningLogger.SetOutput(io.Discard) - fallthrough - case LogLevelWarning: - InfoLogger.SetOutput(io.Discard) - fallthrough - case LogLevelInfo: - DebugLogger.SetOutput(io.Discard) - case LogLevelDebug: - flags |= log.Llongfile - ErrorLogger.SetFlags(flags) - WarningLogger.SetFlags(flags) - InfoLogger.SetFlags(flags) - DebugLogger.SetFlags(flags) - default: - panic("Invalid log level") - } -} - -type LogLevel uint8 - -const ( - LogLevelDebug LogLevel = iota - LogLevelInfo - LogLevelWarning - LogLevelError -) - -func toLogLevelFromString(s string) (LogLevel, error) { - var m = map[string]LogLevel{ - "debug": LogLevelDebug, - "info": LogLevelInfo, - "warning": LogLevelWarning, - "error": LogLevelError, - } - if v, ok := m[s]; ok { - return v, nil - } else { - var zeroValue LogLevel - return zeroValue, fmt.Errorf(`invalid log level "%s"`, s) - } -} diff --git a/internal/config/to_test.go b/internal/config/to_test.go deleted file mode 100644 index 41b23c5e4..000000000 --- a/internal/config/to_test.go +++ /dev/null @@ -1,101 +0,0 @@ -// (c) Cartesi and individual authors (see AUTHORS) -// SPDX-License-Identifier: Apache-2.0 (see LICENSE) - -package config - -import ( - "testing" - - "github.com/stretchr/testify/require" -) - -func TestToBool(t *testing.T) { - require := require.New(t) - { // true - v, err := toBool("true") - require.Nil(err) - require.True(v) - } - { // false - v, err := toBool("false") - require.Nil(err) - require.False(v) - } - { // fail - _, err := toBool("not bool") - require.NotNil(err) - } -} - -func TestToInt(t *testing.T) { - require := require.New(t) - { // ok - v, err := toInt("10") - require.Nil(err) - require.Equal(int(10), v) - } - { // fail - _, err := toInt("not int") - require.NotNil(err) - } -} - -func TestToInt64(t *testing.T) { - require := require.New(t) - { // ok - v, err := toInt64("10") - require.Nil(err) - require.Equal(int64(10), v) - } - { // fail - _, err := toInt64("not int64") - require.NotNil(err) - } -} - -func TestToString(t *testing.T) { - s := "nugget" - v := parse(s, toString) - require.Equal(t, s, v) -} - -func TestToDuration(t *testing.T) { - require := require.New(t) - { // ok - v, err := toDuration("60") - require.Nil(err) - require.Equal(float64(60), v.Seconds()) - } - { // fail - _, err := toDuration("not duration") - require.NotNil(err) - } -} - -func TestToLogLevel(t *testing.T) { - require := require.New(t) - { // info - v, err := toLogLevel("debug") - require.Nil(err) - require.Equal(LogLevelDebug, v) - } - { // debug - v, err := toLogLevel("info") - require.Nil(err) - require.Equal(LogLevelInfo, v) - } - { // warning - v, err := toLogLevel("warning") - require.Nil(err) - require.Equal(LogLevelWarning, v) - } - { // error (not an error, but LogLevelError) - v, err := toLogLevel("error") - require.Nil(err) - require.Equal(LogLevelError, v) - } - { // fail - _, err := toLogLevel("not log level") - require.NotNil(err) - } -} diff --git a/internal/deps/deps.go b/internal/deps/deps.go index 746286a7c..cc8220332 100644 --- a/internal/deps/deps.go +++ b/internal/deps/deps.go @@ -6,11 +6,12 @@ package deps import ( "context" + "fmt" + "log/slog" "strings" "sync" "time" - "github.com/cartesi/rollups-node/internal/config" "github.com/testcontainers/testcontainers-go" "github.com/testcontainers/testcontainers-go/wait" ) @@ -53,11 +54,11 @@ type DepsContainers struct { waitGroup *sync.WaitGroup } -// A dummy Logging to write all Test Containers logs with DEBUG priority +// debugLogging implements the testcontainers.Logging interface by printing the log to slog.Debug. type debugLogging struct{} -func (debug debugLogging) Printf(format string, v ...interface{}) { - config.DebugLogger.Printf(format, v...) +func (d debugLogging) Printf(format string, v ...interface{}) { + slog.Debug(fmt.Sprintf(format, v...)) } func createHook(containerName string, @@ -79,7 +80,7 @@ func createHook(containerName string, // The returned DepContainers struct can be used to gracefully // terminate the containers using the Terminate method func Run(ctx context.Context, depsConfig DepsConfig) (*DepsContainers, error) { - nolog := debugLogging{} + debugLogger := debugLogging{} var waitGroup sync.WaitGroup // wait strategy copied from testcontainers docs @@ -102,7 +103,7 @@ func Run(ctx context.Context, depsConfig DepsConfig) (*DepsContainers, error) { postgres, err := testcontainers.GenericContainer(ctx, testcontainers.GenericContainerRequest{ ContainerRequest: postgresReq, Started: true, - Logger: nolog, + Logger: debugLogger, }) if err != nil { @@ -124,7 +125,7 @@ func Run(ctx context.Context, depsConfig DepsConfig) (*DepsContainers, error) { devnet, err := testcontainers.GenericContainer(ctx, testcontainers.GenericContainerRequest{ ContainerRequest: devNetReq, Started: true, - Logger: nolog, + Logger: debugLogger, }) if err != nil { return nil, err diff --git a/internal/machine/snapshot.go b/internal/machine/snapshot.go index dfec31928..675743632 100644 --- a/internal/machine/snapshot.go +++ b/internal/machine/snapshot.go @@ -9,22 +9,22 @@ import ( "errors" "fmt" "io/fs" + "log/slog" "os" "os/exec" "strings" - - "github.com/cartesi/rollups-node/internal/config" ) const SNAPSHOT_CONTAINER_PATH = "/usr/share/cartesi/snapshot" func runCommand(name string, args ...string) error { - config.InfoLogger.Printf("%v %v", name, strings.Join(args, " ")) + slog.Debug("Running command", "name", name, "args", strings.Join(args, " ")) cmd := exec.Command(name, args...) output, err := cmd.CombinedOutput() if err != nil { - return fmt.Errorf("'%v %v' failed with %v: %v", - name, strings.Join(args, " "), err, string(output)) + return fmt.Errorf("'%v %v' failed with %w: %v", + name, strings.Join(args, " "), err, string(output), + ) } return nil } @@ -40,7 +40,7 @@ func Save(sourceDockerImage string, destDir string, tempContainerName string) er // Remove previous snapshot dir if fileExists(destDir) { - config.InfoLogger.Println("removing previous snapshot") + slog.Info("Removing previous snapshot") err := os.RemoveAll(destDir) if err != nil { return err @@ -56,7 +56,9 @@ func Save(sourceDockerImage string, destDir string, tempContainerName string) er defer func() { err := runCommand("docker", "rm", tempContainerName) if err != nil { - config.ErrorLogger.Printf("Error trying to delete %v: %v", tempContainerName, err) + slog.Warn("Error trying to delete container", + "container", tempContainerName, + "error", err) } }() @@ -66,8 +68,8 @@ func Save(sourceDockerImage string, destDir string, tempContainerName string) er return err } - config.InfoLogger.Printf("Cartesi machine snapshot from %v saved to %v", - sourceDockerImage, destDir) + slog.Info("Cartesi machine snapshot saved", + "docker-image", sourceDockerImage, + "destination-dir", destDir) return nil - } diff --git a/cmd/cartesi-rollups-node/chainid.go b/internal/node/chainid.go similarity index 75% rename from cmd/cartesi-rollups-node/chainid.go rename to internal/node/chainid.go index f7f7d7507..21115f7e8 100644 --- a/cmd/cartesi-rollups-node/chainid.go +++ b/internal/node/chainid.go @@ -1,14 +1,13 @@ // (c) Cartesi and individual authors (see AUTHORS) // SPDX-License-Identifier: Apache-2.0 (see LICENSE) -package main +package node import ( "context" "fmt" "time" - "github.com/cartesi/rollups-node/internal/config" "github.com/ethereum/go-ethereum/ethclient" ) @@ -19,10 +18,10 @@ const defaultTimeout = 3 * time.Second func validateChainId(ctx context.Context, chainId uint64, ethereumNodeAddr string) error { remoteChainId, err := getChainId(ctx, ethereumNodeAddr) if err != nil { - config.ErrorLogger.Printf("Couldn't validate chainId: %v\n", err) + return err } else if chainId != remoteChainId { return fmt.Errorf( - "chainId mismatch. Expected %v but Ethereum node returned %v", + "chainId mismatch; expected %v but Ethereum node returned %v", chainId, remoteChainId, ) @@ -36,11 +35,11 @@ func getChainId(ctx context.Context, ethereumNodeAddr string) (uint64, error) { client, err := ethclient.Dial(ethereumNodeAddr) if err != nil { - return 0, fmt.Errorf("Failed to create RPC client: %v", err) + return 0, fmt.Errorf("create RPC client: %w", err) } chainId, err := client.ChainID(ctx) if err != nil { - return 0, fmt.Errorf("Failed to get chain id: %v", err) + return 0, fmt.Errorf("get chain id: %w", err) } return chainId.Uint64(), nil } diff --git a/cmd/cartesi-rollups-node/chainid_test.go b/internal/node/chainid_test.go similarity index 98% rename from cmd/cartesi-rollups-node/chainid_test.go rename to internal/node/chainid_test.go index f8e5dede3..9e4fb9d0b 100644 --- a/cmd/cartesi-rollups-node/chainid_test.go +++ b/internal/node/chainid_test.go @@ -1,7 +1,7 @@ // (c) Cartesi and individual authors (see AUTHORS) // SPDX-License-Identifier: Apache-2.0 (see LICENSE) -package main +package node import ( "context" diff --git a/internal/node/config/config.go b/internal/node/config/config.go new file mode 100644 index 000000000..ad48ef99c --- /dev/null +++ b/internal/node/config/config.go @@ -0,0 +1,151 @@ +// (c) Cartesi and individual authors (see AUTHORS) +// SPDX-License-Identifier: Apache-2.0 (see LICENSE) + +// The config package manages the node configuration, which comes from environment variables. +// The sub-package generate specifies these environment variables. +package config + +import ( + "fmt" + "os" +) + +// NodeConfig contains all the Node variables. +// See the corresponding environment variable for the variable documentation. +type NodeConfig struct { + LogLevel LogLevel + LogPretty bool + RollupsEpochDuration Duration + BlockchainID uint64 + BlockchainHttpEndpoint Redacted[string] + BlockchainWsEndpoint Redacted[string] + BlockchainIsLegacy bool + BlockchainFinalityOffset int + BlockchainBlockTimeout int + ContractsApplicationAddress string + ContractsApplicationDeploymentBlockNumber int64 + ContractsHistoryAddress string + ContractsAuthorityAddress string + ContractsInputBoxAddress string + ContractsInputBoxDeploymentBlockNumber int64 + SnapshotDir string + PostgresEndpoint Redacted[string] + HttpAddress string + HttpPort int + FeatureHostMode bool + FeatureDisableClaimer bool + FeatureDisableMachineHashCheck bool + ExperimentalServerManagerBypassLog bool + ExperimentalSunodoValidatorEnabled bool + ExperimentalSunodoValidatorRedisEndpoint string + Auth Auth +} + +// Auth is used to sign transactions. +type Auth any + +// AuthPrivateKey allows signing through private keys. +type AuthPrivateKey struct { + PrivateKey Redacted[string] +} + +// AuthMnemonic allows signing through mnemonics. +type AuthMnemonic struct { + Mnemonic Redacted[string] + AccountIndex Redacted[int] +} + +// AuthAWS allows signing through AWS services. +type AuthAWS struct { + KeyID Redacted[string] + Region Redacted[string] +} + +// Redacted is a wrapper that redacts a given field from the logs. +type Redacted[T any] struct { + Value T +} + +func (r Redacted[T]) String() string { + return "[REDACTED]" +} + +// FromEnv loads the config from environment variables. +func FromEnv() NodeConfig { + var config NodeConfig + config.LogLevel = getLogLevel() + config.LogPretty = getLogPretty() + config.RollupsEpochDuration = getEpochDuration() + config.BlockchainID = getBlockchainId() + config.BlockchainHttpEndpoint = Redacted[string]{getBlockchainHttpEndpoint()} + config.BlockchainWsEndpoint = Redacted[string]{getBlockchainWsEndpoint()} + config.BlockchainIsLegacy = getBlockchainIsLegacy() + config.BlockchainFinalityOffset = getBlockchainFinalityOffset() + config.BlockchainBlockTimeout = getBlockchainBlockTimeout() + config.ContractsApplicationAddress = getContractsApplicationAddress() + config.ContractsApplicationDeploymentBlockNumber = + getContractsApplicationDeploymentBlockNumber() + config.ContractsHistoryAddress = getContractsHistoryAddress() + config.ContractsAuthorityAddress = getContractsAuthorityAddress() + config.ContractsInputBoxAddress = getContractsInputBoxAddress() + config.ContractsInputBoxDeploymentBlockNumber = getContractsInputBoxDeploymentBlockNumber() + if !getFeatureHostMode() { + config.SnapshotDir = getSnapshotDir() + } + config.PostgresEndpoint = Redacted[string]{getPostgresEndpoint()} + config.HttpAddress = getHttpAddress() + config.HttpPort = getHttpPort() + config.FeatureHostMode = getFeatureHostMode() + config.FeatureDisableClaimer = getFeatureDisableClaimer() + config.FeatureDisableMachineHashCheck = getFeatureDisableMachineHashCheck() + config.ExperimentalServerManagerBypassLog = getExperimentalServerManagerBypassLog() + config.ExperimentalSunodoValidatorEnabled = getExperimentalSunodoValidatorEnabled() + if getExperimentalSunodoValidatorEnabled() { + config.ExperimentalSunodoValidatorRedisEndpoint = + getExperimentalSunodoValidatorRedisEndpoint() + } + if !getFeatureDisableClaimer() && !getExperimentalSunodoValidatorEnabled() { + config.Auth = authFromEnv() + } + return config +} + +func authFromEnv() Auth { + switch getAuthKind() { + case AuthKindPrivateKeyVar: + return AuthPrivateKey{ + PrivateKey: Redacted[string]{getAuthPrivateKey()}, + } + case AuthKindPrivateKeyFile: + path := getAuthPrivateKeyFile() + privateKey, err := os.ReadFile(path) + if err != nil { + panic(fmt.Sprintf("failed to read private-key file: %v", err)) + } + return AuthPrivateKey{ + PrivateKey: Redacted[string]{string(privateKey)}, + } + case AuthKindMnemonicVar: + return AuthMnemonic{ + Mnemonic: Redacted[string]{getAuthMnemonic()}, + AccountIndex: Redacted[int]{getAuthMnemonicAccountIndex()}, + } + case AuthKindMnemonicFile: + path := getAuthMnemonicFile() + mnemonic, err := os.ReadFile(path) + if err != nil { + panic(fmt.Sprintf("failed to read mnemonic file: %v", err)) + } + return AuthMnemonic{ + Mnemonic: Redacted[string]{string(mnemonic)}, + AccountIndex: Redacted[int]{getAuthMnemonicAccountIndex()}, + } + case AuthKindAWS: + return AuthAWS{ + KeyID: Redacted[string]{getAuthAwsKmsKeyId()}, + Region: Redacted[string]{getAuthAwsKmsRegion()}, + } + default: + panic("invalid auth kind") + } +} diff --git a/internal/config/generate/Config.toml b/internal/node/config/generate/Config.toml similarity index 85% rename from internal/config/generate/Config.toml rename to internal/node/config/generate/Config.toml index 269041c3e..3e5bdf273 100644 --- a/internal/config/generate/Config.toml +++ b/internal/node/config/generate/Config.toml @@ -9,13 +9,13 @@ default = "info" go-type = "LogLevel" description = """ -One of "debug", "info", "warning", "error".""" +One of "debug", "info", "warn", "error".""" -[logging.CARTESI_LOG_TIMESTAMP] +[logging.CARTESI_LOG_PRETTY] default = "false" go-type = "bool" description = """ -If set to true, the node will print the timestamp when logging.""" +If set to true, the node will add colors to its log output.""" # # Features @@ -104,7 +104,7 @@ description = """ Address of the DApp's contract.""" [contracts.CARTESI_CONTRACTS_APPLICATION_DEPLOYMENT_BLOCK_NUMBER] -go-type = "string" +go-type = "int64" description = """ Block in which the DApp's contract was deployed.""" @@ -142,56 +142,42 @@ Path to the directory with the cartesi-machine snapshot that will be loaded by t # Auth # -[auth.CARTESI_AUTH_MNEMONIC] +[auth.CARTESI_AUTH_KIND] +default = "mnemonic" +go-type = "AuthKind" +description = """ +One of "private_key", "private_key_file", "mnemonic", "mnemonic_file", "aws".""" + +[auth.CARTESI_AUTH_PRIVATE_KEY] +go-type = "string" +description = """ +The node will use this private key to sign transactions.""" + +[auth.CARTESI_AUTH_PRIVATE_KEY_FILE] go-type = "string" -export = false -redact = true description = """ -The node will use the private key generated from this mnemonic to sign transactions. +The node will use the private key contained in this file to sign transactions.""" -Overrides `CARTESI_AUTH_MNEMONIC_FILE` and `CARTESI_AUTH_AWS_KMS_*`.""" +[auth.CARTESI_AUTH_MNEMONIC] +go-type = "string" +description = """ +The node will use the private key generated from this mnemonic to sign transactions.""" [auth.CARTESI_AUTH_MNEMONIC_FILE] go-type = "string" -export = false -redact = true description = """ The node will use the private key generated from the mnemonic contained in this file -to sign transactions. - -Overrides `CARTESI_AUTH_AWS_KMS_*`.""" +to sign transactions.""" [auth.CARTESI_AUTH_MNEMONIC_ACCOUNT_INDEX] default = "0" go-type = "int" -export = false -redact = true description = """ When using mnemonics to sign transactions, the node will use this account index to generate the private key.""" -[auth.CARTESI_AUTH_PRIVATE_KEY] -go-type = "string" -export = false -redact = true -description = """ -The node will use this private key to sign transactions. - -Overrides `CARTESI_AUTH_PRIVATE_KEY_FILE`, `CARTESI_AUTH_MNEMONIC`, `CARTESI_AUTH_MNEMONIC_FILE` and `CARTESI_AUTH_AWS_KMS_*`.""" - -[auth.CARTESI_AUTH_PRIVATE_KEY_FILE] -go-type = "string" -export = false -redact = true -description = """ -The node will use the private key contained in this file to sign transactions. - -Overrides `CARTESI_AUTH_MNEMONIC`, `CARTESI_AUTH_MNEMONIC_FILE` and `CARTESI_AUTH_AWS_KMS_*`.""" - [auth.CARTESI_AUTH_AWS_KMS_KEY_ID] go-type = "string" -export = false -redact = true description = """ If set, the node will use the AWS KMS service with this key ID to sign transactions. @@ -199,8 +185,6 @@ Must be set alongside `CARTESI_AUTH_AWS_KMS_REGION`.""" [auth.CARTESI_AUTH_AWS_KMS_REGION] go-type = "string" -export = false -redact = true description = """ An AWS KMS Region. @@ -213,7 +197,6 @@ Must be set alongside `CARTESI_AUTH_AWS_KMS_KEY_ID`.""" [postgres.CARTESI_POSTGRES_ENDPOINT] default = "" go-type = "string" -redact = true description = """ Postgres endpoint in the 'postgres://user:password@hostname:port/database' format. @@ -262,10 +245,3 @@ go-type = "bool" description = """ When enabled, prints server-manager output to stdout and stderr directly. All other log configurations are ignored.""" - -[experimental.CARTESI_EXPERIMENTAL_DISABLE_CONFIG_LOG] -default = "false" -go-type = "bool" -redact = true -description = """ -Disables all log entries related to the node's configuration""" \ No newline at end of file diff --git a/internal/node/config/generate/code.go b/internal/node/config/generate/code.go new file mode 100644 index 000000000..841d7c6e5 --- /dev/null +++ b/internal/node/config/generate/code.go @@ -0,0 +1,176 @@ +// (c) Cartesi and individual authors (see AUTHORS) +// SPDX-License-Identifier: Apache-2.0 (see LICENSE) + +package main + +import ( + "bytes" + "os" + "strings" + "text/template" + + "go/format" + + "golang.org/x/text/cases" + "golang.org/x/text/language" +) + +// generateCodeFile generates a Go file with the getters for the config variables. +func generateCodeFile(path string, env []Env) { + // Load template + funcMap := template.FuncMap{ + "toFunctionName": func(env string) string { + caser := cases.Title(language.English) + words := strings.Split(env, "_") + for i, word := range words { + words[i] = caser.String(word) + } + return strings.Join(words[1:], "") + }, + "toGoFunc": func(goType string) string { + return "to" + strings.ToUpper(goType[:1]) + goType[1:] + }, + } + tmpl := template.Must(template.New("code").Funcs(funcMap).Parse(codeTemplate)) + + // Execute template + var buff bytes.Buffer + err := tmpl.Execute(&buff, env) + if err != nil { + panic(err) + } + + // Format code + code, err := format.Source(buff.Bytes()) + if err != nil { + panic(err) + } + + // Write file + var perm os.FileMode = 0644 + err = os.WriteFile(path, code, perm) + if err != nil { + panic(err) + } +} + +const codeTemplate string = `// Code generated by internal/node/config/generate. +// DO NOT EDIT. + +// (c) Cartesi and individual authors (see AUTHORS) +// SPDX-License-Identifier: Apache-2.0 (see LICENSE) + +package config + +import ( + "fmt" + "log/slog" + "os" + "strconv" + "time" +) + +type ( + Duration = time.Duration + LogLevel = slog.Level +) + +// ------------------------------------------------------------------------------------------------ +// Auth Kind +// ------------------------------------------------------------------------------------------------ + +type AuthKind uint8 + +const ( + AuthKindPrivateKeyVar AuthKind = iota + AuthKindPrivateKeyFile + AuthKindMnemonicVar + AuthKindMnemonicFile + AuthKindAWS +) + +// ------------------------------------------------------------------------------------------------ +// Parsing functions +// ------------------------------------------------------------------------------------------------ + +func toInt64FromString(s string) (int64, error) { + return strconv.ParseInt(s, 10, 64) +} + +func toUint64FromString(s string) (uint64, error) { + value, err := strconv.ParseUint(s, 10, 64) + return value, err +} + +func toStringFromString(s string) (string, error) { + return s, nil +} + +func toDurationFromSeconds(s string) (time.Duration, error) { + return time.ParseDuration(s + "s") +} + +func toLogLevelFromString(s string) (LogLevel, error) { + var m = map[string]LogLevel{ + "debug": slog.LevelDebug, + "info": slog.LevelInfo, + "warn": slog.LevelWarn, + "error": slog.LevelError, + } + if v, ok := m[s]; ok { + return v, nil + } else { + var zeroValue LogLevel + return zeroValue, fmt.Errorf("invalid log level '%s'", s) + } +} + +func toAuthKindFromString(s string) (AuthKind, error) { + var m = map[string]AuthKind{ + "private_key": AuthKindPrivateKeyVar, + "private_key_file": AuthKindPrivateKeyFile, + "mnemonic": AuthKindMnemonicVar, + "mnemonic_file": AuthKindMnemonicFile, + "aws": AuthKindAWS, + } + if v, ok := m[s]; ok { + return v, nil + } else { + var zeroValue AuthKind + return zeroValue, fmt.Errorf("invalid auth kind '%s'", s) + } +} + +// Aliases to be used by the generated functions. +var ( + toBool = strconv.ParseBool + toInt = strconv.Atoi + toInt64 = toInt64FromString + toUint64 = toUint64FromString + toString = toStringFromString + toDuration = toDurationFromSeconds + toLogLevel = toLogLevelFromString + toAuthKind = toAuthKindFromString +) + +// ------------------------------------------------------------------------------------------------ +// Getters +// ------------------------------------------------------------------------------------------------ + +{{range .}} +func get{{toFunctionName .Name}}() {{.GoType}} { + s, ok := os.LookupEnv("{{.Name}}") + if !ok { + {{- if .Default}} + s = "{{.Default}}" + {{- else}} + panic("missing env var {{.Name}}") + {{- end}} + } + val, err := {{toGoFunc .GoType}}(s) + if err != nil { + panic(fmt.Sprintf("failed to parse {{.Name}}: %v", err)) + } + return val +} +{{end}}` diff --git a/internal/node/config/generate/docs.go b/internal/node/config/generate/docs.go new file mode 100644 index 000000000..be2af96cd --- /dev/null +++ b/internal/node/config/generate/docs.go @@ -0,0 +1,63 @@ +// (c) Cartesi and individual authors (see AUTHORS) +// SPDX-License-Identifier: Apache-2.0 (see LICENSE) + +package main + +import ( + "os" + "text/template" +) + +// generateDocsFile generates a Markdown file with the documentation of the config variables. +func generateDocsFile(path string, env []Env) { + // Open output file + file, err := os.Create(path) + if err != nil { + panic(err) + } + defer file.Close() + + // Load template + funcMap := template.FuncMap{ + "backtick": func(s string) string { + return "`" + s + "`" + }, + "quote": func(s string) string { + return `"` + s + `"` + }, + } + tmpl := template.Must(template.New("docs").Funcs(funcMap).Parse(docsTemplate)) + + // Execute template + err = tmpl.Execute(file, env) + if err != nil { + panic(err) + } +} + +const docsTemplate string = ` + + +# Node Configuration + +The node is configurable through environment variables. +(There is no other way to configure it.) + +This file documents the configuration options. + + +{{- range .}} + +## {{backtick .Name}} + +{{.Description}} + +* **Type:** {{backtick .GoType}} +{{- if .Default}} +* **Default:** {{.Default | quote | backtick}} +{{- end}} +{{- end}} +` diff --git a/internal/node/config/generate/env.go b/internal/node/config/generate/env.go new file mode 100644 index 000000000..91590b1a6 --- /dev/null +++ b/internal/node/config/generate/env.go @@ -0,0 +1,33 @@ +// (c) Cartesi and individual authors (see AUTHORS) +// SPDX-License-Identifier: Apache-2.0 (see LICENSE) + +package main + +// An entry in the toml's top level table representing an environment variable. +type Env struct { + // Name of the environment variable. + Name string + + // The default value for the variable. + // This field is optional. + Default *string `toml:"default"` + + // The Go type for the environment variable. + // This field is required. + GoType string `toml:"go-type"` + + // A brief description of the environment variable. + // This field is required. + Description string `toml:"description"` +} + +// Validates whether the fields of the environment variables were initialized correctly +// and sets defaults for optional fields. +func (e *Env) validate() { + if e.GoType == "" { + panic("missing go-type for " + e.Name) + } + if e.Description == "" { + panic("missing description for " + e.Name) + } +} diff --git a/internal/node/config/generate/helpers.go b/internal/node/config/generate/helpers.go new file mode 100644 index 000000000..fa017ca3b --- /dev/null +++ b/internal/node/config/generate/helpers.go @@ -0,0 +1,59 @@ +// (c) Cartesi and individual authors (see AUTHORS) +// SPDX-License-Identifier: Apache-2.0 (see LICENSE) + +package main + +import ( + "os" + "sort" + + "github.com/BurntSushi/toml" +) + +func readTOML(name string) string { + bytes, err := os.ReadFile(name) + if err != nil { + panic(err) + } + return string(bytes) +} + +type configTOML = map[string](map[string]*Env) + +func decodeTOML(data string) configTOML { + var config configTOML + _, err := toml.Decode(data, &config) + if err != nil { + panic(err) + } + return config +} + +// Creates sorted lists of environment variables from the config +// to make the generated files deterministic. +func sortConfig(config configTOML) []Env { + var topics []string + mapping := make(map[string]([]string)) // topic names to env names + + for name, topic := range config { + var envs []string + for name, env := range topic { + env.Name = name // initializes the environment variable's name + envs = append(envs, name) + } + sort.Strings(envs) + + topics = append(topics, name) + mapping[name] = envs + } + sort.Strings(topics) + + var envs []Env + for _, topic := range topics { + for _, name := range mapping[topic] { + envs = append(envs, *config[topic][name]) + } + } + + return envs +} diff --git a/internal/node/config/generate/main.go b/internal/node/config/generate/main.go new file mode 100644 index 000000000..c90fca4c9 --- /dev/null +++ b/internal/node/config/generate/main.go @@ -0,0 +1,24 @@ +// (c) Cartesi and individual authors (see AUTHORS) +// SPDX-License-Identifier: Apache-2.0 (see LICENSE) + +//go:generate go run . + +// This script will read the Config.toml file and create: +// - a formatted get.go file, with get functions for each environment variable; +// - a config.md file with documentation for the environment variables. +// +// Each table entry in the toml file translates into an environment variable. +// In Go, this becomes a map[string](map[string]Env), with the keys of the outter map being topic +// names, and the keys of the inner map being variable names. +package main + +func main() { + data := readTOML("Config.toml") + config := decodeTOML(data) + envs := sortConfig(config) + for _, env := range envs { + env.validate() + } + generateDocsFile("../../../../docs/config.md", envs) + generateCodeFile("../generated.go", envs) +} diff --git a/internal/node/config/generated.go b/internal/node/config/generated.go new file mode 100644 index 000000000..af23edbb6 --- /dev/null +++ b/internal/node/config/generated.go @@ -0,0 +1,498 @@ +// Code generated by internal/node/config/generate. +// DO NOT EDIT. + +// (c) Cartesi and individual authors (see AUTHORS) +// SPDX-License-Identifier: Apache-2.0 (see LICENSE) + +package config + +import ( + "fmt" + "log/slog" + "os" + "strconv" + "time" +) + +type ( + Duration = time.Duration + LogLevel = slog.Level +) + +// ------------------------------------------------------------------------------------------------ +// Auth Kind +// ------------------------------------------------------------------------------------------------ + +type AuthKind uint8 + +const ( + AuthKindPrivateKeyVar AuthKind = iota + AuthKindPrivateKeyFile + AuthKindMnemonicVar + AuthKindMnemonicFile + AuthKindAWS +) + +// ------------------------------------------------------------------------------------------------ +// Parsing functions +// ------------------------------------------------------------------------------------------------ + +func toInt64FromString(s string) (int64, error) { + return strconv.ParseInt(s, 10, 64) +} + +func toUint64FromString(s string) (uint64, error) { + value, err := strconv.ParseUint(s, 10, 64) + return value, err +} + +func toStringFromString(s string) (string, error) { + return s, nil +} + +func toDurationFromSeconds(s string) (time.Duration, error) { + return time.ParseDuration(s + "s") +} + +func toLogLevelFromString(s string) (LogLevel, error) { + var m = map[string]LogLevel{ + "debug": slog.LevelDebug, + "info": slog.LevelInfo, + "warn": slog.LevelWarn, + "error": slog.LevelError, + } + if v, ok := m[s]; ok { + return v, nil + } else { + var zeroValue LogLevel + return zeroValue, fmt.Errorf("invalid log level '%s'", s) + } +} + +func toAuthKindFromString(s string) (AuthKind, error) { + var m = map[string]AuthKind{ + "private_key": AuthKindPrivateKeyVar, + "private_key_file": AuthKindPrivateKeyFile, + "mnemonic": AuthKindMnemonicVar, + "mnemonic_file": AuthKindMnemonicFile, + "aws": AuthKindAWS, + } + if v, ok := m[s]; ok { + return v, nil + } else { + var zeroValue AuthKind + return zeroValue, fmt.Errorf("invalid auth kind '%s'", s) + } +} + +// Aliases to be used by the generated functions. +var ( + toBool = strconv.ParseBool + toInt = strconv.Atoi + toInt64 = toInt64FromString + toUint64 = toUint64FromString + toString = toStringFromString + toDuration = toDurationFromSeconds + toLogLevel = toLogLevelFromString + toAuthKind = toAuthKindFromString +) + +// ------------------------------------------------------------------------------------------------ +// Getters +// ------------------------------------------------------------------------------------------------ + +func getAuthAwsKmsKeyId() string { + s, ok := os.LookupEnv("CARTESI_AUTH_AWS_KMS_KEY_ID") + if !ok { + panic("missing env var CARTESI_AUTH_AWS_KMS_KEY_ID") + } + val, err := toString(s) + if err != nil { + panic(fmt.Sprintf("failed to parse CARTESI_AUTH_AWS_KMS_KEY_ID: %v", err)) + } + return val +} + +func getAuthAwsKmsRegion() string { + s, ok := os.LookupEnv("CARTESI_AUTH_AWS_KMS_REGION") + if !ok { + panic("missing env var CARTESI_AUTH_AWS_KMS_REGION") + } + val, err := toString(s) + if err != nil { + panic(fmt.Sprintf("failed to parse CARTESI_AUTH_AWS_KMS_REGION: %v", err)) + } + return val +} + +func getAuthKind() AuthKind { + s, ok := os.LookupEnv("CARTESI_AUTH_KIND") + if !ok { + s = "mnemonic" + } + val, err := toAuthKind(s) + if err != nil { + panic(fmt.Sprintf("failed to parse CARTESI_AUTH_KIND: %v", err)) + } + return val +} + +func getAuthMnemonic() string { + s, ok := os.LookupEnv("CARTESI_AUTH_MNEMONIC") + if !ok { + panic("missing env var CARTESI_AUTH_MNEMONIC") + } + val, err := toString(s) + if err != nil { + panic(fmt.Sprintf("failed to parse CARTESI_AUTH_MNEMONIC: %v", err)) + } + return val +} + +func getAuthMnemonicAccountIndex() int { + s, ok := os.LookupEnv("CARTESI_AUTH_MNEMONIC_ACCOUNT_INDEX") + if !ok { + s = "0" + } + val, err := toInt(s) + if err != nil { + panic(fmt.Sprintf("failed to parse CARTESI_AUTH_MNEMONIC_ACCOUNT_INDEX: %v", err)) + } + return val +} + +func getAuthMnemonicFile() string { + s, ok := os.LookupEnv("CARTESI_AUTH_MNEMONIC_FILE") + if !ok { + panic("missing env var CARTESI_AUTH_MNEMONIC_FILE") + } + val, err := toString(s) + if err != nil { + panic(fmt.Sprintf("failed to parse CARTESI_AUTH_MNEMONIC_FILE: %v", err)) + } + return val +} + +func getAuthPrivateKey() string { + s, ok := os.LookupEnv("CARTESI_AUTH_PRIVATE_KEY") + if !ok { + panic("missing env var CARTESI_AUTH_PRIVATE_KEY") + } + val, err := toString(s) + if err != nil { + panic(fmt.Sprintf("failed to parse CARTESI_AUTH_PRIVATE_KEY: %v", err)) + } + return val +} + +func getAuthPrivateKeyFile() string { + s, ok := os.LookupEnv("CARTESI_AUTH_PRIVATE_KEY_FILE") + if !ok { + panic("missing env var CARTESI_AUTH_PRIVATE_KEY_FILE") + } + val, err := toString(s) + if err != nil { + panic(fmt.Sprintf("failed to parse CARTESI_AUTH_PRIVATE_KEY_FILE: %v", err)) + } + return val +} + +func getBlockchainBlockTimeout() int { + s, ok := os.LookupEnv("CARTESI_BLOCKCHAIN_BLOCK_TIMEOUT") + if !ok { + s = "60" + } + val, err := toInt(s) + if err != nil { + panic(fmt.Sprintf("failed to parse CARTESI_BLOCKCHAIN_BLOCK_TIMEOUT: %v", err)) + } + return val +} + +func getBlockchainFinalityOffset() int { + s, ok := os.LookupEnv("CARTESI_BLOCKCHAIN_FINALITY_OFFSET") + if !ok { + s = "10" + } + val, err := toInt(s) + if err != nil { + panic(fmt.Sprintf("failed to parse CARTESI_BLOCKCHAIN_FINALITY_OFFSET: %v", err)) + } + return val +} + +func getBlockchainHttpEndpoint() string { + s, ok := os.LookupEnv("CARTESI_BLOCKCHAIN_HTTP_ENDPOINT") + if !ok { + panic("missing env var CARTESI_BLOCKCHAIN_HTTP_ENDPOINT") + } + val, err := toString(s) + if err != nil { + panic(fmt.Sprintf("failed to parse CARTESI_BLOCKCHAIN_HTTP_ENDPOINT: %v", err)) + } + return val +} + +func getBlockchainId() uint64 { + s, ok := os.LookupEnv("CARTESI_BLOCKCHAIN_ID") + if !ok { + panic("missing env var CARTESI_BLOCKCHAIN_ID") + } + val, err := toUint64(s) + if err != nil { + panic(fmt.Sprintf("failed to parse CARTESI_BLOCKCHAIN_ID: %v", err)) + } + return val +} + +func getBlockchainIsLegacy() bool { + s, ok := os.LookupEnv("CARTESI_BLOCKCHAIN_IS_LEGACY") + if !ok { + s = "false" + } + val, err := toBool(s) + if err != nil { + panic(fmt.Sprintf("failed to parse CARTESI_BLOCKCHAIN_IS_LEGACY: %v", err)) + } + return val +} + +func getBlockchainWsEndpoint() string { + s, ok := os.LookupEnv("CARTESI_BLOCKCHAIN_WS_ENDPOINT") + if !ok { + panic("missing env var CARTESI_BLOCKCHAIN_WS_ENDPOINT") + } + val, err := toString(s) + if err != nil { + panic(fmt.Sprintf("failed to parse CARTESI_BLOCKCHAIN_WS_ENDPOINT: %v", err)) + } + return val +} + +func getContractsInputBoxDeploymentBlockNumber() int64 { + s, ok := os.LookupEnv("CARTESI_CONTRACTS_INPUT_BOX_DEPLOYMENT_BLOCK_NUMBER") + if !ok { + panic("missing env var CARTESI_CONTRACTS_INPUT_BOX_DEPLOYMENT_BLOCK_NUMBER") + } + val, err := toInt64(s) + if err != nil { + panic(fmt.Sprintf("failed to parse CARTESI_CONTRACTS_INPUT_BOX_DEPLOYMENT_BLOCK_NUMBER: %v", err)) + } + return val +} + +func getContractsApplicationAddress() string { + s, ok := os.LookupEnv("CARTESI_CONTRACTS_APPLICATION_ADDRESS") + if !ok { + panic("missing env var CARTESI_CONTRACTS_APPLICATION_ADDRESS") + } + val, err := toString(s) + if err != nil { + panic(fmt.Sprintf("failed to parse CARTESI_CONTRACTS_APPLICATION_ADDRESS: %v", err)) + } + return val +} + +func getContractsApplicationDeploymentBlockNumber() int64 { + s, ok := os.LookupEnv("CARTESI_CONTRACTS_APPLICATION_DEPLOYMENT_BLOCK_NUMBER") + if !ok { + panic("missing env var CARTESI_CONTRACTS_APPLICATION_DEPLOYMENT_BLOCK_NUMBER") + } + val, err := toInt64(s) + if err != nil { + panic(fmt.Sprintf("failed to parse CARTESI_CONTRACTS_APPLICATION_DEPLOYMENT_BLOCK_NUMBER: %v", err)) + } + return val +} + +func getContractsAuthorityAddress() string { + s, ok := os.LookupEnv("CARTESI_CONTRACTS_AUTHORITY_ADDRESS") + if !ok { + panic("missing env var CARTESI_CONTRACTS_AUTHORITY_ADDRESS") + } + val, err := toString(s) + if err != nil { + panic(fmt.Sprintf("failed to parse CARTESI_CONTRACTS_AUTHORITY_ADDRESS: %v", err)) + } + return val +} + +func getContractsHistoryAddress() string { + s, ok := os.LookupEnv("CARTESI_CONTRACTS_HISTORY_ADDRESS") + if !ok { + panic("missing env var CARTESI_CONTRACTS_HISTORY_ADDRESS") + } + val, err := toString(s) + if err != nil { + panic(fmt.Sprintf("failed to parse CARTESI_CONTRACTS_HISTORY_ADDRESS: %v", err)) + } + return val +} + +func getContractsInputBoxAddress() string { + s, ok := os.LookupEnv("CARTESI_CONTRACTS_INPUT_BOX_ADDRESS") + if !ok { + panic("missing env var CARTESI_CONTRACTS_INPUT_BOX_ADDRESS") + } + val, err := toString(s) + if err != nil { + panic(fmt.Sprintf("failed to parse CARTESI_CONTRACTS_INPUT_BOX_ADDRESS: %v", err)) + } + return val +} + +func getExperimentalServerManagerBypassLog() bool { + s, ok := os.LookupEnv("CARTESI_EXPERIMENTAL_SERVER_MANAGER_BYPASS_LOG") + if !ok { + s = "false" + } + val, err := toBool(s) + if err != nil { + panic(fmt.Sprintf("failed to parse CARTESI_EXPERIMENTAL_SERVER_MANAGER_BYPASS_LOG: %v", err)) + } + return val +} + +func getExperimentalSunodoValidatorEnabled() bool { + s, ok := os.LookupEnv("CARTESI_EXPERIMENTAL_SUNODO_VALIDATOR_ENABLED") + if !ok { + s = "false" + } + val, err := toBool(s) + if err != nil { + panic(fmt.Sprintf("failed to parse CARTESI_EXPERIMENTAL_SUNODO_VALIDATOR_ENABLED: %v", err)) + } + return val +} + +func getExperimentalSunodoValidatorRedisEndpoint() string { + s, ok := os.LookupEnv("CARTESI_EXPERIMENTAL_SUNODO_VALIDATOR_REDIS_ENDPOINT") + if !ok { + panic("missing env var CARTESI_EXPERIMENTAL_SUNODO_VALIDATOR_REDIS_ENDPOINT") + } + val, err := toString(s) + if err != nil { + panic(fmt.Sprintf("failed to parse CARTESI_EXPERIMENTAL_SUNODO_VALIDATOR_REDIS_ENDPOINT: %v", err)) + } + return val +} + +func getFeatureDisableClaimer() bool { + s, ok := os.LookupEnv("CARTESI_FEATURE_DISABLE_CLAIMER") + if !ok { + s = "false" + } + val, err := toBool(s) + if err != nil { + panic(fmt.Sprintf("failed to parse CARTESI_FEATURE_DISABLE_CLAIMER: %v", err)) + } + return val +} + +func getFeatureDisableMachineHashCheck() bool { + s, ok := os.LookupEnv("CARTESI_FEATURE_DISABLE_MACHINE_HASH_CHECK") + if !ok { + s = "false" + } + val, err := toBool(s) + if err != nil { + panic(fmt.Sprintf("failed to parse CARTESI_FEATURE_DISABLE_MACHINE_HASH_CHECK: %v", err)) + } + return val +} + +func getFeatureHostMode() bool { + s, ok := os.LookupEnv("CARTESI_FEATURE_HOST_MODE") + if !ok { + s = "false" + } + val, err := toBool(s) + if err != nil { + panic(fmt.Sprintf("failed to parse CARTESI_FEATURE_HOST_MODE: %v", err)) + } + return val +} + +func getHttpAddress() string { + s, ok := os.LookupEnv("CARTESI_HTTP_ADDRESS") + if !ok { + s = "127.0.0.1" + } + val, err := toString(s) + if err != nil { + panic(fmt.Sprintf("failed to parse CARTESI_HTTP_ADDRESS: %v", err)) + } + return val +} + +func getHttpPort() int { + s, ok := os.LookupEnv("CARTESI_HTTP_PORT") + if !ok { + s = "10000" + } + val, err := toInt(s) + if err != nil { + panic(fmt.Sprintf("failed to parse CARTESI_HTTP_PORT: %v", err)) + } + return val +} + +func getLogLevel() LogLevel { + s, ok := os.LookupEnv("CARTESI_LOG_LEVEL") + if !ok { + s = "info" + } + val, err := toLogLevel(s) + if err != nil { + panic(fmt.Sprintf("failed to parse CARTESI_LOG_LEVEL: %v", err)) + } + return val +} + +func getLogPretty() bool { + s, ok := os.LookupEnv("CARTESI_LOG_PRETTY") + if !ok { + s = "false" + } + val, err := toBool(s) + if err != nil { + panic(fmt.Sprintf("failed to parse CARTESI_LOG_PRETTY: %v", err)) + } + return val +} + +func getPostgresEndpoint() string { + s, ok := os.LookupEnv("CARTESI_POSTGRES_ENDPOINT") + if !ok { + s = "" + } + val, err := toString(s) + if err != nil { + panic(fmt.Sprintf("failed to parse CARTESI_POSTGRES_ENDPOINT: %v", err)) + } + return val +} + +func getEpochDuration() Duration { + s, ok := os.LookupEnv("CARTESI_EPOCH_DURATION") + if !ok { + s = "86400" + } + val, err := toDuration(s) + if err != nil { + panic(fmt.Sprintf("failed to parse CARTESI_EPOCH_DURATION: %v", err)) + } + return val +} + +func getSnapshotDir() string { + s, ok := os.LookupEnv("CARTESI_SNAPSHOT_DIR") + if !ok { + panic("missing env var CARTESI_SNAPSHOT_DIR") + } + val, err := toString(s) + if err != nil { + panic(fmt.Sprintf("failed to parse CARTESI_SNAPSHOT_DIR: %v", err)) + } + return val +} diff --git a/internal/node/handlers.go b/internal/node/handlers.go new file mode 100644 index 000000000..250344194 --- /dev/null +++ b/internal/node/handlers.go @@ -0,0 +1,50 @@ +// (c) Cartesi and individual authors (see AUTHORS) +// SPDX-License-Identifier: Apache-2.0 (see LICENSE) + +package node + +import ( + "fmt" + "log/slog" + "net/http" + "net/http/httputil" + "net/url" + + "github.com/cartesi/rollups-node/internal/node/config" +) + +func newHttpServiceHandler(c config.NodeConfig) http.Handler { + handler := http.NewServeMux() + handler.Handle("/healthz", http.HandlerFunc(healthcheckHandler)) + + graphqlProxy := newReverseProxy(c.HttpAddress, getPort(c, portOffsetGraphQLServer)) + handler.Handle("/graphql", graphqlProxy) + + dispatcherProxy := newReverseProxy(c.HttpAddress, getPort(c, portOffsetDispatcher)) + handler.Handle("/metrics", dispatcherProxy) + + inspectProxy := newReverseProxy(c.HttpAddress, getPort(c, portOffsetInspectServer)) + handler.Handle("/inspect", inspectProxy) + handler.Handle("/inspect/", inspectProxy) + + if c.FeatureHostMode { + hostProxy := newReverseProxy(c.HttpAddress, getPort(c, portOffsetHostRunnerRollups)) + handler.Handle("/rollup/", http.StripPrefix("/rollup", hostProxy)) + } + return handler +} + +func healthcheckHandler(w http.ResponseWriter, r *http.Request) { + slog.Debug("Node received a healthcheck request") + w.WriteHeader(http.StatusOK) +} + +func newReverseProxy(address string, port int) *httputil.ReverseProxy { + urlStr := fmt.Sprintf("http://%v:%v/", address, port) + url, err := url.Parse(urlStr) + if err != nil { + panic(fmt.Sprintf("failed to parse url: %v", err)) + } + proxy := httputil.NewSingleHostReverseProxy(url) + return proxy +} diff --git a/internal/node/node.go b/internal/node/node.go new file mode 100644 index 000000000..86eeb80a9 --- /dev/null +++ b/internal/node/node.go @@ -0,0 +1,23 @@ +// (c) Cartesi and individual authors (see AUTHORS) +// SPDX-License-Identifier: Apache-2.0 (see LICENSE) + +package node + +import ( + "context" + + "github.com/cartesi/rollups-node/internal/node/config" + "github.com/cartesi/rollups-node/internal/services" +) + +// Setup creates the Node top-level supervisor. +func Setup(ctx context.Context, c config.NodeConfig) (services.Service, error) { + // checks + err := validateChainId(ctx, c.BlockchainID, c.BlockchainHttpEndpoint.Value) + if err != nil { + return nil, err + } + + // create service + return newSupervisorService(c), nil +} diff --git a/internal/node/services.go b/internal/node/services.go new file mode 100644 index 000000000..44d2a990d --- /dev/null +++ b/internal/node/services.go @@ -0,0 +1,345 @@ +// (c) Cartesi and individual authors (see AUTHORS) +// SPDX-License-Identifier: Apache-2.0 (see LICENSE) + +package node + +import ( + "fmt" + "log/slog" + "os" + + "github.com/cartesi/rollups-node/internal/node/config" + "github.com/cartesi/rollups-node/internal/services" +) + +// We use an enum to define the ports of each service and avoid conflicts. +type portOffset = int + +const ( + portOffsetProxy = iota + portOffsetAdvanceRunner + portOffsetAuthorityClaimer + portOffsetDispatcher + portOffsetGraphQLServer + portOffsetGraphQLHealthcheck + portOffsetHostRunnerHealthcheck + portOffsetHostRunnerRollups + portOffsetIndexer + portOffsetInspectServer + portOffsetInspectHealthcheck + portOffsetRedis + portOffsetServerManager + portOffsetStateServer +) + +const ( + localhost = "127.0.0.1" + serverManagerSessionId = "default_session_id" +) + +// Get the port of the given service. +func getPort(c config.NodeConfig, offset portOffset) int { + return c.HttpPort + int(offset) +} + +// Get the redis endpoint based on whether the experimental sunodo validator mode is enabled. +func getRedisEndpoint(c config.NodeConfig) string { + if c.ExperimentalSunodoValidatorEnabled { + return c.ExperimentalSunodoValidatorRedisEndpoint + } else { + return fmt.Sprintf("redis://%v:%v", localhost, getPort(c, portOffsetRedis)) + } +} + +// Create the RUST_LOG variable using the config log level. +// If the log level is set to debug, set tracing log for the given rust module. +func getRustLog(c config.NodeConfig, rustModule string) string { + switch c.LogLevel { + case slog.LevelDebug: + return fmt.Sprintf("RUST_LOG=info,%v=trace", rustModule) + case slog.LevelInfo: + return "RUST_LOG=info" + case slog.LevelWarn: + return "RUST_LOG=warn" + case slog.LevelError: + return "RUST_LOG=error" + default: + panic("impossible") + } +} + +func newAdvanceRunner(c config.NodeConfig) services.CommandService { + var s services.CommandService + s.Name = "advance-runner" + s.HealthcheckPort = getPort(c, portOffsetAdvanceRunner) + s.Path = "cartesi-rollups-advance-runner" + s.Env = append(s.Env, "LOG_ENABLE_TIMESTAMP=false") + s.Env = append(s.Env, "LOG_ENABLE_COLOR=false") + s.Env = append(s.Env, getRustLog(c, "advance_runner")) + s.Env = append(s.Env, fmt.Sprintf("SERVER_MANAGER_ENDPOINT=http://%v:%v", + localhost, getPort(c, portOffsetServerManager))) + s.Env = append(s.Env, fmt.Sprintf("SESSION_ID=%v", serverManagerSessionId)) + s.Env = append(s.Env, fmt.Sprintf("REDIS_ENDPOINT=%v", getRedisEndpoint(c))) + s.Env = append(s.Env, fmt.Sprintf("CHAIN_ID=%v", c.BlockchainID)) + s.Env = append(s.Env, fmt.Sprintf("DAPP_CONTRACT_ADDRESS=%v", + c.ContractsApplicationAddress)) + s.Env = append(s.Env, fmt.Sprintf("PROVIDER_HTTP_ENDPOINT=%v", + c.BlockchainHttpEndpoint.Value)) + s.Env = append(s.Env, fmt.Sprintf("ADVANCE_RUNNER_HEALTHCHECK_PORT=%v", + getPort(c, portOffsetAdvanceRunner))) + s.Env = append(s.Env, fmt.Sprintf("READER_MODE=%v", c.FeatureDisableClaimer)) + if c.FeatureHostMode || c.FeatureDisableMachineHashCheck { + s.Env = append(s.Env, "SNAPSHOT_VALIDATION_ENABLED=false") + } + if !c.FeatureHostMode { + s.Env = append(s.Env, fmt.Sprintf("MACHINE_SNAPSHOT_PATH=%v", c.SnapshotDir)) + } + s.Env = append(s.Env, os.Environ()...) + return s +} + +func newAuthorityClaimer(c config.NodeConfig) services.CommandService { + var s services.CommandService + s.Name = "authority-claimer" + s.HealthcheckPort = getPort(c, portOffsetAuthorityClaimer) + s.Path = "cartesi-rollups-authority-claimer" + s.Env = append(s.Env, "LOG_ENABLE_TIMESTAMP=false") + s.Env = append(s.Env, "LOG_ENABLE_COLOR=false") + s.Env = append(s.Env, getRustLog(c, "authority_claimer")) + s.Env = append(s.Env, fmt.Sprintf("TX_PROVIDER_HTTP_ENDPOINT=%v", + c.BlockchainHttpEndpoint.Value)) + s.Env = append(s.Env, fmt.Sprintf("TX_CHAIN_ID=%v", c.BlockchainID)) + s.Env = append(s.Env, fmt.Sprintf("TX_CHAIN_IS_LEGACY=%v", c.BlockchainIsLegacy)) + s.Env = append(s.Env, fmt.Sprintf("TX_DEFAULT_CONFIRMATIONS=%v", + c.BlockchainFinalityOffset)) + s.Env = append(s.Env, fmt.Sprintf("REDIS_ENDPOINT=%v", getRedisEndpoint(c))) + s.Env = append(s.Env, fmt.Sprintf("HISTORY_ADDRESS=%v", c.ContractsHistoryAddress)) + s.Env = append(s.Env, fmt.Sprintf("AUTHORITY_ADDRESS=%v", c.ContractsAuthorityAddress)) + s.Env = append(s.Env, fmt.Sprintf("INPUT_BOX_ADDRESS=%v", c.ContractsInputBoxAddress)) + s.Env = append(s.Env, fmt.Sprintf("GENESIS_BLOCK=%v", + c.ContractsInputBoxDeploymentBlockNumber)) + s.Env = append(s.Env, fmt.Sprintf("AUTHORITY_CLAIMER_HTTP_SERVER_PORT=%v", + getPort(c, portOffsetAuthorityClaimer))) + switch auth := c.Auth.(type) { + case config.AuthPrivateKey: + s.Env = append(s.Env, fmt.Sprintf("TX_SIGNING_PRIVATE_KEY=%v", + auth.PrivateKey.Value)) + case config.AuthMnemonic: + s.Env = append(s.Env, fmt.Sprintf("TX_SIGNING_MNEMONIC=%v", auth.Mnemonic.Value)) + s.Env = append(s.Env, fmt.Sprintf("TX_SIGNING_MNEMONIC_ACCOUNT_INDEX=%v", + auth.AccountIndex.Value)) + case config.AuthAWS: + s.Env = append(s.Env, fmt.Sprintf("TX_SIGNING_AWS_KMS_KEY_ID=%v", auth.KeyID.Value)) + s.Env = append(s.Env, fmt.Sprintf("TX_SIGNING_AWS_KMS_REGION=%v", + auth.Region.Value)) + default: + panic("invalid auth config") + } + s.Env = append(s.Env, os.Environ()...) + return s +} + +func newDispatcher(c config.NodeConfig) services.CommandService { + var s services.CommandService + s.Name = "dispatcher" + s.HealthcheckPort = getPort(c, portOffsetDispatcher) + s.Path = "cartesi-rollups-dispatcher" + s.Env = append(s.Env, "LOG_ENABLE_TIMESTAMP=false") + s.Env = append(s.Env, "LOG_ENABLE_COLOR=false") + s.Env = append(s.Env, getRustLog(c, "dispatcher")) + s.Env = append(s.Env, fmt.Sprintf("SC_GRPC_ENDPOINT=http://%v:%v", localhost, + getPort(c, portOffsetStateServer))) + s.Env = append(s.Env, fmt.Sprintf("SC_DEFAULT_CONFIRMATIONS=%v", + c.BlockchainFinalityOffset)) + s.Env = append(s.Env, fmt.Sprintf("REDIS_ENDPOINT=%v", getRedisEndpoint(c))) + s.Env = append(s.Env, fmt.Sprintf("DAPP_ADDRESS=%v", c.ContractsApplicationAddress)) + s.Env = append(s.Env, fmt.Sprintf("DAPP_DEPLOYMENT_BLOCK_NUMBER=%v", + c.ContractsApplicationDeploymentBlockNumber)) + s.Env = append(s.Env, fmt.Sprintf("HISTORY_ADDRESS=%v", c.ContractsHistoryAddress)) + s.Env = append(s.Env, fmt.Sprintf("AUTHORITY_ADDRESS=%v", c.ContractsAuthorityAddress)) + s.Env = append(s.Env, fmt.Sprintf("INPUT_BOX_ADDRESS=%v", c.ContractsInputBoxAddress)) + s.Env = append(s.Env, fmt.Sprintf("RD_EPOCH_DURATION=%v", + int(c.RollupsEpochDuration.Seconds()))) + s.Env = append(s.Env, fmt.Sprintf("CHAIN_ID=%v", c.BlockchainID)) + s.Env = append(s.Env, fmt.Sprintf("DISPATCHER_HTTP_SERVER_PORT=%v", + getPort(c, portOffsetDispatcher))) + s.Env = append(s.Env, os.Environ()...) + return s +} + +func newGraphQLServer(c config.NodeConfig) services.CommandService { + var s services.CommandService + s.Name = "graphql-server" + s.HealthcheckPort = getPort(c, portOffsetGraphQLHealthcheck) + s.Path = "cartesi-rollups-graphql-server" + s.Env = append(s.Env, "LOG_ENABLE_TIMESTAMP=false") + s.Env = append(s.Env, "LOG_ENABLE_COLOR=false") + s.Env = append(s.Env, getRustLog(c, "graphql_server")) + s.Env = append(s.Env, fmt.Sprintf("POSTGRES_ENDPOINT=%v", c.PostgresEndpoint.Value)) + s.Env = append(s.Env, fmt.Sprintf("GRAPHQL_HOST=%v", localhost)) + s.Env = append(s.Env, fmt.Sprintf("GRAPHQL_PORT=%v", getPort(c, portOffsetGraphQLServer))) + s.Env = append(s.Env, fmt.Sprintf("GRAPHQL_HEALTHCHECK_PORT=%v", + getPort(c, portOffsetGraphQLHealthcheck))) + s.Env = append(s.Env, os.Environ()...) + return s +} + +func newHostRunner(c config.NodeConfig) services.CommandService { + var s services.CommandService + s.Name = "host-runner" + s.HealthcheckPort = getPort(c, portOffsetHostRunnerHealthcheck) + s.Path = "cartesi-rollups-host-runner" + s.Env = append(s.Env, "LOG_ENABLE_TIMESTAMP=false") + s.Env = append(s.Env, "LOG_ENABLE_COLOR=false") + s.Env = append(s.Env, getRustLog(c, "host_runner")) + s.Env = append(s.Env, fmt.Sprintf("GRPC_SERVER_MANAGER_ADDRESS=%v", localhost)) + s.Env = append(s.Env, fmt.Sprintf("GRPC_SERVER_MANAGER_PORT=%v", + getPort(c, portOffsetServerManager))) + s.Env = append(s.Env, fmt.Sprintf("HTTP_ROLLUP_SERVER_ADDRESS=%v", localhost)) + s.Env = append(s.Env, fmt.Sprintf("HTTP_ROLLUP_SERVER_PORT=%v", + getPort(c, portOffsetHostRunnerRollups))) + s.Env = append(s.Env, fmt.Sprintf("HOST_RUNNER_HEALTHCHECK_PORT=%v", + getPort(c, portOffsetHostRunnerHealthcheck))) + s.Env = append(s.Env, os.Environ()...) + return s +} + +func newIndexer(c config.NodeConfig) services.CommandService { + var s services.CommandService + s.Name = "indexer" + s.HealthcheckPort = getPort(c, portOffsetIndexer) + s.Path = "cartesi-rollups-indexer" + s.Env = append(s.Env, "LOG_ENABLE_TIMESTAMP=false") + s.Env = append(s.Env, "LOG_ENABLE_COLOR=false") + s.Env = append(s.Env, getRustLog(c, "indexer")) + s.Env = append(s.Env, fmt.Sprintf("POSTGRES_ENDPOINT=%v", c.PostgresEndpoint.Value)) + s.Env = append(s.Env, fmt.Sprintf("CHAIN_ID=%v", c.BlockchainID)) + s.Env = append(s.Env, fmt.Sprintf("DAPP_CONTRACT_ADDRESS=%v", + c.ContractsApplicationAddress)) + s.Env = append(s.Env, fmt.Sprintf("REDIS_ENDPOINT=%v", getRedisEndpoint(c))) + s.Env = append(s.Env, fmt.Sprintf("INDEXER_HEALTHCHECK_PORT=%v", + getPort(c, portOffsetIndexer))) + s.Env = append(s.Env, os.Environ()...) + return s +} + +func newInspectServer(c config.NodeConfig) services.CommandService { + var s services.CommandService + s.Name = "inspect-server" + s.HealthcheckPort = getPort(c, portOffsetInspectHealthcheck) + s.Path = "cartesi-rollups-inspect-server" + s.Env = append(s.Env, "LOG_ENABLE_TIMESTAMP=false") + s.Env = append(s.Env, "LOG_ENABLE_COLOR=false") + s.Env = append(s.Env, getRustLog(c, "inspect_server")) + s.Env = append(s.Env, fmt.Sprintf("INSPECT_SERVER_ADDRESS=%v:%v", localhost, + getPort(c, portOffsetInspectServer))) + s.Env = append(s.Env, fmt.Sprintf("SERVER_MANAGER_ADDRESS=%v:%v", localhost, + getPort(c, portOffsetServerManager))) + s.Env = append(s.Env, fmt.Sprintf("SESSION_ID=%v", serverManagerSessionId)) + s.Env = append(s.Env, fmt.Sprintf("INSPECT_SERVER_HEALTHCHECK_PORT=%v", + getPort(c, portOffsetInspectHealthcheck))) + s.Env = append(s.Env, os.Environ()...) + return s +} + +func newRedis(c config.NodeConfig) services.CommandService { + var s services.CommandService + s.Name = "redis" + s.HealthcheckPort = getPort(c, portOffsetRedis) + s.Path = "redis-server" + s.Args = append(s.Args, "--port", fmt.Sprint(getPort(c, portOffsetRedis))) + // Disable persistence with --save and --appendonly config + s.Args = append(s.Args, "--save", "") + s.Args = append(s.Args, "--appendonly", "no") + s.Env = append(s.Env, os.Environ()...) + return s +} + +func newServerManager(c config.NodeConfig) services.ServerManager { + var s services.ServerManager + s.Name = "server-manager" + s.HealthcheckPort = getPort(c, portOffsetServerManager) + s.Path = "server-manager" + s.Args = append(s.Args, + fmt.Sprintf("--manager-address=%v:%v", localhost, getPort(c, portOffsetServerManager))) + s.Env = append(s.Env, "REMOTE_CARTESI_MACHINE_LOG_LEVEL=info") + if c.LogLevel == slog.LevelDebug { + s.Env = append(s.Env, "SERVER_MANAGER_LOG_LEVEL=info") + } else { + s.Env = append(s.Env, "SERVER_MANAGER_LOG_LEVEL=warning") + } + s.Env = append(s.Env, os.Environ()...) + s.BypassLog = c.ExperimentalServerManagerBypassLog + return s +} + +func newStateServer(c config.NodeConfig) services.CommandService { + var s services.CommandService + s.Name = "state-server" + s.HealthcheckPort = getPort(c, portOffsetStateServer) + s.Path = "cartesi-rollups-state-server" + s.Env = append(s.Env, "LOG_ENABLE_TIMESTAMP=false") + s.Env = append(s.Env, "LOG_ENABLE_COLOR=false") + s.Env = append(s.Env, getRustLog(c, "state_server")) + s.Env = append(s.Env, "SF_CONCURRENT_EVENTS_FETCH=1") + s.Env = append(s.Env, fmt.Sprintf("SF_GENESIS_BLOCK=%v", + c.ContractsInputBoxDeploymentBlockNumber)) + s.Env = append(s.Env, fmt.Sprintf("SF_SAFETY_MARGIN=%v", c.BlockchainFinalityOffset)) + s.Env = append(s.Env, fmt.Sprintf("BH_WS_ENDPOINT=%v", c.BlockchainWsEndpoint.Value)) + s.Env = append(s.Env, fmt.Sprintf("BH_HTTP_ENDPOINT=%v", + c.BlockchainHttpEndpoint.Value)) + s.Env = append(s.Env, fmt.Sprintf("BLOCKCHAIN_BLOCK_TIMEOUT=%v", c.BlockchainBlockTimeout)) + s.Env = append(s.Env, fmt.Sprintf("SS_SERVER_ADDRESS=%v:%v", localhost, + getPort(c, portOffsetStateServer))) + s.Env = append(s.Env, os.Environ()...) + return s +} + +func newSupervisorService(c config.NodeConfig) services.SupervisorService { + var s []services.Service + + if !c.ExperimentalSunodoValidatorEnabled { + // add Redis first + s = append(s, newRedis(c)) + } + + // add services without dependencies + s = append(s, newGraphQLServer(c)) + s = append(s, newIndexer(c)) + s = append(s, newStateServer(c)) + + // start either the server manager or host runner + if c.FeatureHostMode { + s = append(s, newHostRunner(c)) + } else { + s = append(s, newServerManager(c)) + } + + // enable claimer if reader mode and sunodo validator mode are disabled + if !c.FeatureDisableClaimer && !c.ExperimentalSunodoValidatorEnabled { + s = append(s, newAuthorityClaimer(c)) + } + + // add services with dependencies + s = append(s, newAdvanceRunner(c)) // Depends on the server-manager/host-runner + s = append(s, newDispatcher(c)) // Depends on the state server + s = append(s, newInspectServer(c)) // Depends on the server-manager/host-runner + + s = append(s, newHttpService(c)) + + supervisor := services.SupervisorService{ + Name: "rollups-node", + Services: s, + } + return supervisor +} + +func newHttpService(c config.NodeConfig) services.HttpService { + addr := fmt.Sprintf("%v:%v", c.HttpAddress, getPort(c, portOffsetProxy)) + handler := newHttpServiceHandler(c) + return services.HttpService{ + Name: "http", + Address: addr, + Handler: handler, + } +} diff --git a/internal/services/command.go b/internal/services/command.go index 1cfd246f5..ad33b3910 100644 --- a/internal/services/command.go +++ b/internal/services/command.go @@ -6,12 +6,13 @@ package services import ( "context" "fmt" + "log/slog" "net" "os/exec" + "regexp" + "strings" "syscall" "time" - - "github.com/cartesi/rollups-node/internal/config" ) const ( @@ -47,8 +48,7 @@ func (s CommandService) Start(ctx context.Context, ready chan<- struct{}) error cmd.Cancel = func() error { err := cmd.Process.Signal(syscall.SIGTERM) if err != nil { - msg := "failed to send SIGTERM to %v: %v\n" - config.WarningLogger.Printf(msg, s, err) + slog.Warn("Failed to send SIGTERM", "service", s, "error", err) } return err } @@ -68,7 +68,7 @@ func (s CommandService) pollTcp(ctx context.Context, ready chan<- struct{}) { for { conn, err := net.Dial("tcp", fmt.Sprintf("0.0.0.0:%v", s.HealthcheckPort)) if err == nil { - config.DebugLogger.Printf("%s is ready\n", s) + slog.Debug("Service is ready", "service", s) conn.Close() ready <- struct{}{} return @@ -85,11 +85,42 @@ func (s CommandService) String() string { return s.Name } +// A wrapper around slog.Default that writes log output from a services.CommandService +// to the correct log level type commandLogger struct { Name string } func (l commandLogger) Write(data []byte) (int, error) { - config.InfoLogger.Printf("%v: %v", l.Name, string(data)) - return len(data), nil + // If data does has no alphanumeric characters, ignore it. + if match := alphanumericRegex.Find(data); match == nil { + return 0, nil + } + msg := strings.TrimSpace(string(data)) + level := l.logLevelForMessage(msg) + slog.Log(context.Background(), level, msg, "service", l.Name) + return len(msg), nil +} + +var ( + errorRegex = regexp.MustCompile(`(?i)(error|fatal)`) + warnRegex = regexp.MustCompile(`(?i)warn`) + infoRegex = regexp.MustCompile(`(?i)info`) + debugRegex = regexp.MustCompile(`(?i)(debug|trace)`) + alphanumericRegex = regexp.MustCompile("[a-zA-Z0-9]") +) + +// Uses regular expressions to determine the correct log level. If there is no match, +// returns slog.LevelInfo +func (l commandLogger) logLevelForMessage(msg string) slog.Level { + if match := infoRegex.FindString(msg); len(match) > 0 { + return slog.LevelInfo + } else if match = debugRegex.FindString(msg); len(match) > 0 { + return slog.LevelDebug + } else if match = warnRegex.FindString(msg); len(match) > 0 { + return slog.LevelWarn + } else if match = errorRegex.FindString(msg); len(match) > 0 { + return slog.LevelError + } + return slog.LevelInfo } diff --git a/internal/services/http.go b/internal/services/http.go index 4dd9ede83..aac8d52ec 100644 --- a/internal/services/http.go +++ b/internal/services/http.go @@ -6,10 +6,9 @@ package services import ( "context" "errors" + "log/slog" "net" "net/http" - - "github.com/cartesi/rollups-node/internal/config" ) type HttpService struct { @@ -26,7 +25,7 @@ func (s HttpService) Start(ctx context.Context, ready chan<- struct{}) error { server := http.Server{ Addr: s.Address, Handler: s.Handler, - ErrorLog: config.ErrorLogger, + ErrorLog: slog.NewLogLogger(slog.Default().Handler(), slog.LevelError), } listener, err := net.Listen("tcp", s.Address) @@ -34,14 +33,14 @@ func (s HttpService) Start(ctx context.Context, ready chan<- struct{}) error { return err } - config.InfoLogger.Printf("%v: listening at %v\n", s, listener.Addr()) + slog.Info("HTTP server started listening", "service", s, "port", listener.Addr()) ready <- struct{}{} done := make(chan error, 1) go func() { err := server.Serve(listener) if !errors.Is(err, http.ErrServerClosed) { - config.WarningLogger.Printf("%v: %v", s, err) + slog.Warn("Service exited with error", "service", s, "error", err) } done <- err }() diff --git a/internal/services/server-manager.go b/internal/services/server-manager.go index 9e84f4b14..cb2719d0c 100644 --- a/internal/services/server-manager.go +++ b/internal/services/server-manager.go @@ -6,6 +6,7 @@ package services import ( "context" "fmt" + "log/slog" "net" "os" "os/exec" @@ -13,8 +14,6 @@ import ( "strings" "syscall" "time" - - "github.com/cartesi/rollups-node/internal/config" ) // ServerManager is a variation of CommandService used to manually stop @@ -35,6 +34,9 @@ type ServerManager struct { // Environment variables. Env []string + + // Bypass the log and write directly to stdout/stderr. + BypassLog bool } const waitDelay = 200 * time.Millisecond @@ -42,7 +44,7 @@ const waitDelay = 200 * time.Millisecond func (s ServerManager) Start(ctx context.Context, ready chan<- struct{}) error { cmd := exec.CommandContext(ctx, s.Path, s.Args...) cmd.Env = s.Env - if config.GetCartesiExperimentalServerManagerBypassLog() { + if s.BypassLog { cmd.Stderr = os.Stderr cmd.Stdout = os.Stdout } else { @@ -55,12 +57,11 @@ func (s ServerManager) Start(ctx context.Context, ready chan<- struct{}) error { cmd.Cancel = func() error { err := killChildProcesses(cmd.Process.Pid) if err != nil { - config.WarningLogger.Println(err) + slog.Warn("Failed to kill child processes", "service", s, "error", err) } - err = cmd.Process.Signal(syscall.SIGTERM) if err != nil { - config.WarningLogger.Printf("failed to send SIGTERM to %v: %v\n", s, err) + slog.Warn("Failed to send SIGTERM", "service", s, "error", err) } return err } @@ -81,7 +82,7 @@ func (s ServerManager) pollTcp(ctx context.Context, ready chan<- struct{}) { for { conn, err := net.Dial("tcp", fmt.Sprintf("0.0.0.0:%v", s.HealthcheckPort)) if err == nil { - config.DebugLogger.Printf("%s is ready\n", s) + slog.Debug("Service is ready", "service", s) conn.Close() ready <- struct{}{} return diff --git a/internal/services/supervisor.go b/internal/services/supervisor.go index 2516428ac..81c54fe81 100644 --- a/internal/services/supervisor.go +++ b/internal/services/supervisor.go @@ -6,9 +6,9 @@ package services import ( "context" "errors" + "log/slog" "time" - "github.com/cartesi/rollups-node/internal/config" "golang.org/x/sync/errgroup" ) @@ -66,9 +66,12 @@ Loop: group.Go(func() error { err := service.Start(ctx, serviceReady) if err != nil && !errors.Is(err, context.Canceled) { - config.ErrorLogger.Printf("%v: %v exited with error. %v", s, service, err) + slog.Error("Service exited with error", + "service", service, + "error", err, + ) } else { - config.InfoLogger.Printf("%v: %v exited successfully\n", s, service) + slog.Info("Service exited successfully", "service", service) } return err }) @@ -76,13 +79,13 @@ Loop: select { // service is ready, move along case <-serviceReady: - config.InfoLogger.Printf("%v: %v is ready\n", s, service) + slog.Info("Service is ready", "service", service) // a service exited with error case <-ctx.Done(): break Loop // service took too long to become ready case <-time.After(readyTimeout): - config.ErrorLogger.Printf("%v: %v timed out\n", s, service) + slog.Error("Service timed out", "service", service) cancel() serviceTimedOut = true break Loop @@ -92,7 +95,7 @@ Loop: // if nothing went wrong while starting services, SupervisorService is ready if ctx.Err() == nil { ready <- struct{}{} - config.InfoLogger.Printf("%v: all services are ready\n", s) + slog.Info("All services are ready", "service", s.Name) } // wait until a service exits with error or the external context is canceled @@ -106,13 +109,13 @@ Loop: select { case err := <-wait: - config.InfoLogger.Printf("%v: all services exited", s) + slog.Info("All services exited successfully", "service", s.Name) if serviceTimedOut { return ServiceTimeoutError } return err case <-time.After(stopTimeout): - config.ErrorLogger.Printf("%v: %v", s, SupervisorTimeoutError) + slog.Error("Service timed out", "service", s.Name, "error", SupervisorTimeoutError) return SupervisorTimeoutError } } diff --git a/setup_env.sh b/setup_env.sh index 58f4557b8..9efe26e2f 100644 --- a/setup_env.sh +++ b/setup_env.sh @@ -1,24 +1,27 @@ -export CARTESI_POSTGRES_ENDPOINT=postgres://postgres:password@localhost:5432/postgres -export CARTESI_BLOCKCHAIN_ID=31337 -export CARTESI_BLOCKCHAIN_HTTP_ENDPOINT=http://localhost:8545 -export CARTESI_BLOCKCHAIN_WS_ENDPOINT=ws://localhost:8545 -export CARTESI_BLOCKCHAIN_IS_LEGACY=false -export CARTESI_BLOCKCHAIN_FINALITY_OFFSET=1 -export CARTESI_CONTRACTS_APPLICATION_ADDRESS=0x7FFdf694A877067DE99462A7243b29972D19cf72 -export CARTESI_CONTRACTS_APPLICATION_DEPLOYMENT_BLOCK_NUMBER=20 -export CARTESI_CONTRACTS_HISTORY_ADDRESS=0x325272217ae6815b494bF38cED004c5Eb8a7CdA7 -export CARTESI_CONTRACTS_AUTHORITY_ADDRESS=0x58c93F83fb3304730C95aad2E360cdb88b782010 -export CARTESI_CONTRACTS_INPUT_BOX_ADDRESS=0x59b22D57D4f067708AB0c00552767405926dc768 -export CARTESI_CONTRACTS_INPUT_BOX_DEPLOYMENT_BLOCK_NUMBER=20 -export CARTESI_EPOCH_DURATION=120 +export CARTESI_LOG_LEVEL="info" +export CARTESI_LOG_PRETTY="true" +export CARTESI_FEATURE_HOST_MODE="false" +export CARTESI_FEATURE_DISABLE_CLAIMER="false" +export CARTESI_FEATURE_DISABLE_MACHINE_HASH_CHECK="false" +export CARTESI_EPOCH_DURATION="120" +export CARTESI_BLOCKCHAIN_ID="31337" +export CARTESI_BLOCKCHAIN_HTTP_ENDPOINT="http://localhost:8545" +export CARTESI_BLOCKCHAIN_WS_ENDPOINT="ws://localhost:8545" +export CARTESI_BLOCKCHAIN_IS_LEGACY="false" +export CARTESI_BLOCKCHAIN_FINALITY_OFFSET="1" +export CARTESI_BLOCKCHAIN_BLOCK_TIMEOUT="60" +export CARTESI_CONTRACTS_APPLICATION_ADDRESS="0x7FFdf694A877067DE99462A7243b29972D19cf72" +export CARTESI_CONTRACTS_APPLICATION_DEPLOYMENT_BLOCK_NUMBER="20" +export CARTESI_CONTRACTS_HISTORY_ADDRESS="0x325272217ae6815b494bF38cED004c5Eb8a7CdA7" +export CARTESI_CONTRACTS_AUTHORITY_ADDRESS="0x58c93F83fb3304730C95aad2E360cdb88b782010" +export CARTESI_CONTRACTS_INPUT_BOX_ADDRESS="0x59b22D57D4f067708AB0c00552767405926dc768" +export CARTESI_CONTRACTS_INPUT_BOX_DEPLOYMENT_BLOCK_NUMBER="20" +export CARTESI_SNAPSHOT_DIR="$PWD/machine-snapshot" +export CARTESI_AUTH_KIND="mnemonic" export CARTESI_AUTH_MNEMONIC="test test test test test test test test test test test junk" -export CARTESI_LOG_LEVEL=info -export CARTESI_LOG_TIMESTAMP=false -export CARTESI_FEATURE_HOST_MODE=false -export CARTESI_FEATURE_READER_MODE=false -export CARTESI_HTTP_ADDRESS=0.0.0.0 -export CARTESI_HTTP_PORT=10000 -export CARTESI_SNAPSHOT_DIR=$PWD/machine-snapshot +export CARTESI_POSTGRES_ENDPOINT="postgres://postgres:password@localhost:5432/postgres" +export CARTESI_HTTP_ADDRESS="0.0.0.0" +export CARTESI_HTTP_PORT="10000" rust_bin_path="$PWD/offchain/target/debug" # Check if the path is already in $PATH