diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 24d809706..57ea68386 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' --parameter 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 a88a3d8d5..d4534df35 100644 --- a/build/localnet/Tiltfile +++ b/build/localnet/Tiltfile @@ -2,7 +2,8 @@ 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://tests/golang", "test_go") +load("ext://syncback", "syncback") tiltfile_dir = os.path.dirname(config.main_dir) root_dir = os.path.dirname(tiltfile_dir + "/../..") @@ -13,7 +14,7 @@ localnet_config_defaults = { "validators": {"count": 4}, "servicers": {"count": 1}, "fishermen": {"count": 1}, - "full_nodes": {"count": 1} + "full_nodes": {"count": 1}, } localnet_config_file = read_yaml(localnet_config_path, default=localnet_config_defaults) @@ -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 = [ @@ -49,6 +60,7 @@ deps = [ deps_full_path = [root_dir + "/" + depdir for depdir in deps] + # Avoid downloading dependencies if no missing/outdated charts are found def check_helm_dependencies_for_chart(path): check_helm_dependencies = local( @@ -58,6 +70,7 @@ def check_helm_dependencies_for_chart(path): if helm_dependencies_not_ok_count > 1: local("helm dependency update " + path) + check_helm_dependencies_for_chart("dependencies") k8s_yaml(helm("dependencies", name="dependencies")) @@ -78,7 +91,7 @@ local_resource( root_dir=root_dir ), deps=deps_full_path, - labels=['watchers'] + labels=["watchers"], ) local_resource( "debug client: Watch & Compile", @@ -86,16 +99,16 @@ local_resource( root_dir=root_dir ), deps=deps_full_path, - labels=['watchers'] + labels=["watchers"], ) # Builds the cluster manager binary local_resource( - 'cluster manager: Watch & Compile', - 'GOOS=linux go build -o {root_dir}/bin/cluster-manager {root_dir}/build/localnet/cluster-manager/*.go'.format( + "cluster manager: Watch & Compile", + "GOOS=linux go build -o {root_dir}/bin/cluster-manager {root_dir}/build/localnet/cluster-manager/*.go".format( root_dir=root_dir ), deps=deps_full_path, - labels=['watchers'] + labels=["watchers"], ) # Builds and maintains the pocket container image after the binary is built on local machine, restarts a process on code change @@ -126,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 @@ -141,12 +158,12 @@ WORKDIR / COPY bin/cluster-manager /usr/local/bin/cluster-manager COPY bin/p1-linux /usr/local/bin/p1 """, - only=['bin/cluster-manager', 'bin/p1-linux'], + only=["bin/cluster-manager", "bin/p1-linux"], entrypoint=["/usr/local/bin/cluster-manager"], live_update=[ sync("bin/cluster-manager", "/usr/local/bin/cluster-manager"), sync("bin/p1-linux", "/usr/local/bin/p1"), - ] + ], ) # Pushes localnet manifests to the cluster. @@ -162,9 +179,9 @@ k8s_yaml( ) k8s_yaml(["manifests/cli-client.yaml"]) -k8s_resource('dev-cli-client', labels=['client']) -k8s_yaml(['manifests/cluster-manager.yaml']) -k8s_resource('pocket-v1-cluster-manager', labels=['cluster-manager']) +k8s_resource("dev-cli-client", labels=["client"]) +k8s_yaml(["manifests/cluster-manager.yaml"]) +k8s_resource("pocket-v1-cluster-manager", labels=["cluster-manager"]) chart_dir = root_dir + "/charts/pocket" check_helm_dependencies_for_chart(chart_dir) @@ -173,30 +190,36 @@ check_helm_dependencies_for_chart(chart_dir) def formatted_actor_number(n): return local('printf "%03d" ' + str(n)) + # Provisions validators actor_number = 0 for x in range(localnet_config["validators"]["count"]): actor_number = actor_number + 1 formatted_number = formatted_actor_number(actor_number) - k8s_yaml(helm(chart_dir, - name="validator-%s-pocket" % formatted_number, - set=[ - "global.postgresql.auth.postgresPassword=LocalNetPassword", - "image.repository=pocket-image", - "privateKeySecretKeyRef.name=validators-private-keys", - "privateKeySecretKeyRef.key=%s" % formatted_number, - "genesis.preProvisionedGenesis.enabled=false", - "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"] if os.path.exists(chart_dir + "/pocket-validator-overrides.yaml") else [],)) - - k8s_resource("validator-%s-pocket" % formatted_number, labels=['pocket-validators']) + k8s_yaml( + helm( + chart_dir, + name="validator-%s-pocket" % formatted_number, + set=[ + "global.postgresql.auth.postgresPassword=LocalNetPassword", + "image.repository=pocket-image", + "privateKeySecretKeyRef.name=validators-private-keys", + "privateKeySecretKeyRef.key=%s" % formatted_number, + "genesis.preProvisionedGenesis.enabled=false", + "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"] + if os.path.exists(chart_dir + "/pocket-validator-overrides.yaml") + else [], + ) + ) + k8s_resource("validator-%s-pocket" % formatted_number, labels=["pocket-validators"]) # Provisions servicer nodes actor_number = 0 @@ -204,25 +227,30 @@ for x in range(localnet_config["servicers"]["count"]): actor_number = actor_number + 1 formatted_number = formatted_actor_number(actor_number) - k8s_yaml(helm(chart_dir, - name="servicer-%s-pocket" % formatted_number, - set=[ - "global.postgresql.auth.postgresPassword=LocalNetPassword", - "image.repository=pocket-image", - "privateKeySecretKeyRef.name=servicers-private-keys", - "privateKeySecretKeyRef.key=%s" % formatted_number, - "genesis.preProvisionedGenesis.enabled=false", - "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", - ], - values=[chart_dir + "/pocket-servicer-overrides.yaml"] if os.path.exists(chart_dir + "/pocket-servicer-overrides.yaml") else [],)) - - k8s_resource("servicer-%s-pocket" % formatted_number, labels=['pocket-servicers']) + k8s_yaml( + helm( + chart_dir, + name="servicer-%s-pocket" % formatted_number, + set=[ + "global.postgresql.auth.postgresPassword=LocalNetPassword", + "image.repository=pocket-image", + "privateKeySecretKeyRef.name=servicers-private-keys", + "privateKeySecretKeyRef.key=%s" % formatted_number, + "genesis.preProvisionedGenesis.enabled=false", + "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", + ], + values=[chart_dir + "/pocket-servicer-overrides.yaml"] + if os.path.exists(chart_dir + "/pocket-servicer-overrides.yaml") + else [], + ) + ) + k8s_resource("servicer-%s-pocket" % formatted_number, labels=["pocket-servicers"]) # Provisions fishermen nodes actor_number = 0 @@ -230,50 +258,61 @@ for x in range(localnet_config["fishermen"]["count"]): actor_number = actor_number + 1 formatted_number = formatted_actor_number(actor_number) - k8s_yaml(helm(chart_dir, - name="fisherman-%s-pocket" % formatted_number, - set=[ - "global.postgresql.auth.postgresPassword=LocalNetPassword", - "image.repository=pocket-image", - "privateKeySecretKeyRef.name=fishermen-private-keys", - "privateKeySecretKeyRef.key=%s" % formatted_number, - "genesis.preProvisionedGenesis.enabled=false", - "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", - ], - values=[chart_dir + "/pocket-fisherman-overrides.yaml"] if os.path.exists(chart_dir + "/pocket-fisherman-overrides.yaml") else [],)) - - k8s_resource("fisherman-%s-pocket" % formatted_number, labels=['pocket-fishermen']) + k8s_yaml( + helm( + chart_dir, + name="fisherman-%s-pocket" % formatted_number, + set=[ + "global.postgresql.auth.postgresPassword=LocalNetPassword", + "image.repository=pocket-image", + "privateKeySecretKeyRef.name=fishermen-private-keys", + "privateKeySecretKeyRef.key=%s" % formatted_number, + "genesis.preProvisionedGenesis.enabled=false", + "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", + ], + values=[chart_dir + "/pocket-fisherman-overrides.yaml"] + if os.path.exists(chart_dir + "/pocket-fisherman-overrides.yaml") + else [], + ) + ) + + k8s_resource("fisherman-%s-pocket" % formatted_number, labels=["pocket-fishermen"]) # Provisions full nodes 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", + name="full-node-%s-pocket" % formatted_number, + set=[ + "global.postgresql.auth.postgresPassword=LocalNetPassword", + "image.repository=pocket-image", + "privateKeySecretKeyRef.name=misc-private-keys", + "privateKeySecretKeyRef.key=%s" % formatted_number, + "genesis.preProvisionedGenesis.enabled=false", + "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"] + if os.path.exists(chart_dir + "/pocket-full-node-overrides.yaml") + else [], + ) + ) - k8s_yaml(helm(root_dir + "/charts/pocket", - name="full-node-%s-pocket" % formatted_number, - set=[ - "global.postgresql.auth.postgresPassword=LocalNetPassword", - "image.repository=pocket-image", - "privateKeySecretKeyRef.name=misc-private-keys", - "privateKeySecretKeyRef.key=%s" % formatted_number, - "genesis.preProvisionedGenesis.enabled=false", - "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"] if os.path.exists(chart_dir + "/pocket-full-node-overrides.yaml") else [],)) - - k8s_resource("full-node-%s-pocket" % formatted_number, labels=['pocket-full-nodes']) + k8s_resource("full-node-%s-pocket" % formatted_number, labels=["pocket-full-nodes"]) # Exposes grafana k8s_resource( @@ -281,12 +320,15 @@ k8s_resource( workload="dependencies-grafana", extra_pod_selectors=[{"app.kubernetes.io/name": "grafana"}], port_forwards=["42000:3000"], - labels=["monitoring"] + labels=["monitoring"], ) # E2E test button -test_go('e2e-tests', '{root_dir}/e2e/tests'.format(root_dir=root_dir), '.', - extra_args=["-v", "-count=1", "-tags=e2e"], - labels=['e2e-tests'], - trigger_mode=TRIGGER_MODE_MANUAL, +test_go( + "e2e-tests", + "{root_dir}/e2e/tests".format(root_dir=root_dir), + ".", + extra_args=["-v", "-count=1", "-tags=e2e"], + labels=["e2e-tests"], + trigger_mode=TRIGGER_MODE_MANUAL, ) 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/consensus/module_consensus_debugging.go b/consensus/module_consensus_debugging.go index a302f1444..137643df6 100644 --- a/consensus/module_consensus_debugging.go +++ b/consensus/module_consensus_debugging.go @@ -13,11 +13,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 91e3e4eb9..74cc60180 100644 --- a/e2e/tests/query.feature +++ b/e2e/tests/query.feature @@ -1,14 +1,14 @@ Feature: Query Namespace - Scenario: User Wants Help Using The Query Command - Given the user has a validator + Scenario: User Wants Help Using The Query Command + 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 + 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 52% rename from e2e/tests/valdator.feature rename to e2e/tests/validator.feature index ec8a2ca47..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 + Scenario: User Wants Help Using The Validator 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 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 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/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!**