diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 24d809706..c2c58e235 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -197,4 +197,4 @@ jobs: - id: "run-e2e-tests" run: | - ./argo-linux-amd64 submit --wait --log --namespace devnet-issue-${{ github.event.pull_request.number }} --from 'wftmpl/dev-e2e-tests' --parameter gitsha="${{ github.event.pull_request.head.sha }}" + ./argo-linux-amd64 submit --wait --log --namespace devnet-issue-${{ github.event.pull_request.number }} --from 'wftmpl/dev-e2e-tests' --paramter tags="~@skip_in_ci" --parameter gitsha="${{ github.event.pull_request.head.sha }}" diff --git a/.gitignore b/.gitignore index d049b2729..4996b6c24 100644 --- a/.gitignore +++ b/.gitignore @@ -55,9 +55,6 @@ temp_test.go test_results.json coverage.out -# Output of `make build_and_watch` -main - # generated RPC server and client from openapi.yaml rpc/server.gen.go rpc/client.gen.go @@ -90,3 +87,6 @@ tools/wiki # ggshield .cache_ggshield + +# mock temporary files +**/gomock_reflect_*/ diff --git a/.tiltignore b/.tiltignore index 63afc9698..9a59a3fde 100644 --- a/.tiltignore +++ b/.tiltignore @@ -41,9 +41,6 @@ temp_test.go test_results.json coverage.out -# Output of `make build_and_watch` -main - # generated RPC server and client from openapi.yaml rpc/server.gen.go rpc/client.gen.go diff --git a/Makefile b/Makefile index 9ec1c636a..e268d49d8 100644 --- a/Makefile +++ b/Makefile @@ -38,7 +38,7 @@ help: docker_check: { \ if ( ! ( command -v docker >/dev/null && (docker compose version >/dev/null || command -v docker-compose >/dev/null) )); then \ - echo "Seems like you don't have Docker or docker-compose installed. Make sure you review docs/development/README.md before continuing"; \ + echo "Seems like you don't have Docker or docker-compose installed. Make sure you review build/localnet/README.md and docs/development/README.md before continuing"; \ exit 1; \ fi; \ } @@ -47,11 +47,21 @@ docker_check: kubectl_check: { \ if ( ! ( command -v kubectl >/dev/null )); then \ - echo "Seems like you don't have Kubectl installed. Make sure you review docs/development/README.md before continuing"; \ + echo "Seems like you don't have Kubectl installed. Make sure you review build/localnet/README.md and docs/development/README.md before continuing"; \ exit 1; \ fi; \ } +# Internal helper target - check if rsync is installed. +rsync_check: + { \ + if ( ! ( command -v kubectl >/dev/null )); then \ + echo "Seems like you don't have rsync installed. Make sure you review build/localnet/README.md and docs/development/README.md before continuing"; \ + exit 1; \ + fi; \ + } + + .PHONY: trigger_ci trigger_ci: ## Trigger the CI pipeline by submitting an empty commit; See https://github.com/pokt-network/pocket/issues/900 for details git commit --allow-empty -m "Empty commit" @@ -133,6 +143,9 @@ go_imports: ## Group imports using rinchsan/gosimports go_fmt: ## Format all the .go files in the project in place. gofmt -w -s . +# TODO(#964): add `rsync_check`, `kubectl_check`, `docker_check` as a validation in `install_cli_deps`; https://github.com/pokt-network/pocket/assets/1892194/a7a24a11-f54d-46e2-a73e-9e8ea7d06726 +# .PHONY: install_cli_deps +# install_cli_deps: rsync_check kubectl_check docker_check ## Installs `helm`, `tilt` and the underlying `ci_deps` .PHONY: install_cli_deps install_cli_deps: ## Installs `helm`, `tilt` and the underlying `ci_deps` make install_ci_deps @@ -163,33 +176,27 @@ develop_test: docker_check ## Run all of the make commands necessary to develop make develop_start && \ make test_all -.PHONY: client_start -client_start: docker_check ## Run a client daemon which is only used for debugging purposes +.PHONY: lightweight_localnet_client +lightweight_localnet_client: docker_check ## Run a client daemon which is only used for debugging purposes +# Add `--build` to rebuild the client ${docker-compose} up -d client -.PHONY: rebuild_client_start -rebuild_client_start: docker_check ## Rebuild and run a client daemon which is only used for debugging purposes - ${docker-compose} up -d --build client - -.PHONY: client_connect -client_connect: docker_check ## Connect to the running client debugging daemon +.PHONY: lightweight_localnet_client_debug +lightweight_localnet_client_debug: docker_check ## Connect to the running client debugging daemon docker exec -it client /bin/bash -c "go run -tags=debug app/client/*.go DebugUI" -.PHONY: build_and_watch -build_and_watch: ## Continous build Pocket's main entrypoint as files change - /bin/sh ${PWD}/build/scripts/watch_build.sh +# IMPROVE: Avoid building the binary on every shell execution and sync it from local instead +.PHONY: lightweight_localnet_shell +lightweight_localnet_shell: docker_check ## Connect to the running client debugging daemon + docker exec -it client /bin/bash -c "go build -tags=debug -o p1 ./app/client/*.go && chmod +x p1 && mv p1 /usr/bin && echo \"Finished building a new p1 binary\" && /bin/bash" -# TODO(olshansky): Need to think of a Pocket related name for `compose_and_watch`, maybe just `pocket_watch`? -.PHONY: compose_and_watch -compose_and_watch: docker_check db_start monitoring_start ## Run a localnet composed of 4 consensus validators w/ hot reload & debugging +.PHONY: lightweight_localnet +lightweight_localnet: docker_check db_start monitoring_start ## Run a lightweight localnet composed of 4 validators w/ hot reload & debugging +# Add `--build` to rebuild the client ${docker-compose} up --force-recreate validator1 validator2 validator3 validator4 servicer1 fisherman1 -.PHONY: rebuild_and_compose_and_watch -rebuild_and_compose_and_watch: docker_check db_start monitoring_start ## Rebuilds the container from scratch and launches compose_and_watch - ${docker-compose} up --build --force-recreate validator1 validator2 validator3 validator4 servicer1 fisherman1 - .PHONY: db_start -db_start: docker_check ## Start a detached local postgres and admin instance; compose_and_watch is responsible for instantiating the actual schemas +db_start: docker_check ## Start a detached local postgres and admin instance; lightweight_localnet is responsible for instantiating the actual schemas ${docker-compose} up --no-recreate -d db pgadmin .PHONY: db_cli @@ -245,7 +252,7 @@ docker_wipe_nodes: docker_check prompt_user db_drop ## [WARNING] Remove all the docker ps -a -q --filter="name=node*" | xargs -r -I {} docker rm {} .PHONY: monitoring_start -monitoring_start: docker_check ## Start grafana, metrics and logging system (this is auto-triggered by compose_and_watch) +monitoring_start: docker_check ## Start grafana, metrics and logging system (this is auto-triggered by lightweight_localnet) ${docker-compose} up --no-recreate -d grafana loki vm .PHONY: docker_loki_install diff --git a/app/client/cli/debug.go b/app/client/cli/debug.go index 4cc1ea632..e2f715289 100644 --- a/app/client/cli/debug.go +++ b/app/client/cli/debug.go @@ -1,11 +1,15 @@ package cli import ( + "fmt" + "log" "os" + "os/exec" "time" "github.com/manifoldco/promptui" "github.com/spf13/cobra" + "golang.org/x/exp/slices" "google.golang.org/protobuf/types/known/anypb" "github.com/pokt-network/pocket/app/client/cli/helpers" @@ -35,31 +39,103 @@ var items = []string{ } func init() { + dbg := newDebugCommand() + dbg.AddCommand(newDebugSubCommands()...) + rootCmd.AddCommand(dbg) + dbgUI := newDebugUICommand() - dbgUI.AddCommand(newDebugUISubCommands()...) rootCmd.AddCommand(dbgUI) } -// newDebugUISubCommands builds out the list of debug subcommands by matching the -// handleSelect dispatch to the appropriate command. -// * To add a debug subcommand, you must add it to the `items` array and then -// write a function handler to match for it in `handleSelect`. -func newDebugUISubCommands() []*cobra.Command { - commands := make([]*cobra.Command, len(items)) - for idx, promptItem := range items { - commands[idx] = &cobra.Command{ - Use: promptItem, +// newDebugCommand returns the cobra CLI for the Debug command. +func newDebugCommand() *cobra.Command { + return &cobra.Command{ + Use: "Debug", + Aliases: []string{"d"}, + Short: "Debug utility for rapid development", + Long: "Debug utility to send fire-and-forget messages to the network for development purposes", + Args: cobra.MaximumNArgs(1), + } +} + +// newDebugSubCommands is a list of commands that can be "fired & forgotten" (no selection necessary) +func newDebugSubCommands() []*cobra.Command { + cmds := []*cobra.Command{ + { + Use: "PrintNodeState", + Aliases: []string{"print", "state"}, + Short: "Prints the node state", + Long: "Sends a message to all visible nodes to log the current state of their consensus", + Args: cobra.ExactArgs(0), PersistentPreRunE: helpers.P2PDependenciesPreRunE, - Run: func(cmd *cobra.Command, _ []string) { - // TECHDEBT(#874): this is a magic number, but an alternative would be to have the p2p module wait until connections are open and to flush the message correctly - time.Sleep(500 * time.Millisecond) // give p2p module time to start - handleSelect(cmd, cmd.Use) - time.Sleep(500 * time.Millisecond) // give p2p module time to broadcast + Run: func(cmd *cobra.Command, args []string) { + runWithSleep(func() { + handleSelect(cmd, PromptPrintNodeState) + }) }, - ValidArgs: items, - } + }, + { + Use: "ResetToGenesis", + Aliases: []string{"reset", "genesis"}, + Short: "Reset to genesis", + Long: "Broadcast a message to all visible nodes to reset the state to genesis", + Args: cobra.ExactArgs(0), + PersistentPreRunE: helpers.P2PDependenciesPreRunE, + Run: func(cmd *cobra.Command, args []string) { + runWithSleep(func() { + handleSelect(cmd, PromptResetToGenesis) + }) + }, + }, + { + Use: "TriggerView", + Aliases: []string{"next", "trigger", "view"}, + Short: "Trigger the next view in consensus", + Long: "Sends a message to all visible nodes on the network to start the next view (height/step/round) in consensus", + Args: cobra.ExactArgs(0), + PersistentPreRunE: helpers.P2PDependenciesPreRunE, + Run: func(cmd *cobra.Command, args []string) { + runWithSleep(func() { + handleSelect(cmd, PromptTriggerNextView) + }) + }, + }, + { + Use: "TogglePacemakerMode", + Aliases: []string{"toggle", "pcm"}, + Short: "Toggle the pacemaker", + Long: "Toggle the consensus pacemaker either on or off so the chain progresses on its own or loses liveness", + Args: cobra.ExactArgs(0), + PersistentPreRunE: helpers.P2PDependenciesPreRunE, + Run: func(cmd *cobra.Command, args []string) { + runWithSleep(func() { + handleSelect(cmd, PromptTogglePacemakerMode) + }) + }, + }, + { + Use: "ScaleActor", + Aliases: []string{"scale"}, + Short: "Scales the number of actors up or down", + Long: "Scales the type of actor specified to the number provided", + Args: cobra.ExactArgs(2), + PersistentPreRunE: helpers.P2PDependenciesPreRunE, + Run: func(cmd *cobra.Command, args []string) { + actor := args[0] + numActors := args[1] + validActors := []string{"fishermen", "full_nodes", "servicers", "validators"} + if !slices.Contains(validActors, actor) { + logger.Global.Fatal().Msg("Invalid actor type provided") + } + sedReplaceCmd := fmt.Sprintf("/%s:/,/count:/ s/count: [0-9]*/count: %s/", actor, numActors) + sedCmd := exec.Command("sed", "-i", sedReplaceCmd, "/usr/local/localnet_config.yaml") + if err := sedCmd.Run(); err != nil { + log.Fatal(err) + } + }, + }, } - return commands + return cmds } // newDebugUICommand returns the cobra CLI for the Debug UI interface. @@ -67,14 +143,19 @@ func newDebugUICommand() *cobra.Command { return &cobra.Command{ Aliases: []string{"dui", "debug"}, Use: "DebugUI", - Short: "Debug selection ui for rapid development", + Short: "Debug utility with an interactive UI for development purposes", + Long: "Opens a shell-driven selection UI to view and select from a list of debug actions for development purposes", Args: cobra.MaximumNArgs(0), PersistentPreRunE: helpers.P2PDependenciesPreRunE, - RunE: runDebug, + RunE: selectDebugCommand, } } -func runDebug(cmd *cobra.Command, _ []string) (err error) { +// selectDebugCommand builds out the list of debug subcommands by matching the +// handleSelect dispatch to the appropriate command. +// - To add a debug subcommand, you must add it to the `items` array and then +// write a function handler to match for it in `handleSelect`. +func selectDebugCommand(cmd *cobra.Command, _ []string) error { for { if selection, err := promptGetInput(); err == nil { handleSelect(cmd, selection) @@ -162,7 +243,17 @@ func handleSelect(cmd *cobra.Command, selection string) { } } -// Broadcast to the entire network. +// HACK: Because of how the p2p module works, we need to surround it with sleep both BEFORE and AFTER the task. +// - Starting the task too early after the debug client initializes results in a lack of visibility of the nodes in the network +// - Ending the task too early before the debug client completes its task results in a lack of propagation of the message or retrieval of the result +// TECHDEBT: There is likely an event based solution to this but it would require a lot more refactoring of the p2p module. +func runWithSleep(task func()) { + time.Sleep(1000 * time.Millisecond) + task() + time.Sleep(1000 * time.Millisecond) +} + +// broadcastDebugMessage broadcasts the debug message to the entire visible network. func broadcastDebugMessage(cmd *cobra.Command, debugMsg *messaging.DebugMessage) { anyProto, err := anypb.New(debugMsg) if err != nil { @@ -178,7 +269,7 @@ func broadcastDebugMessage(cmd *cobra.Command, debugMsg *messaging.DebugMessage) } } -// Send to just a single (i.e. first) validator in the set +// sendDebugMessage sends the debug message to just a single (i.e. first) node visible func sendDebugMessage(cmd *cobra.Command, debugMsg *messaging.DebugMessage) { anyProto, err := anypb.New(debugMsg) if err != nil { diff --git a/build/config/README.md b/build/config/README.md index 24d8d110c..fffb1883b 100644 --- a/build/config/README.md +++ b/build/config/README.md @@ -12,7 +12,7 @@ It is not recommended at this time to build infrastructure components that rely ## Origin Document -Currently, the Genesis and Configuration generator is necessary to create development `localnet` environments for iterating on V1. A current example (as of 09/2022) of this is the `make compose_and_watch` debug utility that generates a `localnet` using `docker-compose` by injecting the appropriate `config.json` and `genesis.json` files. +Currently, the Genesis and Configuration generator is necessary to create development `localnet` environments for iterating on V1. A current example (as of 09/2022) of this is the `make lightweight_localnet` debug utility that generates a `localnet` using `docker-compose` by injecting the appropriate `config.json` and `genesis.json` files. ## Usage diff --git a/build/docs/CHANGELOG.md b/build/docs/CHANGELOG.md index a23ab1698..aec7e163e 100644 --- a/build/docs/CHANGELOG.md +++ b/build/docs/CHANGELOG.md @@ -233,7 +233,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [0.0.0.1] - 2022-12-29 - Updated all `config*.json` files with the missing `max_mempool_count` value -- Added `is_client_only` to `config1.json` so Viper knows it can be overridden. The config override is done in the Makefile's `client_connect` target. Setting this can be avoided if we merge the changes in https://github.com/pokt-network/pocket/compare/main...issue/cli-viper-environment-vars-fix +- Added `is_client_only` to `config1.json` so Viper knows it can be overridden. The config override is done in the Makefile's `lightweight_localnet_client_debug` target. Setting this can be avoided if we merge the changes in https://github.com/pokt-network/pocket/compare/main...issue/cli-viper-environment-vars-fix ## [0.0.0.0] - 2022-12-22 diff --git a/build/localnet/README.md b/build/localnet/README.md index ae52c319a..02d9a1494 100644 --- a/build/localnet/README.md +++ b/build/localnet/README.md @@ -2,7 +2,7 @@ This guide shows how to deploy a LocalNet using [pocket-operator](https://github.com/pokt-network/pocket-operator). -- [TLDR](#tldr) +- [TL;DR](#tldr) - [Dependencies](#dependencies) - [Choosing Kubernetes Distribution](#choosing-kubernetes-distribution) - [How to create Kind Kubernetes cluster](#how-to-create-kind-kubernetes-cluster) @@ -16,6 +16,8 @@ This guide shows how to deploy a LocalNet using [pocket-operator](https://github - [Interacting w/ LocalNet](#interacting-w-localnet) - [Make Targets](#make-targets) - [Addresses and keys on LocalNet](#addresses-and-keys-on-localnet) + - [Applications staked on LocalNet](#applications-staked-on-localnet) + - [Servicers staked on LocalNet](#servicers-staked-on-localnet) - [How to change configuration files](#how-to-change-configuration-files) - [Overriding default values for localnet with Tilt](#overriding-default-values-for-localnet-with-tilt) - [How does it work?](#how-does-it-work) @@ -26,7 +28,7 @@ This guide shows how to deploy a LocalNet using [pocket-operator](https://github - [Full Cleanup](#full-cleanup) - [Code Structure](#code-structure) -## TLDR +## TL;DR If you feel adventurous, and you know what you're doing, here is a rapid guide to start LocalNet: @@ -46,6 +48,7 @@ All necessary dependencies, except Docker and Kubernetes cluster, are installed 3. `Kubernetes cluster`: refer to [Choosing Kubernetes Distribution](#choosing-kubernetes-distribution) section for more details. 4. `kubectl`: CLI is required and should be configured to access the cluster. This should happen automatically if using Docker Desktop, Rancher Desktop, k3s, k3d, minikube, etc. 5. [helm](https://helm.sh/docs/intro/install): required to template the YAML manifests for the dependencies (e.g., Postgres, Grafana). Installation instructions available. +6. [rsync](https://www.hostinger.com/tutorials/how-to-use-rsync): required to for some extensions used with `Tilt`; https://github.com/tilt-dev/tilt-extensions/tree/master/syncback#usage ### Choosing Kubernetes Distribution @@ -149,8 +152,8 @@ For example: - `0010297b55fc9278e4be4f1bcfe52bf9bd0443f8` is a servicer #001. - `314019dbb7faf8390c1f0cf4976ef1215c90b7e4` is an application #314. - #### Applications staked on LocalNet + Applications with the following addresses are staked on LocalNet, through the [applications field of the genesis.json in the LocalNet configuration](https://github.com/pokt-network/pocket/blob/main/build/localnet/manifests/configs.yaml#L4088) - `00001fff518b1cdddd74c197d76ba5b5dedc0301` @@ -159,6 +162,7 @@ Applications with the following addresses are staked on LocalNet, through the [a These addresses can be used for e.g. testing the CLI. #### Servicers staked on LocalNet + Servicers with the following addresses are staked on LocalNet, through the [servicers field of the genesis.json in the LocalNet configuration](https://github.com/pokt-network/pocket/blob/main/build/localnet/manifests/configs.yaml#L4120) - `00002b8cea1bcc3dadc72ebecf95564ceb9c2e2a` diff --git a/build/localnet/Tiltfile b/build/localnet/Tiltfile index 60124b573..d4534df35 100644 --- a/build/localnet/Tiltfile +++ b/build/localnet/Tiltfile @@ -3,6 +3,7 @@ load("ext://helm_resource", "helm_resource", "helm_repo") load("ext://namespace", "namespace_create") load("ext://restart_process", "docker_build_with_restart") load("ext://tests/golang", "test_go") +load("ext://syncback", "syncback") tiltfile_dir = os.path.dirname(config.main_dir) root_dir = os.path.dirname(tiltfile_dir + "/../..") @@ -22,6 +23,7 @@ localnet_config = {} localnet_config.update(localnet_config_defaults) localnet_config.update(localnet_config_file) + # Create a default config file if it does not exist if (localnet_config_file != localnet_config) or ( not os.path.exists(localnet_config_path) @@ -29,6 +31,15 @@ if (localnet_config_file != localnet_config) or ( print("Updating " + localnet_config_path + " with defaults") local("cat - > " + localnet_config_path, stdin=encode_yaml(localnet_config)) +syncback( + name="syncback_localnet_config", + k8s_object="deploy/dev-cli-client", + src_dir="/usr/local/", + paths=["localnet_config.yaml"], + target_dir=root_dir, + labels=["watchers"], +) + # List of directories Tilt watches to trigger a hot-reload on changes. # CONSIDERATION: This can potentially can be replaced with a list of excluded directories. deps = [ @@ -128,10 +139,14 @@ RUN echo "source /etc/bash_completion" >> ~/.bashrc RUN echo "source <(p1 completion bash | tail -n +2)" >> ~/.bashrc WORKDIR /root COPY bin/p1-linux /usr/local/bin/p1 +COPY localnet_config.yaml /usr/local/localnet_config.yaml """, - only=["bin/p1-linux"], + only=["bin/p1-linux", localnet_config_path], entrypoint=["sleep", "infinity"], - live_update=[sync("bin/p1-linux", "/usr/local/bin/p1")], + live_update=[ + sync("bin/p1-linux", "/usr/local/bin/p1"), + sync(localnet_config_path, "/usr/local/localnet_config.yaml"), + ], ) # Builds and maintains the cluster-manager container image after the binary is built on local machine @@ -195,6 +210,8 @@ for x in range(localnet_config["validators"]["count"]): "genesis.externalConfigMap.name=v1-localnet-genesis", "genesis.externalConfigMap.key=genesis.json", "postgresql.primary.persistence.enabled=false", + "podAnnotations.prometheus\\.io/scrape=true", + "podAnnotations.prometheus\\.io/port=9000", "nodeType=validator", ], values=[chart_dir + "/pocket-validator-overrides.yaml"] @@ -202,7 +219,6 @@ for x in range(localnet_config["validators"]["count"]): else [], ) ) - k8s_resource("validator-%s-pocket" % formatted_number, labels=["pocket-validators"]) # Provisions servicer nodes @@ -224,6 +240,8 @@ for x in range(localnet_config["servicers"]["count"]): "genesis.externalConfigMap.name=v1-localnet-genesis", "genesis.externalConfigMap.key=genesis.json", "postgresql.primary.persistence.enabled=false", + "podAnnotations.prometheus\\.io/scrape=true", + "podAnnotations.prometheus\\.io/port=9000", "config.servicer.enabled=true", "nodeType=servicer", ], @@ -232,7 +250,6 @@ for x in range(localnet_config["servicers"]["count"]): else [], ) ) - k8s_resource("servicer-%s-pocket" % formatted_number, labels=["pocket-servicers"]) # Provisions fishermen nodes @@ -254,6 +271,8 @@ for x in range(localnet_config["fishermen"]["count"]): "genesis.externalConfigMap.name=v1-localnet-genesis", "genesis.externalConfigMap.key=genesis.json", "postgresql.primary.persistence.enabled=false", + "podAnnotations.prometheus\\.io/scrape=true", + "podAnnotations.prometheus\\.io/port=9000", "config.fisherman.enabled=true", "nodeType=fisherman", ], @@ -270,7 +289,6 @@ actor_number = 0 for x in range(localnet_config["full_nodes"]["count"]): actor_number = actor_number + 1 formatted_number = formatted_actor_number(actor_number) - k8s_yaml( helm( root_dir + "/charts/pocket", @@ -284,6 +302,8 @@ for x in range(localnet_config["full_nodes"]["count"]): "genesis.externalConfigMap.name=v1-localnet-genesis", "genesis.externalConfigMap.key=genesis.json", "postgresql.primary.persistence.enabled=false", + "podAnnotations.prometheus\\.io/scrape=true", + "podAnnotations.prometheus\\.io/port=9000", "nodeType=full", ], values=[chart_dir + "/pocket-full-node-overrides.yaml"] diff --git a/build/scripts/watch.sh b/build/scripts/watch.sh index 01d55d544..b2fbdd892 100755 --- a/build/scripts/watch.sh +++ b/build/scripts/watch.sh @@ -19,7 +19,8 @@ else fi reflex \ - --start-service \ - -r '\.go' \ - --decoration="none" \ - -s -- sh -c "$command"; + --start-service \ + -R '^app/client' \ + -r '\.go' \ + --decoration="none" \ + -s -- sh -c "$command" diff --git a/build/scripts/watch_build.sh b/build/scripts/watch_build.sh deleted file mode 100755 index 5f5e5b920..000000000 --- a/build/scripts/watch_build.sh +++ /dev/null @@ -1,8 +0,0 @@ -#!/bin/bash - -if command -v reflex >/dev/null -then - reflex -r '\.go$' -s --decoration="none" -- sh -c "go build -v app/pocket/main.go" -else - echo "reflex not found. Install with `go install github.com/cespare/reflex@latest`" -fi diff --git a/charts/pocket/templates/statefulset.yaml b/charts/pocket/templates/statefulset.yaml index ef2d21a30..7f68f29c5 100644 --- a/charts/pocket/templates/statefulset.yaml +++ b/charts/pocket/templates/statefulset.yaml @@ -2,10 +2,6 @@ apiVersion: apps/v1 kind: StatefulSet metadata: name: {{ include "pocket.fullname" . }} - {{- with .Values.podAnnotations }} - annotations: - {{- toYaml . | nindent 4 }} - {{- end }} labels: {{- include "pocket.labels" . | nindent 4 }} spec: @@ -17,10 +13,15 @@ spec: metadata: {{- with .Values.podAnnotations }} annotations: - {{- toYaml . | nindent 8 }} + {{- range $key, $value := . }} + {{ $key }}: {{ $value | quote }} + {{- end }} {{- end }} labels: {{- include "pocket.selectorLabels" . | nindent 8 }} + {{- if .Values.podLabels }} + {{- toYaml .Values.podLabels | nindent 8 }} + {{- end }} spec: {{- with .Values.imagePullSecrets }} imagePullSecrets: diff --git a/consensus/module_consensus_debugging.go b/consensus/module_consensus_debugging.go index 9c066f8bf..c1003670e 100644 --- a/consensus/module_consensus_debugging.go +++ b/consensus/module_consensus_debugging.go @@ -15,11 +15,11 @@ func (m *consensusModule) HandleDebugMessage(debugMessage *messaging.DebugMessag m.m.Lock() defer m.m.Unlock() + m.logger.Debug().Msgf("Consensus module handling debug message: %s", debugMessage.Action) + switch debugMessage.Action { case messaging.DebugMessageAction_DEBUG_CONSENSUS_RESET_TO_GENESIS: - if err := m.resetToGenesis(debugMessage); err != nil { - return err - } + return m.resetToGenesis(debugMessage) case messaging.DebugMessageAction_DEBUG_CONSENSUS_PRINT_NODE_STATE: m.printNodeState(debugMessage) case messaging.DebugMessageAction_DEBUG_CONSENSUS_TRIGGER_NEXT_VIEW: diff --git a/docs/demos/iteration_3_end_to_end_tx.md b/docs/demos/iteration_3_end_to_end_tx.md index 1ace95335..86d2dab19 100644 --- a/docs/demos/iteration_3_end_to_end_tx.md +++ b/docs/demos/iteration_3_end_to_end_tx.md @@ -43,13 +43,13 @@ make protogen_local # generate the protobuf files make generate_rpc_openapi # generate the OpenAPI spec make docker_wipe_nodes # clear all the 4 validator nodes make db_drop # clear the existing database -make compose_and_watch # Start 4 validator node LocalNet +make lightweight_localnet # Start 4 validator node LocalNet ``` ## Shell #2: Setup Consensus debugger ```bash -make client_start && make client_connect # start the consensus debugger +make lightweight_localnet_client && make lightweight_localnet_client_debug # start the consensus debugger ``` Use `TriggerNextView` and `PrintNodeState` to increment and inspect each node's `height/round/step`. diff --git a/docs/development/FAQ.md b/docs/development/FAQ.md index 4d4eaf1ff..8b7de79a7 100644 --- a/docs/development/FAQ.md +++ b/docs/development/FAQ.md @@ -11,9 +11,9 @@ _NOTE: Consider turning off the `gofmt` in your IDE to prevent unexpected format ## Unable to start LocalNet - permission denied -- **Issue**: when trying to run `make compose_and_watch` on an operating system with SELinux, the command gives the error: +- **Issue**: when trying to run `make lightweight_localnet` on an operating system with SELinux, the command gives the error: -``` +```bash Recreating validator2 ... done Recreating validator4 ... done Recreating validator1 ... done diff --git a/docs/development/README.md b/docs/development/README.md index 3f62d8a6b..84dfad020 100644 --- a/docs/development/README.md +++ b/docs/development/README.md @@ -86,6 +86,7 @@ Optionally activate changelog pre-commit hook cp .githooks/pre-commit .git/hooks/pre-commit chmod +x .git/hooks/pre-commit ``` + _**NOTE**: The pre-commit changelog verification has been disabled during the developement of V1 as of 2023-05-16 to unblock development velocity; see more details [here](https://github.com/pokt-network/pocket/assets/1892194/394fdb09-e388-44aa-820d-e9d5a23578cf). This check is no longer done in the CI and is not recommended for local development either currently._ ### Pocket Network CLI @@ -167,7 +168,7 @@ Note that there are a few tests in the library that are prone to race conditions ### Running LocalNet -At the time of writing, we have two basic approaches to running a LocalNet. We suggest getting started with the `Docker Compose` approach outlined below before moving to the advanced Kubernetes configuration. +At the time of writing, we have two basic approaches to running a LocalNet. We suggest getting started with the `Docker Compose` (aka `lightweight LocalNet`) approach outlined below before moving to the advanced Kubernetes (aka LocalNet) configuration. #### [Advanced] Kubernetes @@ -186,13 +187,13 @@ make docker_wipe 2. In one shell, run the 4 nodes setup: ```bash -make compose_and_watch +make lightweight_localnet ``` 4. In another shell, run the development client: ```bash -make client_start && make client_connect +make lightweight_localnet_client && make lightweight_localnet_client_debug ``` 4. Check the state of each node: diff --git a/e2e/README.md b/e2e/README.md index a87c4fcf2..5e3ee41c9 100644 --- a/e2e/README.md +++ b/e2e/README.md @@ -7,6 +7,7 @@ - [Build Tags](#build-tags) - [Issue templates](#issue-templates) - [Implementation](#implementation) +- [Keywords](#keywords) > tl; dr - `make localnet_up` and then `make test_e2e` @@ -35,8 +36,8 @@ Issues can formally define features by attaching an erroring `feature` file to b ```gherkin Feature: Example Namespace - Scenario: User Needs Example - Given the user has a validator + Scenario: User Needs Example + Given the user has a node When the user runs the command "example" Then the user should be able to see standard output containing "Example Output" And the pocket client should have exited without error @@ -46,7 +47,7 @@ Feature: Example Namespace The test suite is located in `e2e/tests` and it contains a set of Cucumber feature files and the associated Go tests to run them. `make test_e2e` sees any files named with the pattern `*.feature` in `e2e/tests` and runs them with [godog](https://github.com/cucumber/godog), the Go test runner for Cucumber tests. The LocalNet must be up and running for the E2E test suite to run. -The Validator issues RPC commands on the container by calling `kubectl exec` and targeting the pod in the cluster by name. It records the results of the command including stdout and stderr, allowing for assertions about the results of the command. +The Node issues RPC commands on the container by calling `kubectl exec` and targeting the pod in the cluster by name. It records the results of the command including stdout and stderr, allowing for assertions about the results of the command. ```mermaid --- @@ -60,10 +61,26 @@ flowchart TD Kubeconfig --> Kubectl Kubeconfig --> DevNet subgraph E2E [E2E scenarios] - Kubectl -- commandResult --> Validator - Validator -- args --> Kubectl + Kubectl -- commandResult --> Node + Node -- args --> Kubectl end subgraph DevNet [DevNet] Runner[E2E Test Runner] end ``` + +## Keywords + +The keywords below are a summary of the source documentation available [here](https://cucumber.io/docs/gherkin/reference/#keywords). + +- **Feature**: This keyword, followed by the name and optional description, is used to describe a feature of the system that you're testing. It should provide a high-level description of a software feature, and to group related scenarios. +- **Scenario**: This keyword, followed by the name and optional description, is used to describe a particular behavior of the system that you're testing. A feature can have multiple scenarios, and each scenario should follow the 'Given-When-Then' structure. +- **Given**: This keyword is used to set up a situation or a context. It puts the system in a known state before the user interacts with the system. +- **When**: This keyword is used to describe an action or event. This is something the user does or the system does. +- **Then**: This keyword is used to describe an expected outcome or result. +- **And**, But: These keywords are used when you have more than one Given, When, or Then step. They help to make the specifications more readable. +- **Background**: This keyword provides the context for the following scenarios. It allows you to add some context to the scenarios in a single place. +- **Scenario Outline**: This keyword can be used when the same test is performed multiple times with a different combination of values. +- **Examples**: This keyword is used in conjunction with **Scenario Outline** to provide the values for the test. +- **Rule**: This keyword is used to represent one business rule that should be implemented. It provides additional information for a feature. +- **Tags**: This is not a Gherkin keyword but an integral part of organizing your Cucumber features. They are preceded by '@' symbol and can be used before Feature, Scenario, Scenario Outline, or Examples. diff --git a/e2e/docs/E2E_ADR.md b/e2e/docs/E2E_ADR.md index d3e7dee53..ecefeda62 100644 --- a/e2e/docs/E2E_ADR.md +++ b/e2e/docs/E2E_ADR.md @@ -79,7 +79,7 @@ Below is an example of testing the `help` command of the Pocket binary. Feature: Root Namespace Scenario: User Needs Help - Given the user has a validator + Given the user has a node When the user runs the command "help" Then the user should be able to see standard output containing "Available Commands" And the pocket client should have exited without error @@ -124,16 +124,16 @@ type PocketClient interface { ``` - The `PocketClient` interface is included in the test suite and defines a single function interface with the `RunCommand` method. -- The `validatorPod` adapter fulfills the `PocketClient` interface and lets us call commands through Kubernetes. This is the main way that tests assemble the environment for later assertions. +- The `nodePod` adapter fulfills the `PocketClient` interface and lets us call commands through Kubernetes. This is the main way that tests assemble the environment for later assertions. ```go -// validatorPod holds the connection information to pod validator-001 for testing -type validatorPod struct { +// nodePod holds the connection information to pod validator-001 for testing +type nodePod struct { result *commandResult // stores the result of the last command that was run } // RunCommand runs a command on the pocket binary -func (v *validatorPod) RunCommand(args ...string) (*commandResult, error) { +func (v *nodePod) RunCommand(args ...string) (*commandResult, error) { base := []string{ "exec", "-i", "deploy/pocket-v1-cli-client", "--container", "pocket", diff --git a/e2e/tests/account.feature b/e2e/tests/account.feature new file mode 100644 index 000000000..8e793dcd9 --- /dev/null +++ b/e2e/tests/account.feature @@ -0,0 +1,27 @@ +Feature: Node Namespace + + Scenario: User Wants Help Using The Node Command + Given the user has a node + When the user runs the command "Validator help" + Then the user should be able to see standard output containing "Available Commands" + And the node should have exited without error + + Scenario: User Can Stake A Validator + Given the user has a node + When the user stakes their validator with amount 150000000001 uPOKT + Then the user should be able to see standard output containing "" + And the node should have exited without error + + Scenario: User Can Unstake A Validator + Given the user has a node + When the user stakes their validator with amount 150000000001 uPOKT + Then the user should be able to see standard output containing "" + Then the user should be able to unstake their validator + Then the user should be able to see standard output containing "" + And the node should have exited without error + + Scenario: User Can Send To An Address + Given the user has a node + When the user sends 150000000 uPOKT to another address + Then the user should be able to see standard output containing "" + And the node should have exited without error diff --git a/e2e/tests/debug.feature b/e2e/tests/debug.feature new file mode 100644 index 000000000..38026897c --- /dev/null +++ b/e2e/tests/debug.feature @@ -0,0 +1,18 @@ +Feature: Debug Namespace + + # IMPROVE(#959): Remove time-based waits from tests + + # Since the configuration for consensus is optimistically responsive, we need to be in manual + # Pacemaker mode and call TriggerView to further the blockchain. + # 1 second was chosen arbitrarily for the time for block propagation. + Scenario: 4 Validator blockchain from genesis reaches block 2 when TriggerView is executed twice + Given the network is at genesis + And the network has "4" actors of type "Validator" + When the developer runs the command "TriggerView" + And the developer waits for "1000" milliseconds + Then "validator-001" should be at height "1" + And "validator-004" should be at height "1" + When the developer runs the command "TriggerView" + And the developer waits for "1000" milliseconds + Then "validator-001" should be at height "2" + And "validator-004" should be at height "2" \ No newline at end of file diff --git a/e2e/tests/node.go b/e2e/tests/node.go new file mode 100644 index 000000000..422e6e009 --- /dev/null +++ b/e2e/tests/node.go @@ -0,0 +1,76 @@ +//go:build e2e + +package e2e + +import ( + "fmt" + "os/exec" + + "github.com/pokt-network/pocket/runtime" + "github.com/pokt-network/pocket/runtime/defaults" +) + +// cliPath is the path of the binary installed and is set by the Tiltfile +const cliPath = "/usr/local/bin/p1" + +var ( + // defaultRPCURL used by targetPod to build commands + defaultRPCURL string + // targetDevClientPod is the kube pod that executes calls to the pocket binary under test + targetDevClientPod = "deploy/dev-cli-client" +) + +func init() { + defaultRPCHost := runtime.GetEnv("RPC_HOST", defaults.RandomValidatorEndpointK8SHostname) + defaultRPCURL = fmt.Sprintf("http://%s:%s", defaultRPCHost, defaults.DefaultRPCPort) +} + +// commandResult combines the stdout, stderr, and err of an operation +type commandResult struct { + Stdout string + Stderr string + Err error +} + +// PocketClient is a single function interface for interacting with a node +type PocketClient interface { + RunCommand(...string) (*commandResult, error) + RunCommandOnHost(string, ...string) (*commandResult, error) +} + +// Ensure that Validator fulfills PocketClient +var _ PocketClient = &nodePod{} + +// nodePod holds the connection information to a specific pod in between different instructions during testing +type nodePod struct { + targetPodName string + result *commandResult // stores the result of the last command that was run +} + +// RunCommand runs a command on a pre-configured kube pod with the given args +func (n *nodePod) RunCommand(args ...string) (*commandResult, error) { + return n.RunCommandOnHost(defaultRPCURL, args...) +} + +// RunCommandOnHost runs a command on specified kube pod with the given args +func (n *nodePod) RunCommandOnHost(rpcUrl string, args ...string) (*commandResult, error) { + base := []string{ + "exec", "-i", targetDevClientPod, + "--container", "pocket", + "--", cliPath, + "--non_interactive=true", + "--remote_cli_url=" + rpcUrl, + } + args = append(base, args...) + cmd := exec.Command("kubectl", args...) + r := &commandResult{} + out, err := cmd.Output() + if err != nil { + return nil, err + } + r.Stdout = string(out) + n.result = r + // IMPROVE: make targetPodName configurable + n.targetPodName = targetDevClientPod + return r, nil +} diff --git a/e2e/tests/query.feature b/e2e/tests/query.feature index e7993e1f2..74cc60180 100644 --- a/e2e/tests/query.feature +++ b/e2e/tests/query.feature @@ -2,13 +2,13 @@ Feature: Query Namespace Scenario: User Wants Help Using The Query Command - Given the user has a validator + Given the user has a node When the user runs the command "Query help" Then the user should be able to see standard output containing "Available Commands" - And the validator should have exited without error + And the node should have exited without error - Scenario: User Wants To See The Block At Current Height - Given the user has a validator + Scenario: User Wants To See The Block At Current Height + Given the user has a node When the user runs the command "Query Block" Then the user should be able to see standard output containing "state_hash" - And the validator should have exited without error \ No newline at end of file + And the node should have exited without error \ No newline at end of file diff --git a/e2e/tests/root.feature b/e2e/tests/root.feature index 754534f2e..b9d6225d4 100644 --- a/e2e/tests/root.feature +++ b/e2e/tests/root.feature @@ -1,7 +1,7 @@ Feature: Root Namespace Scenario: User Needs Help - Given the user has a validator + Given the user has a node When the user runs the command "help" Then the user should be able to see standard output containing "Available Commands" - And the validator should have exited without error \ No newline at end of file + And the node should have exited without error \ No newline at end of file diff --git a/e2e/tests/state_sync.feature b/e2e/tests/state_sync.feature new file mode 100644 index 000000000..1aa85fe0e --- /dev/null +++ b/e2e/tests/state_sync.feature @@ -0,0 +1,23 @@ +Feature: State Sync Namespace + + # IMPROVE(#959): Remove time-based waits from tests + # TODO(#964): Remove the `skip_in_ci` tag for these tests + @skip_in_ci + Scenario: New FullNode does not sync to Blockchain at height 2 + Given the network is at genesis + And the network has "4" actors of type "Validator" + When the developer runs the command "ScaleActor full_nodes 1" + And the developer waits for "3000" milliseconds + Then "full-node-002" should be unreachable + When the developer runs the command "TriggerView" + And the developer waits for "1000" milliseconds + And the developer runs the command "TriggerView" + And the developer waits for "1000" milliseconds + Then "validator-001" should be at height "2" + And "validator-004" should be at height "2" + # full_nodes is the key used in `localnet_config.yaml` + When the developer runs the command "ScaleActor full_nodes 2" + # IMPROVE: Figure out if there's something better to do then waiting for a node to spin up + And the developer waits for "40000" milliseconds + # TODO(#812): The full node should be at height "2" after state sync is implemented + Then "full-node-002" should be at height "0" \ No newline at end of file diff --git a/e2e/tests/steps_init_test.go b/e2e/tests/steps_init_test.go index ee680cd82..1f83171f1 100644 --- a/e2e/tests/steps_init_test.go +++ b/e2e/tests/steps_init_test.go @@ -3,11 +3,13 @@ package e2e import ( + "encoding/json" "fmt" "os" "path/filepath" "strings" "testing" + "time" pocketLogger "github.com/pokt-network/pocket/logger" "github.com/pokt-network/pocket/runtime/defaults" @@ -15,6 +17,8 @@ import ( pocketk8s "github.com/pokt-network/pocket/shared/k8s" "github.com/regen-network/gocuke" "github.com/stretchr/testify/require" + "golang.org/x/text/cases" + "golang.org/x/text/language" "k8s.io/client-go/kubernetes" "k8s.io/client-go/rest" "k8s.io/client-go/tools/clientcmd" @@ -25,13 +29,12 @@ var e2eLogger = pocketLogger.Global.CreateLoggerForModule("e2e") const ( // Each actor is represented e.g. validator-001-pocket:42069 thru validator-999-pocket:42069. // Defines the host & port scheme that LocalNet uses for naming actors. - validatorServiceURLTmpl = "validator-%s-pocket:%d" - // validatorA maps to suffix ID 001 and is also used by the cluster-manager - // though it has no special permissions. + validatorServiceURLTemplate = "validator-%s-pocket:%d" + // Mapping from validators to suffix IDs as convienece for some of the tests validatorA = "001" - // validatorB maps to suffix ID 002 and receives in the Send test. validatorB = "002" - chainId = "0001" + // Placeholder chainID + chainId = "0001" ) type rootSuite struct { @@ -42,53 +45,164 @@ type rootSuite struct { validatorKeys map[string]string // clientset is the kubernetes API we acquire from the user's $HOME/.kube/config clientset *kubernetes.Clientset - // validator holds command results between runs and reports errors to the test suite - // TECHDEBT: Rename `validator` to something more appropriate - validator *validatorPod - // validatorA maps to suffix ID 001 of the kube pod that we use as our control agent + // node holds command results between runs and reports errors to the test suite + node *nodePod } func (s *rootSuite) Before() { clientSet, err := getClientset(s) require.NoErrorf(s, err, "failed to get clientset") - vkmap, err := pocketk8s.FetchValidatorPrivateKeys(clientSet) + validatorKeyMap, err := pocketk8s.FetchValidatorPrivateKeys(clientSet) if err != nil { e2eLogger.Fatal().Err(err).Msg("failed to get validator key map") } - s.validator = new(validatorPod) + s.node = new(nodePod) s.clientset = clientSet - s.validatorKeys = vkmap + s.validatorKeys = validatorKeyMap } // TestFeatures runs the e2e tests specified in any .features files in this directory // * This test suite assumes that a LocalNet is running that can be accessed by `kubectl` func TestFeatures(t *testing.T) { - gocuke.NewRunner(t, &rootSuite{}).Path("*.feature").Run() + e2eTestTags := os.Getenv("POCKET_E2E_TEST_TAGS") + gocuke.NewRunner(t, &rootSuite{}).Path("*.feature").Tags(e2eTestTags).Run() } // InitializeScenario registers step regexes to function handlers -func (s *rootSuite) TheUserHasAValidator() { - res, err := s.validator.RunCommand("help") +func (s *rootSuite) TheUserHasANode() { + res, err := s.node.RunCommand("help") require.NoErrorf(s, err, res.Stderr) - s.validator.result = res + s.node.result = res } -func (s *rootSuite) TheValidatorShouldHaveExitedWithoutError() { - require.NoError(s, s.validator.result.Err) +func (s *rootSuite) TheNodeShouldHaveExitedWithoutError() { + require.NoError(s, s.node.result.Err) } func (s *rootSuite) TheUserRunsTheCommand(cmd string) { cmds := strings.Split(cmd, " ") - res, err := s.validator.RunCommand(cmds...) + res, err := s.node.RunCommand(cmds...) require.NoError(s, err) - s.validator.result = res + s.node.result = res +} + +// TheDeveloperRunsTheCommand is similar to TheUserRunsTheCommand but exclusive to `Debug` commands +func (s *rootSuite) TheDeveloperRunsTheCommand(cmd string) { + cmds := strings.Split(cmd, " ") + cmds = append([]string{"Debug"}, cmds...) + res, err := s.node.RunCommand(cmds...) + require.NoError(s, err, fmt.Sprintf("failed to run command: '%s' due to error: %s", cmd, err)) + s.node.result = res + e2eLogger.Debug().Msgf("TheDeveloperRunsTheCommand: '%s' with result: %s", cmd, res.Stdout) + + // Special case for managing LocalNet config when scaling actors + if cmds[1] == "ScaleActor" { + s.syncLocalNetConfigFromHostToLocalFS() + } +} + +func (s *rootSuite) TheNetworkIsAtGenesis() { + s.TheDeveloperRunsTheCommand("ResetToGenesis") +} + +func (s *rootSuite) TheDeveloperWaitsForMilliseconds(millis int64) { + time.Sleep(time.Duration(millis) * time.Millisecond) +} + +func (s *rootSuite) TheNetworkHasActorsOfType(num int64, actor string) { + // normalize actor to Title case and plural + caser := cases.Title(language.AmericanEnglish) + actor = caser.String(strings.ToLower(actor)) + if len(actor) > 0 && actor[len(actor)-1] != 's' { + actor += "s" + } + args := []string{ + "Query", + actor, + } + + // Depending on the type of `actor` we're querying, we'll have a different set of expected responses + // so not all of these fields will be populated, but at least one will be. + type expectedResponse struct { + NumValidators *int64 `json:"total_validators"` + NumApps *int64 `json:"total_apps"` + NumFishermen *int64 `json:"total_fishermen"` + NumServicers *int64 `json:"total_servicers"` + NumAccounts *int64 `json:"total_accounts"` + } + validate := func(res *expectedResponse) bool { + return res != nil && ((res.NumValidators != nil && *res.NumValidators > 0) || + (res.NumApps != nil && *res.NumApps > 0) || + (res.NumFishermen != nil && *res.NumFishermen > 0) || + (res.NumServicers != nil && *res.NumServicers > 0) || + (res.NumAccounts != nil && *res.NumAccounts > 0)) + } + + resRaw, err := s.node.RunCommand(args...) + require.NoError(s, err) + + res := getResponseFromStdout[expectedResponse](s, resRaw.Stdout, validate) + require.NotNil(s, res) + + // Validate that at least one of the fields that is populated has the right number of actors + if res.NumValidators != nil { + require.Equal(s, num, *res.NumValidators) + } else if res.NumApps != nil { + require.Equal(s, num, *res.NumApps) + } else if res.NumFishermen != nil { + require.Equal(s, num, *res.NumFishermen) + } else if res.NumServicers != nil { + require.Equal(s, num, *res.NumServicers) + } else if res.NumAccounts != nil { + require.Equal(s, num, *res.NumAccounts) + } +} + +func (s *rootSuite) ShouldBeUnreachable(pod string) { + validate := func(res string) bool { + return strings.Contains(res, "Unable to connect to the RPC") + } + args := []string{ + "Query", + "Height", + } + rpcURL := fmt.Sprintf("http://%s-pocket:%s", pod, defaults.DefaultRPCPort) + resRaw, err := s.node.RunCommandOnHost(rpcURL, args...) + require.NoError(s, err) + + res := getStrFromStdout(s, resRaw.Stdout, validate) + require.NotNil(s, res) + + require.Equal(s, fmt.Sprintf("❌ Unable to connect to the RPC @ \x1b[1mhttp://%s-pocket:%s\x1b[0m", pod, defaults.DefaultRPCPort), *res) +} + +func (s *rootSuite) ShouldBeAtHeight(pod string, height int64) { + args := []string{ + "Query", + "Height", + } + type expectedResponse struct { + Height *int64 `json:"Height"` + } + validate := func(res *expectedResponse) bool { + return res != nil && res.Height != nil + } + + rpcURL := fmt.Sprintf("http://%s-pocket:%s", pod, defaults.DefaultRPCPort) + resRaw, err := s.node.RunCommandOnHost(rpcURL, args...) + require.NoError(s, err) + + res := getResponseFromStdout[expectedResponse](s, resRaw.Stdout, validate) + require.NotNil(s, res) + + require.Equal(s, height, *res.Height) } func (s *rootSuite) TheUserShouldBeAbleToSeeStandardOutputContaining(arg1 string) { - require.Contains(s, s.validator.result.Stdout, arg1) + require.Contains(s, s.node.result.Stdout, arg1) } func (s *rootSuite) TheUserStakesTheirValidatorWithAmountUpokt(amount int64) { @@ -111,15 +225,15 @@ func (s *rootSuite) TheUserSendsUpoktToAnotherAddress(amount int64) { valB.Address().String(), fmt.Sprintf("%d", amount), } - res, err := s.validator.RunCommand(args...) + res, err := s.node.RunCommand(args...) require.NoError(s, err) - s.validator.result = res + s.node.result = res } // stakeValidator runs Validator stake command with the address, amount, chains..., and serviceURL provided func (s *rootSuite) stakeValidator(privKey cryptoPocket.PrivateKey, amount string) { - validatorServiceUrl := fmt.Sprintf(validatorServiceURLTmpl, validatorA, defaults.DefaultP2PPort) + validatorServiceUrl := fmt.Sprintf(validatorServiceURLTemplate, validatorA, defaults.DefaultP2PPort) args := []string{ "Validator", "Stake", @@ -128,10 +242,10 @@ func (s *rootSuite) stakeValidator(privKey cryptoPocket.PrivateKey, amount strin chainId, validatorServiceUrl, } - res, err := s.validator.RunCommand(args...) + res, err := s.node.RunCommand(args...) require.NoError(s, err) - s.validator.result = res + s.node.result = res } // unstakeValidator unstakes the Validator at the same address that stakeValidator uses @@ -142,10 +256,10 @@ func (s *rootSuite) unstakeValidator() { "Unstake", privKey.Address().String(), } - res, err := s.validator.RunCommand(args...) + res, err := s.node.RunCommand(args...) require.NoError(s, err) - s.validator.result = res + s.node.result = res } // getPrivateKey generates a new keypair from the private hex key that we get from the clientset @@ -190,3 +304,39 @@ func inClusterConfig(t gocuke.TestingT) *rest.Config { return config } + +// getResponseFromStdout returns the first output from stdout that passes the validate function provided. +// For example, when running `p1 Query Height`, the output is: +// +// {"level":"info","module":"e2e","time":"2023-07-11T15:46:07-07:00","message":"..."} +// {"height":3} +// +// And will return the following map so it can be used by the caller: +// +// map[height:3] +func getResponseFromStdout[T any](t gocuke.TestingT, stdout string, validate func(res *T) bool) *T { + t.Helper() + + for _, s := range strings.Split(stdout, "\n") { + var m T + if err := json.Unmarshal([]byte(s), &m); err != nil { + continue + } + if !validate(&m) { + continue + } + return &m + } + return nil +} + +func getStrFromStdout(t gocuke.TestingT, stdout string, validate func(res string) bool) *string { + t.Helper() + for _, s := range strings.Split(stdout, "\n") { + if !validate(s) { + continue + } + return &s + } + return nil +} diff --git a/e2e/tests/tilt_helpers.go b/e2e/tests/tilt_helpers.go new file mode 100644 index 000000000..a605ee22e --- /dev/null +++ b/e2e/tests/tilt_helpers.go @@ -0,0 +1,34 @@ +//go:build e2e + +package e2e + +import ( + "log" + "os/exec" +) + +// HACK: Dynamic scaling actors using `p1` and the `e2e test framework` is still a WIP so this is a +// functional interim solution until there's a need for a proper design. +func (s *rootSuite) syncLocalNetConfigFromHostToLocalFS() { + if !isPackageInstalled("tilt") { + e2eLogger.Debug().Msgf("syncLocalNetConfigFromHostToLocalFS: 'tilt' is not installed, skipping...") + return + } + tiltLocalnetConfigSyncbackTrigger := exec.Command("tilt", "trigger", "syncback_localnet_config") + if err := tiltLocalnetConfigSyncbackTrigger.Run(); err != nil { + e2eLogger.Err(err).Msgf("syncLocalNetConfigFromHostToLocalFS: failed to run command: '%s'", tiltLocalnetConfigSyncbackTrigger.String()) + log.Fatal(err) + } +} + +func isPackageInstalled(pkg string) bool { + if _, err := exec.LookPath(pkg); err != nil { + // the executable is not found, return false + if execErr, ok := err.(*exec.Error); ok && execErr.Err == exec.ErrNotFound { + return false + } + // another kind of error happened, let's log and exit + log.Fatal(err) + } + return true +} diff --git a/e2e/tests/valdator.feature b/e2e/tests/validator.feature similarity index 61% rename from e2e/tests/valdator.feature rename to e2e/tests/validator.feature index 1cea8e3cc..e1bd22c4f 100644 --- a/e2e/tests/valdator.feature +++ b/e2e/tests/validator.feature @@ -1,28 +1,27 @@ -# TECHDEBT: Validator should eventually be changed to full node or just node. Feature: Validator Namespace Scenario: User Wants Help Using The Validator Command - Given the user has a validator + Given the user has a node When the user runs the command "Validator help" Then the user should be able to see standard output containing "Available Commands" - And the validator should have exited without error + And the node should have exited without error - Scenario: User Can Stake An Address - Given the user has a validator + Scenario: User Can Stake A Validator + Given the user has a node When the user stakes their validator with amount 150000000001 uPOKT Then the user should be able to see standard output containing "" - And the validator should have exited without error + And the node should have exited without error - Scenario: User Can Unstake An Address - Given the user has a validator + Scenario: User Can Unstake A Validator + Given the user has a node When the user stakes their validator with amount 150000000001 uPOKT Then the user should be able to see standard output containing "" Then the user should be able to unstake their validator Then the user should be able to see standard output containing "" - And the validator should have exited without error + And the node should have exited without error Scenario: User Can Send To An Address - Given the user has a validator + Given the user has a node When the user sends 150000000 uPOKT to another address Then the user should be able to see standard output containing "" - And the validator should have exited without error + And the node should have exited without error diff --git a/e2e/tests/validator.go b/e2e/tests/validator.go deleted file mode 100644 index 04b27bf7f..000000000 --- a/e2e/tests/validator.go +++ /dev/null @@ -1,67 +0,0 @@ -//go:build e2e - -package e2e - -import ( - "fmt" - "os/exec" - - "github.com/pokt-network/pocket/runtime" - "github.com/pokt-network/pocket/runtime/defaults" -) - -var ( - // rpcURL used by targetPod to build commands - rpcURL string - // targetPod is the kube pod that executes calls to the pocket binary under test - targetPod = "deploy/dev-cli-client" -) - -func init() { - rpcHost := runtime.GetEnv("RPC_HOST", defaults.RandomValidatorEndpointK8SHostname) - rpcURL = fmt.Sprintf("http://%s:%s", rpcHost, defaults.DefaultRPCPort) -} - -// cliPath is the path of the binary installed and is set by the Tiltfile -const cliPath = "/usr/local/bin/p1" - -// commandResult combines the stdout, stderr, and err of an operation -type commandResult struct { - Stdout string - Stderr string - Err error -} - -// PocketClient is a single function interface for interacting with a node -type PocketClient interface { - RunCommand(...string) (*commandResult, error) -} - -// Ensure that Validator fulfills PocketClient -var _ PocketClient = &validatorPod{} - -// validatorPod holds the connection information to pod validator-001 for testing -type validatorPod struct { - result *commandResult // stores the result of the last command that was run -} - -// RunCommand runs a command on a target kube pod -func (v *validatorPod) RunCommand(args ...string) (*commandResult, error) { - base := []string{ - "exec", "-i", targetPod, - "--container", "pocket", - "--", cliPath, - "--non_interactive=true", - "--remote_cli_url=" + rpcURL, - } - args = append(base, args...) - cmd := exec.Command("kubectl", args...) - r := &commandResult{} - out, err := cmd.Output() - r.Stdout = string(out) - v.result = r - if err != nil { - return r, err - } - return r, nil -} diff --git a/go.mod b/go.mod index b5029a9ce..bb68ad331 100644 --- a/go.mod +++ b/go.mod @@ -251,7 +251,7 @@ require ( github.com/valyala/fasttemplate v1.2.2 // indirect golang.org/x/mod v0.7.0 // indirect golang.org/x/sys v0.6.0 // indirect - golang.org/x/text v0.7.0 // indirect + golang.org/x/text v0.7.0 golang.org/x/time v0.0.0-20220411224347-583f2d630306 // indirect golang.org/x/tools v0.3.0 // indirect gopkg.in/ini.v1 v1.67.0 // indirect diff --git a/persistence/docs/CHANGELOG.md b/persistence/docs/CHANGELOG.md index 7f6dfb0b3..1016cf5eb 100644 --- a/persistence/docs/CHANGELOG.md +++ b/persistence/docs/CHANGELOG.md @@ -426,7 +426,7 @@ Deprecate PrePersistence - Added PopulateGenesisState function to persistence module - Fixed the stake status iota issue - Discovered and documented (with TODO) double setting parameters issue -- Attached to the Utility Module and using in `make compose_and_watch` +- Attached to the Utility Module and using in `make lightweight_localnet` ## [0.0.0.1] - 2022-07-05 diff --git a/persistence/docs/README.md b/persistence/docs/README.md index d29b5fab0..08d8398e1 100644 --- a/persistence/docs/README.md +++ b/persistence/docs/README.md @@ -99,7 +99,7 @@ A subset of these are explained below. Any targets or helpers to configure and launch the database instances do not populate the actual database. -A LocalNet (see `make compose_and_watch`) must have been executed in order to trigger creation of schemas and hydration of the relevant tables. +A LocalNet (see `make lightweight_localnet`) must have been executed in order to trigger creation of schemas and hydration of the relevant tables. #### CLI Access - db_cli_node diff --git a/runtime/configs/proto/servicer_config.proto b/runtime/configs/proto/servicer_config.proto index 0a3bc65bf..916495fec 100644 --- a/runtime/configs/proto/servicer_config.proto +++ b/runtime/configs/proto/servicer_config.proto @@ -10,16 +10,15 @@ option go_package = "github.com/pokt-network/pocket/runtime/configs"; message ServicerConfig { // Enabled defines whether or not the node is a servicer. bool enabled = 1; - string public_key = 2; - string address = 3; - map services = 4; + string private_key = 2; + map services = 3; // relay_mining_volume_accuracy is a parameter used to adjust the calculated number of service tokens for an application. // It is introduced to minimize the chance of under-utilization of application's tokens, while removing the overhead of // communication between servicers which would be necessary otherwise. // See the following for more details: // https://arxiv.org/abs/2305.10672 - double relay_mining_volume_accuracy = 5; + double relay_mining_volume_accuracy = 4; } // ServiceConfig holds configurations related to where/how the application/client can access the backing RPC service. It is analogous to "ChainConfig" in v0 but can support any RPC service. diff --git a/shared/modules/doc/CHANGELOG.md b/shared/modules/doc/CHANGELOG.md index d6d965cce..207e7a92a 100644 --- a/shared/modules/doc/CHANGELOG.md +++ b/shared/modules/doc/CHANGELOG.md @@ -125,7 +125,7 @@ UtilityModule - Opened followup issue #163 - Added config and genesis generator to build package - Deprecated old build files -- Use new config and genesis files for make compose_and_watch -- Use new config and genesis files for make client_start && make client_connect +- Use new config and genesis files for make lightweight_localnet +- Use new config and genesis files for make lightweight_localnet_client && make lightweight_localnet_client_debug diff --git a/telemetry/README.md b/telemetry/README.md index eed3bb8b9..fd61c911a 100644 --- a/telemetry/README.md +++ b/telemetry/README.md @@ -158,7 +158,7 @@ make docker_loki_install 1. Spin up the stack ```bash -make compose_and_watch +make lightweight_localnet ``` 2. Wait a few seconds and **Voila!** diff --git a/utility/module_enable_actors_test.go b/utility/module_enable_actors_test.go index b2879bd2e..eadeb8f95 100644 --- a/utility/module_enable_actors_test.go +++ b/utility/module_enable_actors_test.go @@ -6,6 +6,7 @@ import ( "github.com/golang/mock/gomock" "github.com/pokt-network/pocket/runtime" "github.com/pokt-network/pocket/runtime/configs" + "github.com/pokt-network/pocket/shared/crypto" "github.com/pokt-network/pocket/shared/modules" mocks "github.com/pokt-network/pocket/shared/modules/mocks" "github.com/stretchr/testify/assert" @@ -13,6 +14,9 @@ import ( ) func TestEnableActorModules(t *testing.T) { + privateKey, err := crypto.GeneratePrivateKey() + require.NoError(t, err) + tests := []struct { name string config *configs.Config @@ -24,7 +28,10 @@ func TestEnableActorModules(t *testing.T) { { name: "servicer only", config: &configs.Config{ - Servicer: &configs.ServicerConfig{Enabled: true}, + Servicer: &configs.ServicerConfig{ + Enabled: true, + PrivateKey: privateKey.String(), + }, }, expectedNames: []string{"servicer"}, }, @@ -46,7 +53,10 @@ func TestEnableActorModules(t *testing.T) { name: "validator and servicer", config: &configs.Config{ Validator: &configs.ValidatorConfig{Enabled: true}, - Servicer: &configs.ServicerConfig{Enabled: true}, + Servicer: &configs.ServicerConfig{ + Enabled: true, + PrivateKey: privateKey.String(), + }, }, expectedNames: []string{"validator", "servicer"}, }, diff --git a/utility/servicer/module.go b/utility/servicer/module.go index dcd4c48f1..18871633d 100644 --- a/utility/servicer/module.go +++ b/utility/servicer/module.go @@ -17,7 +17,7 @@ import ( "github.com/pokt-network/pocket/runtime/configs" "github.com/pokt-network/pocket/shared/codec" coreTypes "github.com/pokt-network/pocket/shared/core/types" - "github.com/pokt-network/pocket/shared/crypto" + cryptoPocket "github.com/pokt-network/pocket/shared/crypto" "github.com/pokt-network/pocket/shared/modules" "github.com/pokt-network/pocket/shared/modules/base_modules" "github.com/pokt-network/pocket/shared/utils" @@ -60,6 +60,13 @@ type servicer struct { // totalTokens is a mapping from application public keys to session metadata to keep track of session tokens // OPTIMIZE: There is an opportunity to simplify the code through various means such as, but not limited to, avoiding extra math.big operations or excess GetParam calls totalTokens map[string]*sessionTokens + + // private key of the servicer, used to sign the served relays. It is parsed from the private key provided in the servicer's configuration. + privateKey cryptoPocket.Ed25519PrivateKey + // address of the servicer, calculated from the provided private key. + address string + // public key of the servicer, calculated from the provided private key. + publicKey string } var ( @@ -90,6 +97,15 @@ func (*servicer) Create(bus modules.Bus, options ...modules.ModuleOption) (modul cfg := bus.GetRuntimeMgr().GetConfig() s.config = cfg.Servicer + privateKey, err := cryptoPocket.NewPrivateKey(cfg.Servicer.PrivateKey) + if err != nil { + return nil, err + } + + s.privateKey = privateKey.(cryptoPocket.Ed25519PrivateKey) + s.address = s.privateKey.Address().String() + s.publicKey = s.privateKey.PublicKey().String() + return s, nil } @@ -161,8 +177,12 @@ func (s *servicer) isRelayVolumeApplicable(session *coreTypes.Session, relay *co return nil, nil, false, fmt.Errorf("Error marshalling relay and/or response: %w", err) } - relayDigest := crypto.SHA3Hash(relayReqResBytes) - signedDigest := s.sign(relayDigest) + relayDigest := cryptoPocket.SHA3Hash(relayReqResBytes) + signedDigest, err := s.sign(relayDigest) + if err != nil { + return nil, relayReqResBytes, false, fmt.Errorf("Error checking volume applicability for relay in session %s: %w", session.Id, err) + } + response.ServicerSignature = hex.EncodeToString(signedDigest) collision, err := s.isRelayVolumeApplicableOnChain(session, relayDigest) if err != nil { @@ -173,9 +193,13 @@ func (s *servicer) isRelayVolumeApplicable(session *coreTypes.Session, relay *co return signedDigest, relayReqResBytes, collision, nil } -// INCOMPLETE(#832): provide a private key to the servicer and use it to sign all relays -func (s *servicer) sign(bz []byte) []byte { - return bz +// sign uses the servicer's private key, provided through configuration, to sign all relay digests. +func (s *servicer) sign(bz []byte) ([]byte, error) { + signature, err := s.privateKey.Sign(bz) + if err != nil { + return nil, fmt.Errorf("Error signing message: %w", err) + } + return signature, nil } // INCOMPLETE: implement this according to the comment below @@ -218,7 +242,7 @@ func (s *servicer) validateRelayMeta(meta *coreTypes.RelayMeta, currentHeight in func (s *servicer) validateRelayChainSupport(relayChain *coreTypes.Identifiable, currentHeight int64) error { if _, ok := s.config.Services[relayChain.Id]; !ok { - return fmt.Errorf("service %s not supported by servicer %s configuration", relayChain.Id, s.config.Address) + return fmt.Errorf("service %s not supported by servicer %s configuration", relayChain.Id, s.address) } // DISCUSS: either update NewReadContext to take a uint64, or the GetCurrentHeight to return an int64. @@ -229,13 +253,13 @@ func (s *servicer) validateRelayChainSupport(relayChain *coreTypes.Identifiable, defer readCtx.Release() //nolint:errcheck // We only need to make sure the readCtx is released // DISCUSS: should we update the GetServicer signature to take a string instead? - servicer, err := readCtx.GetServicer([]byte(s.config.Address), currentHeight) + servicer, err := readCtx.GetServicer([]byte(s.address), currentHeight) if err != nil { return fmt.Errorf("error reading servicer from persistence: %w", err) } if !slices.Contains(servicer.Chains, relayChain.Id) { - return fmt.Errorf("chain %s not supported by servicer %s configuration fetched from persistence", relayChain.Id, s.config.Address) + return fmt.Errorf("chain %s not supported by servicer %s configuration fetched from persistence", relayChain.Id, s.address) } return nil @@ -322,8 +346,8 @@ func (s *servicer) setAppSessionTokens(session *coreTypes.Session, tokens *sessi // validateServicer makes sure the servicer is A) active in the current session, and B) has not served more than its allocated relays for the session func (s *servicer) validateServicer(meta *coreTypes.RelayMeta, session *coreTypes.Session) error { - if meta.ServicerPublicKey != s.config.PublicKey { - return fmt.Errorf("relay servicer key %s does not match this servicer instance %s", meta.ServicerPublicKey, s.config.PublicKey) + if meta.ServicerPublicKey != s.publicKey { + return fmt.Errorf("relay servicer key %s does not match this servicer instance %s", meta.ServicerPublicKey, s.publicKey) } var found bool diff --git a/utility/servicer/module_test.go b/utility/servicer/module_test.go index 6ab5a5646..1e6f90e0c 100644 --- a/utility/servicer/module_test.go +++ b/utility/servicer/module_test.go @@ -29,7 +29,8 @@ const ( var ( // Initialized in TestMain - testServicer1 *coreTypes.Actor + testServicer1 *coreTypes.Actor + testServicer1PrivateKey crypto.PrivateKey // Initialized in TestMain testApp1 *coreTypes.Actor @@ -50,11 +51,16 @@ func testPublicKey() (publicKey, address string) { // TestMain initialized the test fixtures for all the unit tests in the servicer package func TestMain(m *testing.M) { - servicerPublicKey, servicerAddr := testPublicKey() + privateKey, err := crypto.GeneratePrivateKey() + if err != nil { + log.Fatalf("Error generating private key: %s", err) + } + + testServicer1PrivateKey = privateKey testServicer1 = &coreTypes.Actor{ ActorType: coreTypes.ActorType_ACTOR_TYPE_SERVICER, - Address: servicerAddr, - PublicKey: servicerPublicKey, + Address: privateKey.Address().String(), + PublicKey: privateKey.PublicKey().String(), Chains: []string{"POKT-UnitTestNet"}, StakedAmount: "1000", } @@ -142,7 +148,7 @@ func TestRelay_Admit(t *testing.T) { sessionHeight(testSessionStartingHeight), sessionServicers(testServicer1), ) - mockBus := mockBus(t, &config, uint64(testCurrentHeight), session, testCase.usedSessionTokens) + mockBus := mockBus(t, config, uint64(testCurrentHeight), session, testCase.usedSessionTokens) servicerMod, err := CreateServicer(mockBus) require.NoError(t, err) @@ -189,7 +195,7 @@ func TestRelay_Execute(t *testing.T) { config.Services[svc].Url = ts.URL } - servicer := &servicer{config: &config} + servicer := &servicer{config: config} _, err := servicer.executeRelay(testCase.relay) require.ErrorIs(t, err, testCase.expectedErr) // INCOMPLETE(@adshmh): verify HTTP request properties: payload/headers/user-agent/etc. @@ -197,6 +203,49 @@ func TestRelay_Execute(t *testing.T) { } } +func TestRelay_Sign(t *testing.T) { + testCases := []struct { + name string + privateKey string + expected []byte + expectErr bool + }{ + { + name: "Create fails if private key is missing from config", + expectErr: true, + }, + { + name: "Message is signed using correct private key", + privateKey: testServicer1PrivateKey.String(), + }, + } + + for _, testCase := range testCases { + t.Run(testCase.name, func(t *testing.T) { + config := testServicerConfig(withPrivateKey(testCase.privateKey)) + mockBus := mockBus(t, config, 0, &coreTypes.Session{}, 0) + + servicerMod, err := CreateServicer(mockBus) + if testCase.expectErr { + require.Error(t, err) + return + } + + require.NoError(t, err) + + servicer, ok := servicerMod.(*servicer) + require.True(t, ok) + + message := []byte("message") + signature, err := servicer.sign(message) + require.NoError(t, err) + + isSignatureValid := testServicer1PrivateKey.PublicKey().Verify(message, signature) + require.True(t, isSignatureValid) + }) + } +} + type relayEditor func(*coreTypes.Relay) func testRelayServicer(publicKey string) relayEditor { @@ -258,15 +307,28 @@ func testRelay(editors ...relayEditor) *coreTypes.Relay { return relay } -func testServicerConfig() configs.ServicerConfig { - return configs.ServicerConfig{ - PublicKey: testServicer1.PublicKey, - Address: testServicer1.Address, +type configModifier func(*configs.ServicerConfig) + +func withPrivateKey(key string) func(*configs.ServicerConfig) { + return func(cfg *configs.ServicerConfig) { + cfg.PrivateKey = key + } +} + +func testServicerConfig(editors ...configModifier) *configs.ServicerConfig { + config := configs.ServicerConfig{ + PrivateKey: testServicer1PrivateKey.String(), Services: map[string]*configs.ServiceConfig{ "POKT-UnitTestNet": testServiceConfig1, "ETH-Goerli": testServiceConfig1, }, } + + for _, editor := range editors { + editor(&config) + } + + return &config } type sessionModifier func(*coreTypes.Session)