diff --git a/.github/label-actions.yml b/.github/label-actions.yml new file mode 100644 index 000000000..b4dc1814d --- /dev/null +++ b/.github/label-actions.yml @@ -0,0 +1,35 @@ +# When `devnet-e2e-test` is added, also assign `devnet` to the PR. +devnet-e2e-test: + prs: + comment: The CI will now also run the e2e tests on devnet, which increases the time it takes to complete all CI checks. + label: + - devnet + +# When `devnet-e2e-test` is removed, also delete `devnet` from the PR. +-devnet-e2e-test: + prs: + unlabel: + - devnet + +# When `devnet` is added, also assign `push-image` to the PR. +devnet: + prs: + label: + - push-image + +# When `devnet` is removed, also delete `devnet-e2e-test` from the PR. +-devnet: + prs: + unlabel: + - devnet-e2e-test + +# Let the developer know that they need to push another commit after attaching the label to PR. +push-image: + prs: + comment: The image is going to be pushed after the next commit. If you want to run an e2e test, it is necessary to push another commit. You can use `make trigger_ci` to push an empty commit. + +# When `push-image` is removed, also delete `devnet` from the PR. +-push-image: + prs: + unlabel: + - devnet diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md index 88c355a48..1599a1f49 100644 --- a/.github/pull_request_template.md +++ b/.github/pull_request_template.md @@ -44,7 +44,7 @@ Select one or more: ## Testing -- [ ] **Run all unit tests**: `make go_test` +- [ ] **Run all unit tests**: `make go_develop_and_test` - [ ] **Verify Localnet manually**: See the instructions [here](TODO: add link to instructions) ## Sanity Checklist diff --git a/.github/workflows/go.yml b/.github/workflows/go.yml index 1ebbe4a07..6f76334c1 100644 --- a/.github/workflows/go.yml +++ b/.github/workflows/go.yml @@ -8,11 +8,16 @@ on: branches: ["main"] pull_request: +concurrency: + group: ${{ github.head_ref || github.ref_name }} + cancel-in-progress: true + jobs: build: runs-on: ubuntu-latest steps: - name: install ignite + # If this step fails due to ignite.com failing, see #116 for a temporary workaround run: | curl https://get.ignite.com/cli! | bash ignite version @@ -35,8 +40,58 @@ jobs: - name: Generate mocks run: make go_mockgen + - name: Run golangci-lint + run: make go_lint + - name: Build - run: ignite chain build --debug --skip-proto + run: ignite chain build -v --debug --skip-proto - name: Test run: make go_test + + - name: Set up Docker Buildx + if: (github.ref == 'refs/heads/main') || (contains(github.event.pull_request.labels.*.name, 'push-image')) + uses: docker/setup-buildx-action@v3 + + - name: Docker Metadata action + if: (github.ref == 'refs/heads/main') || (contains(github.event.pull_request.labels.*.name, 'push-image')) + id: meta + uses: docker/metadata-action@v5 + env: + DOCKER_METADATA_PR_HEAD_SHA: "true" + with: + images: | + ghcr.io/pokt-network/pocketd + tags: | + type=ref,event=branch + type=ref,event=pr + type=sha + type=sha,format=long + + - name: Login to GitHub Container Registry + if: (github.ref == 'refs/heads/main') || (contains(github.event.pull_request.labels.*.name, 'push-image')) + uses: docker/login-action@v3 + with: + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Copy binary to inside of the Docker context + if: (github.ref == 'refs/heads/main') || (contains(github.event.pull_request.labels.*.name, 'push-image')) + run: | + mkdir -p ./bin # Make sure the bin directory exists + cp $(go env GOPATH)/bin/poktrolld ./bin # Copy the binary to the repo's bin directory + + - name: Build and push Docker image + if: (github.ref == 'refs/heads/main') || (contains(github.event.pull_request.labels.*.name, 'push-image')) + uses: docker/build-push-action@v5 + with: + push: true + tags: ${{ steps.meta.outputs.tags }} + labels: ${{ steps.meta.outputs.labels }} + # NB: Uncomment below if arm64 build is needed; arm64 builds are off by default because build times are significant. + platforms: linux/amd64 #,linux/arm64 + file: Dockerfile.dev + cache-from: type=gha + cache-to: type=gha,mode=max + context: . diff --git a/.github/workflows/label-actions.yml b/.github/workflows/label-actions.yml new file mode 100644 index 000000000..caf1a31cc --- /dev/null +++ b/.github/workflows/label-actions.yml @@ -0,0 +1,21 @@ +name: 'Label Actions' + +on: + issues: + types: [labeled, unlabeled] + pull_request_target: + types: [labeled, unlabeled] + discussion: + types: [labeled, unlabeled] + +permissions: + contents: read + issues: write + pull-requests: write + discussions: write + +jobs: + action: + runs-on: ubuntu-latest + steps: + - uses: dessant/label-actions@v3 diff --git a/.gitignore b/.gitignore index aa7066b08..5dc239e58 100644 --- a/.gitignore +++ b/.gitignore @@ -22,7 +22,6 @@ go.work # Don't commit binaries bin -!bin/.keep # Before we provision the localnet, `ignite` creates the accounts, genesis, etc. for us # As many of the files are dynamic, we only preserve the config files in git history. @@ -57,4 +56,7 @@ ts-client/ **/*_mock.go # Localnet config -localnet_config.yaml \ No newline at end of file +localnet_config.yaml + +# Relase artifacts produced by `ignite chain build --release` +release diff --git a/.golangci.yml b/.golangci.yml new file mode 100644 index 000000000..3dc12bb05 --- /dev/null +++ b/.golangci.yml @@ -0,0 +1,56 @@ +linters-settings: + govet: + check-shadowing: true + +# TODO_TECHDEBT: Enable each linter listed, 1 by 1, fixing issues as they appear. +# Don't forget to delete the `disable-all: true` line as well. +linters: + disable-all: true + enable: +# - govet +# - revive +# - errcheck +# - unused + - goimports + +issues: + exclude-use-default: true + max-issues-per-linter: 0 + max-same-issues: 0 + # TODO_CONSIDERATION/TODO_TECHDEBT: Perhaps we should prefer enforcing the best + # practices suggested by the linters over convention or the default in generated + # code (where possible). The more exceptions we have, the bigger the gaps will be + # in our linting coverage. We could eliminate or reduce these exceptions step- + # by-step. + exclude-rules: + # Exclude cosmos-sdk module genesis.go files as they are generated with an + # empty import block containing a comment used by ignite CLI. + - path: ^x/.+/genesis\.go$ + linters: + - goimports + # Exclude cosmos-sdk module module.go files as they are generated with unused + # parameters and unchecked errors. + - path: ^x/.+/module\.go$ + linters: + - revive + - errcheck + # Exclude cosmos-sdk module tx.go files as they are generated with unused + # constants. + - path: ^x/.+/cli/tx\.go$ + linters: + - unused + # Exclude simulation code as it's generated with lots of unused parameters. + - path: .*/simulation/.*|_simulation\.go$ + linters: + - revive + # Exclude cosmos-sdk module codec files as they are scaffolded with a unused + # paramerters and a comment used by ignite CLI. + - path: ^x/.+/codec.go$ + linters: + - revive + - path: _test\.go$ + linters: + - errcheck + # TODO_IMPROVE: see https://golangci-lint.run/usage/configuration/#issues-configuration + #new: true, + #fix: true, diff --git a/Dockerfile.dev b/Dockerfile.dev new file mode 100644 index 000000000..2d10955e0 --- /dev/null +++ b/Dockerfile.dev @@ -0,0 +1,23 @@ +# This Dockerfile is used to build container image for development purposes. +# It intentionally contains no security features, ships with code and troubleshooting tools. + +FROM golang:1.20 as base + +RUN apt update && \ + apt-get install -y \ + ca-certificates \ + curl jq make + +# enable faster module downloading. +ENV GOPROXY https://proxy.golang.org + +COPY . /poktroll + +WORKDIR /poktroll + +RUN mv /poktroll/bin/poktrolld /usr/bin/poktrolld + +EXPOSE 8545 +EXPOSE 8546 + +ENTRYPOINT ["ignite"] diff --git a/Makefile b/Makefile index e624aedaa..19c8fe59e 100644 --- a/Makefile +++ b/Makefile @@ -12,6 +12,8 @@ POCKET_ADDR_PREFIX = pokt .PHONY: install_ci_deps install_ci_deps: ## Installs `mockgen` go install "github.com/golang/mock/mockgen@v1.6.0" && mockgen --version + go install github.com/golangci/golangci-lint/cmd/golangci-lint@latest && golangci-lint --version + go install golang.org/x/tools/cmd/goimports@latest ######################## ### Makefile Helpers ### @@ -35,9 +37,9 @@ help: ## Prints all the targets in all the Makefiles ### Checks ### ############## -.PHONY: go_version_check +.PHONY: check_go_version # Internal helper target - check go version -go_version_check: +check_go_version: @# Extract the version number from the `go version` command. @GO_VERSION=$$(go version | cut -d " " -f 3 | cut -c 3-) && \ MAJOR_VERSION=$$(echo $$GO_VERSION | cut -d "." -f 1) && \ @@ -48,9 +50,9 @@ go_version_check: exit 1; \ fi -.PHONY: docker_check +.PHONY: check_docker # Internal helper target - check if docker is installed -docker_check: +check_docker: { \ 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 build/localnet/README.md and docs/development/README.md before continuing"; \ @@ -58,6 +60,17 @@ docker_check: fi; \ } +.PHONY: check_godoc +# Internal helper target - check if godoc is installed +check_godoc: + { \ + if ( ! ( command -v godoc >/dev/null )); then \ + echo "Seems like you don't have godoc installed. Make sure you install it via 'go install golang.org/x/tools/cmd/godoc@latest' before continuing"; \ + exit 1; \ + fi; \ + } + + .PHONY: warn_destructive warn_destructive: ## Print WARNING to the user @echo "This is a destructive action that will affect docker resources outside the scope of this repo!" @@ -76,7 +89,7 @@ proto_regen: ## Delete existing protobuf artifacts and regenerate them ####################### .PHONY: docker_wipe -docker_wipe: docker_check warn_destructive prompt_user ## [WARNING] Remove all the docker containers, images and volumes. +docker_wipe: check_docker warn_destructive prompt_user ## [WARNING] Remove all the docker containers, images and volumes. docker ps -a -q | xargs -r -I {} docker stop {} docker ps -a -q | xargs -r -I {} docker rm {} docker images -q | xargs -r -I {} docker rmi {} @@ -104,6 +117,17 @@ localnet_regenesis: ## Regenerate the localnet genesis file cp ${HOME}/.pocket/config/*_key.json $(POCKETD_HOME)/config/ cp ${HOME}/.pocket/config/genesis.json $(POCKETD_HOME)/config/ +############### +### Linting ### +############### + +.PHONY: go_lint +go_lint: ## Run all go linters + golangci-lint run --timeout 5m + +go_imports: check_go_version ## Run goimports on all go files + go run ./tools/scripts/goimports + ############# ### Tests ### ############# @@ -113,22 +137,21 @@ test_e2e: ## Run all E2E tests export POCKET_NODE=$(POCKET_NODE) POCKETD_HOME=../../$(POCKETD_HOME) && go test -v ./e2e/tests/... -tags=e2e .PHONY: go_test -go_test: go_version_check ## Run all go tests +go_test: check_go_version ## Run all go tests go test -v -race -tags test ./... .PHONY: go_test_integration -go_test_integration: go_version_check ## Run all go tests, including integration +go_test_integration: check_go_version ## Run all go tests, including integration go test -v -race -tags test,integration ./... .PHONY: itest -itest: go_version_check ## Run tests iteratively (see usage for more) +itest: check_go_version ## Run tests iteratively (see usage for more) ./tools/scripts/itest.sh $(filter-out $@,$(MAKECMDGOALS)) # catch-all target for itest %: # no-op @: - .PHONY: go_mockgen go_mockgen: ## Use `mockgen` to generate mocks used for testing purposes of all the modules. find . -name "*_mock.go" | xargs --no-run-if-empty rm @@ -136,6 +159,7 @@ go_mockgen: ## Use `mockgen` to generate mocks used for testing purposes of all go generate ./x/gateway/types/ go generate ./x/supplier/types/ go generate ./x/session/types/ + go generate ./pkg/... .PHONY: go_develop go_develop: proto_regen go_mockgen ## Generate protos and mocks @@ -162,6 +186,7 @@ go_develop_and_test: go_develop go_test ## Generate protos, mocks and run all te # TODO - General Purpose catch-all. # TODO_DECIDE - A TODO indicating we need to make a decision and document it using an ADR in the future; https://github.com/pokt-network/pocket-network-protocol/tree/main/ADRs # TODO_TECHDEBT - Not a great implementation, but we need to fix it later. +# TODO_BLOCKER - Similar to TECHDEBT, but of higher priority, urgency & risk prior to the next release # TODO_IMPROVE - A nice to have, but not a priority. It's okay if we never get to this. # TODO_OPTIMIZE - An opportunity for performance improvement if/when it's necessary # TODO_DISCUSS - Probably requires a lengthy offline discussion to understand next steps. @@ -206,11 +231,11 @@ todo_this_commit: ## List all the TODOs needed to be done in this commit .PHONY: gateway_list gateway_list: ## List all the staked gateways - pocketd --home=$(POCKETD_HOME) q gateway list-gateway --node $(POCKET_NODE) + poktrolld --home=$(POCKETD_HOME) q gateway list-gateway --node $(POCKET_NODE) .PHONY: gateway_stake gateway_stake: ## Stake tokens for the gateway specified (must specify the gateway env var) - pocketd --home=$(POCKETD_HOME) tx gateway stake-gateway 1000upokt --keyring-backend test --from $(GATEWAY) --node $(POCKET_NODE) + poktrolld --home=$(POCKETD_HOME) tx gateway stake-gateway 1000upokt --keyring-backend test --from $(GATEWAY) --node $(POCKET_NODE) .PHONY: gateway1_stake gateway1_stake: ## Stake gateway1 @@ -226,7 +251,7 @@ gateway3_stake: ## Stake gateway3 .PHONY: gateway_unstake gateway_unstake: ## Unstake an gateway (must specify the GATEWAY env var) - pocketd --home=$(POCKETD_HOME) tx gateway unstake-gateway --keyring-backend test --from $(GATEWAY) --node $(POCKET_NODE) + poktrolld --home=$(POCKETD_HOME) tx gateway unstake-gateway --keyring-backend test --from $(GATEWAY) --node $(POCKET_NODE) .PHONY: gateway1_unstake gateway1_unstake: ## Unstake gateway1 @@ -246,27 +271,27 @@ gateway3_unstake: ## Unstake gateway3 .PHONY: app_list app_list: ## List all the staked applications - pocketd --home=$(POCKETD_HOME) q application list-application --node $(POCKET_NODE) + poktrolld --home=$(POCKETD_HOME) q application list-application --node $(POCKET_NODE) .PHONY: app_stake -app_stake: ## Stake tokens for the application specified (must specify the APP env var) - pocketd --home=$(POCKETD_HOME) tx application stake-application 1000upokt --keyring-backend test --from $(APP) --node $(POCKET_NODE) +app_stake: ## Stake tokens for the application specified (must specify the APP and SERVICES env vars) + poktrolld --home=$(POCKETD_HOME) tx application stake-application 1000upokt $(SERVICES) --keyring-backend test --from $(APP) --node $(POCKET_NODE) .PHONY: app1_stake app1_stake: ## Stake app1 - APP=app1 make app_stake + APP=app1 SERVICES=anvil,svc1,svc2 make app_stake .PHONY: app2_stake app2_stake: ## Stake app2 - APP=app2 make app_stake + APP=app2 SERVICES=anvil,svc2,svc3 make app_stake .PHONY: app3_stake app3_stake: ## Stake app3 - APP=app3 make app_stake + APP=app3 SERVICES=anvil,svc3,svc4 make app_stake .PHONY: app_unstake app_unstake: ## Unstake an application (must specify the APP env var) - pocketd --home=$(POCKETD_HOME) tx application unstake-application --keyring-backend test --from $(APP) --node $(POCKET_NODE) + poktrolld --home=$(POCKETD_HOME) tx application unstake-application --keyring-backend test --from $(APP) --node $(POCKET_NODE) .PHONY: app1_unstake app1_unstake: ## Unstake app1 @@ -280,33 +305,67 @@ app2_unstake: ## Unstake app2 app3_unstake: ## Unstake app3 APP=app3 make app_unstake +.PHONY: app_delegate +app_delegate: ## Delegate trust to a gateway (must specify the APP and GATEWAY_ADDR env vars). Requires the app to be staked + poktrolld --home=$(POCKETD_HOME) tx application delegate-to-gateway $(GATEWAY_ADDR) --keyring-backend test --from $(APP) --node $(POCKET_NODE) + +.PHONY: app1_delegate_gateway1 +app1_delegate_gateway1: ## Delegate trust to gateway1 + APP=app1 GATEWAY_ADDR=pokt15vzxjqklzjtlz7lahe8z2dfe9nm5vxwwmscne4 make app_delegate + +.PHONY: app2_delegate_gateway2 +app2_delegate_gateway2: ## Delegate trust to gateway2 + APP=app2 GATEWAY_ADDR=pokt15w3fhfyc0lttv7r585e2ncpf6t2kl9uh8rsnyz make app_delegate + +.PHONY: app3_delegate_gateway3 +app3_delegate_gateway3: ## Delegate trust to gateway3 + APP=app3 GATEWAY_ADDR=pokt1zhmkkd0rh788mc9prfq0m2h88t9ge0j83gnxya make app_delegate + +.PHONY: app_undelegate +app_undelegate: ## Undelegate trust to a gateway (must specify the APP and GATEWAY_ADDR env vars). Requires the app to be staked + poktrolld --home=$(POCKETD_HOME) tx application undelegate-from-gateway $(GATEWAY_ADDR) --keyring-backend test --from $(APP) --node $(POCKET_NODE) + +.PHONY: app1_undelegate_gateway1 +app1_undelegate_gateway1: ## Undelegate trust to gateway1 + APP=app1 GATEWAY_ADDR=pokt15vzxjqklzjtlz7lahe8z2dfe9nm5vxwwmscne4 make app_undelegate + +.PHONY: app2_undelegate_gateway2 +app2_undelegate_gateway2: ## Undelegate trust to gateway2 + APP=app2 GATEWAY_ADDR=pokt15w3fhfyc0lttv7r585e2ncpf6t2kl9uh8rsnyz make app_undelegate + +.PHONY: app3_undelegate_gateway3 +app3_undelegate_gateway3: ## Undelegate trust to gateway3 + APP=app3 GATEWAY_ADDR=pokt1zhmkkd0rh788mc9prfq0m2h88t9ge0j83gnxya make app_undelegate + ################# ### Suppliers ### ################# .PHONY: supplier_list supplier_list: ## List all the staked supplier - pocketd --home=$(POCKETD_HOME) q supplier list-supplier --node $(POCKET_NODE) + poktrolld --home=$(POCKETD_HOME) q supplier list-supplier --node $(POCKET_NODE) +# TODO(@Olshansk, @okdas): Add more services (in addition to anvil) for apps and suppliers to stake for. +# TODO_TECHDEBT: svc1, svc2 and svc3 below are only in place to make GetSession testable .PHONY: supplier_stake supplier_stake: ## Stake tokens for the supplier specified (must specify the APP env var) - pocketd --home=$(POCKETD_HOME) tx supplier stake-supplier 1000upokt --keyring-backend test --from $(SUPPLIER) --node $(POCKET_NODE) + poktrolld --home=$(POCKETD_HOME) tx supplier stake-supplier 1000upokt "$(SERVICES)" --keyring-backend test --from $(SUPPLIER) --node $(POCKET_NODE) .PHONY: supplier1_stake supplier1_stake: ## Stake supplier1 - SUPPLIER=supplier1 make supplier_stake + SUPPLIER=supplier1 SERVICES="anvil;http://anvil:8547,svc1;http://localhost:8081" make supplier_stake .PHONY: supplier2_stake supplier2_stake: ## Stake supplier2 - SUPPLIER=supplier2 make supplier_stake + SUPPLIER=supplier2 SERVICES="anvil;http://anvil:8547,svc2;http://localhost:8082" make supplier_stake .PHONY: supplier3_stake supplier3_stake: ## Stake supplier3 - SUPPLIER=supplier3 make supplier_stake + SUPPLIER=supplier3 SERVICES="anvil;http://anvil:8547,svc3;http://localhost:8083" make supplier_stake .PHONY: supplier_unstake supplier_unstake: ## Unstake an supplier (must specify the SUPPLIER env var) - pocketd --home=$(POCKETD_HOME) tx supplier unstake-supplier --keyring-backend test --from $(SUPPLIER) --node $(POCKET_NODE) + poktrolld --home=$(POCKETD_HOME) tx supplier unstake-supplier --keyring-backend test --from $(SUPPLIER) --node $(POCKET_NODE) .PHONY: supplier1_unstake supplier1_unstake: ## Unstake supplier1 @@ -327,22 +386,26 @@ supplier3_unstake: ## Unstake supplier3 .PHONY: acc_balance_query acc_balance_query: ## Query the balance of the account specified (make acc_balance_query ACC=pokt...) @echo "~~~ Balances ~~~" - pocketd --home=$(POCKETD_HOME) q bank balances $(ACC) --node $(POCKET_NODE) + poktrolld --home=$(POCKETD_HOME) q bank balances $(ACC) --node $(POCKET_NODE) @echo "~~~ Spendable Balances ~~~" @echo "Querying spendable balance for $(ACC)" - pocketd --home=$(POCKETD_HOME) q bank spendable-balances $(ACC) --node $(POCKET_NODE) + poktrolld --home=$(POCKETD_HOME) q bank spendable-balances $(ACC) --node $(POCKET_NODE) -.PHONY: acc_balance_query_app_module -acc_balance_query_app_module: ## Query the balance of the network level "application" module +.PHONY: acc_balance_query_module_app +acc_balance_query_module_app: ## Query the balance of the network level "application" module make acc_balance_query ACC=pokt1rl3gjgzexmplmds3tq3r3yk84zlwdl6djzgsvm +.PHONY: acc_balance_query_module_supplier +acc_balance_query_module_supplier: ## Query the balance of the network level "supplier" module + make acc_balance_query ACC=pokt1j40dzzmn6cn9kxku7a5tjnud6hv37vesr5ccaa + .PHONY: acc_balance_query_app1 acc_balance_query_app1: ## Query the balance of app1 make acc_balance_query ACC=pokt1mrqt5f7qh8uxs27cjm9t7v9e74a9vvdnq5jva4 .PHONY: acc_balance_total_supply acc_balance_total_supply: ## Query the total supply of the network - pocketd --home=$(POCKETD_HOME) q bank total --node $(POCKET_NODE) + poktrolld --home=$(POCKETD_HOME) q bank total --node $(POCKET_NODE) ###################### ### Ignite Helpers ### @@ -351,3 +414,21 @@ acc_balance_total_supply: ## Query the total supply of the network .PHONY: ignite_acc_list ignite_acc_list: ## List all the accounts in LocalNet ignite account list --keyring-dir=$(POCKETD_HOME) --keyring-backend test --address-prefix $(POCKET_ADDR_PREFIX) + +################## +### CI Helpers ### +################## + +.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" + git push + +##################### +### Documentation ### +##################### +.PHONY: go_docs +go_docs: check_godoc ## Generate documentation for the project + echo "Visit http://localhost:6060/pkg/pocket/" + godoc -http=:6060 + diff --git a/README.md b/README.md index c73a4b62d..a6b55c5ad 100644 --- a/README.md +++ b/README.md @@ -2,11 +2,35 @@ **poktroll** is a rollup built using [Rollkit](https://rollkit.dev/), [Cosmos SDK](https://docs.cosmos.network) and [CometBFT](https://cometbft.com/), created with [Ignite CLI](https://ignite.com/cli) for the Shannon upgrade of the [Pocket Network](https://pokt.network) blockchain. +- [Where are the docs?](#where-are-the-docs) + - [Roadmap](#roadmap) + - [Godoc](#godoc) + - [Pocket V1 (Shannon) Docs](#pocket-v1-shannon-docs) - [Getting Started](#getting-started) - [Makefile](#makefile) - [Development](#development) - [LocalNet](#localnet) +## Where are the docs? + +_This repository is still young & early. We're focusing on development right now._ + +### Roadmap + +You can find our Roadmap Changelog [here](https://github.com/pokt-network/poktroll/blob/main/docs/roadmap_changelog.md). + +### Godoc + +The godocs for this repository can be found at [pkg.go.dev/github.com/pokt-network/poktroll](https://pkg.go.dev/github.com/pokt-network/poktroll). + +### Pocket V1 (Shannon) Docs + +It is the result of a research spike conducted by the Core [Pocket Network](https://pokt.network/) Protocol Team at [GROVE](https://grove.city/) documented [here](https://www.pokt.network/why-pokt-network-is-rolling-with-rollkit-a-technical-deep-dive/) (deep dive) and [here](https://www.pokt.network/a-sovereign-rollup-and-a-modular-future/) (summary). + +For now, we recommend visiting the links in [pokt-network/pocket/README.md](https://github.com/pokt-network/pocket/blob/main/README.md) as a starting point. + +If you want to contribute to this repository at this stage, you know where to find us. + ## Getting Started ### Makefile diff --git a/Tiltfile b/Tiltfile index 34717c9d2..17521b14b 100644 --- a/Tiltfile +++ b/Tiltfile @@ -1,16 +1,16 @@ -load('ext://restart_process', 'docker_build_with_restart') -load('ext://helm_resource', "helm_resource", 'helm_repo') +load("ext://restart_process", "docker_build_with_restart") +load("ext://helm_resource", "helm_resource", "helm_repo") # A list of directories where changes trigger a hot-reload of the sequencer -hot_reload_dirs = ['app', 'cmd', 'tools', 'x'] +hot_reload_dirs = ["app", "cmd", "tools", "x"] # Create a localnet config file from defaults, and if a default configuration doesn't exist, populate it with default values localnet_config_path = "localnet_config.yaml" localnet_config_defaults = { "relayers": {"count": 1}, "gateways": {"count": 1}, - # By default, we use the `helm_repo` function below to point to the remote repository - # but can update it to the locally cloned repo for testing & development + # By default, we use the `helm_repo` function below to point to the remote repository + # but can update it to the locally cloned repo for testing & development "helm_chart_local_repo": {"enabled": False, "path": "../helm-charts"}, } localnet_config_file = read_yaml(localnet_config_path, default=localnet_config_defaults) @@ -35,6 +35,7 @@ if localnet_config["helm_chart_local_repo"]["enabled"]: sequencer_chart = helm_chart_local_repo + "/charts/poktroll-sequencer" poktroll_chart = helm_chart_local_repo + "/charts/poktroll" + # Import files into Kubernetes ConfigMap def read_files_from_directory(directory): files = listdir(directory) @@ -45,53 +46,109 @@ def read_files_from_directory(directory): config_map_data[filename] = content return config_map_data + def generate_config_map_yaml(name, data): config_map_object = { "apiVersion": "v1", "kind": "ConfigMap", "metadata": {"name": name}, - "data": data + "data": data, } return encode_yaml(config_map_object) + # Import keyring/keybase files into Kubernetes ConfigMap -k8s_yaml(generate_config_map_yaml("pocketd-keys", read_files_from_directory("localnet/pocketd/keyring-test/"))) # pocketd/keys +k8s_yaml( + generate_config_map_yaml( + "pocketd-keys", read_files_from_directory("localnet/pocketd/keyring-test/") + ) +) # pocketd/keys # Import configuration files into Kubernetes ConfigMap -k8s_yaml(generate_config_map_yaml("pocketd-configs", read_files_from_directory("localnet/pocketd/config/"))) # pocketd/configs +k8s_yaml( + generate_config_map_yaml( + "pocketd-configs", read_files_from_directory("localnet/pocketd/config/") + ) +) # pocketd/configs # Hot reload protobuf changes -local_resource('hot-reload: generate protobufs', 'ignite generate proto-go -y', deps=['proto'], labels=["hot-reloading"]) +local_resource( + "hot-reload: generate protobufs", + "ignite generate proto-go -y", + deps=["proto"], + labels=["hot-reloading"], +) # Hot reload the pocketd binary used by the k8s cluster -local_resource('hot-reload: pocketd', 'GOOS=linux ignite chain build --skip-proto --output=./bin --debug -v', deps=hot_reload_dirs, labels=["hot-reloading"], resource_deps=['hot-reload: generate protobufs']) +local_resource( + "hot-reload: pocketd", + "GOOS=linux ignite chain build --skip-proto --output=./bin --debug -v", + deps=hot_reload_dirs, + labels=["hot-reloading"], + resource_deps=["hot-reload: generate protobufs"], +) # Hot reload the local pocketd binary used by the CLI -local_resource('hot-reload: pocketd - local cli', 'ignite chain build --skip-proto --debug -v -o $(go env GOPATH)/bin', deps=hot_reload_dirs, labels=["hot-reloading"], resource_deps=['hot-reload: generate protobufs']) +local_resource( + "hot-reload: pocketd - local cli", + "ignite chain build --skip-proto --debug -v -o $(go env GOPATH)/bin", + deps=hot_reload_dirs, + labels=["hot-reloading"], + resource_deps=["hot-reload: generate protobufs"], +) # Build an image with a pocketd binary docker_build_with_restart( "pocketd", - '.', + ".", dockerfile_contents="""FROM golang:1.20.8 RUN apt-get -q update && apt-get install -qyy curl jq RUN go install github.com/go-delve/delve/cmd/dlv@latest -COPY bin/pocketd /usr/local/bin/pocketd +COPY bin/poktrolld /usr/local/bin/pocketd WORKDIR / """, - only=["./bin/pocketd"], - entrypoint=[ - "/bin/sh", "/scripts/pocket.sh" - ], - live_update=[sync("bin/pocketd", "/usr/local/bin/pocketd")], + only=["./bin/poktrolld"], + entrypoint=["/bin/sh", "/scripts/pocket.sh"], + live_update=[sync("bin/poktrolld", "/usr/local/bin/pocketd")], ) # Run celestia and anvil nodes -k8s_yaml(['localnet/kubernetes/celestia-rollkit.yaml', 'localnet/kubernetes/anvil.yaml']) +k8s_yaml( + ["localnet/kubernetes/celestia-rollkit.yaml", "localnet/kubernetes/anvil.yaml"] +) # Run pocket-specific nodes (sequencer, relayers, etc...) -helm_resource("sequencer", sequencer_chart, flags=['--values=./localnet/kubernetes/values-common.yaml'], image_deps=["pocketd"], image_keys=[('image.repository', 'image.tag')]) -helm_resource("relayers", poktroll_chart, flags=['--values=./localnet/kubernetes/values-common.yaml', '--set=replicaCount=' + str(localnet_config["relayers"]["count"])], image_deps=["pocketd"], image_keys=[('image.repository', 'image.tag')]) +helm_resource( + "sequencer", + sequencer_chart, + flags=["--values=./localnet/kubernetes/values-common.yaml"], + image_deps=["pocketd"], + image_keys=[("image.repository", "image.tag")], +) +helm_resource( + "relayers", + poktroll_chart, + flags=[ + "--values=./localnet/kubernetes/values-common.yaml", + "--set=replicaCount=" + str(localnet_config["relayers"]["count"]), + ], + image_deps=["pocketd"], + image_keys=[("image.repository", "image.tag")], +) -# Configure tilt resources (tilt labels and port forawards) for all of the nodes above -k8s_resource('celestia-rollkit', labels=["blockchains"], port_forwards=['26657', '26658', '26659']) -k8s_resource('sequencer', labels=["blockchains"], resource_deps=['celestia-rollkit'], port_forwards=['36657', '40004']) -k8s_resource('relayers', labels=["blockchains"], resource_deps=['sequencer'], port_forwards=['8545', '8546', '40005']) -k8s_resource('anvil', labels=["blockchains"], port_forwards=['8547']) +# Configure tilt resources (tilt labels and port forwards) for all of the nodes above +k8s_resource( + "celestia-rollkit", + labels=["blockchains"], + port_forwards=["26657", "26658", "26659"], +) +k8s_resource( + "sequencer", + labels=["blockchains"], + resource_deps=["celestia-rollkit"], + port_forwards=["36657", "40004"], +) +k8s_resource( + "relayers", + labels=["blockchains"], + resource_deps=["sequencer"], + port_forwards=["8545", "8546", "40005"], +) +k8s_resource("anvil", labels=["blockchains"], port_forwards=["8547"]) diff --git a/app/app.go b/app/app.go index f640a6ee7..c6a48ee69 100644 --- a/app/app.go +++ b/app/app.go @@ -6,6 +6,7 @@ import ( "io" "os" "path/filepath" + // this line is used by starport scaffolding # stargate/app/moduleImport autocliv1 "cosmossdk.io/api/cosmos/autocli/v1" @@ -111,26 +112,26 @@ import ( ibctm "github.com/cosmos/ibc-go/v7/modules/light-clients/07-tendermint" "github.com/spf13/cast" - appparams "pocket/app/params" - "pocket/docs" - applicationmodule "pocket/x/application" - applicationmodulekeeper "pocket/x/application/keeper" - applicationmoduletypes "pocket/x/application/types" - gatewaymodule "pocket/x/gateway" - gatewaymodulekeeper "pocket/x/gateway/keeper" - gatewaymoduletypes "pocket/x/gateway/types" - pocketmodule "pocket/x/pocket" - pocketmodulekeeper "pocket/x/pocket/keeper" - pocketmoduletypes "pocket/x/pocket/types" - servicemodule "pocket/x/service" - servicemodulekeeper "pocket/x/service/keeper" - servicemoduletypes "pocket/x/service/types" - sessionmodule "pocket/x/session" - sessionmodulekeeper "pocket/x/session/keeper" - sessionmoduletypes "pocket/x/session/types" - suppliermodule "pocket/x/supplier" - suppliermodulekeeper "pocket/x/supplier/keeper" - suppliermoduletypes "pocket/x/supplier/types" + appparams "github.com/pokt-network/poktroll/app/params" + "github.com/pokt-network/poktroll/docs" + applicationmodule "github.com/pokt-network/poktroll/x/application" + applicationmodulekeeper "github.com/pokt-network/poktroll/x/application/keeper" + applicationmoduletypes "github.com/pokt-network/poktroll/x/application/types" + gatewaymodule "github.com/pokt-network/poktroll/x/gateway" + gatewaymodulekeeper "github.com/pokt-network/poktroll/x/gateway/keeper" + gatewaymoduletypes "github.com/pokt-network/poktroll/x/gateway/types" + pocketmodule "github.com/pokt-network/poktroll/x/pocket" + pocketmodulekeeper "github.com/pokt-network/poktroll/x/pocket/keeper" + pocketmoduletypes "github.com/pokt-network/poktroll/x/pocket/types" + servicemodule "github.com/pokt-network/poktroll/x/service" + servicemodulekeeper "github.com/pokt-network/poktroll/x/service/keeper" + servicemoduletypes "github.com/pokt-network/poktroll/x/service/types" + sessionmodule "github.com/pokt-network/poktroll/x/session" + sessionmodulekeeper "github.com/pokt-network/poktroll/x/session/keeper" + sessionmoduletypes "github.com/pokt-network/poktroll/x/session/types" + suppliermodule "github.com/pokt-network/poktroll/x/supplier" + suppliermodulekeeper "github.com/pokt-network/poktroll/x/supplier/keeper" + suppliermoduletypes "github.com/pokt-network/poktroll/x/supplier/types" ) const ( @@ -585,16 +586,6 @@ func New( ) sessionModule := sessionmodule.NewAppModule(appCodec, app.SessionKeeper, app.AccountKeeper, app.BankKeeper) - app.ApplicationKeeper = *applicationmodulekeeper.NewKeeper( - appCodec, - keys[applicationmoduletypes.StoreKey], - keys[applicationmoduletypes.MemStoreKey], - app.GetSubspace(applicationmoduletypes.ModuleName), - - app.BankKeeper, - ) - applicationModule := applicationmodule.NewAppModule(appCodec, app.ApplicationKeeper, app.AccountKeeper, app.BankKeeper) - app.SupplierKeeper = *suppliermodulekeeper.NewKeeper( appCodec, keys[suppliermoduletypes.StoreKey], @@ -612,10 +603,21 @@ func New( app.GetSubspace(gatewaymoduletypes.ModuleName), app.BankKeeper, - app.AccountKeeper, ) gatewayModule := gatewaymodule.NewAppModule(appCodec, app.GatewayKeeper, app.AccountKeeper, app.BankKeeper) + app.ApplicationKeeper = *applicationmodulekeeper.NewKeeper( + appCodec, + keys[applicationmoduletypes.StoreKey], + keys[applicationmoduletypes.MemStoreKey], + app.GetSubspace(applicationmoduletypes.ModuleName), + + app.BankKeeper, + app.AccountKeeper, + app.GatewayKeeper, + ) + applicationModule := applicationmodule.NewAppModule(appCodec, app.ApplicationKeeper, app.AccountKeeper, app.BankKeeper) + // this line is used by starport scaffolding # stargate/app/keeperDefinition /**** IBC Routing ****/ diff --git a/app/encoding.go b/app/encoding.go index e56329d30..3e32bee43 100644 --- a/app/encoding.go +++ b/app/encoding.go @@ -6,7 +6,7 @@ import ( "github.com/cosmos/cosmos-sdk/std" "github.com/cosmos/cosmos-sdk/x/auth/tx" - "pocket/app/params" + "github.com/pokt-network/poktroll/app/params" ) // makeEncodingConfig creates an EncodingConfig for an amino based test configuration. diff --git a/app/simulation_test.go b/app/simulation_test.go index 62df08755..ee0ed33ad 100644 --- a/app/simulation_test.go +++ b/app/simulation_test.go @@ -36,7 +36,7 @@ import ( stakingtypes "github.com/cosmos/cosmos-sdk/x/staking/types" "github.com/stretchr/testify/require" - "pocket/app" + "github.com/pokt-network/poktroll/app" ) type storeKeysPrefixes struct { diff --git a/cmd/pocketd/cmd/config.go b/cmd/pocketd/cmd/config.go index af1cbb064..1182b1159 100644 --- a/cmd/pocketd/cmd/config.go +++ b/cmd/pocketd/cmd/config.go @@ -3,7 +3,7 @@ package cmd import ( sdk "github.com/cosmos/cosmos-sdk/types" - "pocket/app" + "github.com/pokt-network/poktroll/app" ) // InitSDKConfig initializes the SDK's config with the appropriate parameters diff --git a/cmd/pocketd/cmd/root.go b/cmd/pocketd/cmd/root.go index 59c45dcce..5b1fb3276 100644 --- a/cmd/pocketd/cmd/root.go +++ b/cmd/pocketd/cmd/root.go @@ -41,8 +41,8 @@ import ( // this line is used by starport scaffolding # root/moduleImport - "pocket/app" - appparams "pocket/app/params" + "github.com/pokt-network/poktroll/app" + appparams "github.com/pokt-network/poktroll/app/params" ) // NewRootCmd creates a new root command for a Cosmos SDK application diff --git a/cmd/pocketd/main.go b/cmd/pocketd/main.go index ef717a1d5..25c7ebcef 100644 --- a/cmd/pocketd/main.go +++ b/cmd/pocketd/main.go @@ -6,8 +6,8 @@ import ( "github.com/cosmos/cosmos-sdk/server" svrcmd "github.com/cosmos/cosmos-sdk/server/cmd" - "pocket/app" - "pocket/cmd/pocketd/cmd" + "github.com/pokt-network/poktroll/app" + "github.com/pokt-network/poktroll/cmd/pocketd/cmd" ) func main() { diff --git a/config.yml b/config.yml index 4ad66fbc9..8b235fc3f 100644 --- a/config.yml +++ b/config.yml @@ -1,4 +1,6 @@ version: 1 +build: + main: cmd/pocketd accounts: - name: faucet mnemonic: "baby advance work soap slow exclude blur humble lucky rough teach wide chuckle captain rack laundry butter main very cannon donate armor dress follow" @@ -79,6 +81,8 @@ genesis: - amount: "10000" denom: upokt application: + params: + maxDelegatedGateways: 7 applicationList: - address: pokt1mrqt5f7qh8uxs27cjm9t7v9e74a9vvdnq5jva4 stake: diff --git a/docs/pkg/client/README.md b/docs/pkg/client/README.md new file mode 100644 index 000000000..6f4032800 --- /dev/null +++ b/docs/pkg/client/README.md @@ -0,0 +1,147 @@ +# Package `client` + +## Table of Contents + +- [Overview](#overview) +- [Features](#features) +- [Architecture Overview](#architecture-overview) + - [Component Diagram Legend](#component-diagram-legend) + - [Clients Dependency Tree](#clients-dependency-tree) + - [Network Interaction](#network-interaction) +- [Installation](#installation) +- [Usage](#usage) + - [Basic Example](#basic-example) + - [Advanced Usage](#advanced-usage) + - [Configuration](#configuration) +- [API Reference](#api-reference) +- [Best Practices](#best-practices) +- [FAQ](#faq) + + +## Overview + +The `client` package exposes go APIs to facilitate interactions with the Pocket network. +It includes lower-level interfaces for working with transactions and subscribing to events generally, as well as higher-level interfaces for tracking blocks and broadcasting protocol-specific transactions. + +## Features + +| Interface | Description | +|-------------------------|----------------------------------------------------------------------------------------------------| +| **`SupplierClient`** | A high-level client for use by the "supplier" actor. | +| **`TxClient`** | A high-level client used to build, sign, and broadcast transaction from cosmos-sdk messages. | +| **`TxContext`** | Abstracts and encapsulates the transaction building, signing, encoding, and broadcasting concerns. | +| **`BlockClient`** | Exposes methods for receiving notifications about newly committed blocks. | +| **`EventsQueryClient`** | Encapsulates blockchain event subscriptions. | +| **`Connection`** | A transport agnostic communication channel for sending and receiving messages. | +| **`Dialer`** | Abstracts the establishment of connections. | + +## Architecture Overview + +```mermaid +--- +title: Component Diagram Legend +--- +flowchart + +c[Component] +d[Dependency Component] +s[[Subcomponent]] +r[Remote Component] + +c --"direct usage via #DependencyMethod()"--> d +c -."usage via network I/O".-> r +c --> s +``` + +> **Figure 1**: A legend for the component diagrams in this document. + +```mermaid +--- +title: Clients Dependency Tree +--- +flowchart + +sup[SupplierClient] +tx[TxClient] +txctx[[TxContext]] +bl[BlockClient] +evt[EventsQueryClient] +conn[[Connection]] +dial[[Dialer]] + +sup --"#SignAndBroadcast()"--> tx + +tx --"#CommittedBlocksSequence()"--> bl +tx --"#BroadcastTx"--> txctx +tx --"#EventsBytes()"--> evt +bl --"#EventsBytes()"--> evt +evt --> conn +evt --"#DialContext()"--> dial +dial --"(returns)"--> conn +``` + +> **Figure 2**: An overview which articulates the dependency relationships between the various client interfaces and their subcompnents. + +```mermaid +--- +title: Network Interaction +--- +flowchart + +txctx[[TxContext]] +conn[[Connection]] +dial[[Dialer]] + +chain[Blockchain] + +conn <-."subscribed events".-> chain +dial -."RPC subscribe".-> chain +txctx -."tx broadcast".-> chain +txctx -."tx query".-> chain +``` + +> **Figure 3**: An overview of how client subcomponents interact with the network. + +## Installation + +```bash +go get github.com/pokt-network/poktroll/pkg/client +``` + +## Usage + +### Basic Example + +```go +// TODO: Code example showcasing the use of TxClient or any other primary interface. +``` + +### Advanced Usage + +```go +// TODO: Example illustrating advanced features or edge cases of the package. +``` + +### Configuration + +- **TxClientOption**: Function type that modifies the `TxClient` allowing for flexible and optional configurations. +- **EventsQueryClientOption**: Modifies the `EventsQueryClient` to apply custom behaviors or configurations. + +## API Reference + +For the complete API details, see the [godoc](https://pkg.go.dev/github.com/pokt-network/poktroll/pkg/client). + +## Best Practices + +- **Use Abstractions**: Instead of directly communicating with blockchain platforms, leverage the provided interfaces for consistent and error-free interactions. +- **Stay Updated**: With evolving blockchain technologies, ensure to keep the package updated for any new features or security patches. + +## FAQ + +#### How does the `TxClient` interface differ from `TxContext`? + +While `TxClient` is centered around signing and broadcasting transactions, `TxContext` consolidates operational dependencies for the transaction lifecycle, like building, encoding, and querying. + +#### Can I extend or customize the provided interfaces? + +Yes, the package is designed with modularity in mind. You can either implement the interfaces based on your requirements or extend them for additional functionalities. \ No newline at end of file diff --git a/docs/pkg/client/events_query.md b/docs/pkg/client/events_query.md index b5086198b..787eecc8e 100644 --- a/docs/pkg/client/events_query.md +++ b/docs/pkg/client/events_query.md @@ -1,6 +1,19 @@ -# Package `pocket/pkg/client/events_query` - -> An event query package for interfacing with cometbft and the Cosmos SDK, facilitating subscriptions to chain event messages. +# Package `pocket/pkg/client/events_query` + +> An event query package for interfacing with [CometBFT](https://cometbft.com/) and the [Cosmos SDK](https://v1.cosmos.network/sdk), facilitating subscriptions to chain event messages. + +- [Overview](#overview) +- [Architecture Diagrams](#architecture-diagrams) +- [Installation](#installation) +- [Features](#features) +- [Usage](#usage) + - [Basic Example](#basic-example) + - [Advanced Usage](#advanced-usage) + - [Configuration](#configuration) +- [Best Practices](#best-practices) +- [FAQ](#faq) + - [Why use `events_query` over directly using Gorilla WebSockets?](#why-use-events_query-over-directly-using-gorilla-websockets) + - [How can I use a different connection mechanism other than WebSockets?](#how-can-i-use-a-different-connection-mechanism-other-than-websockets) ## Overview @@ -13,7 +26,98 @@ The `events_query` package provides a client interface to subscribe to chain eve ## Architecture Diagrams -[Add diagrams here if needed. For the purpose of this mockup, we'll assume none are provided.] +### Components +```mermaid +--- +title: Component Diagram Legend +--- + +flowchart + + a[Component A] + b[Component B] + c[Component C] + d[Component D] + + a --"A uses B via B#MethodName()"--> b +a =="A returns C from A#MethodName()"==> c +b -."A uses D via network IO".-> d +``` +```mermaid +--- +title: EventsQueryClient Components +--- + +flowchart + + subgraph comet[Cometbft Node] + subgraph rpc[JSON-RPC] + sub[subscribe endpoint] + end + end + + subgraph eqc[EventsQueryClient] + q1_eb[EventsBytesObservable] + q1_conn[Connection] + q1_dial[Dialer] + end + + q1_obsvr1[Observer 1] + q1_obsvr2[Observer 2] + + + q1_obsvr1 --"#Subscribe()"--> q1_eb +q1_obsvr2 --"#Subscribe()"--> q1_eb + + +q1_dial =="#DialContext()"==> q1_conn +q1_eb --"#Receive()"--> q1_conn + +q1_conn -.-> sub + +``` + +### Subscriptions +```mermaid +--- +title: Event Subscription Data Flow +--- + +flowchart + +subgraph comet[Cometbft Node] + subgraph rpc[JSON-RPC] + sub[subscribe endpoint] + end +end + +subgraph eqc[EventsQueryClient] + subgraph q1[Query 1] + q1_eb[EventsBytesObservable] + q1_conn[Connection] + end + subgraph q2[Query 2] + q2_conn[Connection] + q2_eb[EventsBytesObservable] + end +end + +q1_obsvr1[Query 1 Observer 1] +q1_obsvr2[Query 1 Observer 2] +q2_obsvr[Query 2 Observer] + +q1_eb -.-> q1_obsvr1 +q1_eb -.-> q1_obsvr2 +q2_eb -.-> q2_obsvr + + +q1_conn -.-> q1_eb +q2_conn -.-> q2_eb + +sub -.-> q1_conn +sub -.-> q2_conn + +``` ## Installation @@ -23,7 +127,7 @@ go get github.com/pokt-network/poktroll/pkg/client/events_query ## Features -- **Websocket Connection**: Uses the Gorilla WebSockets for implementing the connection interface. +- **Websocket Connection**: Uses the [Gorilla WebSockets](https://github.com/gorilla/websocket) for implementing the connection interface. - **Events Subscription**: Subscribe to chain event messages using a simple query mechanism. - **Dialer Interface**: Offers a `Dialer` interface for constructing connections, which can be easily mocked for tests. - **Observable Pattern**: Integrates the observable pattern, making it easier to react to chain events. @@ -33,55 +137,68 @@ go get github.com/pokt-network/poktroll/pkg/client/events_query ### Basic Example ```go -// Creating a new EventsQueryClient with the default websocket dialer: +ctx := context.Background() + +// Creating a new EventsQueryClient with the default, websocket dialer: cometWebsocketURL := "ws://example.com" evtClient := eventsquery.NewEventsQueryClient(cometWebsocketURL) -// Subscribing to a specific event: -observable, errCh := evtClient.EventsBytes(context.Background(), "your-query-string") +// Subscribing to a specific event, e.g. newly committed blocks: +// (see: https://docs.cosmos.network/v0.47/core/events#subscribing-to-events) +observable := evtClient.EventsBytes(ctx, "tm.event='NewBlock'") + +// Subscribe and receive from the observer channel, typically in some other scope. +observer := observable.Subscribe(ctx) + +// Observer channel closes when the context is cancelled, observer is +// unsubscribed, or after the subscription returns an error. +for eitherEvent := range observer.Ch() { + // (see either.Either: https://github.com/pokt-network/poktroll/blob/main/pkg/either/either.go#L3) + eventBz, err := eitherEvent.ValueOrError() + + // ... +} ``` ### Advanced Usage -[Further advanced examples can be added based on more sophisticated use cases, including setting custom dialers and handling observable outputs.] +```go +// Given some custom dialer & connection implementation, e.g.: +var ( + tcpDialer eventsquery.Dialer = exampletcp.NewTcpDialerImpl() + grcpDialer eventsquery.Dialer = examplegrpc.NewGrpcDialerImpl() +) -### Configuration +// Both TCP and gRPC use the TCP scheme as gRPC uses TCP for its transport layer. +cometUrl = "tcp://example.com" -- **WithDialer**: Configure the client to use a custom dialer for connections. +// Creating new EventsQueryClients with a custom tcpDialer: +tcpDialerOpt := eventsquery.WithDialer(tcpDialer) +tcpEvtClient := eventsquery.NewEventsQueryClient(cometUrl, tcpDialerOpt) -## API Reference +// Alternatively, with a custom gRPC dialer: +gcpDialerOpt := eventsquery.WithDialer(grcpDialer) +grpcEvtClient := eventsquery.NewEventsQueryClient(cometUrl, grpcDialerOpt) -- `EventsQueryClient`: Main interface to query events. Methods include: - - `EventsBytes(ctx, query)`: Returns an observable for chain events. - - `Close()`: Close any existing connections and unsubscribe all observers. -- `Connection`: Interface representing a bidirectional message-passing connection. -- `Dialer`: Interface encapsulating the creation of connections. +// ... rest follows the same as the basic example. +``` -For the complete API details, see the [godoc](https://pkg.go.dev/github.com/yourusername/pocket/pkg/client/events_query). +### Configuration + +- **WithDialer**: Configure the client to use a custom dialer for connections. ## Best Practices - **Connection Handling**: Ensure to close the `EventsQueryClient` when done to free up resources and avoid potential leaks. -- **Error Handling**: Always check the error channel returned by `EventsBytes` for asynchronous errors during operation. +- **Error Handling**: Always check both the synchronous error returned by `EventsBytes` as well as asynchronous errors send over the observable. ## FAQ #### Why use `events_query` over directly using Gorilla WebSockets? -`events_query` abstracts many of the underlying details and provides a streamlined interface for subscribing to chain events. It also integrates the observable pattern and provides mockable interfaces for better testing. +`events_query` abstracts many of the underlying details and provides a streamlined interface for subscribing to chain events. +It also integrates the observable pattern and provides mockable interfaces for better testing. #### How can I use a different connection mechanism other than WebSockets? You can implement the `Dialer` and `Connection` interfaces and use the `WithDialer` configuration to provide your custom dialer. - -## Contributing - -If you're interested in improving the `events_query` package or adding new features, please start by discussing your ideas in the project's issues section. Check our main contributing guide for more details. - -## Changelog - -For detailed release notes, see the [CHANGELOG](../CHANGELOG.md). - -## License - -This package is released under the XYZ License. For more information, see the [LICENSE](../LICENSE) file at the root level. \ No newline at end of file diff --git a/docs/roadmap_changelog.md b/docs/roadmap_changelog.md new file mode 100644 index 000000000..e93fb5948 --- /dev/null +++ b/docs/roadmap_changelog.md @@ -0,0 +1,40 @@ +# Roadmap Changelog + +The purpose of this doc is to keep track of the changes made to the [Shannon roadmap](https://github.com/orgs/pokt-network/projects/144). + +- [Relevant links](#relevant-links) +- [11/01/2023](#11012023) + - [Changes](#changes) + - [After](#after) + - [Before](#before) + +## Relevant links + +- [Shannon Project](https://github.com/orgs/pokt-network/projects/144?query=is%3Aopen+sort%3Aupdated-desc) - GitHub dashboard +- [Shannon Roadmap](https://github.com/orgs/pokt-network/projects/144/views/4?query=is%3Aopen+sort%3Aupdated-desc) - GitHub Roadmap +- [PoktRoll Repo](https://github.com/pokt-network/poktroll) - Source Code +- [PoktRoll Issues](https://github.com/pokt-network/poktroll/issues) - GitHub Issues +- [PoktRoll Milestones](https://github.com/pokt-network/poktroll/milestones) - GitHub Milestones + +## 11/01/2023 + +### Changes + +1. We're adding a 1 week `E2E Relay` iteration to focus solely on finishing off `Foundation` & `Integration` related work needed to enable automating end-to-end relays. +2. We've delayed the `Govern` iteration to next year because: + - It is not a blocker for TestNetT + - here are still open-ended questions from PNF that need to be addressed first. +3. We've introduced `TECHDEBT` iterations to tackle `TODOs` left throughout the code. + - The first iteration will be focused on `TODO_BLOCKER` in the source code + - Details to other iterations will be ironed out closer to the iteration. +4. We have decided to have multiple `Test` iterations, each of which will be focused on testing different components. + - The first iteration will be focused on load testing relays to de-risk permissionless applications and verify the Claim & Proof lifecycle. + - Details to each iteration will be ironed out closer to the iteration. + +### After + +![Screenshot 2023-11-01 at 2 15 09 PM](https://github.com/pokt-network/poktroll/assets/1892194/e8ef99e6-aecc-433b-8a32-5fb42c05cb86) + +### Before + +![Screenshot 2023-11-01 at 11 05 21 AM](https://github.com/pokt-network/poktroll/assets/1892194/0826d4af-d0e1-4edc-a173-362425672c64) diff --git a/docs/static/openapi.yml b/docs/static/openapi.yml index fe874c14c..687106365 100644 --- a/docs/static/openapi.yml +++ b/docs/static/openapi.yml @@ -46472,21 +46472,46 @@ paths: custom method signatures required by gogoproto. - service_ids: + service_configs: type: array items: type: object properties: - id: - type: string - title: Unique identifier for the service - name: - type: string - title: Semantic name for the service + service_id: + title: Unique and semantic identifier for the service + type: object + properties: + id: + type: string + description: Unique identifier for the service + title: >- + NOTE: `ServiceId.Id` may seem redundant but + was desigtned created to enable more complex + service identification + + For example, what if we want to request a + session for a certain service but with some + additional configs that identify it? + name: + type: string + description: >- + (Optional) Semantic human readable name for + the service + title: >- + TODO_TECHDEBT: Name is currently unused but + acts as a reminder than an optional onchain + representation of the service is necessary title: >- - ServiceId message to encapsulate unique and semantic - identifiers for a service on the network + ApplicationServiceConfig holds the service + configuration the application stakes for title: The ID of the service this session is servicing + delegatee_gateway_addresses: + type: array + items: + type: string + title: >- + The Bech32 encoded addresses for all delegatee Gateways, + in a non-nullable slice title: >- Application defines the type used to store an on-chain definition and state for an application @@ -46629,21 +46654,46 @@ paths: custom method signatures required by gogoproto. - service_ids: + service_configs: type: array items: type: object properties: - id: - type: string - title: Unique identifier for the service - name: - type: string - title: Semantic name for the service + service_id: + title: Unique and semantic identifier for the service + type: object + properties: + id: + type: string + description: Unique identifier for the service + title: >- + NOTE: `ServiceId.Id` may seem redundant but was + desigtned created to enable more complex service + identification + + For example, what if we want to request a + session for a certain service but with some + additional configs that identify it? + name: + type: string + description: >- + (Optional) Semantic human readable name for the + service + title: >- + TODO_TECHDEBT: Name is currently unused but acts + as a reminder than an optional onchain + representation of the service is necessary title: >- - ServiceId message to encapsulate unique and semantic - identifiers for a service on the network + ApplicationServiceConfig holds the service configuration + the application stakes for title: The ID of the service this session is servicing + delegatee_gateway_addresses: + type: array + items: + type: string + title: >- + The Bech32 encoded addresses for all delegatee Gateways, + in a non-nullable slice title: >- Application defines the type used to store an on-chain definition and state for an application @@ -46685,6 +46735,13 @@ paths: params: description: params holds all the parameters of this module. type: object + properties: + max_delegated_gateways: + type: string + format: int64 + title: >- + The maximum number of gateways an application can delegate + trust to description: >- QueryParamsResponse is response type for the Query/Params RPC method. @@ -47039,19 +47096,31 @@ paths: properties: id: type: string - title: Unique identifier for the service + description: Unique identifier for the service + title: >- + NOTE: `ServiceId.Id` may seem redundant but was + desigtned created to enable more complex service + identification + + For example, what if we want to request a session + for a certain service but with some additional + configs that identify it? name: type: string - title: >- + description: >- (Optional) Semantic human readable name for the service + title: >- + TODO_TECHDEBT: Name is currently unused but acts + as a reminder than an optional onchain + representation of the service is necessary session_start_block_height: type: string format: int64 title: The height at which this session started session_id: type: string - description: A unique pseudoranom ID for this session/ + description: A unique pseudoranom ID for this session title: >- NOTE: session_id can be derived from the above values using on-chain but is included in the header for @@ -47099,23 +47168,46 @@ paths: custom method signatures required by gogoproto. - service_ids: + service_configs: type: array items: type: object properties: - id: - type: string - title: Unique identifier for the service - name: - type: string - title: >- - (Optional) Semantic human readable name for the - service + service_id: + title: Unique and semantic identifier for the service + type: object + properties: + id: + type: string + description: Unique identifier for the service + title: >- + NOTE: `ServiceId.Id` may seem redundant but + was desigtned created to enable more complex + service identification + + For example, what if we want to request a + session for a certain service but with some + additional configs that identify it? + name: + type: string + description: >- + (Optional) Semantic human readable name for + the service + title: >- + TODO_TECHDEBT: Name is currently unused but + acts as a reminder than an optional onchain + representation of the service is necessary title: >- - ServiceId message to encapsulate unique and semantic - identifiers for a service on the network + ApplicationServiceConfig holds the service + configuration the application stakes for title: The ID of the service this session is servicing + delegatee_gateway_addresses: + type: array + items: + type: string + title: >- + The Bech32 encoded addresses for all delegatee + Gateways, in a non-nullable slice suppliers: type: array items: @@ -47154,12 +47246,25 @@ paths: properties: id: type: string - title: Unique identifier for the service + description: Unique identifier for the service + title: >- + NOTE: `ServiceId.Id` may seem redundant + but was desigtned created to enable more + complex service identification + + For example, what if we want to request a + session for a certain service but with + some additional configs that identify it? name: type: string - title: >- + description: >- (Optional) Semantic human readable name for the service + title: >- + TODO_TECHDEBT: Name is currently unused + but acts as a reminder than an optional + onchain representation of the service is + necessary endpoints: type: array items: @@ -47214,7 +47319,7 @@ paths: Additional configuration options for the endpoint title: >- - Endpoint message to hold service + SupplierEndpoint message to hold service configuration details title: List of endpoints for the service title: >- @@ -47259,12 +47364,25 @@ paths: required: false type: string - name: service_id.id - description: Unique identifier for the service + description: >- + NOTE: `ServiceId.Id` may seem redundant but was desigtned created to + enable more complex service identification + + For example, what if we want to request a session for a certain + service but with some additional configs that identify it? + + + Unique identifier for the service in: query required: false type: string - name: service_id.name - description: (Optional) Semantic human readable name for the service + description: >- + TODO_TECHDEBT: Name is currently unused but acts as a reminder than + an optional onchain representation of the service is necessary + + + (Optional) Semantic human readable name for the service in: query required: false type: string @@ -47394,10 +47512,24 @@ paths: properties: id: type: string - title: Unique identifier for the service + description: Unique identifier for the service + title: >- + NOTE: `ServiceId.Id` may seem redundant but + was desigtned created to enable more complex + service identification + + For example, what if we want to request a + session for a certain service but with some + additional configs that identify it? name: type: string - title: Semantic name for the service + description: >- + (Optional) Semantic human readable name for + the service + title: >- + TODO_TECHDEBT: Name is currently unused but + acts as a reminder than an optional onchain + representation of the service is necessary endpoints: type: array items: @@ -47452,8 +47584,8 @@ paths: Additional configuration options for the endpoint title: >- - Endpoint message to hold service configuration - details + SupplierEndpoint message to hold service + configuration details title: List of endpoints for the service title: >- SupplierServiceConfig holds the service configuration @@ -47612,10 +47744,24 @@ paths: properties: id: type: string - title: Unique identifier for the service + description: Unique identifier for the service + title: >- + NOTE: `ServiceId.Id` may seem redundant but was + desigtned created to enable more complex service + identification + + For example, what if we want to request a + session for a certain service but with some + additional configs that identify it? name: type: string - title: Semantic name for the service + description: >- + (Optional) Semantic human readable name for the + service + title: >- + TODO_TECHDEBT: Name is currently unused but acts + as a reminder than an optional onchain + representation of the service is necessary endpoints: type: array items: @@ -47670,8 +47816,8 @@ paths: Additional configuration options for the endpoint title: >- - Endpoint message to hold service configuration - details + SupplierEndpoint message to hold service + configuration details title: List of endpoints for the service title: >- SupplierServiceConfig holds the service configuration @@ -76465,21 +76611,43 @@ definitions: NOTE: The amount field is an Int which implements the custom method signatures required by gogoproto. - service_ids: + service_configs: type: array items: type: object properties: - id: - type: string - title: Unique identifier for the service - name: - type: string - title: Semantic name for the service + service_id: + title: Unique and semantic identifier for the service + type: object + properties: + id: + type: string + description: Unique identifier for the service + title: >- + NOTE: `ServiceId.Id` may seem redundant but was desigtned + created to enable more complex service identification + + For example, what if we want to request a session for a + certain service but with some additional configs that + identify it? + name: + type: string + description: (Optional) Semantic human readable name for the service + title: >- + TODO_TECHDEBT: Name is currently unused but acts as a + reminder than an optional onchain representation of the + service is necessary title: >- - ServiceId message to encapsulate unique and semantic identifiers for - a service on the network + ApplicationServiceConfig holds the service configuration the + application stakes for title: The ID of the service this session is servicing + delegatee_gateway_addresses: + type: array + items: + type: string + title: >- + The Bech32 encoded addresses for all delegatee Gateways, in a + non-nullable slice title: >- Application defines the type used to store an on-chain definition and state for an application @@ -76493,6 +76661,11 @@ definitions: type: object pocket.application.Params: type: object + properties: + max_delegated_gateways: + type: string + format: int64 + title: The maximum number of gateways an application can delegate trust to description: Params defines the parameters for the module. pocket.application.QueryAllApplicationResponse: type: object @@ -76523,21 +76696,46 @@ definitions: method signatures required by gogoproto. - service_ids: + service_configs: type: array items: type: object properties: - id: - type: string - title: Unique identifier for the service - name: - type: string - title: Semantic name for the service + service_id: + title: Unique and semantic identifier for the service + type: object + properties: + id: + type: string + description: Unique identifier for the service + title: >- + NOTE: `ServiceId.Id` may seem redundant but was + desigtned created to enable more complex service + identification + + For example, what if we want to request a session for + a certain service but with some additional configs + that identify it? + name: + type: string + description: >- + (Optional) Semantic human readable name for the + service + title: >- + TODO_TECHDEBT: Name is currently unused but acts as a + reminder than an optional onchain representation of + the service is necessary title: >- - ServiceId message to encapsulate unique and semantic - identifiers for a service on the network + ApplicationServiceConfig holds the service configuration the + application stakes for title: The ID of the service this session is servicing + delegatee_gateway_addresses: + type: array + items: + type: string + title: >- + The Bech32 encoded addresses for all delegatee Gateways, in a + non-nullable slice title: >- Application defines the type used to store an on-chain definition and state for an application @@ -76594,21 +76792,44 @@ definitions: method signatures required by gogoproto. - service_ids: + service_configs: type: array items: type: object properties: - id: - type: string - title: Unique identifier for the service - name: - type: string - title: Semantic name for the service + service_id: + title: Unique and semantic identifier for the service + type: object + properties: + id: + type: string + description: Unique identifier for the service + title: >- + NOTE: `ServiceId.Id` may seem redundant but was + desigtned created to enable more complex service + identification + + For example, what if we want to request a session for a + certain service but with some additional configs that + identify it? + name: + type: string + description: (Optional) Semantic human readable name for the service + title: >- + TODO_TECHDEBT: Name is currently unused but acts as a + reminder than an optional onchain representation of the + service is necessary title: >- - ServiceId message to encapsulate unique and semantic identifiers - for a service on the network + ApplicationServiceConfig holds the service configuration the + application stakes for title: The ID of the service this session is servicing + delegatee_gateway_addresses: + type: array + items: + type: string + title: >- + The Bech32 encoded addresses for all delegatee Gateways, in a + non-nullable slice title: >- Application defines the type used to store an on-chain definition and state for an application @@ -76618,16 +76839,58 @@ definitions: params: description: params holds all the parameters of this module. type: object + properties: + max_delegated_gateways: + type: string + format: int64 + title: >- + The maximum number of gateways an application can delegate trust + to description: QueryParamsResponse is response type for the Query/Params RPC method. + pocket.shared.ApplicationServiceConfig: + type: object + properties: + service_id: + title: Unique and semantic identifier for the service + type: object + properties: + id: + type: string + description: Unique identifier for the service + title: >- + NOTE: `ServiceId.Id` may seem redundant but was desigtned created + to enable more complex service identification + + For example, what if we want to request a session for a certain + service but with some additional configs that identify it? + name: + type: string + description: (Optional) Semantic human readable name for the service + title: >- + TODO_TECHDEBT: Name is currently unused but acts as a reminder + than an optional onchain representation of the service is + necessary + title: >- + ApplicationServiceConfig holds the service configuration the application + stakes for pocket.shared.ServiceId: type: object properties: id: type: string - title: Unique identifier for the service + description: Unique identifier for the service + title: >- + NOTE: `ServiceId.Id` may seem redundant but was desigtned created to + enable more complex service identification + + For example, what if we want to request a session for a certain + service but with some additional configs that identify it? name: type: string - title: Semantic name for the service + description: (Optional) Semantic human readable name for the service + title: >- + TODO_TECHDEBT: Name is currently unused but acts as a reminder than an + optional onchain representation of the service is necessary title: >- ServiceId message to encapsulate unique and semantic identifiers for a service on the network @@ -76786,17 +77049,28 @@ definitions: properties: id: type: string - title: Unique identifier for the service + description: Unique identifier for the service + title: >- + NOTE: `ServiceId.Id` may seem redundant but was desigtned + created to enable more complex service identification + + For example, what if we want to request a session for a + certain service but with some additional configs that + identify it? name: type: string - title: (Optional) Semantic human readable name for the service + description: (Optional) Semantic human readable name for the service + title: >- + TODO_TECHDEBT: Name is currently unused but acts as a + reminder than an optional onchain representation of the + service is necessary session_start_block_height: type: string format: int64 title: The height at which this session started session_id: type: string - description: A unique pseudoranom ID for this session/ + description: A unique pseudoranom ID for this session title: >- NOTE: session_id can be derived from the above values using on-chain but is included in the header for convenience @@ -76842,21 +77116,46 @@ definitions: method signatures required by gogoproto. - service_ids: + service_configs: type: array items: type: object properties: - id: - type: string - title: Unique identifier for the service - name: - type: string - title: (Optional) Semantic human readable name for the service + service_id: + title: Unique and semantic identifier for the service + type: object + properties: + id: + type: string + description: Unique identifier for the service + title: >- + NOTE: `ServiceId.Id` may seem redundant but was + desigtned created to enable more complex service + identification + + For example, what if we want to request a session + for a certain service but with some additional + configs that identify it? + name: + type: string + description: >- + (Optional) Semantic human readable name for the + service + title: >- + TODO_TECHDEBT: Name is currently unused but acts as + a reminder than an optional onchain representation + of the service is necessary title: >- - ServiceId message to encapsulate unique and semantic - identifiers for a service on the network + ApplicationServiceConfig holds the service configuration the + application stakes for title: The ID of the service this session is servicing + delegatee_gateway_addresses: + type: array + items: + type: string + title: >- + The Bech32 encoded addresses for all delegatee Gateways, in a + non-nullable slice suppliers: type: array items: @@ -76894,12 +77193,24 @@ definitions: properties: id: type: string - title: Unique identifier for the service + description: Unique identifier for the service + title: >- + NOTE: `ServiceId.Id` may seem redundant but was + desigtned created to enable more complex service + identification + + For example, what if we want to request a session + for a certain service but with some additional + configs that identify it? name: type: string - title: >- + description: >- (Optional) Semantic human readable name for the service + title: >- + TODO_TECHDEBT: Name is currently unused but acts + as a reminder than an optional onchain + representation of the service is necessary endpoints: type: array items: @@ -76954,8 +77265,8 @@ definitions: Additional configuration options for the endpoint title: >- - Endpoint message to hold service configuration - details + SupplierEndpoint message to hold service + configuration details title: List of endpoints for the service title: >- SupplierServiceConfig holds the service configuration the @@ -76995,17 +77306,28 @@ definitions: properties: id: type: string - title: Unique identifier for the service + description: Unique identifier for the service + title: >- + NOTE: `ServiceId.Id` may seem redundant but was desigtned + created to enable more complex service identification + + For example, what if we want to request a session for a + certain service but with some additional configs that identify + it? name: type: string - title: (Optional) Semantic human readable name for the service + description: (Optional) Semantic human readable name for the service + title: >- + TODO_TECHDEBT: Name is currently unused but acts as a reminder + than an optional onchain representation of the service is + necessary session_start_block_height: type: string format: int64 title: The height at which this session started session_id: type: string - description: A unique pseudoranom ID for this session/ + description: A unique pseudoranom ID for this session title: >- NOTE: session_id can be derived from the above values using on-chain but is included in the header for convenience @@ -77051,21 +77373,44 @@ definitions: method signatures required by gogoproto. - service_ids: + service_configs: type: array items: type: object properties: - id: - type: string - title: Unique identifier for the service - name: - type: string - title: (Optional) Semantic human readable name for the service + service_id: + title: Unique and semantic identifier for the service + type: object + properties: + id: + type: string + description: Unique identifier for the service + title: >- + NOTE: `ServiceId.Id` may seem redundant but was + desigtned created to enable more complex service + identification + + For example, what if we want to request a session for a + certain service but with some additional configs that + identify it? + name: + type: string + description: (Optional) Semantic human readable name for the service + title: >- + TODO_TECHDEBT: Name is currently unused but acts as a + reminder than an optional onchain representation of the + service is necessary title: >- - ServiceId message to encapsulate unique and semantic identifiers - for a service on the network + ApplicationServiceConfig holds the service configuration the + application stakes for title: The ID of the service this session is servicing + delegatee_gateway_addresses: + type: array + items: + type: string + title: >- + The Bech32 encoded addresses for all delegatee Gateways, in a + non-nullable slice suppliers: type: array items: @@ -77103,12 +77448,24 @@ definitions: properties: id: type: string - title: Unique identifier for the service + description: Unique identifier for the service + title: >- + NOTE: `ServiceId.Id` may seem redundant but was + desigtned created to enable more complex service + identification + + For example, what if we want to request a session for + a certain service but with some additional configs + that identify it? name: type: string - title: >- + description: >- (Optional) Semantic human readable name for the service + title: >- + TODO_TECHDEBT: Name is currently unused but acts as a + reminder than an optional onchain representation of + the service is necessary endpoints: type: array items: @@ -77160,7 +77517,9 @@ definitions: Key-value wrapper for config options, as proto maps can't be keyed by enums title: Additional configuration options for the endpoint - title: Endpoint message to hold service configuration details + title: >- + SupplierEndpoint message to hold service configuration + details title: List of endpoints for the service title: >- SupplierServiceConfig holds the service configuration the @@ -77189,17 +77548,27 @@ definitions: properties: id: type: string - title: Unique identifier for the service + description: Unique identifier for the service + title: >- + NOTE: `ServiceId.Id` may seem redundant but was desigtned created + to enable more complex service identification + + For example, what if we want to request a session for a certain + service but with some additional configs that identify it? name: type: string - title: (Optional) Semantic human readable name for the service + description: (Optional) Semantic human readable name for the service + title: >- + TODO_TECHDEBT: Name is currently unused but acts as a reminder + than an optional onchain representation of the service is + necessary session_start_block_height: type: string format: int64 title: The height at which this session started session_id: type: string - description: A unique pseudoranom ID for this session/ + description: A unique pseudoranom ID for this session title: >- NOTE: session_id can be derived from the above values using on-chain but is included in the header for convenience @@ -77294,10 +77663,21 @@ definitions: properties: id: type: string - title: Unique identifier for the service + description: Unique identifier for the service + title: >- + NOTE: `ServiceId.Id` may seem redundant but was desigtned + created to enable more complex service identification + + For example, what if we want to request a session for a + certain service but with some additional configs that + identify it? name: type: string - title: (Optional) Semantic human readable name for the service + description: (Optional) Semantic human readable name for the service + title: >- + TODO_TECHDEBT: Name is currently unused but acts as a + reminder than an optional onchain representation of the + service is necessary endpoints: type: array items: @@ -77348,7 +77728,7 @@ definitions: Key-value wrapper for config options, as proto maps can't be keyed by enums title: Additional configuration options for the endpoint - title: Endpoint message to hold service configuration details + title: SupplierEndpoint message to hold service configuration details title: List of endpoints for the service title: >- SupplierServiceConfig holds the service configuration the supplier @@ -77405,7 +77785,7 @@ definitions: Key-value wrapper for config options, as proto maps can't be keyed by enums title: Additional configuration options for the endpoint - title: Endpoint message to hold service configuration details + title: SupplierEndpoint message to hold service configuration details pocket.shared.SupplierServiceConfig: type: object properties: @@ -77415,10 +77795,20 @@ definitions: properties: id: type: string - title: Unique identifier for the service + description: Unique identifier for the service + title: >- + NOTE: `ServiceId.Id` may seem redundant but was desigtned created + to enable more complex service identification + + For example, what if we want to request a session for a certain + service but with some additional configs that identify it? name: type: string - title: (Optional) Semantic human readable name for the service + description: (Optional) Semantic human readable name for the service + title: >- + TODO_TECHDEBT: Name is currently unused but acts as a reminder + than an optional onchain representation of the service is + necessary endpoints: type: array items: @@ -77469,7 +77859,7 @@ definitions: Key-value wrapper for config options, as proto maps can't be keyed by enums title: Additional configuration options for the endpoint - title: Endpoint message to hold service configuration details + title: SupplierEndpoint message to hold service configuration details title: List of endpoints for the service title: >- SupplierServiceConfig holds the service configuration the supplier stakes @@ -77525,10 +77915,24 @@ definitions: properties: id: type: string - title: Unique identifier for the service + description: Unique identifier for the service + title: >- + NOTE: `ServiceId.Id` may seem redundant but was + desigtned created to enable more complex service + identification + + For example, what if we want to request a session for + a certain service but with some additional configs + that identify it? name: type: string - title: Semantic name for the service + description: >- + (Optional) Semantic human readable name for the + service + title: >- + TODO_TECHDEBT: Name is currently unused but acts as a + reminder than an optional onchain representation of + the service is necessary endpoints: type: array items: @@ -77580,7 +77984,9 @@ definitions: Key-value wrapper for config options, as proto maps can't be keyed by enums title: Additional configuration options for the endpoint - title: Endpoint message to hold service configuration details + title: >- + SupplierEndpoint message to hold service configuration + details title: List of endpoints for the service title: >- SupplierServiceConfig holds the service configuration the @@ -77653,10 +78059,22 @@ definitions: properties: id: type: string - title: Unique identifier for the service + description: Unique identifier for the service + title: >- + NOTE: `ServiceId.Id` may seem redundant but was + desigtned created to enable more complex service + identification + + For example, what if we want to request a session for a + certain service but with some additional configs that + identify it? name: type: string - title: Semantic name for the service + description: (Optional) Semantic human readable name for the service + title: >- + TODO_TECHDEBT: Name is currently unused but acts as a + reminder than an optional onchain representation of the + service is necessary endpoints: type: array items: @@ -77707,7 +78125,9 @@ definitions: Key-value wrapper for config options, as proto maps can't be keyed by enums title: Additional configuration options for the endpoint - title: Endpoint message to hold service configuration details + title: >- + SupplierEndpoint message to hold service configuration + details title: List of endpoints for the service title: >- SupplierServiceConfig holds the service configuration the diff --git a/docs/template/pkg/README.md b/docs/template/pkg/README.md index 44f41885a..10fdc2755 100644 --- a/docs/template/pkg/README.md +++ b/docs/template/pkg/README.md @@ -70,12 +70,7 @@ If the package can be configured in some way, describe it here: ## API Reference -While `godoc` will provide the detailed API reference, you can highlight or briefly describe key functions, types, or methods here. - -- `FunctionOrType1()`: A short description of its purpose. -- `FunctionOrType2(param Type)`: Another brief description. - -For the complete API details, see the [godoc](https://pkg.go.dev/github.com/yourusername/yourproject/[PackageName]). +For the complete API details, see the [godoc](https://pkg.go.dev/github.com/pokt-network/poktroll/[PackageName]). ## Best Practices @@ -90,16 +85,4 @@ Answer for question 1. #### Question 2? -Answer for question 2. - -## Contributing - -Briefly describe how others can contribute to this package. Link to the main contributing guide if you have one. - -## Changelog - -For detailed release notes, see the [CHANGELOG](../CHANGELOG.md) at the root level or link to a separate CHANGELOG specific to this package. - -## License - -This package is released under the XYZ License. For more information, see the [LICENSE](../LICENSE) file at the root level. \ No newline at end of file +Answer for question 2. \ No newline at end of file diff --git a/e2e/tests/help.feature b/e2e/tests/help.feature index 156ec5194..7d0867f99 100644 --- a/e2e/tests/help.feature +++ b/e2e/tests/help.feature @@ -1,7 +1,7 @@ Feature: Root Namespace - Scenario: User Needs Help - Given the user has the pocketd binary installed - When the user runs the command "help" - Then the user should be able to see standard output containing "Available Commands" - And the pocketd binary should exit without error \ No newline at end of file + Scenario: User Needs Help + Given the user has the pocketd binary installed + When the user runs the command "help" + Then the user should be able to see standard output containing "Available Commands" + And the pocketd binary should exit without error diff --git a/e2e/tests/init_test.go b/e2e/tests/init_test.go index fe831a507..e1284417a 100644 --- a/e2e/tests/init_test.go +++ b/e2e/tests/init_test.go @@ -22,8 +22,8 @@ var ( ) func init() { - addrRe = regexp.MustCompile(`address: (\S+)\s+name: (\S+)`) - amountRe = regexp.MustCompile(`amount: "(.+?)"\s+denom: upokt`) + addrRe = regexp.MustCompile(`address:\s+(\S+)\s+name:\s+(\S+)`) + amountRe = regexp.MustCompile(`amount:\s+"(.+?)"\s+denom:\s+upokt`) } type suite struct { @@ -123,6 +123,85 @@ func (s *suite) TheUserShouldWaitForSeconds(dur int64) { time.Sleep(time.Duration(dur) * time.Second) } +func (s *suite) TheUserStakesAWithUpoktFromTheAccount(actorType string, amount int64, accName string) { + args := []string{ + "tx", + actorType, + fmt.Sprintf("stake-%s", actorType), + fmt.Sprintf("%dupokt", amount), + "--from", + accName, + keyRingFlag, + "-y", + } + res, err := s.pocketd.RunCommandOnHost("", args...) + if err != nil { + s.Fatalf("error staking %s: %s", actorType, err) + } + s.pocketd.result = res +} + +func (s *suite) TheUserUnstakesAFromTheAccount(actorType string, accName string) { + args := []string{ + "tx", + actorType, + fmt.Sprintf("unstake-%s", actorType), + "--from", + accName, + keyRingFlag, + "-y", + } + res, err := s.pocketd.RunCommandOnHost("", args...) + if err != nil { + s.Fatalf("error unstaking %s: %s", actorType, err) + } + s.pocketd.result = res +} + +func (s *suite) TheForAccountIsNotStaked(actorType, accName string) { + found, _ := s.getStakedAmount(actorType, accName) + if found { + s.Fatalf("account %s should not be staked", accName) + } +} + +func (s *suite) TheForAccountIsStakedWithUpokt(actorType, accName string, amount int64) { + found, stakeAmount := s.getStakedAmount(actorType, accName) + if !found { + s.Fatalf("account %s should be staked", accName) + } + if int64(stakeAmount) != amount { + s.Fatalf("account %s stake amount is not %d", accName, amount) + } +} + +func (s *suite) getStakedAmount(actorType, accName string) (bool, int) { + s.Helper() + args := []string{ + "query", + actorType, + fmt.Sprintf("list-%s", actorType), + } + res, err := s.pocketd.RunCommandOnHost("", args...) + if err != nil { + s.Fatalf("error getting %s: %s", actorType, err) + } + s.pocketd.result = res + found := strings.Contains(res.Stdout, accNameToAddrMap[accName]) + amount := 0 + if found { + escapedAddress := regexp.QuoteMeta(accNameToAddrMap[accName]) + stakedAmountRe := regexp.MustCompile(`address: ` + escapedAddress + `\s+stake:\s+amount: "(\d+)"`) + matches := stakedAmountRe.FindStringSubmatch(res.Stdout) + if len(matches) < 2 { + s.Fatalf("no stake amount found for %s", accName) + } + amount, err = strconv.Atoi(matches[1]) + require.NoError(s, err) + } + return found, amount +} + func (s *suite) buildAddrMap() { s.Helper() res, err := s.pocketd.RunCommand( @@ -131,6 +210,7 @@ func (s *suite) buildAddrMap() { if err != nil { s.Fatalf("error getting keys: %s", err) } + s.pocketd.result = res matches := addrRe.FindAllStringSubmatch(res.Stdout, -1) for _, match := range matches { name := match[2] diff --git a/e2e/tests/node.go b/e2e/tests/node.go index 4e34fa827..e46ad2889 100644 --- a/e2e/tests/node.go +++ b/e2e/tests/node.go @@ -67,7 +67,7 @@ func (p *pocketdBin) RunCommandOnHost(rpcUrl string, args ...string) (*commandRe func (p *pocketdBin) runCmd(args ...string) (*commandResult, error) { base := []string{"--home", defaultHome} args = append(base, args...) - cmd := exec.Command("pocketd", args...) + cmd := exec.Command("poktrolld", args...) r := &commandResult{} out, err := cmd.Output() if err != nil { diff --git a/e2e/tests/send.feature b/e2e/tests/send.feature index dc5fc4504..4df818bf2 100644 --- a/e2e/tests/send.feature +++ b/e2e/tests/send.feature @@ -1,13 +1,13 @@ Feature: Tx Namespace - Scenario: User can send uPOKT - Given the user has the pocketd binary installed - And the account "app1" has a balance greater than "1000" uPOKT - And an account exists for "app2" - When the user sends "1000" uPOKT from account "app1" to account "app2" - Then the user should be able to see standard output containing "txhash:" - And the user should be able to see standard output containing "code: 0" - And the pocketd binary should exit without error - And the user should wait for "5" seconds - And the account balance of "app1" should be "1000" uPOKT "less" than before - And the account balance of "app2" should be "1000" uPOKT "more" than before + Scenario: User can send uPOKT + Given the user has the pocketd binary installed + And the account "app1" has a balance greater than "1000" uPOKT + And an account exists for "app2" + When the user sends "1000" uPOKT from account "app1" to account "app2" + Then the user should be able to see standard output containing "txhash:" + And the user should be able to see standard output containing "code: 0" + And the pocketd binary should exit without error + And the user should wait for "5" seconds + And the account balance of "app1" should be "1000" uPOKT "less" than before + And the account balance of "app2" should be "1000" uPOKT "more" than before diff --git a/e2e/tests/stake.feature b/e2e/tests/stake.feature new file mode 100644 index 000000000..1171ef3d6 --- /dev/null +++ b/e2e/tests/stake.feature @@ -0,0 +1,25 @@ +Feature: Stake Namespaces + + Scenario: User can stake a Gateway + Given the user has the pocketd binary installed + And the "gateway" for account "gateway1" is not staked + And the account "gateway1" has a balance greater than "1000" uPOKT + When the user stakes a "gateway" with "1000" uPOKT from the account "gateway1" + Then the user should be able to see standard output containing "txhash:" + And the user should be able to see standard output containing "code: 0" + And the pocketd binary should exit without error + And the user should wait for "5" seconds + And the "gateway" for account "gateway1" is staked with "1000" uPOKT + And the account balance of "gateway1" should be "1000" uPOKT "less" than before + + Scenario: User can unstake a Gateway + Given the user has the pocketd binary installed + And the "gateway" for account "gateway1" is staked with "1000" uPOKT + And an account exists for "gateway1" + When the user unstakes a "gateway" from the account "gateway1" + Then the user should be able to see standard output containing "txhash:" + And the user should be able to see standard output containing "code: 0" + And the pocketd binary should exit without error + And the user should wait for "5" seconds + And the "gateway" for account "gateway1" is not staked + And the account balance of "gateway1" should be "1000" uPOKT "more" than before diff --git a/go.mod b/go.mod index d54aac8b8..aff37c2b7 100644 --- a/go.mod +++ b/go.mod @@ -1,4 +1,4 @@ -module pocket +module github.com/pokt-network/poktroll go 1.19 @@ -19,6 +19,7 @@ require ( github.com/gorilla/websocket v1.5.0 github.com/grpc-ecosystem/grpc-gateway v1.16.0 github.com/grpc-ecosystem/grpc-gateway/v2 v2.15.2 + github.com/pokt-network/smt v0.7.1 github.com/regen-network/gocuke v0.6.2 github.com/spf13/cast v1.5.1 github.com/spf13/cobra v1.7.0 @@ -136,7 +137,6 @@ require ( github.com/hashicorp/go-immutable-radix v1.3.1 // indirect github.com/hashicorp/go-multierror v1.1.1 // indirect github.com/hashicorp/go-safetemp v1.0.0 // indirect - github.com/hashicorp/go-uuid v1.0.2 // indirect github.com/hashicorp/go-version v1.6.0 // indirect github.com/hashicorp/golang-lru v0.5.5-0.20210104140557-80c98217689d // indirect github.com/hashicorp/golang-lru/v2 v2.0.2 // indirect diff --git a/go.sum b/go.sum index 3da5ad50c..aa6f2c2c6 100644 --- a/go.sum +++ b/go.sum @@ -937,9 +937,8 @@ github.com/hashicorp/go-safetemp v1.0.0/go.mod h1:oaerMy3BhqiTbVye6QuFhFtIceqFoD github.com/hashicorp/go-sockaddr v1.0.0/go.mod h1:7Xibr9yA9JjQq1JpNB2Vw7kxv8xerXegt+ozgdvDeDU= github.com/hashicorp/go-syslog v1.0.0/go.mod h1:qPfqrKkXGihmCqbJM2mZgkZGvKG1dFdvsLplgctolz4= github.com/hashicorp/go-uuid v1.0.0/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= +github.com/hashicorp/go-uuid v1.0.1 h1:fv1ep09latC32wFoVwnqcnKJGnMSdBanPczbHAYm1BE= github.com/hashicorp/go-uuid v1.0.1/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= -github.com/hashicorp/go-uuid v1.0.2 h1:cfejS+Tpcp13yd5nYHWDI6qVCny6wyX2Mt5SGur2IGE= -github.com/hashicorp/go-uuid v1.0.2/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= github.com/hashicorp/go-version v1.2.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA= github.com/hashicorp/go-version v1.2.1/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA= github.com/hashicorp/go-version v1.6.0 h1:feTTfFNnjP967rlCxM/I9g701jU+RN74YKx2mOkIeek= diff --git a/internal/mocks/mockclient/mocks.go b/internal/mocks/mockclient/mocks.go new file mode 100644 index 000000000..d89152942 --- /dev/null +++ b/internal/mocks/mockclient/mocks.go @@ -0,0 +1,11 @@ +package mockclient + +// This file is in place to declare the package for dynamically generated structs. +// +// Note that this does not follow the Cosmos SDK pattern of committing Mocks to main. +// For example, they commit auto-generate code to main: https://github.com/cosmos/cosmos-sdk/blob/main/x/gov/testutil/expected_keepers_mocks.go +// Documentation on how Cosmos uses mockgen can be found here: https://docs.cosmos.network/main/build/building-modules/testing#unit-tests +// +// IMPORTANT: We have attempted to use `.gitkeep` files instead, but it causes a circular dependency issue with protobuf and mock generation +// since we are leveraging `ignite` to compile `.proto` files which runs `go mod tidy` before generating, requiring the entire dependency tree +// to be valid before mock implementations have been generated. diff --git a/internal/mocks/mocks.go b/internal/mocks/mocks.go new file mode 100644 index 000000000..423f63d3e --- /dev/null +++ b/internal/mocks/mocks.go @@ -0,0 +1,10 @@ +package mocks + +// This file is in place to declare the package for dynamically generated structs. +// +// Note that this does not follow the Cosmos SDK pattern of committing Mocks to main. +// For example, they commit auto-generate code to main: https://github.com/cosmos/cosmos-sdk/blob/main/x/gov/testutil/expected_keepers_mocks.go +// Documentation on how Cosmos uses mockgen can be found here: https://docs.cosmos.network/main/build/building-modules/testing#unit-tests +// +// IMPORTANT: We have attempted to use `.gitkeep` files instead, but it causes a circular dependency issue with protobuf and mock generation +// since we are leveraging `ignite` to compile `.proto` files which requires `.go` files to compile. diff --git a/internal/testchannel/drain.go b/internal/testchannel/drain.go index 4ea41a297..4b7ff00a3 100644 --- a/internal/testchannel/drain.go +++ b/internal/testchannel/drain.go @@ -3,7 +3,7 @@ package testchannel import ( "time" - "pocket/pkg/observable" + "github.com/pokt-network/poktroll/pkg/observable" ) // DrainChannel attempts to receive from the given channel, blocking, until it is diff --git a/internal/testclient/keyring.go b/internal/testclient/keyring.go new file mode 100644 index 000000000..59d54f908 --- /dev/null +++ b/internal/testclient/keyring.go @@ -0,0 +1,31 @@ +package testclient + +import ( + "testing" + + cosmoshd "github.com/cosmos/cosmos-sdk/crypto/hd" + cosmoskeyring "github.com/cosmos/cosmos-sdk/crypto/keyring" + "github.com/stretchr/testify/require" +) + +// NewKey creates a new Secp256k1 key and mnemonic for the given name within +// the provided keyring. +func NewKey( + t *testing.T, + name string, + keyring cosmoskeyring.Keyring, +) (key *cosmoskeyring.Record, mnemonic string) { + t.Helper() + + key, mnemonic, err := keyring.NewMnemonic( + name, + cosmoskeyring.English, + "m/44'/118'/0'/0/0", + cosmoskeyring.DefaultBIP39Passphrase, + cosmoshd.Secp256k1, + ) + require.NoError(t, err) + require.NotNil(t, key) + + return key, mnemonic +} diff --git a/internal/testclient/localnet.go b/internal/testclient/localnet.go new file mode 100644 index 000000000..61d5c0ad8 --- /dev/null +++ b/internal/testclient/localnet.go @@ -0,0 +1,69 @@ +package testclient + +import ( + "testing" + + "github.com/cosmos/cosmos-sdk/client" + "github.com/cosmos/cosmos-sdk/client/flags" + authtypes "github.com/cosmos/cosmos-sdk/x/auth/types" + "github.com/spf13/pflag" + "github.com/stretchr/testify/require" + + "github.com/pokt-network/poktroll/app" + "github.com/pokt-network/poktroll/cmd/pocketd/cmd" +) + +// CometLocalWebsocketURL provides a default URL pointing to the localnet websocket endpoint. +const CometLocalWebsocketURL = "ws://localhost:36657/websocket" + +// EncodingConfig encapsulates encoding configurations for the Pocket application. +var EncodingConfig = app.MakeEncodingConfig() + +// init initializes the SDK configuration upon package import. +func init() { + cmd.InitSDKConfig() +} + +// NewLocalnetClientCtx creates a client context specifically tailored for localnet +// environments. The returned client context is initialized with encoding +// configurations, a default home directory, a default account retriever, and +// command flags. +// +// Parameters: +// - t: The testing.T instance used for the current test. +// - flagSet: The set of flags to be read for initializing the client context. +// +// Returns: +// - A pointer to a populated client.Context instance suitable for localnet usage. +func NewLocalnetClientCtx(t *testing.T, flagSet *pflag.FlagSet) *client.Context { + homedir := app.DefaultNodeHome + clientCtx := client.Context{}. + WithCodec(EncodingConfig.Marshaler). + WithTxConfig(EncodingConfig.TxConfig). + WithHomeDir(homedir). + WithAccountRetriever(authtypes.AccountRetriever{}). + WithInterfaceRegistry(EncodingConfig.InterfaceRegistry) + + clientCtx, err := client.ReadPersistentCommandFlags(clientCtx, flagSet) + require.NoError(t, err) + return &clientCtx +} + +// NewLocalnetFlagSet creates a set of predefined flags suitable for a localnet +// testing environment. +// +// Parameters: +// - t: The testing.T instance used for the current test. +// +// Returns: +// - A flag set populated with flags tailored for localnet environments. +func NewLocalnetFlagSet(t *testing.T) *pflag.FlagSet { + mockFlagSet := pflag.NewFlagSet("test", pflag.ContinueOnError) + mockFlagSet.String(flags.FlagNode, "tcp://127.0.0.1:36657", "use localnet poktrolld node") + mockFlagSet.String(flags.FlagHome, "", "use localnet poktrolld node") + mockFlagSet.String(flags.FlagKeyringBackend, "test", "use test keyring") + err := mockFlagSet.Parse([]string{}) + require.NoError(t, err) + + return mockFlagSet +} diff --git a/internal/testclient/testblock/client.go b/internal/testclient/testblock/client.go index 77024a3a8..ebd2ebcd7 100644 --- a/internal/testclient/testblock/client.go +++ b/internal/testclient/testblock/client.go @@ -5,14 +5,19 @@ import ( "testing" "cosmossdk.io/depinject" + "github.com/golang/mock/gomock" "github.com/stretchr/testify/require" - "pocket/internal/testclient" - "pocket/internal/testclient/testeventsquery" - "pocket/pkg/client" - "pocket/pkg/client/block" + "github.com/pokt-network/poktroll/internal/mocks/mockclient" + "github.com/pokt-network/poktroll/internal/testclient" + "github.com/pokt-network/poktroll/internal/testclient/testeventsquery" + "github.com/pokt-network/poktroll/pkg/client" + "github.com/pokt-network/poktroll/pkg/client/block" + "github.com/pokt-network/poktroll/pkg/observable/channel" ) +// NewLocalnetClient creates and returns a new BlockClient that's configured for +// use with the localnet sequencer. func NewLocalnetClient(ctx context.Context, t *testing.T) client.BlockClient { t.Helper() @@ -20,7 +25,76 @@ func NewLocalnetClient(ctx context.Context, t *testing.T) client.BlockClient { require.NotNil(t, queryClient) deps := depinject.Supply(queryClient) - bClient, err := block.NewBlockClient(ctx, deps, testclient.CometWebsocketURL) + bClient, err := block.NewBlockClient(ctx, deps, testclient.CometLocalWebsocketURL) require.NoError(t, err) + return bClient } + +// NewOneTimeCommittedBlocksSequenceBlockClient creates a new mock BlockClient. +// This mock BlockClient will expect a call to CommittedBlocksSequence, and +// when that call is made, it returns a new BlocksObservable that is notified of +// blocks sent on the given blocksPublishCh. +// blocksPublishCh is the channel the caller can use to publish blocks the observable. +func NewOneTimeCommittedBlocksSequenceBlockClient( + t *testing.T, + blocksPublishCh chan client.Block, +) *mockclient.MockBlockClient { + t.Helper() + + // Create a mock for the block client which expects the LatestBlock method to be called any number of times. + blockClientMock := NewAnyTimeLatestBlockBlockClient(t, nil, 0) + + // Set up the mock expectation for the CommittedBlocksSequence method. When + // the method is called, it returns a new replay observable that publishes + // blocks sent on the given blocksPublishCh. + blockClientMock.EXPECT().CommittedBlocksSequence( + gomock.AssignableToTypeOf(context.Background()), + ).DoAndReturn(func(ctx context.Context) client.BlocksObservable { + // Create a new replay observable with a replay buffer size of 1. Blocks + // are published to this observable via the provided blocksPublishCh. + withPublisherOpt := channel.WithPublisher(blocksPublishCh) + obs, _ := channel.NewReplayObservable[client.Block]( + ctx, 1, withPublisherOpt, + ) + return obs + }) + + return blockClientMock +} + +// NewAnyTimeLatestBlockBlockClient creates a mock BlockClient that expects +// calls to the LatestBlock method any number of times. When the LatestBlock +// method is called, it returns a mock Block with the provided hash and height. +func NewAnyTimeLatestBlockBlockClient( + t *testing.T, + hash []byte, + height int64, +) *mockclient.MockBlockClient { + t.Helper() + ctrl := gomock.NewController(t) + + // Create a mock block that returns the provided hash and height. + blockMock := NewAnyTimesBlock(t, hash, height) + // Create a mock block client that expects calls to LatestBlock method and + // returns the mock block. + blockClientMock := mockclient.NewMockBlockClient(ctrl) + blockClientMock.EXPECT().LatestBlock(gomock.Any()).Return(blockMock).AnyTimes() + + return blockClientMock +} + +// NewAnyTimesBlock creates a mock Block that expects calls to Height and Hash +// methods any number of times. When the methods are called, they return the +// provided height and hash respectively. +func NewAnyTimesBlock(t *testing.T, hash []byte, height int64) *mockclient.MockBlock { + t.Helper() + ctrl := gomock.NewController(t) + + // Create a mock block that returns the provided hash and height AnyTimes. + blockMock := mockclient.NewMockBlock(ctrl) + blockMock.EXPECT().Height().Return(height).AnyTimes() + blockMock.EXPECT().Hash().Return(hash).AnyTimes() + + return blockMock +} diff --git a/internal/testclient/testblock/godoc.go b/internal/testclient/testblock/godoc.go new file mode 100644 index 000000000..866bb4f70 --- /dev/null +++ b/internal/testclient/testblock/godoc.go @@ -0,0 +1,4 @@ +// Package testblock provides helper functions for constructing real (e.g. localnet) +// and mock BlockClient objects with pre-configured and/or parameterized call +// arguments, return value(s), and/or expectations thereof. Intended for use in tests. +package testblock diff --git a/internal/testclient/testeventsquery/client.go b/internal/testclient/testeventsquery/client.go index 5f0461ca0..fbf7daeb1 100644 --- a/internal/testclient/testeventsquery/client.go +++ b/internal/testclient/testeventsquery/client.go @@ -1,17 +1,119 @@ package testeventsquery import ( + "context" + "fmt" "testing" + "time" - "pocket/internal/testclient" - "pocket/pkg/client" - eventsquery "pocket/pkg/client/events_query" + cosmoskeyring "github.com/cosmos/cosmos-sdk/crypto/keyring" + "github.com/golang/mock/gomock" + "github.com/stretchr/testify/require" + + "github.com/pokt-network/poktroll/internal/mocks/mockclient" + "github.com/pokt-network/poktroll/internal/testclient" + "github.com/pokt-network/poktroll/pkg/client" + eventsquery "github.com/pokt-network/poktroll/pkg/client/events_query" + "github.com/pokt-network/poktroll/pkg/either" + "github.com/pokt-network/poktroll/pkg/observable/channel" ) -// NewLocalnetClient returns a new events query client which is configured to -// connect to the localnet sequencer. +// NewLocalnetClient creates and returns a new events query client that's configured +// for use with the localnet sequencer. Any options provided are applied to the client. func NewLocalnetClient(t *testing.T, opts ...client.EventsQueryClientOption) client.EventsQueryClient { t.Helper() - return eventsquery.NewEventsQueryClient(testclient.CometWebsocketURL, opts...) + return eventsquery.NewEventsQueryClient(testclient.CometLocalWebsocketURL, opts...) +} + +// NewOneTimeEventsQuery creates a mock of the EventsQueryClient which expects +// a single call to the EventsBytes method. It returns a mock client whose event +// bytes method always constructs a new observable. query is the query string +// for which event bytes subscription is expected to be for. +// The caller can simulate blockchain events by sending on publishCh, the value +// of which is set to the publish channel of the events bytes observable publish +// channel. +func NewOneTimeEventsQuery( + ctx context.Context, + t *testing.T, + query string, + publishCh *chan<- either.Bytes, +) *mockclient.MockEventsQueryClient { + t.Helper() + ctrl := gomock.NewController(t) + + eventsQueryClient := mockclient.NewMockEventsQueryClient(ctrl) + eventsQueryClient.EXPECT().EventsBytes(gomock.Eq(ctx), gomock.Eq(query)). + DoAndReturn(func( + ctx context.Context, + query string, + ) (eventsBzObservable client.EventsBytesObservable, err error) { + eventsBzObservable, *publishCh = channel.NewObservable[either.Bytes]() + return eventsBzObservable, nil + }).Times(1) + return eventsQueryClient +} + +// NewOneTimeTxEventsQueryClient creates a mock of the Events that expects to to +// a single call to the EventsBytes method where the query is for transaction +// events for sender address matching that of the given key. +// The caller can simulate blockchain events by sending on publishCh, the value +// of which is set to the publish channel of the events bytes observable publish +// channel. +func NewOneTimeTxEventsQueryClient( + ctx context.Context, + t *testing.T, + key *cosmoskeyring.Record, + publishCh *chan<- either.Bytes, +) *mockclient.MockEventsQueryClient { + t.Helper() + + signingAddr, err := key.GetAddress() + require.NoError(t, err) + + expectedEventsQuery := fmt.Sprintf( + "tm.event='Tx' AND message.sender='%s'", + signingAddr, + ) + return NewOneTimeEventsQuery( + ctx, t, + expectedEventsQuery, + publishCh, + ) +} + +// NewAnyTimesEventsBytesEventsQueryClient returns a new events query client which +// is configured to return the expected event bytes when queried with the expected +// query, any number of times. The returned client also expects to be closed once. +func NewAnyTimesEventsBytesEventsQueryClient( + ctx context.Context, + t *testing.T, + expectedQuery string, + expectedEventBytes []byte, +) client.EventsQueryClient { + t.Helper() + + ctrl := gomock.NewController(t) + eventsQueryClient := mockclient.NewMockEventsQueryClient(ctrl) + eventsQueryClient.EXPECT().Close().Times(1) + eventsQueryClient.EXPECT(). + EventsBytes(gomock.AssignableToTypeOf(ctx), gomock.Eq(expectedQuery)). + DoAndReturn( + func(ctx context.Context, query string) (client.EventsBytesObservable, error) { + bytesObsvbl, bytesPublishCh := channel.NewReplayObservable[either.Bytes](ctx, 1) + + // Now that the observable is set up, publish the expected event bytes. + // Only need to send once because it's a ReplayObservable. + bytesPublishCh <- either.Success(expectedEventBytes) + + // Wait a tick for the observables to be set up. This isn't strictly + // necessary but is done to mitigate test flakiness. + time.Sleep(10 * time.Millisecond) + + return bytesObsvbl, nil + }, + ). + AnyTimes() + + return eventsQueryClient } diff --git a/internal/testclient/testeventsquery/connection.go b/internal/testclient/testeventsquery/connection.go index 106eeda97..27a38f2ae 100644 --- a/internal/testclient/testeventsquery/connection.go +++ b/internal/testclient/testeventsquery/connection.go @@ -1,21 +1,21 @@ package testeventsquery import ( - "pocket/pkg/either" "testing" "github.com/golang/mock/gomock" - "pocket/internal/mocks/mockclient" + "github.com/pokt-network/poktroll/internal/mocks/mockclient" + "github.com/pokt-network/poktroll/pkg/either" ) -// OneTimeMockConnAndDialer returns a new mock connection and mock dialer that +// NewOneTimeMockConnAndDialer returns a new mock connection and mock dialer that // will return the mock connection when DialContext is called. The mock dialer // will expect DialContext to be called exactly once. The connection mock will // expect Close to be called exactly once. // Callers must mock the Receive method with an EXPECT call before the connection // mock can be used. -func OneTimeMockConnAndDialer(t *testing.T) ( +func NewOneTimeMockConnAndDialer(t *testing.T) ( *mockclient.MockConnection, *mockclient.MockDialer, ) { @@ -25,15 +25,15 @@ func OneTimeMockConnAndDialer(t *testing.T) ( Return(nil). Times(1) - dialerMock := OneTimeMockDialer(t, either.Success(connMock)) + dialerMock := NewOneTimeMockDialer(t, either.Success(connMock)) return connMock, dialerMock } -// OneTimeMockDialer returns a mock dialer that will return either the given +// NewOneTimeMockDialer returns a mock dialer that will return either the given // connection mock or error when DialContext is called. The mock dialer will // expect DialContext to be called exactly once. -func OneTimeMockDialer( +func NewOneTimeMockDialer( t *testing.T, eitherConnMock either.Either[*mockclient.MockConnection], ) *mockclient.MockDialer { diff --git a/internal/testclient/testeventsquery/godoc.go b/internal/testclient/testeventsquery/godoc.go new file mode 100644 index 000000000..0caa02997 --- /dev/null +++ b/internal/testclient/testeventsquery/godoc.go @@ -0,0 +1,5 @@ +// Package testeventsquery provides helper functions for constructing real +// (e.g. localnet) and mock EventsQueryClient objects with pre-configured and/or +// parameterized call arguments, return value(s), and/or expectations thereof. +// Intended for use in tests. +package testeventsquery diff --git a/internal/testclient/testkeyring/keyring.go b/internal/testclient/testkeyring/keyring.go new file mode 100644 index 000000000..40fbc64c8 --- /dev/null +++ b/internal/testclient/testkeyring/keyring.go @@ -0,0 +1,17 @@ +package testkeyring + +import ( + "testing" + + "github.com/cosmos/cosmos-sdk/crypto/keyring" + + "github.com/pokt-network/poktroll/internal/testclient" +) + +// NewTestKeyringWithKey creates a new in-memory keyring with a test key +// with testSigningKeyName as its name. +func NewTestKeyringWithKey(t *testing.T, keyName string) (keyring.Keyring, *keyring.Record) { + keyring := keyring.NewInMemory(testclient.EncodingConfig.Marshaler) + key, _ := testclient.NewKey(t, keyName, keyring) + return keyring, key +} diff --git a/internal/testclient/testsupplier/client.go b/internal/testclient/testsupplier/client.go new file mode 100644 index 000000000..be5df6507 --- /dev/null +++ b/internal/testclient/testsupplier/client.go @@ -0,0 +1,37 @@ +package testsupplier + +import ( + "testing" + + "cosmossdk.io/depinject" + "github.com/stretchr/testify/require" + + "github.com/pokt-network/poktroll/internal/testclient/testtx" + "github.com/pokt-network/poktroll/pkg/client" + "github.com/pokt-network/poktroll/pkg/client/supplier" + "github.com/pokt-network/poktroll/pkg/client/tx" +) + +// NewLocalnetClient creates and returns a new supplier client that connects to +// the localnet sequencer. +func NewLocalnetClient( + t *testing.T, + signingKeyName string, +) client.SupplierClient { + t.Helper() + + txClientOpt := tx.WithSigningKeyName(signingKeyName) + supplierClientOpt := supplier.WithSigningKeyName(signingKeyName) + + txCtx := testtx.NewLocalnetContext(t) + txClient := testtx.NewLocalnetClient(t, txClientOpt) + + deps := depinject.Supply( + txCtx, + txClient, + ) + + supplierClient, err := supplier.NewSupplierClient(deps, supplierClientOpt) + require.NoError(t, err) + return supplierClient +} diff --git a/internal/testclient/testtx/client.go b/internal/testclient/testtx/client.go new file mode 100644 index 000000000..3e843dd91 --- /dev/null +++ b/internal/testclient/testtx/client.go @@ -0,0 +1,93 @@ +package testtx + +import ( + "context" + "testing" + "time" + + "cosmossdk.io/depinject" + cosmostypes "github.com/cosmos/cosmos-sdk/types" + "github.com/golang/mock/gomock" + "github.com/stretchr/testify/require" + + "github.com/pokt-network/poktroll/internal/mocks/mockclient" + "github.com/pokt-network/poktroll/internal/testclient/testblock" + "github.com/pokt-network/poktroll/internal/testclient/testeventsquery" + "github.com/pokt-network/poktroll/pkg/client" + "github.com/pokt-network/poktroll/pkg/client/tx" + "github.com/pokt-network/poktroll/pkg/either" +) + +type signAndBroadcastFn func(context.Context, cosmostypes.Msg) either.AsyncError + +// TODO_CONSIDERATION: functions like these (NewLocalnetXXX) could probably accept +// and return depinject.Config arguments to support shared dependencies. + +// NewLocalnetClient creates and returns a new client for use with the localnet +// sequencer. +func NewLocalnetClient(t *testing.T, opts ...client.TxClientOption) client.TxClient { + t.Helper() + + ctx := context.Background() + txCtx := NewLocalnetContext(t) + eventsQueryClient := testeventsquery.NewLocalnetClient(t) + blockClient := testblock.NewLocalnetClient(ctx, t) + + deps := depinject.Supply( + txCtx, + eventsQueryClient, + blockClient, + ) + + txClient, err := tx.NewTxClient(ctx, deps, opts...) + require.NoError(t, err) + + return txClient +} + +// NewOneTimeDelayedSignAndBroadcastTxClient constructs a mock TxClient with the +// expectation to perform a SignAndBroadcast operation with a specified delay. +func NewOneTimeDelayedSignAndBroadcastTxClient( + t *testing.T, + delay time.Duration, +) *mockclient.MockTxClient { + t.Helper() + + signAndBroadcast := newSignAndBroadcastSucceedsDelayed(delay) + return NewOneTimeSignAndBroadcastTxClient(t, signAndBroadcast) +} + +// NewOneTimeSignAndBroadcastTxClient constructs a mock TxClient with the +// expectation to perform a SignAndBroadcast operation, which will call and receive +// the return from the given signAndBroadcast function. +func NewOneTimeSignAndBroadcastTxClient( + t *testing.T, + signAndBroadcast signAndBroadcastFn, +) *mockclient.MockTxClient { + t.Helper() + + var ctrl = gomock.NewController(t) + + txClient := mockclient.NewMockTxClient(ctrl) + txClient.EXPECT().SignAndBroadcast( + gomock.AssignableToTypeOf(context.Background()), + gomock.Any(), + ).DoAndReturn(signAndBroadcast).Times(1) + + return txClient +} + +// newSignAndBroadcastSucceedsDelayed returns a signAndBroadcastFn that succeeds +// after the given delay. +func newSignAndBroadcastSucceedsDelayed(delay time.Duration) signAndBroadcastFn { + return func(ctx context.Context, msg cosmostypes.Msg) either.AsyncError { + errCh := make(chan error) + + go func() { + time.Sleep(delay) + close(errCh) + }() + + return either.AsyncErr(errCh) + } +} diff --git a/internal/testclient/testtx/context.go b/internal/testclient/testtx/context.go new file mode 100644 index 000000000..134d7b2c4 --- /dev/null +++ b/internal/testclient/testtx/context.go @@ -0,0 +1,273 @@ +package testtx + +import ( + "context" + "fmt" + "testing" + + "cosmossdk.io/depinject" + abci "github.com/cometbft/cometbft/abci/types" + cometbytes "github.com/cometbft/cometbft/libs/bytes" + cometrpctypes "github.com/cometbft/cometbft/rpc/core/types" + comettypes "github.com/cometbft/cometbft/types" + cosmosclient "github.com/cosmos/cosmos-sdk/client" + cosmostx "github.com/cosmos/cosmos-sdk/client/tx" + cosmoskeyring "github.com/cosmos/cosmos-sdk/crypto/keyring" + cosmostypes "github.com/cosmos/cosmos-sdk/types" + "github.com/golang/mock/gomock" + "github.com/stretchr/testify/require" + + "github.com/pokt-network/poktroll/internal/mocks/mockclient" + "github.com/pokt-network/poktroll/internal/testclient" + "github.com/pokt-network/poktroll/pkg/client" + "github.com/pokt-network/poktroll/pkg/client/tx" +) + +// NewLocalnetContext creates and returns a new transaction context configured +// for use with the localnet sequencer. +func NewLocalnetContext(t *testing.T) client.TxContext { + t.Helper() + + flagSet := testclient.NewLocalnetFlagSet(t) + clientCtx := testclient.NewLocalnetClientCtx(t, flagSet) + txFactory, err := cosmostx.NewFactoryCLI(*clientCtx, flagSet) + require.NoError(t, err) + require.NotEmpty(t, txFactory) + + deps := depinject.Supply( + *clientCtx, + txFactory, + ) + + txCtx, err := tx.NewTxContext(deps) + require.NoError(t, err) + + return txCtx +} + +// TODO_IMPROVE: these mock constructor helpers could include parameters for the +// "times" (e.g. exact, min, max) values which are passed to their respective +// gomock.EXPECT() method calls (i.e. Times(), MinTimes(), MaxTimes()). +// When implementing such a pattern, be careful about making assumptions about +// correlations between these "times" values and the contexts in which the expected +// methods may be called. + +// NewOneTimeErrTxTimeoutTxContext creates a mock transaction context designed to +// simulate a specific timeout error scenario during transaction broadcasting. +// expectedErrMsg is populated with the same error message which is presented in +// the result from the QueryTx method so that it can be asserted against. +func NewOneTimeErrTxTimeoutTxContext( + t *testing.T, + keyring cosmoskeyring.Keyring, + signingKeyName string, + expectedErrMsg *string, +) *mockclient.MockTxContext { + t.Helper() + + signerKey, err := keyring.Key(signingKeyName) + require.NoError(t, err) + + signerAddr, err := signerKey.GetAddress() + require.NoError(t, err) + + *expectedErrMsg = fmt.Sprintf( + "fee payer address: %s does not exist: unknown address", + signerAddr.String(), + ) + + var expectedTx cometbytes.HexBytes + txCtxMock := NewBaseTxContext( + t, signingKeyName, + keyring, + &expectedTx, + ) + + // intercept #BroadcastTx() call to mock response and prevent actual broadcast + txCtxMock.EXPECT().BroadcastTx(gomock.Any()). + DoAndReturn( + func(txBytes []byte) (*cosmostypes.TxResponse, error) { + var expectedTxHash cometbytes.HexBytes = comettypes.Tx(txBytes).Hash() + return &cosmostypes.TxResponse{ + Height: 1, + TxHash: expectedTxHash.String(), + }, nil + }, + ).Times(1) + + txCtxMock.EXPECT().QueryTx( + gomock.AssignableToTypeOf(context.Background()), + gomock.AssignableToTypeOf([]byte{}), + gomock.AssignableToTypeOf(false), + ).DoAndReturn( + func( + ctx context.Context, + txHash []byte, + _ bool, + ) (*cometrpctypes.ResultTx, error) { + return &cometrpctypes.ResultTx{ + Hash: txHash, + Height: 1, + TxResult: abci.ResponseDeliverTx{ + Code: 1, + Log: *expectedErrMsg, + Codespace: "test_codespace", + }, + Tx: expectedTx.Bytes(), + }, nil + }, + ) + + return txCtxMock +} + +// NewOneTimeErrCheckTxTxContext creates a mock transaction context to simulate +// a specific error scenario during the ABCI check-tx phase (i.e., during initial +// validation before the transaction is included in the block). +// expectedErrMsg is populated with the same error message which is presented in +// the result from the QueryTx method so that it can be asserted against. +func NewOneTimeErrCheckTxTxContext( + t *testing.T, + keyring cosmoskeyring.Keyring, + signingKeyName string, + expectedErrMsg *string, +) *mockclient.MockTxContext { + t.Helper() + + signerKey, err := keyring.Key(signingKeyName) + require.NoError(t, err) + + signerAddr, err := signerKey.GetAddress() + require.NoError(t, err) + + *expectedErrMsg = fmt.Sprintf( + "fee payer address: %s does not exist: unknown address", + signerAddr.String(), + ) + + var expectedTx cometbytes.HexBytes + txCtxMock := NewBaseTxContext( + t, signingKeyName, + keyring, + &expectedTx, + ) + + // intercept #BroadcastTx() call to mock response and prevent actual broadcast + txCtxMock.EXPECT().BroadcastTx(gomock.Any()). + DoAndReturn( + func(txBytes []byte) (*cosmostypes.TxResponse, error) { + var expectedTxHash cometbytes.HexBytes = comettypes.Tx(txBytes).Hash() + return &cosmostypes.TxResponse{ + Height: 1, + TxHash: expectedTxHash.String(), + RawLog: *expectedErrMsg, + Code: 1, + Codespace: "test_codespace", + }, nil + }, + ).Times(1) + + return txCtxMock +} + +// NewOneTimeTxTxContext creates a mock transaction context primed to respond with +// a single successful transaction response. +func NewOneTimeTxTxContext( + t *testing.T, + keyring cosmoskeyring.Keyring, + signingKeyName string, + expectedTx *cometbytes.HexBytes, +) *mockclient.MockTxContext { + t.Helper() + + txCtxMock := NewBaseTxContext( + t, signingKeyName, + keyring, + expectedTx, + ) + + // intercept #BroadcastTx() call to mock response and prevent actual broadcast + txCtxMock.EXPECT().BroadcastTx(gomock.Any()). + DoAndReturn( + func(txBytes []byte) (*cosmostypes.TxResponse, error) { + var expectedTxHash cometbytes.HexBytes = comettypes.Tx(txBytes).Hash() + return &cosmostypes.TxResponse{ + Height: 1, + TxHash: expectedTxHash.String(), + }, nil + }, + ).Times(1) + + return txCtxMock +} + +// NewBaseTxContext creates a mock transaction context that's configured to expect +// calls to NewTxBuilder, SignTx, and EncodeTx methods, any number of times. +// EncodeTx is used to intercept the encoded transaction bytes and store them in +// the expectedTx output parameter. Each of these methods proxies to the corresponding +// method on a real transaction context. +func NewBaseTxContext( + t *testing.T, + signingKeyName string, + keyring cosmoskeyring.Keyring, + expectedTx *cometbytes.HexBytes, +) *mockclient.MockTxContext { + t.Helper() + + txCtxMock, txCtx := NewAnyTimesTxTxContext(t, keyring) + txCtxMock.EXPECT().NewTxBuilder(). + DoAndReturn(txCtx.NewTxBuilder). + AnyTimes() + txCtxMock.EXPECT().SignTx( + gomock.Eq(signingKeyName), + gomock.AssignableToTypeOf(txCtx.NewTxBuilder()), + gomock.Eq(false), gomock.Eq(false), + ).DoAndReturn(txCtx.SignTx).AnyTimes() + txCtxMock.EXPECT().EncodeTx(gomock.Any()). + DoAndReturn( + func(txBuilder cosmosclient.TxBuilder) (_ []byte, err error) { + // intercept cosmosTxContext#EncodeTx to get the encoded tx cometbytes + *expectedTx, err = txCtx.EncodeTx(txBuilder) + require.NoError(t, err) + return expectedTx.Bytes(), nil + }, + ).AnyTimes() + + return txCtxMock +} + +// NewAnyTimesTxTxContext initializes a mock transaction context that's configured to allow +// arbitrary calls to certain predefined interactions, primarily concerning the retrieval +// of account numbers and sequences. +func NewAnyTimesTxTxContext( + t *testing.T, + keyring cosmoskeyring.Keyring, +) (*mockclient.MockTxContext, client.TxContext) { + t.Helper() + + var ( + ctrl = gomock.NewController(t) + flagSet = testclient.NewLocalnetFlagSet(t) + ) + + // intercept #GetAccountNumberSequence() call to mock response and prevent actual query + accountRetrieverMock := mockclient.NewMockAccountRetriever(ctrl) + accountRetrieverMock.EXPECT().GetAccountNumberSequence(gomock.Any(), gomock.Any()). + Return(uint64(1), uint64(1), nil). + AnyTimes() + + clientCtx := testclient.NewLocalnetClientCtx(t, flagSet). + WithKeyring(keyring). + WithAccountRetriever(accountRetrieverMock) + + txFactory, err := cosmostx.NewFactoryCLI(clientCtx, flagSet) + require.NoError(t, err) + require.NotEmpty(t, txFactory) + + txCtxDeps := depinject.Supply(txFactory, clientCtx) + txCtx, err := tx.NewTxContext(txCtxDeps) + require.NoError(t, err) + txCtxMock := mockclient.NewMockTxContext(ctrl) + txCtxMock.EXPECT().GetKeyring().Return(keyring).AnyTimes() + + return txCtxMock, txCtx +} diff --git a/pkg/client/block/block.go b/pkg/client/block/block.go index 5fe9a2e1e..f5bd94516 100644 --- a/pkg/client/block/block.go +++ b/pkg/client/block/block.go @@ -5,7 +5,7 @@ import ( "github.com/cometbft/cometbft/types" - "pocket/pkg/client" + "github.com/pokt-network/poktroll/pkg/client" ) // cometBlockEvent is used to deserialize incoming committed block event messages diff --git a/pkg/client/block/client.go b/pkg/client/block/client.go index c4b78a2dc..18526508d 100644 --- a/pkg/client/block/client.go +++ b/pkg/client/block/client.go @@ -3,14 +3,39 @@ package block import ( "context" "fmt" - "log" + "time" "cosmossdk.io/depinject" - "pocket/pkg/client" - "pocket/pkg/either" - "pocket/pkg/observable" - "pocket/pkg/observable/channel" + "github.com/pokt-network/poktroll/pkg/client" + "github.com/pokt-network/poktroll/pkg/either" + "github.com/pokt-network/poktroll/pkg/observable" + "github.com/pokt-network/poktroll/pkg/observable/channel" + "github.com/pokt-network/poktroll/pkg/retry" +) + +const ( + // eventsBytesRetryDelay is the delay between retry attempts when the events + // bytes observable returns an error. + eventsBytesRetryDelay = time.Second + // eventsBytesRetryLimit is the maximum number of times to attempt to + // re-establish the events query bytes subscription when the events bytes + // observable returns an error. + eventsBytesRetryLimit = 10 + eventsBytesRetryResetTimeout = 10 * time.Second + // NB: cometbft event subscription query for newly committed blocks. + // (see: https://docs.cosmos.network/v0.47/core/events#subscribing-to-events) + committedBlocksQuery = "tm.event='NewBlock'" + // latestBlockObsvblsReplayBufferSize is the replay buffer size of the + // latestBlockObsvbls replay observable which is used to cache the latest block observable. + // It is updated with a new "active" observable when a new + // events query subscription is created, for example, after a non-persistent + // connection error. + latestBlockObsvblsReplayBufferSize = 1 + // latestBlockReplayBufferSize is the replay buffer size of the latest block + // replay observable which is notified when block commit events are received + // by the events query client subscription created in goPublishBlocks. + latestBlockReplayBufferSize = 1 ) var ( @@ -27,19 +52,26 @@ type blockClient struct { // newly committed block events. It emits an either value which may contain // an error, at most, once and closes immediately after if it does. eventsClient client.EventsQueryClient - // latestBlockObsvblsReplay is a replay observable with replay buffer size 1, + // latestBlockObsvbls is a replay observable with replay buffer size 1, // which holds the "active latest block observable" which is notified when // block commit events are received by the events query client subscription // created in goPublishBlocks. This observable (and the one it emits) closes // when the events bytes observable returns an error and is updated with a // new "active" observable after a new events query subscription is created. - latestBlockObsvblsReplay observable.ReplayObservable[client.BlocksObservable] - // latestBlockObsvblsReplayPublishCh is the publish channel for latestBlockObsvblsReplay. + latestBlockObsvbls observable.ReplayObservable[client.BlocksObservable] + // latestBlockObsvblsReplayPublishCh is the publish channel for latestBlockObsvbls. // It's used to set blockObsvbl initially and subsequently update it, for // example, when the connection is re-established after erroring. latestBlockObsvblsReplayPublishCh chan<- client.BlocksObservable } +// eventsBytesToBlockMapFn is a convenience type to represent the type of a +// function which maps event subscription message bytes into block event objects. +// This is used as a transformFn in a channel.Map() call and is the type returned +// by the newEventsBytesToBlockMapFn factory function. +type eventBytesToBlockMapFn func(either.Either[[]byte]) (client.Block, bool) + +// NewBlockClient creates a new block client from the given dependencies and cometWebsocketURL. func NewBlockClient( ctx context.Context, deps depinject.Config, @@ -47,15 +79,15 @@ func NewBlockClient( ) (client.BlockClient, error) { // Initialize block client bClient := &blockClient{endpointURL: cometWebsocketURL} - bClient.latestBlockObsvblsReplay, bClient.latestBlockObsvblsReplayPublishCh = - channel.NewReplayObservable[client.BlocksObservable](ctx, 1) + bClient.latestBlockObsvbls, bClient.latestBlockObsvblsReplayPublishCh = + channel.NewReplayObservable[client.BlocksObservable](ctx, latestBlockObsvblsReplayBufferSize) // Inject dependencies if err := depinject.Inject(deps, &bClient.eventsClient); err != nil { return nil, err } - // Concurrently publish blocks to the observable emitted by latestBlockObsvblsReplay. + // Concurrently publish blocks to the observable emitted by latestBlockObsvbls. go bClient.goPublishBlocks(ctx) return bClient, nil @@ -65,16 +97,18 @@ func NewBlockClient( // of 1, which is notified when block commit events are received by the events // query subscription. func (bClient *blockClient) CommittedBlocksSequence(ctx context.Context) client.BlocksObservable { - replayedBlocksObservable := bClient.latestBlockObsvblsReplay.Last(ctx, 1)[0] - return replayedBlocksObservable + // Get the latest block observable from the replay observable. We only ever + // want the last 1 as any prior latest block observable values are closed. + // Directly accessing the zeroth index here is safe because the call to Last + // is guaranteed to return a slice with at least 1 element. + return bClient.latestBlockObsvbls.Last(ctx, 1)[0] } // LatestBlock returns the latest committed block that's been received by the // corresponding events query subscription. // It blocks until at least one block event has been received. -func (bClient *blockClient) LatestBlock(ctx context.Context) (latestBlock client.Block) { - v := bClient.CommittedBlocksSequence(ctx).Last(ctx, 1)[0] - return v +func (bClient *blockClient) LatestBlock(ctx context.Context) client.Block { + return bClient.CommittedBlocksSequence(ctx).Last(ctx, 1)[0] } // Close unsubscribes all observers of the committed blocks sequence observable @@ -84,86 +118,92 @@ func (bClient *blockClient) Close() { bClient.eventsClient.Close() } -// goPublishBlocks receives event bytes from the events query client, maps them -// to block events, and publishes them to the latestBlockObsvblsReplay replay observable. +// goPublishBlocks runs the work function returned by retryPublishBlocksFactory, +// re-invoking it according to the arguments to retry.OnError when the events bytes +// observable returns an asynchronous error. +// This function is intended to be called in a goroutine. func (bClient *blockClient) goPublishBlocks(ctx context.Context) { - // NB: cometbft event subscription query - // (see: https://docs.cosmos.network/v0.47/core/events#subscribing-to-events) - query := "tm.event='NewBlock'" - // React to errors by getting a new events bytes observable, re-mapping it, // and send it to latestBlockObsvblsReplayPublishCh such that - // latestBlockObsvblsReplay.Last(ctx, 1) will return it. - retryOnError(ctx, "goPublishBlocks", func() chan error { + // latestBlockObsvbls.Last(ctx, 1) will return it. + publishErr := retry.OnError( + ctx, + eventsBytesRetryLimit, + eventsBytesRetryDelay, + eventsBytesRetryResetTimeout, + "goPublishBlocks", + bClient.retryPublishBlocksFactory(ctx), + ) + + // If we get here, the retry limit was reached and the retry loop exited. + // Since this function runs in a goroutine, we can't return the error to the + // caller. Instead, we panic. + panic(fmt.Errorf("BlockClient.goPublishBlocks shold never reach this spot: %w", publishErr)) +} + +// retryPublishBlocksFactory returns a function which is intended to be passed to +// retry.OnError. The returned function pipes event bytes from the events query +// client, maps them to block events, and publishes them to the latestBlockObsvbls +// replay observable. +func (bClient *blockClient) retryPublishBlocksFactory(ctx context.Context) func() chan error { + return func() chan error { errCh := make(chan error, 1) - eventsBzObsvbl, err := bClient.eventsClient.EventsBytes(ctx, query) + eventsBzObsvbl, err := bClient.eventsClient.EventsBytes(ctx, committedBlocksQuery) if err != nil { errCh <- err return errCh } // NB: must cast back to generic observable type to use with Map. - // client.BlocksObservable is only used to workaround gomock's lack of + // client.BlocksObservable cannot be an alias due to gomock's lack of // support for generic types. eventsBz := observable.Observable[either.Either[[]byte]](eventsBzObsvbl) - blocksObsvbl := channel.MapReplay(ctx, 1, eventsBz, blockEventFromEventBz) + blockEventFromEventBz := newEventsBytesToBlockMapFn(errCh) + blocksObsvbl := channel.MapReplay(ctx, latestBlockReplayBufferSize, eventsBz, blockEventFromEventBz) - // Initially set latestBlockObsvblsReplay and update if after retrying on error. + // Initially set latestBlockObsvbls and update if after retrying on error. bClient.latestBlockObsvblsReplayPublishCh <- blocksObsvbl return errCh - }) -} - -// retryOnError runs the given function, which is expected to return an error -// channel, and re-runs the function when an error is received. -// TODO_CONSIDERATION: promote to some shared package (perhaps /internal/concurrency) -func retryOnError( - ctx context.Context, - workName string, - workFn func() chan error, -) { - errCh := workFn() - for { - select { - case <-ctx.Done(): - return - case err := <-errCh: - errCh = workFn() - log.Printf("WARN: retrying %s after error: %s", workName, err) - } } } -// blockEventFromEventBz is intended to be used as a transformFn in a channel.Map() -// call. It attempts to deserialize the given byte slice as a committed block event. -// If the events bytes observable contained an error, this value is not emitted +// newEventsBytesToBlockMapFn is a factory for a function which is intended +// to be used as a transformFn in a channel.Map() call. Since the map function +// is called asynchronously, this factory creates a closure around an error channel +// which can be used for asynchronous error signaling from within the map function, +// and handling from the Map call context. +// +// The map function itself attempts to deserialize the given byte slice as a +// committed block event. If the events bytes observable contained an error, this value is not emitted // (skipped) on the destination observable of the map operation. // If deserialization failed because the event bytes were for a different event type, // this value is also skipped. // If deserialization failed for some other reason, this function panics. -func blockEventFromEventBz(eitherEventBz either.Either[[]byte]) (_ client.Block, skip bool) { - eventBz, err := eitherEventBz.ValueOrError() - if err != nil { - log.Printf("WARN: EventsBytes observable returned an unexpected error: %s", err) - // Don't publish (skip) if eitherEventBz contained an error. - // eitherEventBz should automatically close itself in this case. - // (i.e. no more values should be mapped to this transformFn's respective - // dstObservable). - return nil, true - } - - block, err := newCometBlockEvent(eventBz) - if err != nil { - if ErrUnmarshalBlockEvent.Is(err) { - // Don't publish (skip) if the message was not a block event. +func newEventsBytesToBlockMapFn(errCh chan<- error) eventBytesToBlockMapFn { + return func(eitherEventBz either.Either[[]byte]) (_ client.Block, skip bool) { + eventBz, err := eitherEventBz.ValueOrError() + if err != nil { + errCh <- err + // Don't publish (skip) if eitherEventBz contained an error. + // eitherEventBz should automatically close itself in this case. + // (i.e. no more values should be mapped to this transformFn's respective + // dstObservable). return nil, true } - panic(fmt.Sprintf( - "unexpected error deserializing block event: %s; eventBz: %s", - err, string(eventBz), - )) + block, err := newCometBlockEvent(eventBz) + if err != nil { + if ErrUnmarshalBlockEvent.Is(err) { + // Don't publish (skip) if the message was not a block event. + return nil, true + } + + panic(fmt.Sprintf( + "unexpected error deserializing block event: %s; eventBz: %s", + err, string(eventBz), + )) + } + return block, false } - return block, false } diff --git a/pkg/client/block/client_integration_test.go b/pkg/client/block/client_integration_test.go index 221f6ab0e..fd7e633ab 100644 --- a/pkg/client/block/client_integration_test.go +++ b/pkg/client/block/client_integration_test.go @@ -12,8 +12,8 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" - "pocket/internal/testclient/testblock" - "pocket/pkg/client" + "github.com/pokt-network/poktroll/internal/testclient/testblock" + "github.com/pokt-network/poktroll/pkg/client" ) const blockIntegrationSubTimeout = 5 * time.Second @@ -27,6 +27,7 @@ func TestBlockClient_LatestBlock(t *testing.T) { block := blockClient.LatestBlock(ctx) require.NotEmpty(t, block) } + func TestBlockClient_BlocksObservable(t *testing.T) { ctx := context.Background() diff --git a/pkg/client/block/client_test.go b/pkg/client/block/client_test.go index 56ef35703..b2a5515b3 100644 --- a/pkg/client/block/client_test.go +++ b/pkg/client/block/client_test.go @@ -3,27 +3,25 @@ package block_test import ( "context" "encoding/json" - "fmt" "testing" "time" "cosmossdk.io/depinject" comettypes "github.com/cometbft/cometbft/types" - "github.com/golang/mock/gomock" "github.com/stretchr/testify/require" - "pocket/internal/testclient" - "pocket/internal/testclient/testeventsquery" - "pocket/pkg/client" - "pocket/pkg/client/block" - eventsquery "pocket/pkg/client/events_query" + "github.com/pokt-network/poktroll/internal/testclient" + "github.com/pokt-network/poktroll/internal/testclient/testeventsquery" + "github.com/pokt-network/poktroll/pkg/client" + "github.com/pokt-network/poktroll/pkg/client/block" ) -const blockAssertionLoopTimeout = 100 * time.Millisecond +const ( + testTimeoutDuration = 100 * time.Millisecond -func main() { - fmt.Println("HELLOO!!!") -} + // duplicates pkg/client/block/client.go's committedBlocksQuery for testing purposes + committedBlocksQuery = "tm.event='NewBlock'" +) func TestBlockClient(t *testing.T) { var ( @@ -43,83 +41,91 @@ func TestBlockClient(t *testing.T) { ctx = context.Background() ) - // Set up a mock connection and dialer which are expected to be used once. - connMock, dialerMock := testeventsquery.OneTimeMockConnAndDialer(t) - connMock.EXPECT().Send(gomock.Any()).Return(nil).Times(1) - // Mock the Receive method to return the expected block event. - connMock.EXPECT().Receive().DoAndReturn(func() ([]byte, error) { - blockEventJson, err := json.Marshal(expectedBlockEvent) - require.NoError(t, err) - return blockEventJson, nil - }).AnyTimes() - - // Set up events query client dependency. - dialerOpt := eventsquery.WithDialer(dialerMock) - eventsQueryClient := testeventsquery.NewLocalnetClient(t, dialerOpt) + expectedEventBz, err := json.Marshal(expectedBlockEvent) + require.NoError(t, err) + + eventsQueryClient := testeventsquery.NewAnyTimesEventsBytesEventsQueryClient( + ctx, t, + committedBlocksQuery, + expectedEventBz, + ) + deps := depinject.Supply(eventsQueryClient) // Set up block client. - blockClient, err := block.NewBlockClient(ctx, deps, testclient.CometWebsocketURL) + blockClient, err := block.NewBlockClient(ctx, deps, testclient.CometLocalWebsocketURL) require.NoError(t, err) require.NotNil(t, blockClient) - // Run LatestBlock and CommittedBlockSequence concurrently because they can - // block, leading to an unresponsive test. This function sends multiple values - // on the actualBlockCh which are all asserted against in blockAssertionLoop. - // If any of the methods under test hang, the test will time out. - var ( - actualBlockCh = make(chan client.Block, 1) - done = make(chan struct{}, 1) - ) - go func() { - // Test LatestBlock method. - actualBlock := blockClient.LatestBlock(ctx) - require.Equal(t, expectedHeight, actualBlock.Height()) - require.Equal(t, expectedHash, actualBlock.Hash()) - - // Test CommittedBlockSequence method. - blockObservable := blockClient.CommittedBlocksSequence(ctx) - require.NotNil(t, blockObservable) - - // Ensure that the observable is replayable via Last. - actualBlockCh <- blockObservable.Last(ctx, 1)[0] - - // Ensure that the observable is replayable via Subscribe. - blockObserver := blockObservable.Subscribe(ctx) - for block := range blockObserver.Ch() { - actualBlockCh <- block - break - } - - // Signal test completion - done <- struct{}{} - }() - -blockAssertionLoop: - for { - select { - case actualBlock := <-actualBlockCh: - require.Equal(t, expectedHeight, actualBlock.Height()) - require.Equal(t, expectedHash, actualBlock.Hash()) - case <-done: - break blockAssertionLoop - case <-time.After(blockAssertionLoopTimeout): - t.Fatal("timed out waiting for block event") - } + tests := []struct { + name string + fn func() client.Block + }{ + { + name: "LatestBlock successfully returns latest block", + fn: func() client.Block { + lastBlock := blockClient.LatestBlock(ctx) + return lastBlock + }, + }, + { + name: "CommittedBlocksSequence successfully returns latest block", + fn: func() client.Block { + blockObservable := blockClient.CommittedBlocksSequence(ctx) + require.NotNil(t, blockObservable) + + // Ensure that the observable is replayable via Last. + lastBlock := blockObservable.Last(ctx, 1)[0] + require.Equal(t, expectedHeight, lastBlock.Height()) + require.Equal(t, expectedHash, lastBlock.Hash()) + + return lastBlock + }, + }, } - // Wait a tick for the observables to be set up. - time.Sleep(time.Millisecond) + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + var actualBlockCh = make(chan client.Block, 10) + + // Run test functions asynchronously because they can block, leading + // to an unresponsive test. If any of the methods under test hang, + // the test will time out in the select statement that follows. + go func(fn func() client.Block) { + actualBlockCh <- fn() + close(actualBlockCh) + }(tt.fn) + + select { + case actualBlock := <-actualBlockCh: + require.Equal(t, expectedHeight, actualBlock.Height()) + require.Equal(t, expectedHash, actualBlock.Hash()) + case <-time.After(testTimeoutDuration): + t.Fatal("timed out waiting for block event") + } + }) + } blockClient.Close() } -// TODO_TECHDEBT/TODO_CONSIDERATION: -// * we should prefer tests being in their own pkgs (e.g. block_test) -// * we should prefer to not export types which don't require exporting for API consumption -// * the cometBlockEvent isn't and doesn't need to be exported (except for this test) -// * TODO_DISCUSS: we could use the //go:build test constraint on a new file which exports it for testing purposes -// - This would imply that we also add -tags=test to all applicable tooling and add a test which fails if the tag is absent +/* +TODO_TECHDEBT/TODO_CONSIDERATION(#XXX): this duplicates the unexported block event + +type from pkg/client/block/block.go. We seem to have some conflicting preferences +which result in the need for this duplication until a preferred direction is +identified: + + - We should prefer tests being in their own pkgs (e.g. block_test) + - this would resolve if this test were in the block package instead. + - We should prefer to not export types which don't require exporting for API + consumption. + - This test is the only external (to the block pkg) dependency of cometBlockEvent. + - We could use the //go:build test constraint on a new file which exports it + for testing purposes. + - This would imply that we also add -tags=test to all applicable tooling + and add a test which fails if the tag is absent. +*/ type testBlockEvent struct { Block comettypes.Block `json:"block"` } diff --git a/pkg/client/events_query/client.go b/pkg/client/events_query/client.go index ca07b00b9..88ace493f 100644 --- a/pkg/client/events_query/client.go +++ b/pkg/client/events_query/client.go @@ -2,6 +2,8 @@ package eventsquery import ( "context" + "crypto/rand" + "encoding/base64" "encoding/json" "errors" "fmt" @@ -9,39 +11,36 @@ import ( "go.uber.org/multierr" - "pocket/pkg/client" - "pocket/pkg/either" - "pocket/pkg/observable" - "pocket/pkg/observable/channel" + "github.com/pokt-network/poktroll/pkg/client" + "github.com/pokt-network/poktroll/pkg/client/events_query/websocket" + "github.com/pokt-network/poktroll/pkg/either" + "github.com/pokt-network/poktroll/pkg/observable" + "github.com/pokt-network/poktroll/pkg/observable/channel" ) -const requestIdFmt = "request_%d" - var _ client.EventsQueryClient = (*eventsQueryClient)(nil) -// TODO_CONSIDERATION: the cosmos-sdk CLI code seems to use a cometbft RPC client -// which includes a `#EventsBytes()` method for a similar purpose. Perhaps we could +// TODO_TECHDEBT: the cosmos-sdk CLI code seems to use a cometbft RPC client +// which includes a `#Subscribe()` method for a similar purpose. Perhaps we could // replace this custom websocket client with that. -// (see: https://github.com/cometbft/cometbft/blob/main/rpc/client/http/http.go#L110) -// (see: https://github.com/cosmos/cosmos-sdk/blob/main/client/rpc/tx.go#L114) +// See: +// - https://github.com/cometbft/cometbft/blob/main/rpc/client/http/http.go#L110 +// - https://github.com/cometbft/cometbft/blob/main/rpc/client/http/http.go#L656 +// - https://github.com/cosmos/cosmos-sdk/blob/main/client/rpc/tx.go#L114 +// - https://github.com/pokt-network/poktroll/pull/64#discussion_r1372378241 // eventsQueryClient implements the EventsQueryClient interface. type eventsQueryClient struct { // cometWebsocketURL is the websocket URL for the cometbft node. It is assigned // in NewEventsQueryClient. cometWebsocketURL string - // nextRequestId is a *unique* ID intended to be monotonically incremented - // and used to uniquely identify distinct RPC requests. - // TODO_CONSIDERATION: Consider changing `nextRequestId` to a random entropy field - nextRequestId uint64 - - // dialer is resopnsible for createing the connection instance which + // dialer is responsible for creating the connection instance which // facilitates communication with the cometbft node via message passing. dialer client.Dialer // eventsBytesAndConnsMu protects the eventsBytesAndConns map. eventsBytesAndConnsMu sync.RWMutex // eventsBytesAndConns maps event subscription queries to their respective - // eventsBytes observable, connection, and closed status. + // eventsBytes observable, connection, and isClosed status. eventsBytesAndConns map[string]*eventsBytesAndConn } @@ -49,11 +48,18 @@ type eventsQueryClient struct { // corresponding connection which produces its inputs. type eventsBytesAndConn struct { // eventsBytes is an observable which is notified about chain event messages - // matching the given query. It receives an either.Either[[]byte] which is + // matching the given query. It receives an either.Bytes which is // either an error or the event message bytes. - eventsBytes observable.Observable[either.Either[[]byte]] + eventsBytes observable.Observable[either.Bytes] conn client.Connection - closed bool + isClosed bool +} + +// Close unsubscribes all observers of eventsBytesAndConn's observable and also +// closes its connection. +func (ebc *eventsBytesAndConn) Close() { + ebc.eventsBytes.UnsubscribeAll() + _ = ebc.conn.Close() } func NewEventsQueryClient(cometWebsocketURL string, opts ...client.EventsQueryClientOption) client.EventsQueryClient { @@ -68,14 +74,14 @@ func NewEventsQueryClient(cometWebsocketURL string, opts ...client.EventsQueryCl if evtClient.dialer == nil { // default to using the websocket dialer - evtClient.dialer = NewWebsocketDialer() + evtClient.dialer = websocket.NewWebsocketDialer() } return evtClient } // EventsBytes returns an eventsBytes observable which is notified about chain -// event messages matching the given query. It receives an either.Either[[]byte] +// event messages matching the given query. It receives an either.Bytes // which is either an error or the event message bytes. // (see: https://pkg.go.dev/github.com/cometbft/cometbft/types#pkg-constants) // (see: https://docs.cosmos.network/v0.47/core/events#subscribing-to-events) @@ -124,36 +130,33 @@ func (eqc *eventsQueryClient) close() { eqc.eventsBytesAndConnsMu.Lock() defer eqc.eventsBytesAndConnsMu.Unlock() - for query, obsvblConn := range eqc.eventsBytesAndConns { - _ = obsvblConn.conn.Close() - obsvblConn.eventsBytes.UnsubscribeAll() - - // remove closed eventsBytesAndConns + for query, eventsBzConn := range eqc.eventsBytesAndConns { + // Unsubscribe all observers of the eventsBzConn observable and close the + // connection for the given query. + eventsBzConn.Close() + // remove isClosed eventsBytesAndConns delete(eqc.eventsBytesAndConns, query) } } -// getNextRequestId increments and returns the JSON-RPC request ID which should -// be used for the next request. These IDs are expected to be unique (per request). -func (eqc *eventsQueryClient) getNextRequestId() string { - eqc.nextRequestId++ - return fmt.Sprintf(requestIdFmt, eqc.nextRequestId) -} - // newEventwsBzAndConn creates a new eventsBytes and connection for the given query. func (eqc *eventsQueryClient) newEventsBytesAndConn( ctx context.Context, query string, ) (*eventsBytesAndConn, error) { + // Get a connection for the query. conn, err := eqc.openEventsBytesAndConn(ctx, query) if err != nil { return nil, err } // Construct an eventsBytes for the given query. - eventsBzObservable, eventsBzPublishCh := channel.NewObservable[either.Either[[]byte]]() + eventsBzObservable, eventsBzPublishCh := channel.NewObservable[either.Bytes]() - // TODO_INVESTIGATE: does this require retry on error? + // Publish either events bytes or an error received from the connection to + // the eventsBz observable. + // NB: intentionally not retrying on error, leaving that to the caller. + // (see: https://github.com/pokt-network/poktroll/pull/64#discussion_r1373826542) go eqc.goPublishEventsBz(ctx, conn, eventsBzPublishCh) return &eventsBytesAndConn{ @@ -168,10 +171,8 @@ func (eqc *eventsQueryClient) openEventsBytesAndConn( ctx context.Context, query string, ) (client.Connection, error) { - // If no event subscription exists for the given query, create a new one. - // Generate a new unique request ID. - requestId := eqc.getNextRequestId() - req, err := eventSubscriptionRequest(requestId, query) + // Get a request for subscribing to events matching the given query. + req, err := eqc.eventSubscriptionRequest(query) if err != nil { return nil, err } @@ -197,12 +198,12 @@ func (eqc *eventsQueryClient) openEventsBytesAndConn( func (eqc *eventsQueryClient) goPublishEventsBz( ctx context.Context, conn client.Connection, - eventsBzPublishCh chan<- either.Either[[]byte], + eventsBzPublishCh chan<- either.Bytes, ) { // Read and handle messages from the websocket. This loop will exit when the - // websocket connection is closed and/or returns an error. + // websocket connection is isClosed and/or returns an error. for { - event, err := conn.Receive() + eventBz, err := conn.Receive() if err != nil { // TODO_CONSIDERATION: should we close the publish channel here too? @@ -225,7 +226,7 @@ func (eqc *eventsQueryClient) goPublishEventsBz( } // Populate the []byte side (right) of the either and publish it. - eventsBzPublishCh <- either.Success(event) + eventsBzPublishCh <- either.Success(eventBz) } } @@ -235,32 +236,30 @@ func (eqc *eventsQueryClient) goUnsubscribeOnDone( ctx context.Context, query string, ) { - // wait for the context to be done + // Wait for the context to be done. <-ctx.Done() - // only close the eventsBytes for the give query + // Only close the eventsBytes for the given query. eqc.eventsBytesAndConnsMu.RLock() defer eqc.eventsBytesAndConnsMu.RUnlock() - if toClose, ok := eqc.eventsBytesAndConns[query]; ok { - toClose.eventsBytes.UnsubscribeAll() - } - for compareQuery, eventsBzConn := range eqc.eventsBytesAndConns { - if query == compareQuery { - eventsBzConn.eventsBytes.UnsubscribeAll() - return - } + if eventsBzConn, ok := eqc.eventsBytesAndConns[query]; ok { + // Unsubscribe all observers of the given query's eventsBzConn's observable + // and close its connection. + eventsBzConn.Close() + // Remove the eventsBytesAndConn for the given query. + delete(eqc.eventsBytesAndConns, query) } } // eventSubscriptionRequest returns a JSON-RPC request for subscribing to events -// matching the given query. +// matching the given query. The request is serialized as JSON to a byte slice. // (see: https://github.com/cometbft/cometbft/blob/main/rpc/client/http/http.go#L110) // (see: https://github.com/cosmos/cosmos-sdk/blob/main/client/rpc/tx.go#L114) -func eventSubscriptionRequest(requestId, query string) ([]byte, error) { +func (eqc *eventsQueryClient) eventSubscriptionRequest(query string) ([]byte, error) { requestJson := map[string]any{ "jsonrpc": "2.0", "method": "subscribe", - "id": requestId, + "id": randRequestId(), "params": map[string]interface{}{ "query": query, }, @@ -271,3 +270,18 @@ func eventSubscriptionRequest(requestId, query string) ([]byte, error) { } return requestBz, nil } + +// randRequestId returns a random 8 byte, base64 request ID which is intended +// for in JSON-RPC requests to uniquely identify distinct RPC requests. +// These request IDs only need to be unique to the extent that they are useful +// to this client for identifying distinct RPC requests. Their size and keyspace +// are arbitrary. +func randRequestId() string { + requestIdBz := make([]byte, 8) // 8 bytes = 64 bits = uint64 + if _, err := rand.Read(requestIdBz); err != nil { + panic(fmt.Sprintf( + "failed to generate random request ID: %s", err, + )) + } + return base64.StdEncoding.EncodeToString(requestIdBz) +} diff --git a/pkg/client/events_query/client_integration_test.go b/pkg/client/events_query/client_integration_test.go index 8145d3844..05bf09c1a 100644 --- a/pkg/client/events_query/client_integration_test.go +++ b/pkg/client/events_query/client_integration_test.go @@ -9,9 +9,12 @@ import ( "github.com/stretchr/testify/require" - "pocket/internal/testclient/testeventsquery" + "github.com/pokt-network/poktroll/internal/testclient/testeventsquery" ) +// The query use to subscribe for new block events on the websocket endpoint exposed by CometBFT nodes +const committedBlockEventsQuery = "tm.event='NewBlock'" + func TestQueryClient_EventsObservable_Integration(t *testing.T) { const ( eventReceiveTimeout = 5 * time.Second @@ -22,7 +25,9 @@ func TestQueryClient_EventsObservable_Integration(t *testing.T) { queryClient := testeventsquery.NewLocalnetClient(t) require.NotNil(t, queryClient) - eventsObservable, err := queryClient.EventsBytes(ctx, "tm.event='NewBlock'") + // Start a subscription to the committed block events query. This begins + // publishing events to the returned observable. + eventsObservable, err := queryClient.EventsBytes(ctx, committedBlockEventsQuery) require.NoError(t, err) eventsObserver := eventsObservable.Subscribe(ctx) diff --git a/pkg/client/events_query/client_test.go b/pkg/client/events_query/client_test.go index 4b8295492..0ba52ec88 100644 --- a/pkg/client/events_query/client_test.go +++ b/pkg/client/events_query/client_test.go @@ -4,6 +4,7 @@ import ( "context" "errors" "fmt" + "sync" "sync/atomic" "testing" "time" @@ -12,13 +13,14 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" - "pocket/internal/mocks/mockclient" - "pocket/internal/testchannel" - "pocket/internal/testclient/testeventsquery" - "pocket/internal/testerrors" - eventsquery "pocket/pkg/client/events_query" - "pocket/pkg/either" - "pocket/pkg/observable" + "github.com/pokt-network/poktroll/internal/mocks/mockclient" + "github.com/pokt-network/poktroll/internal/testchannel" + "github.com/pokt-network/poktroll/internal/testclient/testeventsquery" + "github.com/pokt-network/poktroll/internal/testerrors" + eventsquery "github.com/pokt-network/poktroll/pkg/client/events_query" + "github.com/pokt-network/poktroll/pkg/client/events_query/websocket" + "github.com/pokt-network/poktroll/pkg/either" + "github.com/pokt-network/poktroll/pkg/observable" ) func TestEventsQueryClient_Subscribe_Succeeds(t *testing.T) { @@ -57,7 +59,12 @@ func TestEventsQueryClient_Subscribe_Succeeds(t *testing.T) { readEventCounter int // HandleEventsLimit is the total number of eventsBytesAndConns to send and // receive through the query client's eventsBytes for this subtest. - handleEventsLimit = 250 + handleEventsLimit = 250 + // delayFirstEvent runs once (per test case) to delay the first event + // published by the mocked connection's Receive method to give the test + // ample time to subscribe to the events bytes observable before it + // starts receiving events, otherwise they will be dropped. + delayFirstEvent sync.Once connClosed atomic.Bool queryCtx, cancelQuery = context.WithCancel(rootCtx) ) @@ -81,7 +88,9 @@ func TestEventsQueryClient_Subscribe_Succeeds(t *testing.T) { // last message. connMock.EXPECT().Receive(). DoAndReturn(func() (any, error) { - // Simulate ErrConnClosed if connection is closed. + delayFirstEvent.Do(func() { time.Sleep(50 * time.Millisecond) }) + + // Simulate ErrConnClosed if connection is isClosed. if connClosed.Load() { return nil, eventsquery.ErrConnClosed } @@ -110,9 +119,9 @@ func TestEventsQueryClient_Subscribe_Succeeds(t *testing.T) { // for the connection to close to satisfy the connection mock expectations. time.Sleep(10 * time.Millisecond) - // Drain the observer channel and assert that it's closed. + // Drain the observer channel and assert that it's isClosed. err := testchannel.DrainChannel(eventObserver.Ch()) - require.NoError(t, err, "eventsBytesAndConns observer channel should be closed") + require.NoError(t, err, "eventsBytesAndConns observer channel should be isClosed") } // Concurrently consume eventsBytesAndConns from the observer channel. @@ -129,18 +138,26 @@ func TestEventsQueryClient_Subscribe_Succeeds(t *testing.T) { func TestEventsQueryClient_Subscribe_Close(t *testing.T) { var ( - readAllEventsTimeout = 50 * time.Millisecond + firstEventDelay = 50 * time.Millisecond + readAllEventsTimeout = 50*time.Millisecond + firstEventDelay handleEventsLimit = 10 readEventCounter int - connClosed atomic.Bool - ctx = context.Background() + // delayFirstEvent runs once (per test case) to delay the first event + // published by the mocked connection's Receive method to give the test + // ample time to subscribe to the events bytes observable before it + // starts receiving events, otherwise they will be dropped. + delayFirstEvent sync.Once + connClosed atomic.Bool + ctx = context.Background() ) - connMock, dialerMock := testeventsquery.OneTimeMockConnAndDialer(t) + connMock, dialerMock := testeventsquery.NewOneTimeMockConnAndDialer(t) connMock.EXPECT().Send(gomock.Any()).Return(nil). Times(1) connMock.EXPECT().Receive(). DoAndReturn(func() (any, error) { + delayFirstEvent.Do(func() { time.Sleep(firstEventDelay) }) + if connClosed.Load() { return nil, eventsquery.ErrConnClosed } @@ -186,7 +203,7 @@ func TestEventsQueryClient_Subscribe_DialError(t *testing.T) { ctx := context.Background() eitherErrDial := either.Error[*mockclient.MockConnection](eventsquery.ErrDial) - dialerMock := testeventsquery.OneTimeMockDialer(t, eitherErrDial) + dialerMock := testeventsquery.NewOneTimeMockDialer(t, eitherErrDial) dialerOpt := eventsquery.WithDialer(dialerMock) queryClient := eventsquery.NewEventsQueryClient("", dialerOpt) @@ -198,7 +215,7 @@ func TestEventsQueryClient_Subscribe_DialError(t *testing.T) { func TestEventsQueryClient_Subscribe_RequestError(t *testing.T) { ctx, cancel := context.WithCancel(context.Background()) - connMock, dialerMock := testeventsquery.OneTimeMockConnAndDialer(t) + connMock, dialerMock := testeventsquery.NewOneTimeMockConnAndDialer(t) connMock.EXPECT().Send(gomock.Any()). Return(fmt.Errorf("mock send error")). Times(1) @@ -229,13 +246,13 @@ func TestEventsQueryClient_Subscribe_ReceiveError(t *testing.T) { ctx, cancel := context.WithCancel(context.Background()) t.Cleanup(cancel) - connMock, dialerMock := testeventsquery.OneTimeMockConnAndDialer(t) + connMock, dialerMock := testeventsquery.NewOneTimeMockConnAndDialer(t) connMock.EXPECT().Send(gomock.Any()).Return(nil). Times(1) connMock.EXPECT().Receive(). DoAndReturn(func() (any, error) { if readEventCounter >= handleEventLimit { - return nil, eventsquery.ErrReceive + return nil, websocket.ErrReceive } event := testEvent(int32(readEventCounter)) @@ -258,7 +275,7 @@ func TestEventsQueryClient_Subscribe_ReceiveError(t *testing.T) { behavesLikeEitherObserver( t, eventsObserver, handleEventLimit, - eventsquery.ErrReceive, + websocket.ErrReceive, readAllEventsTimeout, nil, ) @@ -272,12 +289,12 @@ func TestEventsQueryClient_EventsBytes_MultipleObservers(t *testing.T) { // behavesLikeEitherObserver asserts that the given observer behaves like an // observable.Observer[either.Either[V]] by consuming notifications from the // observer channel and asserting that they match the expected notification. -// It also asserts that the observer channel is closed after the expected number +// It also asserts that the observer channel is isClosed after the expected number // of eventsBytes have been received. // If onLimit is not nil, it is called when the expected number of events have // been received. // Otherwise, the observer channel is drained and the test fails if it is not -// closed after the timeout duration. +// isClosed after the timeout duration. func behavesLikeEitherObserver[V any]( t *testing.T, observer observable.Observer[either.Either[V]], @@ -286,6 +303,8 @@ func behavesLikeEitherObserver[V any]( timeout time.Duration, onLimit func(), ) { + t.Helper() + var ( // eventsCounter is the number of events which have been received from the // eventsBytes since this function was called. @@ -336,7 +355,7 @@ func behavesLikeEitherObserver[V any]( atomic.AddInt32(&eventsCounter, 1) // unbounded consumption here can result in the condition below never - // being met due to the connection being closed before the "last" event + // being met due to the connection being isClosed before the "last" event // is received time.Sleep(10 * time.Microsecond) } @@ -361,7 +380,7 @@ func behavesLikeEitherObserver[V any]( } err := testchannel.DrainChannel(observer.Ch()) - require.NoError(t, err, "eventsBytesAndConns observer should be closed") + require.NoError(t, err, "eventsBytesAndConns observer should be isClosed") } func testEvent(idx int32) []byte { diff --git a/pkg/client/events_query/errors.go b/pkg/client/events_query/errors.go index 9efd894bf..48d60f0a7 100644 --- a/pkg/client/events_query/errors.go +++ b/pkg/client/events_query/errors.go @@ -6,6 +6,6 @@ var ( ErrDial = errorsmod.Register(codespace, 1, "dialing for connection failed") ErrConnClosed = errorsmod.Register(codespace, 2, "connection closed") ErrSubscribe = errorsmod.Register(codespace, 3, "failed to subscribe to events") - ErrReceive = errorsmod.Register(codespace, 4, "failed to receive event") - codespace = "events_query_client" + + codespace = "events_query_client" ) diff --git a/pkg/client/events_query/options.go b/pkg/client/events_query/options.go index affa437f3..0e2a622fe 100644 --- a/pkg/client/events_query/options.go +++ b/pkg/client/events_query/options.go @@ -1,6 +1,6 @@ package eventsquery -import "pocket/pkg/client" +import "github.com/pokt-network/poktroll/pkg/client" // WithDialer returns a client.EventsQueryClientOption which sets the given dialer on the // resulting eventsQueryClient when passed to NewEventsQueryClient(). diff --git a/pkg/client/events_query/websocket/connection.go b/pkg/client/events_query/websocket/connection.go new file mode 100644 index 000000000..b9311bea3 --- /dev/null +++ b/pkg/client/events_query/websocket/connection.go @@ -0,0 +1,35 @@ +package websocket + +import ( + gorillaws "github.com/gorilla/websocket" + + "github.com/pokt-network/poktroll/pkg/client" +) + +var _ client.Connection = (*websocketConn)(nil) + +// websocketConn implements the Connection interface using the gorilla websocket +// transport implementation. +type websocketConn struct { + conn *gorillaws.Conn +} + +// Receive implements the respective interface method using the underlying websocket. +func (wsConn *websocketConn) Receive() ([]byte, error) { + _, msg, err := wsConn.conn.ReadMessage() + if err != nil { + return nil, ErrReceive.Wrapf("%s", err) + } + return msg, nil +} + +// Send implements the respective interface method using the underlying websocket. +func (wsConn *websocketConn) Send(msg []byte) error { + // Using the TextMessage message to indicate that msg is UTF-8 encoded. + return wsConn.conn.WriteMessage(gorillaws.TextMessage, msg) +} + +// Close implements the respective interface method using the underlying websocket. +func (wsConn *websocketConn) Close() error { + return wsConn.conn.Close() +} diff --git a/pkg/client/events_query/websocket/dialer.go b/pkg/client/events_query/websocket/dialer.go new file mode 100644 index 000000000..bd0597d03 --- /dev/null +++ b/pkg/client/events_query/websocket/dialer.go @@ -0,0 +1,35 @@ +package websocket + +import ( + "context" + + "github.com/gorilla/websocket" + + "github.com/pokt-network/poktroll/pkg/client" +) + +var _ client.Dialer = (*websocketDialer)(nil) + +// websocketDialer implements the Dialer interface using the gorilla websocket +// transport implementation. +type websocketDialer struct{} + +// NewWebsocketDialer creates a new websocketDialer. +func NewWebsocketDialer() client.Dialer { + return &websocketDialer{} +} + +// DialContext implements the respective interface method using the default gorilla +// websocket dialer. +func (wsDialer *websocketDialer) DialContext( + ctx context.Context, + urlString string, +) (client.Connection, error) { + // TODO_IMPROVE: check http response status and potential err + // TODO_TECHDEBT: add test coverage and ensure support for a 3xx responses + conn, _, err := websocket.DefaultDialer.DialContext(ctx, urlString, nil) + if err != nil { + return nil, err + } + return &websocketConn{conn: conn}, nil +} diff --git a/pkg/client/events_query/websocket/errors.go b/pkg/client/events_query/websocket/errors.go new file mode 100644 index 000000000..3c70d1eec --- /dev/null +++ b/pkg/client/events_query/websocket/errors.go @@ -0,0 +1,8 @@ +package websocket + +import errorsmod "cosmossdk.io/errors" + +var ( + ErrReceive = errorsmod.Register(codespace, 4, "failed to receive event") + codespace = "events_query_client_websocket_connection" +) diff --git a/pkg/client/godoc.go b/pkg/client/godoc.go new file mode 100644 index 000000000..66da550dd --- /dev/null +++ b/pkg/client/godoc.go @@ -0,0 +1,12 @@ +// Package client defines interfaces and types that facilitate interactions +// with blockchain functionalities, both transactional and observational. It is +// built to provide an abstraction layer for sending, receiving, and querying +// blockchain data, thereby offering a standardized way of integrating with +// various blockchain platforms. +// +// The client package leverages external libraries like cosmos-sdk and cometbft, +// but there is a preference to minimize direct dependencies on these external +// libraries, when defining interfaces, aiming for a cleaner decoupling. +// It seeks to provide a flexible and comprehensive interface layer, adaptable to +// different blockchain configurations and requirements. +package client diff --git a/pkg/client/interface.go b/pkg/client/interface.go index fd58ce13f..c088df35c 100644 --- a/pkg/client/interface.go +++ b/pkg/client/interface.go @@ -1,47 +1,143 @@ //go:generate mockgen -destination=../../internal/mocks/mockclient/events_query_client_mock.go -package=mockclient . Dialer,Connection,EventsQueryClient +//go:generate mockgen -destination=../../internal/mocks/mockclient/block_client_mock.go -package=mockclient . Block,BlockClient +//go:generate mockgen -destination=../../internal/mocks/mockclient/tx_client_mock.go -package=mockclient . TxContext,TxClient +//go:generate mockgen -destination=../../internal/mocks/mockclient/cosmos_tx_builder_mock.go -package=mockclient github.com/cosmos/cosmos-sdk/client TxBuilder +//go:generate mockgen -destination=../../internal/mocks/mockclient/cosmos_keyring_mock.go -package=mockclient github.com/cosmos/cosmos-sdk/crypto/keyring Keyring +//go:generate mockgen -destination=../../internal/mocks/mockclient/cosmos_client_mock.go -package=mockclient github.com/cosmos/cosmos-sdk/client AccountRetriever package client import ( "context" - "pocket/pkg/either" - "pocket/pkg/observable" + comettypes "github.com/cometbft/cometbft/rpc/core/types" + cosmosclient "github.com/cosmos/cosmos-sdk/client" + cosmoskeyring "github.com/cosmos/cosmos-sdk/crypto/keyring" + cosmostypes "github.com/cosmos/cosmos-sdk/types" + "github.com/pokt-network/smt" + + "github.com/pokt-network/poktroll/pkg/either" + "github.com/pokt-network/poktroll/pkg/observable" + sessiontypes "github.com/pokt-network/poktroll/x/session/types" ) -type BlockClient interface { - // Blocks returns an observable which emits newly committed blocks. - CommittedBlocksSequence(context.Context) BlocksObservable - //CommittedBlocksSequence() observable.Observable[Block] - // LatestBlock returns the latest block that has been committed. - LatestBlock(context.Context) Block - // Close unsubscribes all observers of the committed blocks sequence observable - // and closes the events query client. - Close() +// SupplierClient is an interface for sufficient for a supplier operator to be +// able to construct blockchain transactions from pocket protocol-specific messages +// related to its role. +type SupplierClient interface { + // CreateClaim sends a claim message which creates an on-chain commitment by + // calling supplier to the given smt.SparseMerkleSumTree root hash of the given + // session's mined relays. + CreateClaim( + ctx context.Context, + sessionHeader sessiontypes.SessionHeader, + rootHash []byte, + ) error + // SubmitProof sends a proof message which contains the + // smt.SparseMerkleClosestProof, corresponding to some previously created claim + // for the same session. The proof is validated on-chain as part of the pocket + // protocol. + SubmitProof( + ctx context.Context, + sessionHeader sessiontypes.SessionHeader, + proof *smt.SparseMerkleClosestProof, + ) error +} + +// TxClient provides a synchronous interface initiating and waiting for transactions +// derived from cosmos-sdk messages, in a cosmos-sdk based blockchain network. +type TxClient interface { + SignAndBroadcast( + ctx context.Context, + msgs ...cosmostypes.Msg, + ) either.AsyncError +} + +// TxContext provides an interface which consolidates the operational dependencies +// required to facilitate the sender side of the transaction lifecycle: build, sign, +// encode, broadcast, and query (optional). +// +// TODO_IMPROVE: Avoid depending on cosmos-sdk structs or interfaces; add Pocket +// interface types to substitute: +// - ResultTx +// - TxResponse +// - Keyring +// - TxBuilder +type TxContext interface { + // GetKeyring returns the associated key management mechanism for the transaction context. + GetKeyring() cosmoskeyring.Keyring + + // NewTxBuilder creates and returns a new transaction builder instance. + NewTxBuilder() cosmosclient.TxBuilder + + // SignTx signs a transaction using the specified key name. It can operate in offline mode, + // and can overwrite any existing signatures based on the provided flags. + SignTx( + keyName string, + txBuilder cosmosclient.TxBuilder, + offline, overwriteSig bool, + ) error + + // EncodeTx takes a transaction builder and encodes it, returning its byte representation. + EncodeTx(txBuilder cosmosclient.TxBuilder) ([]byte, error) + + // BroadcastTx broadcasts the given transaction to the network. + BroadcastTx(txBytes []byte) (*cosmostypes.TxResponse, error) + + // QueryTx retrieves a transaction status based on its hash and optionally provides + // proof of the transaction. + QueryTx( + ctx context.Context, + txHash []byte, + prove bool, + ) (*comettypes.ResultTx, error) } // BlocksObservable is an observable which is notified with an either // value which contains either an error or the event message bytes. +// // TODO_HACK: The purpose of this type is to work around gomock's lack of // support for generic types. For the same reason, this type cannot be an // alias (i.e. EventsBytesObservable = observable.Observable[either.Either[[]byte]]). type BlocksObservable observable.ReplayObservable[Block] +// BlockClient is an interface which provides notifications about newly committed +// blocks as well as direct access to the latest block via some blockchain API. +type BlockClient interface { + // CommittedBlocksSequence returns an observable which emits newly committed blocks. + CommittedBlocksSequence(context.Context) BlocksObservable + // LatestBlock returns the latest block that has been committed. + LatestBlock(context.Context) Block + // Close unsubscribes all observers of the committed blocks sequence observable + // and closes the events query client. + Close() +} + +// Block is an interface which abstracts the details of a block to its minimal +// necessary components. type Block interface { Height() int64 Hash() []byte } +// EventsBytesObservable is an observable which is notified with an either +// value which contains either an error or the event message bytes. +// +// TODO_HACK: The purpose of this type is to work around gomock's lack of +// support for generic types. For the same reason, this type cannot be an +// alias (i.e. EventsBytesObservable = observable.Observable[either.Bytes]). +type EventsBytesObservable observable.Observable[either.Bytes] + +// EventsQueryClient is used to subscribe to chain event messages matching the given query, +// // TODO_CONSIDERATION: the cosmos-sdk CLI code seems to use a cometbft RPC client // which includes a `#Subscribe()` method for a similar purpose. Perhaps we could -// replace this custom websocket client with that. +// replace our custom implementation with one which wraps that. // (see: https://github.com/cometbft/cometbft/blob/main/rpc/client/http/http.go#L110) // (see: https://github.com/cosmos/cosmos-sdk/blob/main/client/rpc/tx.go#L114) // // NOTE: a branch which attempts this is available at: // https://github.com/pokt-network/poktroll/pull/74 - -// EventsQueryClient is used to subscribe to chain event messages matching the given query, type EventsQueryClient interface { // EventsBytes returns an observable which is notified about chain event messages // matching the given query. It receives an either value which contains either an @@ -55,13 +151,6 @@ type EventsQueryClient interface { Close() } -// EventsBytesObservable is an observable which is notified with an either -// value which contains either an error or the event message bytes. -// TODO_HACK: The purpose of this type is to work around gomock's lack of -// support for generic types. For the same reason, this type cannot be an -// alias (i.e. EventsBytesObservable = observable.Observable[either.Either[[]byte]]). -type EventsBytesObservable observable.Observable[either.Either[[]byte]] - // Connection is a transport agnostic, bi-directional, message-passing interface. type Connection interface { // Receive blocks until a message is received or an error occurs. @@ -79,8 +168,11 @@ type Dialer interface { DialContext(ctx context.Context, urlStr string) (Connection, error) } -// EventsQueryClientOption is an interface-wide type which can be implemented to use or modify the -// query client during construction. This would likely be done in an -// implementation-specific way; e.g. using a type assertion to assign to an -// implementation struct field(s). +// EventsQueryClientOption defines a function type that modifies the EventsQueryClient. type EventsQueryClientOption func(EventsQueryClient) + +// TxClientOption defines a function type that modifies the TxClient. +type TxClientOption func(TxClient) + +// SupplierClientOption defines a function type that modifies the SupplierClient. +type SupplierClientOption func(SupplierClient) diff --git a/pkg/client/keyring/errors.go b/pkg/client/keyring/errors.go new file mode 100644 index 000000000..7be8a677a --- /dev/null +++ b/pkg/client/keyring/errors.go @@ -0,0 +1,19 @@ +package keyring + +import "cosmossdk.io/errors" + +var ( + // ErrEmptySigningKeyName represents an error which indicates that the + // provided signing key name is empty or unspecified. + ErrEmptySigningKeyName = errors.Register(codespace, 1, "empty signing key name") + + // ErrNoSuchSigningKey represents an error signifying that the requested + // signing key does not exist or could not be located. + ErrNoSuchSigningKey = errors.Register(codespace, 2, "signing key does not exist") + + // ErrSigningKeyAddr is raised when there's a failure in retrieving the + // associated address for the provided signing key. + ErrSigningKeyAddr = errors.Register(codespace, 3, "failed to get address for signing key") + + codespace = "keyring" +) diff --git a/pkg/client/keyring/keyring.go b/pkg/client/keyring/keyring.go new file mode 100644 index 000000000..a77d35b6e --- /dev/null +++ b/pkg/client/keyring/keyring.go @@ -0,0 +1,29 @@ +package keyring + +import ( + cosmoskeyring "github.com/cosmos/cosmos-sdk/crypto/keyring" + cosmostypes "github.com/cosmos/cosmos-sdk/types" +) + +// KeyNameToAddr attempts to retrieve the key with the given name from the +// given keyring and compute its address. +func KeyNameToAddr( + keyName string, + keyring cosmoskeyring.Keyring, +) (cosmostypes.AccAddress, error) { + if keyName == "" { + return nil, ErrEmptySigningKeyName + } + + keyRecord, err := keyring.Key(keyName) + if err != nil { + return nil, ErrNoSuchSigningKey.Wrapf("name %q: %s", keyName, err) + } + + signingAddr, err := keyRecord.GetAddress() + if err != nil { + return nil, ErrSigningKeyAddr.Wrapf("name %q: %s", keyName, err) + } + + return signingAddr, nil +} diff --git a/pkg/client/services.go b/pkg/client/services.go new file mode 100644 index 000000000..0d2ca060d --- /dev/null +++ b/pkg/client/services.go @@ -0,0 +1,19 @@ +package client + +import ( + "fmt" + + sharedtypes "github.com/pokt-network/poktroll/x/shared/types" +) + +// NewTestApplicationServiceConfig returns a slice of application service configs for testing. +func NewTestApplicationServiceConfig(prefix string, count int) []*sharedtypes.ApplicationServiceConfig { + appSvcCfg := make([]*sharedtypes.ApplicationServiceConfig, count) + for i, _ := range appSvcCfg { + serviceId := fmt.Sprintf("%s%d", prefix, i) + appSvcCfg[i] = &sharedtypes.ApplicationServiceConfig{ + ServiceId: &sharedtypes.ServiceId{Id: serviceId}, + } + } + return appSvcCfg +} diff --git a/pkg/client/supplier/client.go b/pkg/client/supplier/client.go new file mode 100644 index 000000000..a0172c3c5 --- /dev/null +++ b/pkg/client/supplier/client.go @@ -0,0 +1,127 @@ +package supplier + +import ( + "context" + + "cosmossdk.io/depinject" + cosmostypes "github.com/cosmos/cosmos-sdk/types" + "github.com/pokt-network/smt" + + "github.com/pokt-network/poktroll/pkg/client" + "github.com/pokt-network/poktroll/pkg/client/keyring" + sessiontypes "github.com/pokt-network/poktroll/x/session/types" + suppliertypes "github.com/pokt-network/poktroll/x/supplier/types" +) + +var _ client.SupplierClient = (*supplierClient)(nil) + +// supplierClient +type supplierClient struct { + signingKeyName string + signingKeyAddr cosmostypes.AccAddress + + txClient client.TxClient + txCtx client.TxContext +} + +// NewSupplierClient constructs a new SupplierClient with the given dependencies +// and options. If a signingKeyName is not configured, an error will be returned. +// +// Required dependencies: +// - client.TxClient +// - client.TxContext +// +// Available options: +// - WithSigningKeyName +func NewSupplierClient( + deps depinject.Config, + opts ...client.SupplierClientOption, +) (*supplierClient, error) { + sClient := &supplierClient{} + + if err := depinject.Inject( + deps, + &sClient.txClient, + &sClient.txCtx, + ); err != nil { + return nil, err + } + + for _, opt := range opts { + opt(sClient) + } + + if err := sClient.validateConfigAndSetDefaults(); err != nil { + return nil, err + } + + return sClient, nil +} + +// SubmitProof constructs a submit proof message then signs and broadcasts it +// to the network via #txClient. It blocks until the transaction is included in +// a block or times out. +func (sClient *supplierClient) SubmitProof( + ctx context.Context, + sessionHeader sessiontypes.SessionHeader, + proof *smt.SparseMerkleClosestProof, +) error { + proofBz, err := proof.Marshal() + if err != nil { + return err + } + + msg := &suppliertypes.MsgSubmitProof{ + SupplierAddress: sClient.signingKeyAddr.String(), + SessionHeader: &sessionHeader, + Proof: proofBz, + } + eitherErr := sClient.txClient.SignAndBroadcast(ctx, msg) + err, errCh := eitherErr.SyncOrAsyncError() + if err != nil { + return err + } + + return <-errCh +} + +// CreateClaim constructs a creates claim message then signs and broadcasts it +// to the network via #txClient. It blocks until the transaction is included in +// a block or times out. +func (sClient *supplierClient) CreateClaim( + ctx context.Context, + sessionHeader sessiontypes.SessionHeader, + rootHash []byte, +) error { + msg := &suppliertypes.MsgCreateClaim{ + SupplierAddress: sClient.signingKeyAddr.String(), + SessionHeader: &sessionHeader, + RootHash: rootHash, + } + eitherErr := sClient.txClient.SignAndBroadcast(ctx, msg) + err, errCh := eitherErr.SyncOrAsyncError() + if err != nil { + return err + } + + err = <-errCh + return err +} + +// validateConfigAndSetDefaults attempts to get the address from the keyring +// corresponding to the key whose name matches the configured signingKeyName. +// If signingKeyName is empty or the keyring does not contain the corresponding +// key, an error is returned. +func (sClient *supplierClient) validateConfigAndSetDefaults() error { + signingAddr, err := keyring.KeyNameToAddr( + sClient.signingKeyName, + sClient.txCtx.GetKeyring(), + ) + if err != nil { + return err + } + + sClient.signingKeyAddr = signingAddr + + return nil +} diff --git a/pkg/client/supplier/client_integration_test.go b/pkg/client/supplier/client_integration_test.go new file mode 100644 index 000000000..8af759186 --- /dev/null +++ b/pkg/client/supplier/client_integration_test.go @@ -0,0 +1,36 @@ +//go:build integration + +package supplier_test + +import ( + "context" + "testing" + + "github.com/stretchr/testify/require" + + "github.com/pokt-network/poktroll/internal/testclient/testsupplier" + sessiontypes "github.com/pokt-network/poktroll/x/session/types" +) + +func TestNewSupplierClient_Localnet(t *testing.T) { + t.Skip("TODO_TECHDEBT: this test depends on some setup which is currently not implemented in this test: staked application and servicer with matching services") + + var ( + signingKeyName = "app1" + ctx = context.Background() + ) + + supplierClient := testsupplier.NewLocalnetClient(t, signingKeyName) + require.NotNil(t, supplierClient) + + var rootHash []byte + sessionHeader := sessiontypes.SessionHeader{ + ApplicationAddress: "", + SessionStartBlockHeight: 0, + SessionId: "", + } + err := supplierClient.CreateClaim(ctx, sessionHeader, rootHash) + require.NoError(t, err) + + require.True(t, false) +} diff --git a/pkg/client/supplier/client_test.go b/pkg/client/supplier/client_test.go new file mode 100644 index 000000000..9cb6e85c6 --- /dev/null +++ b/pkg/client/supplier/client_test.go @@ -0,0 +1,190 @@ +package supplier_test + +import ( + "context" + "crypto/sha256" + "testing" + "time" + + "cosmossdk.io/depinject" + "github.com/golang/mock/gomock" + "github.com/pokt-network/smt" + "github.com/stretchr/testify/require" + + "github.com/pokt-network/poktroll/internal/mocks/mockclient" + "github.com/pokt-network/poktroll/internal/testclient/testkeyring" + "github.com/pokt-network/poktroll/internal/testclient/testtx" + "github.com/pokt-network/poktroll/pkg/client/keyring" + "github.com/pokt-network/poktroll/pkg/client/supplier" + sessiontypes "github.com/pokt-network/poktroll/x/session/types" +) + +var testSigningKeyName = "test_signer" + +func TestNewSupplierClient(t *testing.T) { + ctrl := gomock.NewController(t) + + memKeyring, _ := testkeyring.NewTestKeyringWithKey(t, testSigningKeyName) + txCtxMock, _ := testtx.NewAnyTimesTxTxContext(t, memKeyring) + txClientMock := mockclient.NewMockTxClient(ctrl) + + deps := depinject.Supply( + txCtxMock, + txClientMock, + ) + + tests := []struct { + name string + signingKeyName string + expectedErr error + }{ + { + name: "valid signing key name", + signingKeyName: testSigningKeyName, + expectedErr: nil, + }, + { + name: "empty signing key name", + signingKeyName: "", + expectedErr: keyring.ErrEmptySigningKeyName, + }, + { + name: "no such signing key name", + signingKeyName: "nonexistent", + expectedErr: keyring.ErrNoSuchSigningKey, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + signingKeyOpt := supplier.WithSigningKeyName(tt.signingKeyName) + + supplierClient, err := supplier.NewSupplierClient(deps, signingKeyOpt) + if tt.expectedErr != nil { + require.ErrorIs(t, err, tt.expectedErr) + require.Nil(t, supplierClient) + } else { + require.NoError(t, err) + require.NotNil(t, supplierClient) + } + }) + } +} + +func TestSupplierClient_CreateClaim(t *testing.T) { + var ( + signAndBroadcastDelay = 50 * time.Millisecond + doneCh = make(chan struct{}, 1) + ctx = context.Background() + ) + + keyring, testAppKey := testkeyring.NewTestKeyringWithKey(t, testSigningKeyName) + + testAppAddr, err := testAppKey.GetAddress() + require.NoError(t, err) + + txCtxMock, _ := testtx.NewAnyTimesTxTxContext(t, keyring) + txClientMock := testtx.NewOneTimeDelayedSignAndBroadcastTxClient(t, signAndBroadcastDelay) + + signingKeyOpt := supplier.WithSigningKeyName(testAppKey.Name) + deps := depinject.Supply( + txCtxMock, + txClientMock, + ) + + supplierClient, err := supplier.NewSupplierClient(deps, signingKeyOpt) + require.NoError(t, err) + require.NotNil(t, supplierClient) + + var rootHash []byte + sessionHeader := sessiontypes.SessionHeader{ + ApplicationAddress: testAppAddr.String(), + SessionStartBlockHeight: 0, + SessionId: "", + } + + go func() { + err = supplierClient.CreateClaim(ctx, sessionHeader, rootHash) + require.NoError(t, err) + close(doneCh) + }() + + // TODO_IMPROVE: this could be rewritten to record the times at which + // things happen and then compare them to the expected times. + + select { + case <-doneCh: + t.Fatal("expected CreateClaim to block for signAndBroadcastDelay") + case <-time.After(signAndBroadcastDelay * 95 / 100): + t.Log("OK: CreateClaim blocked for at least 95% of signAndBroadcastDelay") + } + + select { + case <-time.After(signAndBroadcastDelay): + t.Fatal("expected CreateClaim to unblock after signAndBroadcastDelay") + case <-doneCh: + t.Log("OK: CreateClaim unblocked after signAndBroadcastDelay") + } +} + +func TestSupplierClient_SubmitProof(t *testing.T) { + var ( + signAndBroadcastDelay = 50 * time.Millisecond + doneCh = make(chan struct{}, 1) + ctx = context.Background() + ) + + keyring, testAppKey := testkeyring.NewTestKeyringWithKey(t, testSigningKeyName) + + testAppAddr, err := testAppKey.GetAddress() + require.NoError(t, err) + + txCtxMock, _ := testtx.NewAnyTimesTxTxContext(t, keyring) + txClientMock := testtx.NewOneTimeDelayedSignAndBroadcastTxClient(t, signAndBroadcastDelay) + + signingKeyOpt := supplier.WithSigningKeyName(testAppKey.Name) + deps := depinject.Supply( + txCtxMock, + txClientMock, + ) + + supplierClient, err := supplier.NewSupplierClient(deps, signingKeyOpt) + require.NoError(t, err) + require.NotNil(t, supplierClient) + + sessionHeader := sessiontypes.SessionHeader{ + ApplicationAddress: testAppAddr.String(), + SessionStartBlockHeight: 0, + SessionId: "", + } + + kvStore, err := smt.NewKVStore("") + require.NoError(t, err) + + tree := smt.NewSparseMerkleSumTree(kvStore, sha256.New()) + proof, err := tree.ProveClosest([]byte{1}) + require.NoError(t, err) + + go func() { + err = supplierClient.SubmitProof(ctx, sessionHeader, proof) + require.NoError(t, err) + close(doneCh) + }() + + // TODO_IMPROVE: this could be rewritten to record the times at which + // things happen and then compare them to the expected times. + + select { + case <-doneCh: + t.Fatal("expected SubmitProof to block for signAndBroadcastDelay") + case <-time.After(signAndBroadcastDelay * 95 / 100): + t.Log("OK: SubmitProof blocked for at least 95% of signAndBroadcastDelay") + } + + select { + case <-time.After(signAndBroadcastDelay): + t.Fatal("expected SubmitProof to unblock after signAndBroadcastDelay") + case <-doneCh: + t.Log("OK: SubmitProof unblocked after signAndBroadcastDelay") + } +} diff --git a/pkg/client/supplier/options.go b/pkg/client/supplier/options.go new file mode 100644 index 000000000..f4460c8c9 --- /dev/null +++ b/pkg/client/supplier/options.go @@ -0,0 +1,14 @@ +package supplier + +import ( + "github.com/pokt-network/poktroll/pkg/client" +) + +// WithSigningKeyName sets the name of the key which the supplier client should +// retrieve from the keyring to use for authoring and signing CreateClaim and +// SubmitProof messages. +func WithSigningKeyName(keyName string) client.SupplierClientOption { + return func(sClient client.SupplierClient) { + sClient.(*supplierClient).signingKeyName = keyName + } +} diff --git a/pkg/client/tx/client.go b/pkg/client/tx/client.go new file mode 100644 index 000000000..1c083559b --- /dev/null +++ b/pkg/client/tx/client.go @@ -0,0 +1,564 @@ +package tx + +import ( + "bytes" + "context" + "encoding/hex" + "encoding/json" + "errors" + "fmt" + "sync" + + "cosmossdk.io/depinject" + abciTypes "github.com/cometbft/cometbft/abci/types" + comettypes "github.com/cometbft/cometbft/types" + cosmostypes "github.com/cosmos/cosmos-sdk/types" + "go.uber.org/multierr" + + "github.com/pokt-network/poktroll/pkg/client" + "github.com/pokt-network/poktroll/pkg/client/keyring" + "github.com/pokt-network/poktroll/pkg/either" + "github.com/pokt-network/poktroll/pkg/observable" + "github.com/pokt-network/poktroll/pkg/observable/channel" +) + +const ( + // DefaultCommitTimeoutHeightOffset is the default number of blocks after the + // latest block (when broadcasting) that a transactions should be considered + // errored if it has not been committed. + DefaultCommitTimeoutHeightOffset = 5 + // txWithSenderAddrQueryFmt is the query used to subscribe to cometbft transactions + // events where the sender address matches the interpolated address. + // (see: https://docs.cosmos.network/v0.47/core/events#subscribing-to-events) + txWithSenderAddrQueryFmt = "tm.event='Tx' AND message.sender='%s'" +) + +var _ client.TxClient = (*txClient)(nil) + +// txClient orchestrates building, signing, broadcasting, and querying of +// transactions. It maintains a single events query subscription to its own +// transactions (via the EventsQueryClient) in order to receive notifications +// regarding their status. +// It also depends on the BlockClient as a timer, synchronized to block height, +// to facilitate transaction timeout logic. If a transaction doesn't appear to +// have been committed by commitTimeoutHeightOffset number of blocks have elapsed, +// it is considered as timed out. Upon timeout, the client queries the network for +// the last status of the transaction, which is used to derive the asynchronous +// error that's populated in the either.AsyncError. +type txClient struct { + // TODO_TECHDEBT: this should be configurable & integrated w/ viper, flags, etc. + // commitTimeoutHeightOffset is the number of blocks after the latest block + // that a transactions should be considered errored if it has not been committed. + commitTimeoutHeightOffset int64 + // signingKeyName is the name of the key in the keyring to use for signing + // transactions. + signingKeyName string + // signingAddr is the address of the signing key referenced by signingKeyName. + // It is hydrated from the keyring by calling Keyring#Key() with signingKeyName. + signingAddr cosmostypes.AccAddress + // txCtx is the transactions context which encapsulates transactions building, signing, + // broadcasting, and querying, as well as keyring access. + txCtx client.TxContext + // eventsQueryClient is the client used to subscribe to transactions events from this + // sender. It is used to receive notifications about transactions events corresponding + // to transactions which it has constructed, signed, and broadcast. + eventsQueryClient client.EventsQueryClient + // blockClient is the client used to query for the latest block height. + // It is used to implement timout logic for transactions which weren't committed. + blockClient client.BlockClient + + // txsMutex protects txErrorChans and txTimeoutPool maps. + txsMutex sync.Mutex + // txErrorChans maps tx_hash->channel which will receive an error or nil, + // and close, when the transactions with the given hash is committed. + txErrorChans txErrorChansByHash + // txTimeoutPool maps timeout_block_height->map_of_txsByHash. It + // is used to ensure that transactions error channels receive and close in the event + // that they have not already by the given timeout height. + txTimeoutPool txTimeoutPool +} + +type ( + txTimeoutPool map[height]txErrorChansByHash + txErrorChansByHash map[txHash]chan error + height = int64 + txHash = string +) + +// TxEvent is used to deserialize incoming websocket messages from +// the transactions subscription. +type TxEvent struct { + // Tx is the binary representation of the tx hash. + Tx []byte `json:"tx"` + Events []abciTypes.Event `json:"events"` +} + +// NewTxClient attempts to construct a new TxClient using the given dependencies +// and options. +// +// It performs the following steps: +// 1. Initializes a default txClient with the default commit timeout height +// offset, an empty error channel map, and an empty transaction timeout pool. +// 2. Injects the necessary dependencies using depinject. +// 3. Applies any provided options to customize the client. +// 4. Validates and sets any missing default configurations using the +// validateConfigAndSetDefaults method. +// 5. Subscribes the client to its own transactions. This step might be +// reconsidered for relocation to a potential Start() method in the future. +func NewTxClient( + ctx context.Context, + deps depinject.Config, + opts ...client.TxClientOption, +) (client.TxClient, error) { + tClient := &txClient{ + commitTimeoutHeightOffset: DefaultCommitTimeoutHeightOffset, + txErrorChans: make(txErrorChansByHash), + txTimeoutPool: make(txTimeoutPool), + } + + if err := depinject.Inject( + deps, + &tClient.txCtx, + &tClient.eventsQueryClient, + &tClient.blockClient, + ); err != nil { + return nil, err + } + + for _, opt := range opts { + opt(tClient) + } + + if err := tClient.validateConfigAndSetDefaults(); err != nil { + return nil, err + } + + // Start an events query subscription for transactions originating from this + // client's signing address. + // TODO_CONSIDERATION: move this into a #Start() method + if err := tClient.subscribeToOwnTxs(ctx); err != nil { + return nil, err + } + + // Launch a separate goroutine to handle transaction timeouts. + // TODO_CONSIDERATION: move this into a #Start() method + go tClient.goTimeoutPendingTransactions(ctx) + + return tClient, nil +} + +// SignAndBroadcast signs a set of Cosmos SDK messages, constructs a transaction, +// and broadcasts it to the network. The function performs several steps to +// ensure the messages and the resultant transaction are valid: +// +// 1. Validates each message in the provided set. +// 2. Constructs the transaction using the Cosmos SDK's transaction builder. +// 3. Calculates and sets the transaction's timeout height. +// 4. Sets a default gas limit (note: this will be made configurable in the future). +// 5. Signs the transaction. +// 6. Validates the constructed transaction. +// 7. Serializes and broadcasts the transaction. +// 8. Checks the broadcast response for errors. +// 9. If all the above steps are successful, the function registers the +// transaction as pending. +// +// If any step encounters an error, it returns an either.AsyncError populated with +// the synchronous error. If the function completes successfully, it returns an +// either.AsyncError populated with the error channel which will receive if the +// transaction results in an asynchronous error or times out. +func (tClient *txClient) SignAndBroadcast( + ctx context.Context, + msgs ...cosmostypes.Msg, +) either.AsyncError { + var validationErrs error + for i, msg := range msgs { + if err := msg.ValidateBasic(); err != nil { + validationErr := ErrInvalidMsg.Wrapf("in msg with index %d: %s", i, err) + validationErrs = multierr.Append(validationErrs, validationErr) + } + } + if validationErrs != nil { + return either.SyncErr(validationErrs) + } + + // Construct the transactions using cosmos' transactions builder. + txBuilder := tClient.txCtx.NewTxBuilder() + if err := txBuilder.SetMsgs(msgs...); err != nil { + // return synchronous error + return either.SyncErr(err) + } + + // Calculate timeout height + timeoutHeight := tClient.blockClient.LatestBlock(ctx). + Height() + tClient.commitTimeoutHeightOffset + + // TODO_TECHDEBT: this should be configurable + txBuilder.SetGasLimit(200000) + txBuilder.SetTimeoutHeight(uint64(timeoutHeight)) + + // sign transactions + err := tClient.txCtx.SignTx( + tClient.signingKeyName, + txBuilder, + false, false, + ) + if err != nil { + return either.SyncErr(err) + } + + // ensure transactions is valid + // NOTE: this makes the transactions valid; i.e. it is *REQUIRED* + if err := txBuilder.GetTx().ValidateBasic(); err != nil { + return either.SyncErr(err) + } + + // serialize transactions + txBz, err := tClient.txCtx.EncodeTx(txBuilder) + if err != nil { + return either.SyncErr(err) + } + + txResponse, err := tClient.txCtx.BroadcastTx(txBz) + if err != nil { + return either.SyncErr(err) + } + + if txResponse.Code != 0 { + return either.SyncErr(ErrCheckTx.Wrapf(txResponse.RawLog)) + } + + return tClient.addPendingTransactions(normalizeTxHashHex(txResponse.TxHash), timeoutHeight) +} + +// validateConfigAndSetDefaults ensures that the necessary configurations for the +// txClient are set, and populates any missing defaults. +// +// 1. It checks if the signing key name is set and returns an error if it's empty. +// 2. It then retrieves the key record from the keyring using the signing key name +// and checks its existence. +// 3. The address of the signing key is computed and assigned to txClient#signgingAddr. +// 4. Lastly, it ensures that commitTimeoutHeightOffset has a valid value, setting +// it to DefaultCommitTimeoutHeightOffset if it's zero or negative. +// +// Returns: +// - ErrEmptySigningKeyName if the signing key name is not provided. +// - ErrNoSuchSigningKey if the signing key is not found in the keyring. +// - ErrSigningKeyAddr if there's an issue retrieving the address for the signing key. +// - nil if validation is successful and defaults are set appropriately. +func (tClient *txClient) validateConfigAndSetDefaults() error { + signingAddr, err := keyring.KeyNameToAddr( + tClient.signingKeyName, + tClient.txCtx.GetKeyring(), + ) + if err != nil { + return err + } + + tClient.signingAddr = signingAddr + + if tClient.commitTimeoutHeightOffset <= 0 { + tClient.commitTimeoutHeightOffset = DefaultCommitTimeoutHeightOffset + } + return nil +} + +// addPendingTransactions registers a new pending transaction for monitoring and +// notification of asynchronous errors. It accomplishes the following: +// +// 1. Creates an error notification channel (if one doesn't already exist) and associates +// it with the provided transaction hash in the txErrorChans map. +// +// 2. Ensures that there's an initialized map of transactions by hash for the +// given timeout height in the txTimeoutPool. The same error notification channel +// is also associated with the transaction hash in this map. +// +// Both txErrorChans and txTimeoutPool store references to the same error notification +// channel for a given transaction hash. This ensures idempotency of error handling +// for any given transaction between asynchronous, transaction-specific errors and +// transaction timeout logic. +// +// Note: The error channels are buffered to prevent blocking on send operations and +// are intended to convey a single error event. +// +// Returns: +// - An either.AsyncError populated with the error notification channel for the +// provided transaction hash. +func (tClient *txClient) addPendingTransactions( + txHash string, + timeoutHeight int64, +) either.AsyncError { + tClient.txsMutex.Lock() + defer tClient.txsMutex.Unlock() + + // Initialize txTimeoutPool map if necessary. + txsByHash, ok := tClient.txTimeoutPool[timeoutHeight] + if !ok { + txsByHash = make(map[string]chan error) + tClient.txTimeoutPool[timeoutHeight] = txsByHash + } + + // Initialize txErrorChans map in txTimeoutPool map if necessary. + errCh, ok := txsByHash[txHash] + if !ok { + // NB: intentionally buffered to avoid blocking on send. Only intended + // to send/receive a single error. + errCh = make(chan error, 1) + txsByHash[txHash] = errCh + } + + // Initialize txErrorChans map if necessary. + if _, ok := tClient.txErrorChans[txHash]; !ok { + // NB: both maps hold a reference to the same channel so that we can check + // if the channel has already been closed when timing out. + tClient.txErrorChans[txHash] = errCh + } + + return either.AsyncErr(errCh) +} + +// subscribeToOwnTxs establishes an event query subscription to monitor transactions +// originating from this client's signing address. +// +// It performs the following steps: +// +// 1. Forms a query to fetch transaction events specific to the client's signing address. +// 2. Maps raw event bytes observable notifications to a new transaction event objects observable. +// 3. Handle each transaction event. +// +// Important considerations: +// There's uncertainty surrounding the potential for asynchronous errors post transaction broadcast. +// Current implementation and observations suggest that errors might be returned synchronously, +// even when using Cosmos' BroadcastTxAsync method. Further investigation is required. +// +// This function also spawns a goroutine to handle transaction timeouts via goTimeoutPendingTransactions. +// +// Parameters: +// - ctx: Context for managing the function's lifecycle and child operations. +// +// Returns: +// - An error if there's a failure during the event query or subscription process. +func (tClient *txClient) subscribeToOwnTxs(ctx context.Context) error { + // Form a query based on the client's signing address. + query := fmt.Sprintf(txWithSenderAddrQueryFmt, tClient.signingAddr) + + // Fetch transaction events matching the query. + eventsBz, err := tClient.eventsQueryClient.EventsBytes(ctx, query) + if err != nil { + return err + } + + // Convert raw event data into a stream of transaction events. + txEventsObservable := channel.Map[ + either.Bytes, either.Either[*TxEvent], + ](ctx, eventsBz, tClient.txEventFromEventBz) + txEventsObserver := txEventsObservable.Subscribe(ctx) + + // Handle transaction events asynchronously. + go tClient.goHandleTxEvents(txEventsObserver) + + return nil +} + +// goHandleTxEvents ranges over the transaction events observable, performing +// the following steps on each: +// +// 1. Normalize hexadeimal transaction hash. +// 2. Retrieves the transaction's error channel from txErrorChans. +// 3. Closes and removes it from txErrorChans. +// 4. Removes the transaction error channel from txTimeoutPool. +// +// It is intended to be called in a goroutine. +func (tClient *txClient) goHandleTxEvents( + txEventsObserver observable.Observer[either.Either[*TxEvent]], +) { + for eitherTxEvent := range txEventsObserver.Ch() { + txEvent, err := eitherTxEvent.ValueOrError() + if err != nil { + return + } + + // Convert transaction hash into its normalized hex form. + txHashHex := txHashBytesToNormalizedHex(comettypes.Tx(txEvent.Tx).Hash()) + + tClient.txsMutex.Lock() + + // Check for a corresponding error channel in the map. + txErrCh, ok := tClient.txErrorChans[txHashHex] + if !ok { + panic("Received tx event without an associated error channel.") + } + + // TODO_INVESTIGATE: it seems like it may not be possible for the + // txEvent to represent an error. Cosmos' #BroadcastTxSync() is being + // called internally, which will return an error if the transaction + // is not accepted by the mempool. + // + // It's unclear if a cosmos chain is capable of returning an async + // error for a transaction at this point; even when substituting + // #BroadcastTxAsync(), the error is returned synchronously: + // + // > error in json rpc client, with http response metadata: (Status: + // > 200 OK, Protocol HTTP/1.1). RPC error -32000 - tx added to local + // > mempool but failed to gossip: validation failed + // + // Potential parse and send transaction error on txErrCh here. + + // Close and remove from txErrChans + close(txErrCh) + delete(tClient.txErrorChans, txHashHex) + + // Remove from the txTimeoutPool. + for timeoutHeight, txErrorChans := range tClient.txTimeoutPool { + // Handled transaction isn't in this timeout height. + if _, ok := txErrorChans[txHashHex]; !ok { + continue + } + + delete(txErrorChans, txHashHex) + if len(txErrorChans) == 0 { + delete(tClient.txTimeoutPool, timeoutHeight) + } + } + + tClient.txsMutex.Unlock() + } +} + +// goTimeoutPendingTransactions monitors blocks and handles transaction timeouts. +// For each block observed, it checks if there are transactions associated with that +// block's height in the txTimeoutPool. If transactions are found, the function +// evaluates whether they have already been processed by the transaction events +// query subscription logic. If not, a timeout error is generated and sent on the +// transaction's error channel. Finally, the error channel is closed and removed +// from the txTimeoutPool. +func (tClient *txClient) goTimeoutPendingTransactions(ctx context.Context) { + // Subscribe to a sequence of committed blocks. + blockCh := tClient.blockClient.CommittedBlocksSequence(ctx).Subscribe(ctx).Ch() + + // Iterate over each incoming block. + for block := range blockCh { + select { + case <-ctx.Done(): + // Exit if the context signals done. + return + default: + } + + tClient.txsMutex.Lock() + + // Retrieve transactions associated with the current block's height. + txsByHash, ok := tClient.txTimeoutPool[block.Height()] + if !ok { + // If no transactions are found for the current block height, continue. + tClient.txsMutex.Unlock() + continue + } + + // Process each transaction for the current block height. + for txHash, txErrCh := range txsByHash { + select { + // Check if the transaction was processed by its subscription. + case err, ok := <-txErrCh: + if ok { + // Unexpected state: error channel should be closed after processing. + panic(fmt.Errorf("Expected txErrCh to be closed; received err: %w", err)) + } + // Remove the processed transaction. + delete(txsByHash, txHash) + tClient.txsMutex.Unlock() + continue + default: + } + + // Transaction was not processed by its subscription: handle timeout. + txErrCh <- tClient.getTxTimeoutError(ctx, txHash) // Send a timeout error. + close(txErrCh) // Close the error channel. + delete(txsByHash, txHash) // Remove the transaction. + } + + // Clean up the txTimeoutPool for the current block height. + delete(tClient.txTimeoutPool, block.Height()) + tClient.txsMutex.Unlock() + } +} + +// txEventFromEventBz deserializes a binary representation of a transaction event +// into a TxEvent structure. +// +// Parameters: +// - eitherEventBz: Binary data of the event, potentially encapsulating an error. +// +// Returns: +// - eitherTxEvent: The TxEvent or an encapsulated error, facilitating clear +// error management in the caller's context. +// - skip: A flag denoting if the event should be bypassed. A value of true +// suggests the event be disregarded, progressing to the succeeding message. +func (tClient *txClient) txEventFromEventBz( + eitherEventBz either.Bytes, +) (eitherTxEvent either.Either[*TxEvent], skip bool) { + + // Extract byte data from the given event. In case of failure, wrap the error + // and denote the event for skipping. + eventBz, err := eitherEventBz.ValueOrError() + if err != nil { + return either.Error[*TxEvent](err), true + } + + // Unmarshal byte data into a TxEvent object. + txEvt, err := tClient.unmarshalTxEvent(eventBz) + switch { + // If the error indicates a non-transactional event, return the TxEvent and + // signal for skipping. + case errors.Is(err, ErrNonTxEventBytes): + return either.Success(txEvt), true + // For other errors, wrap them and flag the event to be skipped. + case err != nil: + return either.Error[*TxEvent](ErrUnmarshalTx.Wrapf("%s", err)), true + } + + // For successful unmarshalling, return the TxEvent. + return either.Success(txEvt), false +} + +// unmarshalTxEvent attempts to deserialize a slice of bytes into a TxEvent. +// It checks if the given bytes correspond to a valid transaction event. +// If the resulting TxEvent has empty transaction bytes, it assumes that +// the message was not a transaction event and returns an ErrNonTxEventBytes error. +func (tClient *txClient) unmarshalTxEvent(eventBz []byte) (*TxEvent, error) { + txEvent := new(TxEvent) + + // Try to deserialize the provided bytes into a TxEvent. + if err := json.Unmarshal(eventBz, txEvent); err != nil { + return nil, err + } + + // Check if the TxEvent has empty transaction bytes, which indicates + // the message might not be a valid transaction event. + if bytes.Equal(txEvent.Tx, []byte{}) { + return nil, ErrNonTxEventBytes.Wrapf("%s", string(eventBz)) + } + + return txEvent, nil +} + +// getTxTimeoutError checks if a transaction with the specified hash has timed out. +// The function decodes the provided hexadecimal hash into bytes and queries the +// transaction using the byte hash. If any error occurs during this process, +// appropriate wrapped errors are returned for easier debugging. +func (tClient *txClient) getTxTimeoutError(ctx context.Context, txHashHex string) error { + + // Decode the provided hex hash into bytes. + txHash, err := hex.DecodeString(txHashHex) + if err != nil { + return ErrInvalidTxHash.Wrapf("%s", txHashHex) + } + + // Query the transaction using the decoded byte hash. + txResponse, err := tClient.txCtx.QueryTx(ctx, txHash, false) + if err != nil { + return ErrQueryTx.Wrapf("with hash: %s: %s", txHashHex, err) + } + + // Return a timeout error with details about the transaction. + return ErrTxTimeout.Wrapf("with hash %s: %s", txHashHex, txResponse.TxResult.Log) +} diff --git a/pkg/client/tx/client_integration_test.go b/pkg/client/tx/client_integration_test.go new file mode 100644 index 000000000..737c8a628 --- /dev/null +++ b/pkg/client/tx/client_integration_test.go @@ -0,0 +1,66 @@ +//go:build integration + +package tx_test + +import ( + "context" + "testing" + + "cosmossdk.io/depinject" + "github.com/cosmos/cosmos-sdk/types" + "github.com/stretchr/testify/require" + + "github.com/pokt-network/poktroll/internal/testclient/testkeyring" + "github.com/pokt-network/poktroll/pkg/client/tx" + + "github.com/pokt-network/poktroll/internal/testclient/testblock" + "github.com/pokt-network/poktroll/internal/testclient/testeventsquery" + "github.com/pokt-network/poktroll/internal/testclient/testtx" + "github.com/pokt-network/poktroll/pkg/client" + apptypes "github.com/pokt-network/poktroll/x/application/types" +) + +func TestTxClient_SignAndBroadcast_Integration(t *testing.T) { + t.Skip("TODO_TECHDEBT: this test depends on some setup which is currently not implemented in this test: staked application and servicer with matching services") + + var ctx = context.Background() + + keyring, signingKey := testkeyring.NewTestKeyringWithKey(t, testSigningKeyName) + + eventsQueryClient := testeventsquery.NewLocalnetClient(t) + + _, txCtx := testtx.NewAnyTimesTxTxContext(t, keyring) + + // Construct a new mock block client because it is a required dependency. Since + // we're not exercising transactions timeouts in this test, we don't need to set any + // particular expectations on it, nor do we care about the value of blockHash + // argument. + blockClientMock := testblock.NewLocalnetClient(ctx, t) + + // Construct a new depinject config with the mocks we created above. + txClientDeps := depinject.Supply( + eventsQueryClient, + txCtx, + blockClientMock, + ) + + // Construct the transaction client. + txClient, err := tx.NewTxClient(ctx, txClientDeps, tx.WithSigningKeyName(testSigningKeyName)) + require.NoError(t, err) + + signingKeyAddr, err := signingKey.GetAddress() + require.NoError(t, err) + + // Construct a valid (arbitrary) message to sign, encode, and broadcast. + appStake := types.NewCoin("upokt", types.NewInt(1000000)) + appStakeMsg := &apptypes.MsgStakeApplication{ + Address: signingKeyAddr.String(), + Stake: &appStake, + Services: client.NewTestApplicationServiceConfig(testServiceIdPrefix, 2), + } + + // Sign and broadcast the message. + eitherErr := txClient.SignAndBroadcast(ctx, appStakeMsg) + err, _ = eitherErr.SyncOrAsyncError() + require.NoError(t, err) +} diff --git a/pkg/client/tx/client_test.go b/pkg/client/tx/client_test.go new file mode 100644 index 000000000..f6f1d08a9 --- /dev/null +++ b/pkg/client/tx/client_test.go @@ -0,0 +1,407 @@ +package tx_test + +import ( + "context" + "encoding/json" + "testing" + "time" + + "cosmossdk.io/depinject" + cometbytes "github.com/cometbft/cometbft/libs/bytes" + cosmoskeyring "github.com/cosmos/cosmos-sdk/crypto/keyring" + "github.com/cosmos/cosmos-sdk/types" + "github.com/golang/mock/gomock" + "github.com/stretchr/testify/require" + + "github.com/pokt-network/poktroll/internal/mocks/mockclient" + "github.com/pokt-network/poktroll/internal/testclient" + "github.com/pokt-network/poktroll/internal/testclient/testblock" + "github.com/pokt-network/poktroll/internal/testclient/testeventsquery" + "github.com/pokt-network/poktroll/internal/testclient/testkeyring" + "github.com/pokt-network/poktroll/internal/testclient/testtx" + "github.com/pokt-network/poktroll/pkg/client" + "github.com/pokt-network/poktroll/pkg/client/keyring" + "github.com/pokt-network/poktroll/pkg/client/tx" + "github.com/pokt-network/poktroll/pkg/either" + apptypes "github.com/pokt-network/poktroll/x/application/types" +) + +const ( + testSigningKeyName = "test_signer" + // NB: testServiceIdPrefix must not be longer than 7 characters due to + // maxServiceIdLen. + testServiceIdPrefix = "testsvc" + txCommitTimeout = 10 * time.Millisecond +) + +// TODO_TECHDEBT: add coverage for the transactions client handling an events bytes error either. + +func TestTxClient_SignAndBroadcast_Succeeds(t *testing.T) { + var ( + // expectedTx is the expected transactions bytes that will be signed and broadcast + // by the transaction client. It is computed and assigned in the + // testtx.NewOneTimeTxTxContext helper function. The same reference needs + // to be used across the expectations that are set on the transactions context mock. + expectedTx cometbytes.HexBytes + // eventsBzPublishCh is the channel that the mock events query client + // will use to publish the transactions event bytes. It is used near the end of + // the test to mock the network signaling that the transactions was committed. + eventsBzPublishCh chan<- either.Bytes + blocksPublishCh chan client.Block + ctx = context.Background() + ) + + keyring, signingKey := testkeyring.NewTestKeyringWithKey(t, testSigningKeyName) + + eventsQueryClient := testeventsquery.NewOneTimeTxEventsQueryClient( + ctx, t, signingKey, &eventsBzPublishCh, + ) + + txCtxMock := testtx.NewOneTimeTxTxContext( + t, keyring, + testSigningKeyName, + &expectedTx, + ) + + // Construct a new mock block client because it is a required dependency. + // Since we're not exercising transactions timeouts in this test, we don't need to + // set any particular expectations on it, nor do we care about the contents + // of the latest block. + blockClientMock := testblock.NewOneTimeCommittedBlocksSequenceBlockClient( + t, blocksPublishCh, + ) + + // Construct a new depinject config with the mocks we created above. + txClientDeps := depinject.Supply( + eventsQueryClient, + txCtxMock, + blockClientMock, + ) + + // Construct the transaction client. + txClient, err := tx.NewTxClient( + ctx, txClientDeps, tx.WithSigningKeyName(testSigningKeyName), + ) + require.NoError(t, err) + + signingKeyAddr, err := signingKey.GetAddress() + require.NoError(t, err) + + // Construct a valid (arbitrary) message to sign, encode, and broadcast. + appStake := types.NewCoin("upokt", types.NewInt(1000000)) + appStakeMsg := &apptypes.MsgStakeApplication{ + Address: signingKeyAddr.String(), + Stake: &appStake, + Services: client.NewTestApplicationServiceConfig(testServiceIdPrefix, 2), + } + + // Sign and broadcast the message. + eitherErr := txClient.SignAndBroadcast(ctx, appStakeMsg) + err, errCh := eitherErr.SyncOrAsyncError() + require.NoError(t, err) + + // Construct the expected transaction event bytes from the expected transaction bytes. + txEventBz, err := json.Marshal(&tx.TxEvent{Tx: expectedTx}) + require.NoError(t, err) + + // Publish the transaction event bytes to the events query client so that the transaction client + // registers the transactions as committed (i.e. removes it from the timeout pool). + eventsBzPublishCh <- either.Success[[]byte](txEventBz) + + // Assert that the error channel was closed without receiving. + select { + case err, ok := <-errCh: + require.NoError(t, err) + require.Falsef(t, ok, "expected errCh to be closed") + case <-time.After(txCommitTimeout): + t.Fatal("test timed out waiting for errCh to receive") + } +} + +func TestTxClient_NewTxClient_Error(t *testing.T) { + // Construct an empty in-memory keyring. + memKeyring := cosmoskeyring.NewInMemory(testclient.EncodingConfig.Marshaler) + + tests := []struct { + name string + signingKeyName string + expectedErr error + }{ + { + name: "empty signing key name", + signingKeyName: "", + expectedErr: keyring.ErrEmptySigningKeyName, + }, + { + name: "signing key does not exist", + signingKeyName: "nonexistent", + expectedErr: keyring.ErrNoSuchSigningKey, + }, + // TODO_TECHDEBT: add coverage for this error case + // { + // name: "failed to get address", + // testSigningKeyName: "incompatible", + // expectedErr: tx.ErrSigningKeyAddr, + // }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + var ( + ctrl = gomock.NewController(t) + ctx = context.Background() + ) + + // Construct a new mock events query client. Since we expect the + // NewTxClient call to fail, we don't need to set any expectations + // on this mock. + eventsQueryClient := mockclient.NewMockEventsQueryClient(ctrl) + + // Construct a new mock transactions context. + txCtxMock, _ := testtx.NewAnyTimesTxTxContext(t, memKeyring) + + // Construct a new mock block client. Since we expect the NewTxClient + // call to fail, we don't need to set any expectations on this mock. + blockClientMock := mockclient.NewMockBlockClient(ctrl) + + // Construct a new depinject config with the mocks we created above. + txClientDeps := depinject.Supply( + eventsQueryClient, + txCtxMock, + blockClientMock, + ) + + // Construct a signing key option using the test signing key name. + signingKeyOpt := tx.WithSigningKeyName(tt.signingKeyName) + + // Attempt to create the transactions client. + txClient, err := tx.NewTxClient(ctx, txClientDeps, signingKeyOpt) + require.ErrorIs(t, err, tt.expectedErr) + require.Nil(t, txClient) + }) + } +} + +func TestTxClient_SignAndBroadcast_SyncError(t *testing.T) { + var ( + // eventsBzPublishCh is the channel that the mock events query client + // will use to publish the transactions event bytes. It is not used in + // this test but is required to use the NewOneTimeTxEventsQueryClient + // helper. + eventsBzPublishCh chan<- either.Bytes + // blocksPublishCh is the channel that the mock block client will use + // to publish the latest block. It is not used in this test but is + // required to use the NewOneTimeCommittedBlocksSequenceBlockClient + // helper. + blocksPublishCh chan client.Block + ctx = context.Background() + ) + + keyring, signingKey := testkeyring.NewTestKeyringWithKey(t, testSigningKeyName) + + // Construct a new mock events query client. Since we expect the + // NewTxClient call to fail, we don't need to set any expectations + // on this mock. + eventsQueryClient := testeventsquery.NewOneTimeTxEventsQueryClient( + ctx, t, signingKey, &eventsBzPublishCh, + ) + + // Construct a new mock transaction context. + txCtxMock, _ := testtx.NewAnyTimesTxTxContext(t, keyring) + + // Construct a new mock block client because it is a required dependency. + // Since we're not exercising transactions timeouts in this test, we don't need to + // set any particular expectations on it, nor do we care about the contents + // of the latest block. + blockClientMock := testblock.NewOneTimeCommittedBlocksSequenceBlockClient( + t, blocksPublishCh, + ) + + // Construct a new depinject config with the mocks we created above. + txClientDeps := depinject.Supply( + eventsQueryClient, + txCtxMock, + blockClientMock, + ) + + // Construct the transaction client. + txClient, err := tx.NewTxClient( + ctx, txClientDeps, tx.WithSigningKeyName(testSigningKeyName), + ) + require.NoError(t, err) + + // Construct an invalid (arbitrary) message to sign, encode, and broadcast. + signingAddr, err := signingKey.GetAddress() + require.NoError(t, err) + appStakeMsg := &apptypes.MsgStakeApplication{ + // Providing address to avoid panic from #GetSigners(). + Address: signingAddr.String(), + Stake: nil, + // NB: explicitly omitting required fields + } + + eitherErr := txClient.SignAndBroadcast(ctx, appStakeMsg) + err, _ = eitherErr.SyncOrAsyncError() + require.ErrorIs(t, err, tx.ErrInvalidMsg) + + time.Sleep(10 * time.Millisecond) +} + +// TODO_INCOMPLETE: add coverage for async error; i.e. insufficient gas or on-chain error +func TestTxClient_SignAndBroadcast_CheckTxError(t *testing.T) { + var ( + // expectedErrMsg is the expected error message that will be returned + // by the transaction client. It is computed and assigned in the + // testtx.NewOneTimeErrCheckTxTxContext helper function. + expectedErrMsg string + // eventsBzPublishCh is the channel that the mock events query client + // will use to publish the transactions event bytes. It is used near the end of + // the test to mock the network signaling that the transactions was committed. + eventsBzPublishCh chan<- either.Bytes + blocksPublishCh chan client.Block + ctx = context.Background() + ) + + keyring, signingKey := testkeyring.NewTestKeyringWithKey(t, testSigningKeyName) + + eventsQueryClient := testeventsquery.NewOneTimeTxEventsQueryClient( + ctx, t, signingKey, &eventsBzPublishCh, + ) + + txCtxMock := testtx.NewOneTimeErrCheckTxTxContext( + t, keyring, + testSigningKeyName, + &expectedErrMsg, + ) + + // Construct a new mock block client because it is a required dependency. + // Since we're not exercising transactions timeouts in this test, we don't need to + // set any particular expectations on it, nor do we care about the contents + // of the latest block. + blockClientMock := testblock.NewOneTimeCommittedBlocksSequenceBlockClient( + t, blocksPublishCh, + ) + + // Construct a new depinject config with the mocks we created above. + txClientDeps := depinject.Supply( + eventsQueryClient, + txCtxMock, + blockClientMock, + ) + + // Construct the transaction client. + txClient, err := tx.NewTxClient(ctx, txClientDeps, tx.WithSigningKeyName(testSigningKeyName)) + require.NoError(t, err) + + signingKeyAddr, err := signingKey.GetAddress() + require.NoError(t, err) + + // Construct a valid (arbitrary) message to sign, encode, and broadcast. + appStake := types.NewCoin("upokt", types.NewInt(1000000)) + appStakeMsg := &apptypes.MsgStakeApplication{ + Address: signingKeyAddr.String(), + Stake: &appStake, + Services: client.NewTestApplicationServiceConfig(testServiceIdPrefix, 2), + } + + // Sign and broadcast the message. + eitherErr := txClient.SignAndBroadcast(ctx, appStakeMsg) + err, _ = eitherErr.SyncOrAsyncError() + require.ErrorIs(t, err, tx.ErrCheckTx) + require.ErrorContains(t, err, expectedErrMsg) +} + +func TestTxClient_SignAndBroadcast_Timeout(t *testing.T) { + var ( + // expectedErrMsg is the expected error message that will be returned + // by the transaction client. It is computed and assigned in the + // testtx.NewOneTimeErrCheckTxTxContext helper function. + expectedErrMsg string + // eventsBzPublishCh is the channel that the mock events query client + // will use to publish the transaction event bytes. It is used near the end of + // the test to mock the network signaling that the transaction was committed. + eventsBzPublishCh chan<- either.Bytes + blocksPublishCh = make(chan client.Block, tx.DefaultCommitTimeoutHeightOffset) + ctx = context.Background() + ) + + keyring, signingKey := testkeyring.NewTestKeyringWithKey(t, testSigningKeyName) + + eventsQueryClient := testeventsquery.NewOneTimeTxEventsQueryClient( + ctx, t, signingKey, &eventsBzPublishCh, + ) + + txCtxMock := testtx.NewOneTimeErrTxTimeoutTxContext( + t, keyring, + testSigningKeyName, + &expectedErrMsg, + ) + + // Construct a new mock block client because it is a required dependency. + // Since we're not exercising transaction timeouts in this test, we don't need to + // set any particular expectations on it, nor do we care about the contents + // of the latest block. + blockClientMock := testblock.NewOneTimeCommittedBlocksSequenceBlockClient( + t, blocksPublishCh, + ) + + // Construct a new depinject config with the mocks we created above. + txClientDeps := depinject.Supply( + eventsQueryClient, + txCtxMock, + blockClientMock, + ) + + // Construct the transaction client. + txClient, err := tx.NewTxClient( + ctx, txClientDeps, tx.WithSigningKeyName(testSigningKeyName), + ) + require.NoError(t, err) + + signingKeyAddr, err := signingKey.GetAddress() + require.NoError(t, err) + + // Construct a valid (arbitrary) message to sign, encode, and broadcast. + appStake := types.NewCoin("upokt", types.NewInt(1000000)) + appStakeMsg := &apptypes.MsgStakeApplication{ + Address: signingKeyAddr.String(), + Stake: &appStake, + Services: client.NewTestApplicationServiceConfig(testServiceIdPrefix, 2), + } + + // Sign and broadcast the message in a transaction. + eitherErr := txClient.SignAndBroadcast(ctx, appStakeMsg) + err, errCh := eitherErr.SyncOrAsyncError() + require.NoError(t, err) + + for i := 0; i < tx.DefaultCommitTimeoutHeightOffset; i++ { + blocksPublishCh <- testblock.NewAnyTimesBlock(t, []byte{}, int64(i+1)) + } + + // Assert that we receive the expected error type & message. + select { + case err := <-errCh: + require.ErrorIs(t, err, tx.ErrTxTimeout) + require.ErrorContains(t, err, expectedErrMsg) + // NB: wait 110% of txCommitTimeout; a bit longer than strictly necessary in + // order to mitigate flakiness. + case <-time.After(txCommitTimeout * 110 / 100): + t.Fatal("test timed out waiting for errCh to receive") + } + + // Assert that the error channel was closed. + select { + case err, ok := <-errCh: + require.Falsef(t, ok, "expected errCh to be closed") + require.NoError(t, err) + // NB: Give the error channel some time to be ready to receive in order to + // mitigate flakiness. + case <-time.After(50 * time.Millisecond): + t.Fatal("expected errCh to be closed") + } +} + +// TODO_TECHDEBT: add coverage for sending multiple messages simultaneously +func TestTxClient_SignAndBroadcast_MultipleMsgs(t *testing.T) { + t.SkipNow() +} diff --git a/pkg/client/tx/context.go b/pkg/client/tx/context.go new file mode 100644 index 000000000..eca32f943 --- /dev/null +++ b/pkg/client/tx/context.go @@ -0,0 +1,95 @@ +package tx + +import ( + "context" + + "cosmossdk.io/depinject" + cometrpctypes "github.com/cometbft/cometbft/rpc/core/types" + cosmosclient "github.com/cosmos/cosmos-sdk/client" + cosmostx "github.com/cosmos/cosmos-sdk/client/tx" + cosmoskeyring "github.com/cosmos/cosmos-sdk/crypto/keyring" + cosmostypes "github.com/cosmos/cosmos-sdk/types" + authclient "github.com/cosmos/cosmos-sdk/x/auth/client" + + "github.com/pokt-network/poktroll/pkg/client" +) + +var _ client.TxContext = (*cosmosTxContext)(nil) + +// cosmosTxContext is an internal implementation of the client.TxContext interface. +// It provides methods related to transaction context within the Cosmos SDK. +type cosmosTxContext struct { + // Holds cosmos-sdk client context. + // (see: https://pkg.go.dev/github.com/cosmos/cosmos-sdk@v0.47.5/client#Context) + clientCtx cosmosclient.Context + // Holds the cosmos-sdk transaction factory. + // (see: https://pkg.go.dev/github.com/cosmos/cosmos-sdk@v0.47.5/client/tx#Factory) + txFactory cosmostx.Factory +} + +// NewTxContext initializes a new cosmosTxContext with the given dependencies. +// It uses depinject to populate its members and returns a client.TxContext +// interface type. +func NewTxContext(deps depinject.Config) (client.TxContext, error) { + txCtx := cosmosTxContext{} + + if err := depinject.Inject( + deps, + &txCtx.clientCtx, + &txCtx.txFactory, + ); err != nil { + return nil, err + } + + return txCtx, nil +} + +// GetKeyring returns the cosmos-sdk client Keyring associated with the transaction factory. +func (txCtx cosmosTxContext) GetKeyring() cosmoskeyring.Keyring { + return txCtx.txFactory.Keybase() +} + +// SignTx signs the provided transaction using the given key name. It can operate in offline mode +// and can optionally overwrite any existing signatures. +// It is a proxy to the cosmos-sdk auth module client SignTx function. +// (see: https://pkg.go.dev/github.com/cosmos/cosmos-sdk@v0.47.5/x/auth/client) +func (txCtx cosmosTxContext) SignTx( + signingKeyName string, + txBuilder cosmosclient.TxBuilder, + offline, overwriteSig bool, +) error { + return authclient.SignTx( + txCtx.txFactory, + txCtx.clientCtx, + signingKeyName, + txBuilder, + offline, overwriteSig, + ) +} + +// NewTxBuilder returns a new transaction builder instance using the cosmos-sdk client transaction config. +func (txCtx cosmosTxContext) NewTxBuilder() cosmosclient.TxBuilder { + return txCtx.clientCtx.TxConfig.NewTxBuilder() +} + +// EncodeTx encodes the provided tx and returns its bytes representation. +func (txCtx cosmosTxContext) EncodeTx(txBuilder cosmosclient.TxBuilder) ([]byte, error) { + return txCtx.clientCtx.TxConfig.TxEncoder()(txBuilder.GetTx()) +} + +// BroadcastTx broadcasts the given transaction to the network, blocking until the check-tx +// ABCI operation completes and returns a TxResponse of the transaction status at that point in time. +func (txCtx cosmosTxContext) BroadcastTx(txBytes []byte) (*cosmostypes.TxResponse, error) { + return txCtx.clientCtx.BroadcastTxAsync(txBytes) + //return txCtx.clientCtx.BroadcastTxSync(txBytes) +} + +// QueryTx queries the transaction based on its hash and optionally provides proof +// of the transaction. It returns the transaction query result. +func (txCtx cosmosTxContext) QueryTx( + ctx context.Context, + txHash []byte, + prove bool, +) (*cometrpctypes.ResultTx, error) { + return txCtx.clientCtx.Client.Tx(ctx, txHash, prove) +} diff --git a/pkg/client/tx/encoding.go b/pkg/client/tx/encoding.go new file mode 100644 index 000000000..78612e7b7 --- /dev/null +++ b/pkg/client/tx/encoding.go @@ -0,0 +1,18 @@ +package tx + +import ( + "fmt" + "strings" +) + +// normalizeTxHashHex defines canonical and unambiguous representation for a +// transaction hash hexadecimal string; lower-case. +func normalizeTxHashHex(txHash string) string { + return strings.ToLower(txHash) +} + +// txHashBytesToNormalizedHex converts a transaction hash bytes to a normalized +// hexadecimal string representation. +func txHashBytesToNormalizedHex(txHash []byte) string { + return normalizeTxHashHex(fmt.Sprintf("%x", txHash)) +} diff --git a/pkg/client/tx/errors.go b/pkg/client/tx/errors.go new file mode 100644 index 000000000..1e43f1d05 --- /dev/null +++ b/pkg/client/tx/errors.go @@ -0,0 +1,41 @@ +package tx + +import errorsmod "cosmossdk.io/errors" + +var ( + // ErrInvalidMsg signifies that there was an issue in validating the + // transaction message. This could be due to format, content, or other + // constraints imposed on the message. + ErrInvalidMsg = errorsmod.Register(codespace, 4, "failed to validate tx message") + + // ErrCheckTx indicates an error occurred during the ABCI check transaction + // process, which verifies the transaction's integrity before it is added + // to the mempool. + ErrCheckTx = errorsmod.Register(codespace, 5, "error during ABCI check tx") + + // ErrTxTimeout is raised when a transaction has taken too long to + // complete, surpassing a predefined threshold. + ErrTxTimeout = errorsmod.Register(codespace, 6, "tx timed out") + + // ErrQueryTx indicates an error occurred while trying to query for the status + // of a specific transaction, likely due to issues with the query parameters + // or the state of the blockchain network. + ErrQueryTx = errorsmod.Register(codespace, 7, "error encountered while querying for tx") + + // ErrInvalidTxHash represents an error which is triggered when the + // transaction hash provided does not adhere to the expected format or + // constraints, implying it may be corrupted or tampered with. + ErrInvalidTxHash = errorsmod.Register(codespace, 8, "invalid tx hash") + + // ErrNonTxEventBytes indicates an attempt to deserialize bytes that do not + // correspond to a transaction event. This error is triggered when the provided + // byte data isn't recognized as a valid transaction event representation. + ErrNonTxEventBytes = errorsmod.Register(codespace, 9, "attempted to deserialize non-tx event bytes") + + // ErrUnmarshalTx signals a failure in the unmarshalling process of a transaction. + // This error is triggered when the system encounters issues translating a set of + // bytes into the corresponding Tx structure or object. + ErrUnmarshalTx = errorsmod.Register(codespace, 10, "failed to unmarshal tx") + + codespace = "tx_client" +) diff --git a/pkg/client/tx/options.go b/pkg/client/tx/options.go new file mode 100644 index 000000000..34e782b6d --- /dev/null +++ b/pkg/client/tx/options.go @@ -0,0 +1,22 @@ +package tx + +import ( + "github.com/pokt-network/poktroll/pkg/client" +) + +// WithCommitTimeoutBlocks sets the timeout duration in terms of number of blocks +// for the client to wait for broadcast transactions to be committed before +// returning a timeout error. +func WithCommitTimeoutBlocks(timeout int64) client.TxClientOption { + return func(client client.TxClient) { + client.(*txClient).commitTimeoutHeightOffset = timeout + } +} + +// WithSigningKeyName sets the name of the key which should be retrieved from the +// keyring and used for signing transactions. +func WithSigningKeyName(keyName string) client.TxClientOption { + return func(client client.TxClient) { + client.(*txClient).signingKeyName = keyName + } +} diff --git a/pkg/either/errors.go b/pkg/either/errors.go new file mode 100644 index 000000000..f464b8886 --- /dev/null +++ b/pkg/either/errors.go @@ -0,0 +1,35 @@ +package either + +// SyncErr creates an AsyncError either from a synchronous error. +// It wraps the Error into the left field (conventionally associated with the +// error value in the Either pattern) of the Either type. It casts the result +// to the AsyncError type. +func SyncErr(err error) AsyncError { + return AsyncError(Error[chan error](err)) +} + +// AsyncErr creates an AsyncError from an error channel. +// It wraps the error channel into the right field (conventionally associated with +// successful values in the Either pattern) of the Either type. +func AsyncErr(errCh chan error) AsyncError { + return AsyncError(Success[chan error](errCh)) +} + +// SyncOrAsyncError decomposes the AsyncError into its components, returning +// a synchronous error and an error channel. If the AsyncError represents a +// synchronous error, the error channel will be nil and vice versa. +func (soaErr AsyncError) SyncOrAsyncError() (error, chan error) { + errCh, err := Either[chan error](soaErr).ValueOrError() + return err, errCh +} + +// IsSyncError checks if the AsyncError represents a synchronous error. +func (soaErr AsyncError) IsSyncError() bool { + return Either[chan error](soaErr).IsError() +} + +// IsAsyncError checks if the AsyncError represents an asynchronous error +// (sent through a channel). +func (soaErr AsyncError) IsAsyncError() bool { + return Either[chan error](soaErr).IsSuccess() +} diff --git a/pkg/either/types.go b/pkg/either/types.go new file mode 100644 index 000000000..ae7092479 --- /dev/null +++ b/pkg/either/types.go @@ -0,0 +1,9 @@ +package either + +type ( + // AsyncError represents a value which could either be a synchronous error or + // an asynchronous error (sent through a channel). It wraps the more generic + // `Either` type specific for error channels. + AsyncError Either[chan error] + Bytes = Either[[]byte] +) diff --git a/pkg/observable/channel/map.go b/pkg/observable/channel/map.go index d70bb459e..c6722e09d 100644 --- a/pkg/observable/channel/map.go +++ b/pkg/observable/channel/map.go @@ -3,9 +3,11 @@ package channel import ( "context" - "pocket/pkg/observable" + "github.com/pokt-network/poktroll/pkg/observable" ) +type MapFn[S, D any] func(src S) (dst D, skip bool) + // Map transforms the given observable by applying the given transformFn to each // notification received from the observable. If the transformFn returns a skip // bool of true, the notification is skipped and not emitted to the resulting @@ -14,7 +16,7 @@ func Map[S, D any]( ctx context.Context, srcObservable observable.Observable[S], // TODO_CONSIDERATION: if this were variadic, it could simplify serial transformations. - transformFn func(src S) (dst D, skip bool), + transformFn MapFn[S, D], ) observable.Observable[D] { dstObservable, dstProducer := NewObservable[D]() srcObserver := srcObservable.Subscribe(ctx) diff --git a/pkg/observable/channel/map_test.go b/pkg/observable/channel/map_test.go index 37d7f5744..98b9aa8ad 100644 --- a/pkg/observable/channel/map_test.go +++ b/pkg/observable/channel/map_test.go @@ -8,7 +8,7 @@ import ( "github.com/stretchr/testify/require" - "pocket/pkg/observable/channel" + "github.com/pokt-network/poktroll/pkg/observable/channel" ) func TestMap_Word_BytesToPalindrome(t *testing.T) { @@ -69,7 +69,7 @@ func TestMap_Word_BytesToPalindrome(t *testing.T) { }() // wait a tick for the observer to receive the word - time.Sleep(time.Millisecond) + time.Sleep(10 * time.Millisecond) // ensure that the observer received the word require.Equal(t, int32(1), atomic.LoadInt32(&wordCounter)) diff --git a/pkg/observable/channel/observable.go b/pkg/observable/channel/observable.go index f6e25ee74..fa898200f 100644 --- a/pkg/observable/channel/observable.go +++ b/pkg/observable/channel/observable.go @@ -2,9 +2,8 @@ package channel import ( "context" - "sync" - "pocket/pkg/observable" + "github.com/pokt-network/poktroll/pkg/observable" ) // TODO_DISCUSS: what should this be? should it be configurable? It seems to be most @@ -13,7 +12,10 @@ import ( // defaultSubscribeBufferSize is the buffer size of a observable's publish channel. const defaultPublishBufferSize = 50 -var _ observable.Observable[any] = (*channelObservable[any])(nil) +var ( + _ observable.Observable[any] = (*channelObservable[any])(nil) + _ observerManager[any] = (*channelObservable[any])(nil) +) // option is a function which receives and can modify the channelObservable state. type option[V any] func(obs *channelObservable[V]) @@ -21,14 +23,14 @@ type option[V any] func(obs *channelObservable[V]) // channelObservable implements the observable.Observable interface and can be notified // by sending on its corresponding publishCh channel. type channelObservable[V any] struct { + // embed observerManager to encapsulate concurrent-safe read/write access to + // observers. This also allows higher-level objects to wrap this observable + // without knowing its specific type by asserting that it implements the + // observerManager interface. + observerManager[V] // publishCh is an observable-wide channel that is used to receive values // which are subsequently fanned out to observers. publishCh chan V - // observersMu protects observers from concurrent access/updates - observersMu *sync.RWMutex - // observers is a list of channelObservers that will be notified when publishCh - // receives a new value. - observers []*channelObserver[V] } // NewObservable creates a new observable which is notified when the publishCh @@ -36,8 +38,7 @@ type channelObservable[V any] struct { func NewObservable[V any](opts ...option[V]) (observable.Observable[V], chan<- V) { // initialize an observable that publishes messages from 1 publishCh to N observers obs := &channelObservable[V]{ - observersMu: &sync.RWMutex{}, - observers: []*channelObserver[V]{}, + observerManager: newObserverManager[V](), } for _, opt := range opts { @@ -66,114 +67,35 @@ func WithPublisher[V any](publishCh chan V) option[V] { // Subscribe returns an observer which is notified when the publishCh channel // receives a value. -func (obsvbl *channelObservable[V]) Subscribe(ctx context.Context) observable.Observer[V] { - // must (write) lock observersMu so that we can safely append to the observers list - obsvbl.observersMu.Lock() - defer obsvbl.observersMu.Unlock() - - observer := NewObserver[V](ctx, obsvbl.onUnsubscribe) - obsvbl.observers = append(obsvbl.observers, observer) +func (obs *channelObservable[V]) Subscribe(ctx context.Context) observable.Observer[V] { + // Create a new observer and add it to the list of observers to be notified + // when publishCh receives a new value. + observer := NewObserver[V](ctx, obs.observerManager.remove) + obs.observerManager.add(observer) - // caller can rely on context cancellation or call UnsubscribeAll() to unsubscribe + // caller can rely on context cancelation or call UnsubscribeAll() to unsubscribe // active observers if ctx != nil { // asynchronously wait for the context to be done and then unsubscribe // this observer. - go goUnsubscribeOnDone[V](ctx, observer) + go obs.observerManager.goUnsubscribeOnDone(ctx, observer) } return observer } // UnsubscribeAll unsubscribes and removes all observers from the observable. -func (obsvbl *channelObservable[V]) UnsubscribeAll() { - obsvbl.unsubscribeAll() -} - -// unsubscribeAll unsubscribes and removes all observers from the observable. -func (obsvbl *channelObservable[V]) unsubscribeAll() { - // Copy currentObservers to avoid holding the lock while unsubscribing them. - // The observers at the time of locking, prior to copying, are the canonical - // set of observers which are unsubscribed. - // New or existing Observers may (un)subscribe while the observable is closing. - // Any such observers won't be isClosed but will also stop receiving notifications - // immediately (if they receive any at all). - currentObservers := obsvbl.copyObservers() - for _, observer := range currentObservers { - observer.Unsubscribe() - } - - // Reset observers to an empty list. This purges any observers which might have - // subscribed while the observable was closing. - obsvbl.observersMu.Lock() - obsvbl.observers = []*channelObserver[V]{} - obsvbl.observersMu.Unlock() +func (obs *channelObservable[V]) UnsubscribeAll() { + obs.observerManager.removeAll() } // goPublish to the publishCh and notify observers when values are received. // This function is blocking and should be run in a goroutine. -func (obsvbl *channelObservable[V]) goPublish() { - for notification := range obsvbl.publishCh { - // Copy currentObservers to avoid holding the lock while notifying them. - // New or existing Observers may (un)subscribe while this notification - // is being fanned out. - // The observers at the time of locking, prior to copying, are the canonical - // set of observers which receive this notification. - currentObservers := obsvbl.copyObservers() - for _, obsvr := range currentObservers { - // TODO_CONSIDERATION: perhaps continue trying to avoid making this - // notification async as it would effectively use goroutines - // in memory as a buffer (unbounded). - obsvr.notify(notification) - } +func (obs *channelObservable[V]) goPublish() { + for notification := range obs.publishCh { + obs.observerManager.notifyAll(notification) } // Here we know that the publisher channel has been closed. // Unsubscribe all observers as they can no longer receive notifications. - obsvbl.unsubscribeAll() -} - -// copyObservers returns a copy of the current observers list. It is safe to -// call concurrently. -func (obsvbl *channelObservable[V]) copyObservers() (observers []*channelObserver[V]) { - defer obsvbl.observersMu.RUnlock() - - // This loop blocks on acquiring a read lock on observersMu. If TryRLock - // fails, the loop continues until it succeeds. This is intended to give - // callers a guarantee that this copy operation won't contribute to a deadlock. - for { - // block until a read lock can be acquired - if obsvbl.observersMu.TryRLock() { - break - } - } - - observers = make([]*channelObserver[V], len(obsvbl.observers)) - copy(observers, obsvbl.observers) - - return observers -} - -// goUnsubscribeOnDone unsubscribes from the subscription when the context is done. -// It is a blocking function and intended to be called in a goroutine. -func goUnsubscribeOnDone[V any](ctx context.Context, observer observable.Observer[V]) { - <-ctx.Done() - if observer.IsClosed() { - return - } - observer.Unsubscribe() -} - -// onUnsubscribe returns a function that removes a given observer from the -// observable's list of observers. -func (obsvbl *channelObservable[V]) onUnsubscribe(toRemove *channelObserver[V]) { - // must (write) lock to iterate over and modify the observers list - obsvbl.observersMu.Lock() - defer obsvbl.observersMu.Unlock() - - for i, observer := range obsvbl.observers { - if observer == toRemove { - obsvbl.observers = append((obsvbl.observers)[:i], (obsvbl.observers)[i+1:]...) - break - } - } + obs.observerManager.removeAll() } diff --git a/pkg/observable/channel/observable_test.go b/pkg/observable/channel/observable_test.go index 6ec301cfa..cb89c79d8 100644 --- a/pkg/observable/channel/observable_test.go +++ b/pkg/observable/channel/observable_test.go @@ -10,15 +10,15 @@ import ( "github.com/stretchr/testify/require" "golang.org/x/sync/errgroup" - "pocket/internal/testchannel" - "pocket/internal/testerrors" - "pocket/pkg/observable" - "pocket/pkg/observable/channel" + "github.com/pokt-network/poktroll/internal/testchannel" + "github.com/pokt-network/poktroll/internal/testerrors" + "github.com/pokt-network/poktroll/pkg/observable" + "github.com/pokt-network/poktroll/pkg/observable/channel" ) const ( - publishDelay = 100 * time.Microsecond - notifyTimeout = publishDelay * 20 + publishDelay = time.Millisecond + notifyTimeout = 50 * time.Millisecond cancelUnsubscribeDelay = publishDelay * 2 ) @@ -101,11 +101,11 @@ func TestChannelObservable_NotifyObservers(t *testing.T) { ctx, cancel := context.WithCancel(context.Background()) t.Cleanup(cancel) - obsvbl, publisher := channel.NewObservable[int]( + obsvbl, publishCh := channel.NewObservable[int]( channel.WithPublisher(tt.publishCh), ) require.NotNil(t, obsvbl) - require.NotNil(t, publisher) + require.NotNil(t, publishCh) // construct 3 distinct observers, each with its own channel observers := make([]observable.Observer[int], 1) @@ -132,9 +132,8 @@ func TestChannelObservable_NotifyObservers(t *testing.T) { // onDone is called when the observer channel closes onDone := func(outputs []int) error { - if !assert.Equalf( - t, len(tt.expectedOutputs), - len(outputs), + if !assert.ElementsMatch( + t, tt.expectedOutputs, outputs, "obsvr addr: %p", obsvr, ) { return testerrors.ErrAsync @@ -148,24 +147,23 @@ func TestChannelObservable_NotifyObservers(t *testing.T) { } // notify with test input - publish := delayedPublishFactory(publisher, publishDelay) + publish := delayedPublishFactory(publishCh, publishDelay) for _, input := range tt.inputs { - inputPtr := new(int) - *inputPtr = input - // simulating IO delay in sequential message publishing publish(input) } - cancel() + + // Finished sending values, close publishCh to unsubscribe all observers + // and close all fan-out channels. + close(publishCh) // wait for obsvbl to be notified or timeout err := group.Wait() require.NoError(t, err) - // unsubscribing should close observer channel(s) + // closing publishCh should unsubscribe all observers, causing them + // to close their channels. for _, observer := range observers { - observer.Unsubscribe() - // must drain the channel first to ensure it is isClosed err := testchannel.DrainChannel(observer.Ch()) require.NoError(t, err) @@ -317,27 +315,17 @@ func TestChannelObservable_SequentialPublishAndUnsubscription(t *testing.T) { obsrvn.Lock() defer obsrvn.Unlock() - require.Equalf( - t, len(expectedNotifications[obsnIdx]), - len(obsrvn.Notifications), - "observation index: %d, expected: %+v, actual: %+v", - obsnIdx, expectedNotifications[obsnIdx], obsrvn.Notifications, + require.EqualValuesf( + t, expectedNotifications[obsnIdx], obsrvn.Notifications, + "observation index: %d", obsnIdx, ) - for notificationIdx, expected := range expectedNotifications[obsnIdx] { - require.Equalf( - t, expected, - (obsrvn.Notifications)[notificationIdx], - "allExpected: %+v, allActual: %+v", - expectedNotifications[obsnIdx], obsrvn.Notifications, - ) - } }) } } // TODO_TECHDEBT/TODO_INCOMPLETE: add coverage for active observers closing when publishCh closes. func TestChannelObservable_ObserversCloseOnPublishChannelClose(t *testing.T) { - t.Skip("add coverage: all observers should unsubscribeAll when publishCh closes") + t.Skip("add coverage: all observers should unsubscribe when publishCh closes") } func delayedPublishFactory[V any](publishCh chan<- V, delay time.Duration) func(value V) { diff --git a/pkg/observable/channel/observation_test.go b/pkg/observable/channel/observation_test.go index 17e20e393..71a3aa098 100644 --- a/pkg/observable/channel/observation_test.go +++ b/pkg/observable/channel/observation_test.go @@ -4,7 +4,7 @@ import ( "context" "sync" - "pocket/pkg/observable" + "github.com/pokt-network/poktroll/pkg/observable" ) // NOTE: this file does not contain any tests, only test helpers. diff --git a/pkg/observable/channel/observer.go b/pkg/observable/channel/observer.go index 3a2455e64..95e796e41 100644 --- a/pkg/observable/channel/observer.go +++ b/pkg/observable/channel/observer.go @@ -6,7 +6,7 @@ import ( "sync" "time" - "pocket/pkg/observable" + "github.com/pokt-network/poktroll/pkg/observable" ) const ( @@ -29,9 +29,10 @@ var _ observable.Observer[any] = (*channelObserver[any])(nil) // channelObserver implements the observable.Observer interface. type channelObserver[V any] struct { ctx context.Context - // onUnsubscribe is called in Observer#Unsubscribe, removing the respective - // observer from observers in a concurrency-safe manner. - onUnsubscribe func(toRemove *channelObserver[V]) + // onUnsubscribe is called in Observer#Unsubscribe, closing this observer's + // channel and removing it from the respective obervable's observers list + // in a concurrency-safe manner. + onUnsubscribe func(toRemove observable.Observer[V]) // observerMu protects the observerCh and isClosed fields. observerMu *sync.RWMutex // observerCh is the channel that is used to emit values to the observer. @@ -43,7 +44,7 @@ type channelObserver[V any] struct { isClosed bool } -type UnsubscribeFunc[V any] func(toRemove *channelObserver[V]) +type UnsubscribeFunc[V any] func(toRemove observable.Observer[V]) func NewObserver[V any]( ctx context.Context, diff --git a/pkg/observable/channel/observer_manager.go b/pkg/observable/channel/observer_manager.go new file mode 100644 index 000000000..65acca5bc --- /dev/null +++ b/pkg/observable/channel/observer_manager.go @@ -0,0 +1,152 @@ +package channel + +import ( + "context" + "sync" + + "github.com/pokt-network/poktroll/pkg/observable" +) + +var _ observerManager[any] = (*channelObserverManager[any])(nil) + +// observerManager is an interface intended to be used between an observable and some +// higher-level abstraction and/or observable implementation which would embed it. +// Embedding this interface rather than a channelObservable directly allows for +// more transparency and flexibility in higher-level code. +// NOTE: this interface MUST be used with a common concrete Observer type. +// TODO_CONSIDERATION: Consider whether `observerManager` and `Observable` should remain as separate +// types after some more time and experience using both. +type observerManager[V any] interface { + notifyAll(notification V) + add(toAdd observable.Observer[V]) + remove(toRemove observable.Observer[V]) + removeAll() + goUnsubscribeOnDone(ctx context.Context, observer observable.Observer[V]) +} + +// TODO_CONSIDERATION: if this were a generic implementation, we wouldn't need +// to cast `toAdd` to a channelObserver in add. There are two things +// currently preventing a generic observerManager implementation: +// 1. channelObserver#notify() is not part of the observable.Observer interface +// and is therefore not accessible here. If we move everything into the +// `observable` pkg so that the unexported member is in scope, then the channel +// pkg can't implement it for the same reason, it's an unexported method defined +// in a different pkg. +// 2. == is not defined for a generic Observer type. We would have to add an Equals() +// to the Observer interface. + +// channelObserverManager implements the observerManager interface using +// channelObservers. +type channelObserverManager[V any] struct { + // observersMu protects observers from concurrent access/updates + observersMu *sync.RWMutex + // observers is a list of channelObservers that will be notified when new value + // are received. + observers []*channelObserver[V] +} + +func newObserverManager[V any]() *channelObserverManager[V] { + return &channelObserverManager[V]{ + observersMu: &sync.RWMutex{}, + observers: make([]*channelObserver[V], 0), + } +} + +func (com *channelObserverManager[V]) notifyAll(notification V) { + // Copy currentObservers to avoid holding the lock while notifying them. + // New or existing Observers may (un)subscribe while this notification + // is being fanned out. + // The observers at the time of locking, prior to copying, are the canonical + // set of observers which receive this notification. + currentObservers := com.copyObservers() + for _, obsvr := range currentObservers { + // TODO_TECHDEBT: since this synchronously notifies all observers in a loop, + // it is possible to block here, part-way through notifying all observers, + // on a slow observer consumer (i.e. full buffer). Instead, we should notify + // observers with some limited concurrency of "worker" goroutines. + // The storj/common repo contains such a `Limiter` implementation, see: + // https://github.com/storj/common/blob/main/sync2/limiter.go. + obsvr.notify(notification) + } +} + +// addObserver implements the respective member of observerManager. It is used +// by the channelObservable implementation as well as embedders of observerManager +// (e.g. replayObservable). +// It panics if toAdd is not a channelObserver. +func (com *channelObserverManager[V]) add(toAdd observable.Observer[V]) { + // must (write) lock observersMu so that we can safely append to the observers list + com.observersMu.Lock() + defer com.observersMu.Unlock() + + com.observers = append(com.observers, toAdd.(*channelObserver[V])) +} + +// remove removes a given observer from the observable's list of observers. +// It implements the respective member of observerManager and is used by +// the channelObservable implementation as well as embedders of observerManager +// (e.g. replayObservable). +func (com *channelObserverManager[V]) remove(toRemove observable.Observer[V]) { + // must (write) lock to iterate over and modify the observers list + com.observersMu.Lock() + defer com.observersMu.Unlock() + + for i, observer := range com.observers { + if observer == toRemove { + com.observers = append((com.observers)[:i], (com.observers)[i+1:]...) + break + } + } +} + +// removeAll unsubscribes and removes all observers from the observable. +// It implements the respective member of observerManager and is used by +// the channelObservable implementation as well as embedders of observerManager +// (e.g. replayObservable). +func (com *channelObserverManager[V]) removeAll() { + // Copy currentObservers to avoid holding the lock while unsubscribing them. + // The observers at the time of locking, prior to copying, are the canonical + // set of observers which are unsubscribed. + // New or existing Observers may (un)subscribe while the observable is closing. + // Any such observers won't be isClosed but will also stop receiving notifications + // immediately (if they receive any at all). + currentObservers := com.copyObservers() + for _, observer := range currentObservers { + observer.Unsubscribe() + } + + // Reset observers to an empty list. This purges any observers which might have + // subscribed while the observable was closing. + com.observersMu.Lock() + com.observers = []*channelObserver[V]{} + com.observersMu.Unlock() +} + +// goUnsubscribeOnDone unsubscribes from the subscription when the context is done. +// It is a blocking function and intended to be called in a goroutine. +func (com *channelObserverManager[V]) goUnsubscribeOnDone( + ctx context.Context, + observer observable.Observer[V], +) { + <-ctx.Done() + if observer.IsClosed() { + return + } + observer.Unsubscribe() +} + +// copyObservers returns a copy of the current observers list. It is safe to +// call concurrently. Notably, it is not part of the observerManager interface. +func (com *channelObserverManager[V]) copyObservers() (observers []*channelObserver[V]) { + defer com.observersMu.RUnlock() + + // This loop blocks on acquiring a read lock on observersMu. If TryRLock + // fails, the loop continues until it succeeds. This is intended to give + // callers a guarantee that this copy operation won't contribute to a deadlock. + com.observersMu.RLock() + + observers = make([]*channelObserver[V], len(com.observers)) + copy(observers, com.observers) + + return observers +} diff --git a/pkg/observable/channel/observer_test.go b/pkg/observable/channel/observer_test.go index f8730a422..034541c85 100644 --- a/pkg/observable/channel/observer_test.go +++ b/pkg/observable/channel/observer_test.go @@ -7,20 +7,23 @@ import ( "time" "github.com/stretchr/testify/require" + + "github.com/pokt-network/poktroll/pkg/observable" ) func TestObserver_Unsubscribe(t *testing.T) { var ( - onUnsubscribeCalled = false publishCh = make(chan int, 1) + onUnsubscribeCalled = false + onUnsubscribe = func(toRemove observable.Observer[int]) { + onUnsubscribeCalled = true + } ) obsvr := &channelObserver[int]{ observerMu: &sync.RWMutex{}, // using a buffered channel to keep the test synchronous - observerCh: publishCh, - onUnsubscribe: func(toRemove *channelObserver[int]) { - onUnsubscribeCalled = true - }, + observerCh: publishCh, + onUnsubscribe: onUnsubscribe, } // should initially be open @@ -37,17 +40,19 @@ func TestObserver_Unsubscribe(t *testing.T) { func TestObserver_ConcurrentUnsubscribe(t *testing.T) { var ( - onUnsubscribeCalled = false publishCh = make(chan int, 1) + onUnsubscribeCalled = false + onUnsubscribe = func(toRemove observable.Observer[int]) { + onUnsubscribeCalled = true + } ) + obsvr := &channelObserver[int]{ ctx: context.Background(), observerMu: &sync.RWMutex{}, // using a buffered channel to keep the test synchronous - observerCh: publishCh, - onUnsubscribe: func(toRemove *channelObserver[int]) { - onUnsubscribeCalled = true - }, + observerCh: publishCh, + onUnsubscribe: onUnsubscribe, } require.Equal(t, false, obsvr.isClosed, "observer channel should initially be open") @@ -65,13 +70,16 @@ func TestObserver_ConcurrentUnsubscribe(t *testing.T) { // publish a value obsvr.notify(idx) + + // Slow this loop to prevent bogging the test down. + time.Sleep(10 * time.Microsecond) } }() // send on done when the test cleans up t.Cleanup(func() { done <- struct{}{} }) // it should still be open after a bit of inactivity - time.Sleep(10 * time.Millisecond) + time.Sleep(time.Millisecond) require.Equal(t, false, obsvr.isClosed) obsvr.Unsubscribe() diff --git a/pkg/observable/channel/replay.go b/pkg/observable/channel/replay.go index 7c5eee0c7..a3935543d 100644 --- a/pkg/observable/channel/replay.go +++ b/pkg/observable/channel/replay.go @@ -6,23 +6,30 @@ import ( "sync" "time" - "pocket/pkg/observable" + "github.com/pokt-network/poktroll/pkg/observable" ) -const replayNotificationTimeout = 1 * time.Second +// replayPartialBufferTimeout is the duration to wait for the replay buffer to +// accumulate at least 1 value before returning the accumulated values. +// TODO_CONSIDERATION: perhaps this should be parameterized. +const replayPartialBufferTimeout = 100 * time.Millisecond var _ observable.ReplayObservable[any] = (*replayObservable[any])(nil) type replayObservable[V any] struct { - *channelObservable[V] + // embed observerManager to encapsulate concurrent-safe read/write access to + // observers. This also allows higher-level objects to wrap this observable + // without knowing its specific type by asserting that it implements the + // observerManager interface. + observerManager[V] // replayBufferSize is the number of notifications to buffer so that they // can be replayed to new observers. replayBufferSize int // replayBufferMu protects replayBuffer from concurrent access/updates. replayBufferMu sync.RWMutex - // replayBuffer is the buffer of notifications into which new notifications - // will be pushed and which will be sent to new subscribers before any new - // notifications are sent. + // replayBuffer holds the last relayBufferSize number of notifications received + // by this observable. This buffer is replayed to new observers, on subscribing, + // prior to any new notifications being propagated. replayBuffer []V } @@ -31,31 +38,31 @@ type replayObservable[V any] struct { func NewReplayObservable[V any]( ctx context.Context, replayBufferSize int, + opts ...option[V], ) (observable.ReplayObservable[V], chan<- V) { - obsvbl, publishCh := NewObservable[V]() - return Replay[V](ctx, replayBufferSize, obsvbl), publishCh + obsvbl, publishCh := NewObservable[V](opts...) + return ToReplayObservable[V](ctx, replayBufferSize, obsvbl), publishCh } -// Replay returns an observable which replays the last replayBufferSize number of -// values published to the source observable to new observers, before publishing -// new values. -func Replay[V any]( +// ToReplayObservable returns an observable which replays the last replayBufferSize +// number of values published to the source observable to new observers, before +// publishing new values. +// It panics if srcObservable does not implement the observerManager interface. +// It should only be used with a srcObservable which contains channelObservers +// (i.e. channelObservable or similar). +func ToReplayObservable[V any]( ctx context.Context, replayBufferSize int, srcObsvbl observable.Observable[V], ) observable.ReplayObservable[V] { - // TODO_HACK/TODO_IMPROVE: more effort is required to make a generic replay - // observable; however, as we only have the one observable package (channel), - // and aren't anticipating need another, we can get away with this for now. - chanObsvbl, ok := srcObsvbl.(*channelObservable[V]) - if !ok { - panic("Replay only supports channelObservable") - } + // Assert that the source observable implements the observerMngr required + // to embed and wrap it. + observerMngr := srcObsvbl.(observerManager[V]) replayObsvbl := &replayObservable[V]{ - channelObservable: chanObsvbl, - replayBufferSize: replayBufferSize, - replayBuffer: make([]V, 0, replayBufferSize), + observerManager: observerMngr, + replayBufferSize: replayBufferSize, + replayBuffer: make([]V, 0, replayBufferSize), } srcObserver := srcObsvbl.Subscribe(ctx) @@ -64,11 +71,18 @@ func Replay[V any]( return replayObsvbl } -// Last synchronously returns the last n values from the replay buffer. If n is -// greater than the replay buffer size, the entire replay buffer is returned. -// It blocks until at least n or replayBufferSize (whichever is smaller) -// notifications have accumulated in the replay buffer. +// Last synchronously returns the last n values from the replay buffer. It blocks +// until at least 1 notification has been accumulated, then waits replayPartialBufferTimeout +// duration before returning all notifications accumulated notifications by that time. +// If the replay buffer contains at least n notifications, this function will only +// block as long as it takes to accumulate and return them. +// If n is greater than the replay buffer size, the entire replay buffer is returned. func (ro *replayObservable[V]) Last(ctx context.Context, n int) []V { + // Use a temporary observer to accumulate replay values. + // Subscribe will always start with the replay buffer, so we can safely + // leverage it here for syncrhonization (i.e. blocking until at least 1 + // notification has been accumulated). This also eliminates the need for + // locking and/or copying the replay buffer. tempObserver := ro.Subscribe(ctx) defer tempObserver.Unsubscribe() @@ -81,14 +95,9 @@ func (ro *replayObservable[V]) Last(ctx context.Context, n int) []V { ) } - // Accumulate replay values in a new slice to avoid (read) locking replayBufferMu. - values := make([]V, n) - for i, _ := range values { - // Receiving from the observer channel blocks if replayBuffer is empty. - value := <-tempObserver.Ch() - values[i] = value - } - return values + // accumulateReplayValues works concurrently and returns a context and cancelation + // function for signaling completion. + return accumulateReplayValues(tempObserver, n) } // Subscribe returns an observer which is notified when the publishCh channel @@ -97,37 +106,37 @@ func (ro *replayObservable[V]) Subscribe(ctx context.Context) observable.Observe ro.replayBufferMu.RLock() defer ro.replayBufferMu.RUnlock() - observer := NewObserver[V](ctx, ro.onUnsubscribe) + observer := NewObserver[V](ctx, ro.observerManager.remove) // Replay all buffered replayBuffer to the observer channel buffer before // any new values have an opportunity to send on observerCh (i.e. appending // observer to ro.observers). // // TODO_IMPROVE: this assumes that the observer channel buffer is large enough - // to hold all replay (buffered) replayBuffer. + // to hold all replay (buffered) notifications. for _, notification := range ro.replayBuffer { observer.notify(notification) } - // must (write) lock observersMu so that we can safely append to the observers list - ro.observersMu.Lock() - defer ro.observersMu.Unlock() - - // Explicitly append the observer to the observers list after replaying the - // values in replayBuffer so that replayed notifications aren't re-added to it. - ro.observers = append(ro.observers, observer) + ro.observerManager.add(observer) - // caller can rely on context cancellation or call UnsubscribeAll() to unsubscribe + // caller can rely on context cancelation or call UnsubscribeAll() to unsubscribe // active observers if ctx != nil { // asynchronously wait for the context to be done and then unsubscribe // this observer. - go goUnsubscribeOnDone[V](ctx, observer) + go ro.observerManager.goUnsubscribeOnDone(ctx, observer) } + return observer } -// goBufferReplayNotifications buffers the last n replayBuffer from a source +// UnsubscribeAll unsubscribes and removes all observers from the observable. +func (ro *replayObservable[V]) UnsubscribeAll() { + ro.observerManager.removeAll() +} + +// goBufferReplayNotifications buffers the last n notifications from a source // observer. It is intended to be run in a goroutine. func (ro *replayObservable[V]) goBufferReplayNotifications(srcObserver observable.Observer[V]) { for notification := range srcObserver.Ch() { @@ -143,3 +152,87 @@ func (ro *replayObservable[V]) goBufferReplayNotifications(srcObserver observabl ro.replayBufferMu.Unlock() } } + +// accumulateReplayValues synchronously (but concurrently) accumulates n values +// from the observer channel into the slice pointed to by accValues and then returns +// said slice. It cancels the context either when n values have been accumulated +// or when at least 1 value has been accumulated and replayPartialBufferTimeout +// has elapsed. +func accumulateReplayValues[V any](observer observable.Observer[V], n int) []V { + var ( + // accValuesMu protects accValues from concurrent access. + accValuesMu sync.Mutex + // Accumulate replay values in a new slice to avoid (read) locking replayBufferMu. + accValues = new([]V) + // canceling the context will cause the loop in the goroutine to exit. + ctx, cancel = context.WithCancel(context.Background()) + ) + + // Concurrently accumulate n values from the observer channel. + go func() { + // Defer canceling the context and unlocking accValuesMu. The function + // assumes that the mutex is locked when it gets execution control back + // from the loop. + defer func() { + cancel() + accValuesMu.Unlock() + }() + for { + // Lock the mutex to read accValues here and potentially write in + // the first case branch in the select below. + accValuesMu.Lock() + + // The context was canceled since the last iteration. + if ctx.Err() != nil { + return + } + + // We've accumulated n values. + if len(*accValues) >= n { + return + } + + // Receive from the observer's channel if we can, otherwise let + // the loop run. + select { + // Receiving from the observer channel blocks if replayBuffer is empty. + case value, ok := <-observer.Ch(): + // tempObserver was closed concurrently. + if !ok { + return + } + + // Update the accumulated values pointed to by accValues. + *accValues = append(*accValues, value) + default: + // If we can't receive from the observer channel immediately, + // let the loop run. + } + + // Unlock accValuesMu so that the select below gets a chance to check + // the length of *accValues to decide whether to cancel, and it can + // be relocked at the top of the loop as it must be locked when the + // loop exits. + accValuesMu.Unlock() + // Wait a tick before continuing the loop. + time.Sleep(time.Millisecond) + } + }() + + // Wait for N values to be accumulated or timeout. When timing out, if we + // have at least 1 value, we can return it. Otherwise, we need to wait for + // the next value to be published (i.e. continue the loop). + for { + select { + case <-ctx.Done(): + return *accValues + case <-time.After(replayPartialBufferTimeout): + accValuesMu.Lock() + if len(*accValues) > 1 { + cancel() + return *accValues + } + accValuesMu.Unlock() + } + } +} diff --git a/pkg/observable/channel/replay_test.go b/pkg/observable/channel/replay_test.go index 6223e50c1..0f9b3e9ac 100644 --- a/pkg/observable/channel/replay_test.go +++ b/pkg/observable/channel/replay_test.go @@ -8,8 +8,8 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" - "pocket/internal/testerrors" - "pocket/pkg/observable/channel" + "github.com/pokt-network/poktroll/internal/testerrors" + "github.com/pokt-network/poktroll/pkg/observable/channel" ) func TestReplayObservable(t *testing.T) { @@ -25,10 +25,10 @@ func TestReplayObservable(t *testing.T) { ) t.Cleanup(cancel) - // NB: intentionally not using NewReplayObservable() to test Replay() directly + // NB: intentionally not using NewReplayObservable() to test ToReplayObservable() directly // and to retain a reference to the wrapped observable for testing. obsvbl, publishCh := channel.NewObservable[int]() - replayObsvbl := channel.Replay[int](ctx, replayBufferSize, obsvbl) + replayObsvbl := channel.ToReplayObservable[int](ctx, replayBufferSize, obsvbl) // vanilla observer, should be able to receive all values published after subscribing observer := obsvbl.Subscribe(ctx) @@ -50,7 +50,9 @@ func TestReplayObservable(t *testing.T) { // send all values to the observable's publish channel for _, value := range values { + time.Sleep(10 * time.Microsecond) publishCh <- value + time.Sleep(10 * time.Microsecond) } // allow some time for values to be buffered by the replay observable @@ -59,27 +61,37 @@ func TestReplayObservable(t *testing.T) { // replay observer, should receive the last lastN values published prior to // subscribing followed by subsequently published values replayObserver := replayObsvbl.Subscribe(ctx) + + // Collect values from replayObserver. + var actualValues []int for _, expected := range expectedValues { select { case v := <-replayObserver.Ch(): - require.Equal(t, expected, v) + actualValues = append(actualValues, v) case <-time.After(1 * time.Second): t.Fatalf("Did not receive expected value %d in time", expected) } } - // second replay observer, should receive the same values as the first - // event though it subscribed after all values were published and the + require.EqualValues(t, expectedValues, actualValues) + + // Second replay observer, should receive the same values as the first + // even though it subscribed after all values were published and the // values were already replayed by the first. replayObserver2 := replayObsvbl.Subscribe(ctx) + + // Collect values from replayObserver2. + var actualValues2 []int for _, expected := range expectedValues { select { case v := <-replayObserver2.Ch(): - require.Equal(t, expected, v) + actualValues2 = append(actualValues2, v) case <-time.After(1 * time.Second): t.Fatalf("Did not receive expected value %d in time", expected) } } + + require.EqualValues(t, expectedValues, actualValues) } func TestReplayObservable_Last_Full_ReplayBuffer(t *testing.T) { @@ -133,55 +145,93 @@ func TestReplayObservable_Last_Full_ReplayBuffer(t *testing.T) { } } -func TestReplayObservable_Last_Blocks_Goroutine(t *testing.T) { +func TestReplayObservable_Last_Blocks_And_Times_Out(t *testing.T) { var ( - lastN = 5 + replayBufferSize = 5 + lastN = 5 + // splitIdx is the index at which this test splits the set of values. + // The two groups of values are published at different points in the + // test to test the behavior of Last under different conditions. splitIdx = 3 values = []int{1, 2, 3, 4, 5} ctx = context.Background() ) - replayObsvbl, publishCh := channel.NewReplayObservable[int](ctx, lastN) + replayObsvbl, publishCh := channel.NewReplayObservable[int](ctx, replayBufferSize) + + // getLastValues is a helper function which returns a channel that will + // receive the result of a call to Last, the method under test. + getLastValues := func() chan []int { + lastValuesCh := make(chan []int, 1) + go func() { + // Last should block until lastN values have been published. + // NOTE: this will produce a warning log which can safely be ignored: + // > WARN: requested replay buffer size 3 is greater than replay buffer + // > capacity 3; returning entire replay buffer + lastValuesCh <- replayObsvbl.Last(ctx, lastN) + }() + return lastValuesCh + } - // Publish values up to splitIdx. + // Ensure that last blocks when the replay buffer is empty + select { + case actualValues := <-getLastValues(): + t.Fatalf( + "Last should block until at lest 1 value has been published; actualValues: %v", + actualValues, + ) + case <-time.After(10 * time.Millisecond): + } + + // Publish some values (up to splitIdx). for _, value := range values[:splitIdx] { publishCh <- value time.Sleep(time.Millisecond) } + // Ensure Last works as expected when n <= len(published_values). require.ElementsMatch(t, []int{1}, replayObsvbl.Last(ctx, 1)) require.ElementsMatch(t, []int{1, 2}, replayObsvbl.Last(ctx, 2)) require.ElementsMatch(t, []int{1, 2, 3}, replayObsvbl.Last(ctx, 3)) - // Concurrently call Last with a value greater than the replay buffer size. - lastValues := make(chan []int, 1) - go func() { - // Last should block until lastN values have been published. - lastValues <- replayObsvbl.Last(ctx, lastN) - }() - + // Ensure that Last blocks when n > len(published_values) and the replay + // buffer is not full. select { - case actualValues := <-lastValues: + case actualValues := <-getLastValues(): t.Fatalf( - "Last should block until the replay buffer is full. Actual values: %v", + "Last should block until replayPartialBufferTimeout has elapsed; received values: %v", actualValues, ) - case <-time.After(10 * time.Millisecond): + default: + t.Log("OK: Last is blocking, as expected") } - // Publish values after splitIdx. + // Ensure that Last returns the correct values when n > len(published_values) + // and the replay buffer is not full. + select { + case actualValues := <-getLastValues(): + require.ElementsMatch(t, values[:splitIdx], actualValues) + case <-time.After(250 * time.Millisecond): + t.Fatal("timed out waiting for Last to return") + } + + // Publish the rest of the values (from splitIdx on). for _, value := range values[splitIdx:] { publishCh <- value time.Sleep(time.Millisecond) } + // Ensure that Last doesn't block when n = len(published_values) and the + // replay buffer is full. select { - case actualValues := <-lastValues: + case actualValues := <-getLastValues(): + require.Len(t, actualValues, lastN) require.ElementsMatch(t, values, actualValues) - case <-time.After(10 * time.Millisecond): + case <-time.After(50 * time.Millisecond): t.Fatal("timed out waiting for Last to return") } + // Ensure that Last still works as expected when n <= len(published_values). require.ElementsMatch(t, []int{1}, replayObsvbl.Last(ctx, 1)) require.ElementsMatch(t, []int{1, 2}, replayObsvbl.Last(ctx, 2)) require.ElementsMatch(t, []int{1, 2, 3}, replayObsvbl.Last(ctx, 3)) diff --git a/pkg/relayer/proxy/errors.go b/pkg/relayer/proxy/errors.go new file mode 100644 index 000000000..1aa42ab7e --- /dev/null +++ b/pkg/relayer/proxy/errors.go @@ -0,0 +1,8 @@ +package proxy + +import sdkerrors "cosmossdk.io/errors" + +var ( + codespace = "relayer/proxy" + ErrUnsupportedRPCType = sdkerrors.Register(codespace, 1, "unsupported rpc type") +) diff --git a/pkg/relayer/proxy/interface.go b/pkg/relayer/proxy/interface.go index 016b27057..27ee83e72 100644 --- a/pkg/relayer/proxy/interface.go +++ b/pkg/relayer/proxy/interface.go @@ -3,24 +3,44 @@ package proxy import ( "context" - "pocket/pkg/observable" - "pocket/x/service/types" + "github.com/pokt-network/poktroll/pkg/observable" + "github.com/pokt-network/poktroll/x/service/types" + sharedtypes "github.com/pokt-network/poktroll/x/shared/types" ) // RelayerProxy is the interface for the proxy that serves relays to the application. -// It is responsible for starting and stopping all supported proxies. +// It is responsible for starting and stopping all supported RelayServers. // While handling requests and responding in a closed loop, it also notifies // the miner about the relays that have been served. type RelayerProxy interface { - // Start starts all supported proxies and returns an error if any of them fail to start. + // Start starts all advertised relay servers and returns an error if any of them fail to start. Start(ctx context.Context) error - // Stop stops all supported proxies and returns an error if any of them fail. + // Stop stops all advertised relay servers and returns an error if any of them fail. Stop(ctx context.Context) error // ServedRelays returns an observable that notifies the miner about the relays that have been served. // A served relay is one whose RelayRequest's signature and session have been verified, // and its RelayResponse has been signed and successfully sent to the client. ServedRelays() observable.Observable[*types.Relay] + + // VerifyRelayRequest is a shared method used by RelayServers to check the + // relay request signature and session validity. + VerifyRelayRequest(relayRequest *types.RelayRequest) (isValid bool, err error) + + // SignRelayResponse is a shared method used by RelayServers to sign the relay response. + SignRelayResponse(relayResponse *types.RelayResponse) ([]byte, error) +} + +// RelayServer is the interface of the advertised relay servers provided by the RelayerProxy. +type RelayServer interface { + // Start starts the service server and returns an error if it fails. + Start(ctx context.Context) error + + // Stop terminates the service server and returns an error if it fails. + Stop(ctx context.Context) error + + // ServiceId returns the serviceId of the service. + ServiceId() *sharedtypes.ServiceId } diff --git a/pkg/relayer/proxy/jsonrpc.go b/pkg/relayer/proxy/jsonrpc.go new file mode 100644 index 000000000..ac9295c6b --- /dev/null +++ b/pkg/relayer/proxy/jsonrpc.go @@ -0,0 +1,85 @@ +package proxy + +import ( + "context" + "net/http" + "net/url" + + "github.com/pokt-network/poktroll/x/service/types" + sharedtypes "github.com/pokt-network/poktroll/x/shared/types" +) + +var _ RelayServer = (*jsonRPCServer)(nil) + +type jsonRPCServer struct { + // serviceId is the id of the service that the server is responsible for. + serviceId *sharedtypes.ServiceId + + // serverEndpoint is the advertised endpoint configuration that the server uses to + // listen for incoming relay requests. + serverEndpoint *sharedtypes.SupplierEndpoint + + // proxiedServiceEndpoint is the address of the proxied service that the server relays requests to. + proxiedServiceEndpoint url.URL + + // server is the http server that listens for incoming relay requests. + server *http.Server + + // relayerProxy is the main relayer proxy that the server uses to perform its operations. + relayerProxy RelayerProxy + + // servedRelaysProducer is a channel that emits the relays that have been served so that the + // servedRelays observable can fan out the notifications to its subscribers. + servedRelaysProducer chan<- *types.Relay +} + +// NewJSONRPCServer creates a new HTTP server that listens for incoming relay requests +// and forwards them to the supported proxied service endpoint. +// It takes the serviceId, endpointUrl, and the main RelayerProxy as arguments and returns +// a RelayServer that listens to incoming RelayRequests +func NewJSONRPCServer( + serviceId *sharedtypes.ServiceId, + supplierEndpoint *sharedtypes.SupplierEndpoint, + proxiedServiceEndpoint url.URL, + servedRelaysProducer chan<- *types.Relay, + proxy RelayerProxy, +) RelayServer { + return &jsonRPCServer{ + serviceId: serviceId, + serverEndpoint: supplierEndpoint, + server: &http.Server{Addr: supplierEndpoint.Url}, + relayerProxy: proxy, + proxiedServiceEndpoint: proxiedServiceEndpoint, + servedRelaysProducer: servedRelaysProducer, + } +} + +// Start starts the service server and returns an error if it fails. +// It also waits for the passed in context to end before shutting down. +// This method is blocking and should be called in a goroutine. +func (j *jsonRPCServer) Start(ctx context.Context) error { + go func() { + <-ctx.Done() + j.server.Shutdown(ctx) + }() + + return j.server.ListenAndServe() +} + +// Stop terminates the service server and returns an error if it fails. +func (j *jsonRPCServer) Stop(ctx context.Context) error { + return j.server.Shutdown(ctx) +} + +// ServiceId returns the serviceId of the JSON-RPC service. +func (j *jsonRPCServer) ServiceId() *sharedtypes.ServiceId { + return j.serviceId +} + +// ServeHTTP listens for incoming relay requests. It implements the respective +// method of the http.Handler interface. It is called by http.ListenAndServe() +// when jsonRPCServer is used as an http.Handler with an http.Server. +// (see https://pkg.go.dev/net/http#Handler) +func (j *jsonRPCServer) ServeHTTP(writer http.ResponseWriter, request *http.Request) { + panic("TODO: implement jsonRPCServer.ServeHTTP") +} diff --git a/pkg/relayer/proxy/proxy.go b/pkg/relayer/proxy/proxy.go index f626f184f..033e9caaf 100644 --- a/pkg/relayer/proxy/proxy.go +++ b/pkg/relayer/proxy/proxy.go @@ -2,22 +2,30 @@ package proxy import ( "context" + "net/url" sdkclient "github.com/cosmos/cosmos-sdk/client" "github.com/cosmos/cosmos-sdk/crypto/keyring" accounttypes "github.com/cosmos/cosmos-sdk/x/auth/types" + "golang.org/x/sync/errgroup" // TODO_INCOMPLETE(@red-0ne): Import the appropriate block client interface once available. - // blocktypes "pocket/pkg/client" - "pocket/pkg/observable" - "pocket/pkg/observable/channel" - "pocket/x/service/types" - sessiontypes "pocket/x/session/types" - suppliertypes "pocket/x/supplier/types" + // blocktypes "github.com/pokt-network/poktroll/pkg/client" + "github.com/pokt-network/poktroll/pkg/observable" + "github.com/pokt-network/poktroll/pkg/observable/channel" + "github.com/pokt-network/poktroll/x/service/types" + sessiontypes "github.com/pokt-network/poktroll/x/session/types" + suppliertypes "github.com/pokt-network/poktroll/x/supplier/types" ) var _ RelayerProxy = (*relayerProxy)(nil) +type ( + serviceId = string + relayServersMap = map[serviceId][]RelayServer + servicesEndpointsMap = map[serviceId]url.URL +) + type relayerProxy struct { // keyName is the supplier's key name in the Cosmos's keybase. It is used along with the keyring to // get the supplier address and sign the relay responses. @@ -41,10 +49,13 @@ type relayerProxy struct { // which is needed to check if the relay proxy should be serving an incoming relay request. sessionQuerier sessiontypes.QueryClient - // providedServices is a map of the services provided by the relayer proxy. Each provided service + // advertisedRelayServers is a map of the services provided by the relayer proxy. Each provided service // has the necessary information to start the server that listens for incoming relay requests and - // the client that proxies the request to the supported native service. - providedServices map[string][]*ProvidedService + // the client that relays the request to the supported proxied service. + advertisedRelayServers relayServersMap + + // proxiedServicesEndpoints is a map of the proxied services endpoints that the relayer proxy supports. + proxiedServicesEndpoints servicesEndpointsMap // servedRelays is an observable that notifies the miner about the relays that have been served. servedRelays observable.Observable[*types.Relay] @@ -59,60 +70,80 @@ func NewRelayerProxy( clientCtx sdkclient.Context, keyName string, keyring keyring.Keyring, - + proxiedServicesEndpoints servicesEndpointsMap, // TODO_INCOMPLETE(@red-0ne): Uncomment once the BlockClient interface is available. // blockClient blocktypes.BlockClient, ) RelayerProxy { accountQuerier := accounttypes.NewQueryClient(clientCtx) supplierQuerier := suppliertypes.NewQueryClient(clientCtx) sessionQuerier := sessiontypes.NewQueryClient(clientCtx) - providedServices := buildProvidedServices(ctx, supplierQuerier) servedRelays, servedRelaysProducer := channel.NewObservable[*types.Relay]() return &relayerProxy{ // TODO_INCOMPLETE(@red-0ne): Uncomment once the BlockClient interface is available. - // blockClient: blockClient, - keyName: keyName, - keyring: keyring, - accountsQuerier: accountQuerier, - supplierQuerier: supplierQuerier, - sessionQuerier: sessionQuerier, - providedServices: providedServices, - servedRelays: servedRelays, - servedRelaysProducer: servedRelaysProducer, + // blockClient: blockClient, + keyName: keyName, + keyring: keyring, + accountsQuerier: accountQuerier, + supplierQuerier: supplierQuerier, + sessionQuerier: sessionQuerier, + proxiedServicesEndpoints: proxiedServicesEndpoints, + servedRelays: servedRelays, + servedRelaysProducer: servedRelaysProducer, } } -// Start starts all supported proxies and returns an error if any of them fail to start. +// Start concurrently starts all advertised relay servers and returns an error if any of them fails to start. +// This method is blocking until all RelayServers are started. func (rp *relayerProxy) Start(ctx context.Context) error { - panic("TODO: implement relayerProxy.Start") + // The provided services map is built from the supplier's on-chain advertised information, + // which is a runtime parameter that can be changed by the supplier. + // NOTE: We build the provided services map at Start instead of NewRelayerProxy to avoid having to + // return an error from the constructor. + if err := rp.BuildProvidedServices(ctx); err != nil { + return err + } + + startGroup, ctx := errgroup.WithContext(ctx) + + for _, relayServer := range rp.advertisedRelayServers { + for _, svr := range relayServer { + server := svr // create a new variable scoped to the anonymous function + startGroup.Go(func() error { return server.Start(ctx) }) + } + } + + return startGroup.Wait() } -// Stop stops all supported proxies and returns an error if any of them fail. +// Stop concurrently stops all advertised relay servers and returns an error if any of them fails. +// This method is blocking until all RelayServers are stopped. func (rp *relayerProxy) Stop(ctx context.Context) error { - panic("TODO: implement relayerProxy.Stop") + stopGroup, ctx := errgroup.WithContext(ctx) + + for _, providedService := range rp.advertisedRelayServers { + for _, svr := range providedService { + server := svr // create a new variable scoped to the anonymous function + stopGroup.Go(func() error { return server.Stop(ctx) }) + } + } + + return stopGroup.Wait() } // ServedRelays returns an observable that notifies the miner about the relays that have been served. // A served relay is one whose RelayRequest's signature and session have been verified, // and its RelayResponse has been signed and successfully sent to the client. func (rp *relayerProxy) ServedRelays() observable.Observable[*types.Relay] { - panic("TODO: implement relayerProxy.ServedRelays") + return rp.servedRelays } -// buildProvidedServices builds the provided services map from the supplier's advertised information. -// It loops over the retrieved `SupplierServiceConfig` and, for each `SupplierEndpoint`, it creates the necessary -// server and client to populate the corresponding `ProvidedService` struct in the map. -func buildProvidedServices( - ctx context.Context, - supplierQuerier suppliertypes.QueryClient, -) map[string][]*ProvidedService { - panic("TODO: implement buildProvidedServices") +// VerifyRelayRequest is a shared method used by RelayServers to check the relay request signature and session validity. +func (rp *relayerProxy) VerifyRelayRequest(relayRequest *types.RelayRequest) (isValid bool, err error) { + panic("TODO: implement relayerProxy.VerifyRelayRequest") } -// TODO_INCOMPLETE(@red-0ne): Add the appropriate server and client interfaces to be implemented by each RPC type. -type ProvidedService struct { - serviceId string - server struct{} - client struct{} +// SignRelayResponse is a shared method used by RelayServers to sign the relay response. +func (rp *relayerProxy) SignRelayResponse(relayResponse *types.RelayResponse) ([]byte, error) { + panic("TODO: implement relayerProxy.SignRelayResponse") } diff --git a/pkg/relayer/proxy/server_builder.go b/pkg/relayer/proxy/server_builder.go new file mode 100644 index 000000000..eb21cc1f2 --- /dev/null +++ b/pkg/relayer/proxy/server_builder.go @@ -0,0 +1,62 @@ +package proxy + +import ( + "context" + + sharedtypes "github.com/pokt-network/poktroll/x/shared/types" + suppliertypes "github.com/pokt-network/poktroll/x/supplier/types" +) + +// BuildProvidedServices builds the advertised relay servers from the supplier's on-chain advertised services. +// It populates the relayerProxy's `advertisedRelayServers` map of servers for each service, where each server +// is responsible for listening for incoming relay requests and relaying them to the supported proxied service. +func (rp *relayerProxy) BuildProvidedServices(ctx context.Context) error { + // Get the supplier address from the keyring + supplierAddress, err := rp.keyring.Key(rp.keyName) + if err != nil { + return err + } + + // Get the supplier's advertised information from the blockchain + supplierQuery := &suppliertypes.QueryGetSupplierRequest{Address: supplierAddress.String()} + supplierQueryResponse, err := rp.supplierQuerier.Supplier(ctx, supplierQuery) + if err != nil { + return err + } + + services := supplierQueryResponse.Supplier.Services + + // Build the advertised relay servers map. For each service's endpoint, create the appropriate RelayServer. + providedServices := make(relayServersMap) + for _, serviceConfig := range services { + serviceId := serviceConfig.ServiceId + proxiedServicesEndpoints := rp.proxiedServicesEndpoints[serviceId.Id] + serviceEndpoints := make([]RelayServer, len(serviceConfig.Endpoints)) + + for _, endpoint := range serviceConfig.Endpoints { + var server RelayServer + + // Switch to the RPC type to create the appropriate RelayServer + switch endpoint.RpcType { + case sharedtypes.RPCType_JSON_RPC: + server = NewJSONRPCServer( + serviceId, + endpoint, + proxiedServicesEndpoints, + rp.servedRelaysProducer, + rp, + ) + default: + return ErrUnsupportedRPCType + } + + serviceEndpoints = append(serviceEndpoints, server) + } + + providedServices[serviceId.Id] = serviceEndpoints + } + + rp.advertisedRelayServers = providedServices + + return nil +} diff --git a/pkg/retry/retry.go b/pkg/retry/retry.go new file mode 100644 index 000000000..f76a7dcf5 --- /dev/null +++ b/pkg/retry/retry.go @@ -0,0 +1,72 @@ +package retry + +import ( + "context" + "log" + "time" +) + +type RetryFunc func() chan error + +// OnError continuously invokes the provided work function (workFn) until either the context (ctx) +// is canceled or the error channel returned by workFn is closed. If workFn encounters an error, +// OnError will retry invoking workFn based on the provided retry parameters. +// +// Parameters: +// - ctx: the context to monitor for cancellation. If canceled, OnError will exit without error. +// - retryLimit: the maximum number of retries for workFn upon encountering an error. +// - retryDelay: the duration to wait before retrying workFn after an error. +// - retryResetCount: Specifies the duration of continuous error-free operation required +// before the retry count is reset. If the work function operates without +// errors for this duration, any subsequent error will restart the retry +// count from the beginning. +// - workName: a name or descriptor for the work function, used for logging purposes. +// - workFn: a function that performs some work and returns an error channel. +// This channel emits errors encountered during the work. +// +// Returns: +// - If the context is canceled, the function returns nil. +// - If the error channel is closed, a warning is logged, and the function returns nil. +// - If the retry limit is reached, the function returns the error from the channel. +// +// Note: After each error, a delay specified by retryDelay is introduced before retrying workFn.func OnError( +func OnError( + ctx context.Context, + retryLimit int, + retryDelay time.Duration, + retryResetTimeout time.Duration, + workName string, + workFn RetryFunc, +) error { + var retryCount int + errCh := workFn() + for { + select { + case <-ctx.Done(): + return nil + case <-time.After(retryResetTimeout): + retryCount = 0 + case err, ok := <-errCh: + // Exit the retry loop if the error channel is closed. + if !ok { + log.Printf( + "WARN: error channel for %s closed, will no longer retry on error\n", + workName, + ) + return nil + } + + if retryCount >= retryLimit { + return err + } + + // Wait retryDelay before retrying. + time.Sleep(retryDelay) + + // Increment retryCount and retry workFn. + retryCount++ + errCh = workFn() + log.Printf("ERROR: retrying %s after error: %s\n", workName, err) + } + } +} diff --git a/pkg/retry/retry_test.go b/pkg/retry/retry_test.go new file mode 100644 index 000000000..d328bda73 --- /dev/null +++ b/pkg/retry/retry_test.go @@ -0,0 +1,337 @@ +package retry_test + +/* TODO_TECHDEBT: improve this test: +- fix race condition around the logOutput buffer +- factor our common setup and assertion code +- drive out flakiness +- improve comments +*/ + +import ( + "bytes" + "context" + "fmt" + "log" + "strings" + "sync/atomic" + "testing" + "time" + + "github.com/stretchr/testify/require" + + "github.com/pokt-network/poktroll/pkg/retry" +) + +var testErr = fmt.Errorf("test error") + +// TestOnError verifies the behavior of the OnError function in the retry package. +// It ensures that the function correctly retries a failing operation for a specified +// number of times with the expected delay between retries. +func TestOnError(t *testing.T) { + t.Skip("TODO_TECHDEBT: this test should pass but contains a race condition around the logOutput buffer") + + // Setting up the test variables. + var ( + // logOutput captures the log output for verification of logged messages. + logOutput bytes.Buffer + // expectedRetryDelay is the duration we expect between retries. + expectedRetryDelay = time.Millisecond + // expectedRetryLimit is the maximum number of retries the test expects. + expectedRetryLimit = 5 + // retryResetTimeout is the duration after which the retry count should reset. + retryResetTimeout = time.Second + // testFnCallCount keeps track of how many times the test function is called. + testFnCallCount int32 + // testFnCallTimeCh is a channel receives a time.Time each when the test + // function is called. + testFnCallTimeCh = make(chan time.Time, expectedRetryLimit) + ctx = context.Background() + ) + + // Redirect the standard logger's output to our custom buffer for later verification. + log.SetOutput(&logOutput) + + // Define testFn, a function that simulates a failing operation and logs its invocation times. + testFn := func() chan error { + // Record the current time to track the delay between retries. + testFnCallTimeCh <- time.Now() + + // Create a channel to return an error, simulating a failing operation. + errCh := make(chan error, 1) + errCh <- testErr + + // Increment the call count safely across goroutine boundaries. + atomic.AddInt32(&testFnCallCount, 1) + + return errCh + } + + // Create a channel to receive the error result from the OnError function. + retryOnErrorErrCh := make(chan error, 1) + + // Start the OnError function in a separate goroutine, simulating concurrent operation. + go func() { + // Call the OnError function with the test parameters and function. + retryOnErrorErrCh <- retry.OnError( + ctx, + expectedRetryLimit, + expectedRetryDelay, + retryResetTimeout, + "TestOnError", + testFn, + ) + }() + + // Calculate the total expected time for all retries to complete. + totalExpectedDelay := expectedRetryDelay * time.Duration(expectedRetryLimit) + // Wait for the OnError function to execute and retry the expected number of times. + time.Sleep(totalExpectedDelay + 100*time.Millisecond) + + // Verify that the test function was called the expected number of times. + require.Equal(t, expectedRetryLimit, int(testFnCallCount), "Test function was not called the expected number of times") + + // Verify the delay between retries of the test function. + var prevCallTime time.Time + for i := 0; i < expectedRetryLimit; i++ { + // Retrieve the next function call time from the channel. + nextCallTime, ok := <-testFnCallTimeCh + if !ok { + t.Fatalf("expected %d calls to testFn, but channel closed after %d", expectedRetryLimit, i) + } + + // For all calls after the first, check that the delay since the previous call meets expectations. + if i != 0 { + actualRetryDelay := nextCallTime.Sub(prevCallTime) + require.GreaterOrEqual(t, actualRetryDelay, expectedRetryDelay, "Retry delay was less than expected") + } + + // Update prevCallTime for the next iteration. + prevCallTime = nextCallTime + } + + // Verify that the OnError function returned the expected error. + select { + case err := <-retryOnErrorErrCh: + require.ErrorIs(t, err, testErr, "OnError did not return the expected error") + case <-time.After(100 * time.Millisecond): + t.Fatal("expected error from OnError, but none received") + } + + // Verify the error messages logged during the retries. + expectedErrorLine := "ERROR: retrying TestOnError after error: test error" + trimmedLogOutput := strings.Trim(logOutput.String(), "\n") + logOutputLines := strings.Split(trimmedLogOutput, "\n") + require.Lenf(t, logOutputLines, expectedRetryLimit, "unexpected number of log lines") + for _, line := range logOutputLines { + require.Contains(t, line, expectedErrorLine, "log line does not contain the expected prefix") + } +} + +// TODO_TECHDEBT: assert that the retry loop exits when the context is closed. +func TestOnError_ExitsWhenCtxCloses(t *testing.T) { + t.SkipNow() +} + +func TestOnError_ExitsWhenErrChCloses(t *testing.T) { + t.Skip("TODO_TECHDEBT: this test should pass but contains a race condition around the logOutput buffer") + + // Setup test variables and log capture + var ( + logOutput bytes.Buffer + testFnCallCount int32 + expectedRetryDelay = time.Millisecond + expectedRetryLimit = 3 + retryLimit = 5 + retryResetTimeout = time.Second + testFnCallTimeCh = make(chan time.Time, expectedRetryLimit) + ctx = context.Background() + ) + + // Redirect the log output for verification later + log.SetOutput(&logOutput) + + // Define the test function that simulates an error and counts its invocations + testFn := func() chan error { + atomic.AddInt32(&testFnCallCount, 1) // Increment the invocation count atomically + testFnCallTimeCh <- time.Now() // Track the invocation time + + errCh := make(chan error, 1) + if atomic.LoadInt32(&testFnCallCount) >= int32(expectedRetryLimit) { + close(errCh) + return errCh + } + + errCh <- testErr + return errCh + } + + retryOnErrorErrCh := make(chan error, 1) + // Spawn a goroutine to test the OnError function + go func() { + retryOnErrorErrCh <- retry.OnError( + ctx, + retryLimit, + expectedRetryDelay, + retryResetTimeout, + "TestOnError_ExitsWhenErrChCloses", + testFn, + ) + }() + + // Wait for the OnError function to execute and retry the expected number of times + totalExpectedDelay := expectedRetryDelay * time.Duration(expectedRetryLimit) + time.Sleep(totalExpectedDelay + 100*time.Millisecond) + + // Assert that the test function was called the expected number of times + require.Equal(t, expectedRetryLimit, int(testFnCallCount)) + + // Assert that the retry delay between function calls matches the expected delay + var prevCallTime = new(time.Time) + for i := 0; i < expectedRetryLimit; i++ { + select { + case nextCallTime := <-testFnCallTimeCh: + if i != 0 { + actualRetryDelay := nextCallTime.Sub(*prevCallTime) + require.GreaterOrEqual(t, actualRetryDelay, expectedRetryDelay) + } + + *prevCallTime = nextCallTime + default: + t.Fatalf( + "expected %d calls to testFn, but only received %d", + expectedRetryLimit, i+1, + ) + } + } + + select { + case err := <-retryOnErrorErrCh: + require.NoError(t, err) + case <-time.After(100 * time.Millisecond): + t.Fatalf("expected error from OnError, but none received") + } + + // Verify the logged error messages + var ( + logOutputLines = strings.Split(strings.Trim(logOutput.String(), "\n"), "\n") + errorLines = logOutputLines[:len(logOutputLines)-1] + warnLine = logOutputLines[len(logOutputLines)-1] + expectedWarnMsg = "WARN: error channel for TestOnError_ExitsWhenErrChCloses closed, will no longer retry on error" + expectedErrorMsg = "ERROR: retrying TestOnError_ExitsWhenErrChCloses after error: test error" + ) + + require.Lenf( + t, logOutputLines, + expectedRetryLimit, + "expected %d log lines, got %d", + expectedRetryLimit, len(logOutputLines), + ) + for _, line := range errorLines { + require.Contains(t, line, expectedErrorMsg) + } + require.Contains(t, warnLine, expectedWarnMsg) +} + +// assert that retryCount resets on success +func TestOnError_RetryCountResetTimeout(t *testing.T) { + t.Skip("TODO_TECHDEBT: this test should pass but contains a race condition around the logOutput buffer") + + // Setup test variables and log capture + var ( + logOutput bytes.Buffer + testFnCallCount int32 + expectedRetryDelay = time.Millisecond + expectedRetryLimit = 9 + retryLimit = 5 + retryResetTimeout = 3 * time.Millisecond + testFnCallTimeCh = make(chan time.Time, expectedRetryLimit) + ctx = context.Background() + ) + + // Redirect the log output for verification later + log.SetOutput(&logOutput) + + // Define the test function that simulates an error and counts its invocations + testFn := func() chan error { + // Track the invocation time + testFnCallTimeCh <- time.Now() + + errCh := make(chan error, 1) + + count := atomic.LoadInt32(&testFnCallCount) + if count == int32(retryLimit) { + go func() { + time.Sleep(retryResetTimeout) + errCh <- testErr + }() + } else { + errCh <- testErr + } + + // Increment the invocation count atomically + atomic.AddInt32(&testFnCallCount, 1) + return errCh + } + + retryOnErrorErrCh := make(chan error, 1) + // Spawn a goroutine to test the OnError function + go func() { + retryOnErrorErrCh <- retry.OnError( + ctx, + retryLimit, + expectedRetryDelay, + retryResetTimeout, + "TestOnError", + testFn, + ) + }() + + // Wait for the OnError function to execute and retry the expected number of times + totalExpectedDelay := expectedRetryDelay * time.Duration(expectedRetryLimit) + time.Sleep(totalExpectedDelay + 100*time.Millisecond) + + // Assert that the test function was called the expected number of times + require.Equal(t, expectedRetryLimit, int(testFnCallCount)) + + // Assert that the retry delay between function calls matches the expected delay + var prevCallTime = new(time.Time) + for i := 0; i < expectedRetryLimit; i++ { + select { + case nextCallTime := <-testFnCallTimeCh: + if i != 0 { + actualRetryDelay := nextCallTime.Sub(*prevCallTime) + require.GreaterOrEqual(t, actualRetryDelay, expectedRetryDelay) + } + + *prevCallTime = nextCallTime + default: + t.Fatalf( + "expected %d calls to testFn, but only received %d", + expectedRetryLimit, i+1, + ) + } + } + + // Verify the logged error messages + var ( + logOutputLines = strings.Split(strings.Trim(logOutput.String(), "\n"), "\n") + expectedPrefix = "ERROR: retrying TestOnError after error: test error" + ) + + select { + case err := <-retryOnErrorErrCh: + require.ErrorIs(t, err, testErr) + case <-time.After(100 * time.Millisecond): + t.Fatalf("expected error from OnError, but none received") + } + + require.Lenf( + t, logOutputLines, + expectedRetryLimit-1, + "expected %d log lines, got %d", + expectedRetryLimit-1, len(logOutputLines), + ) + for _, line := range logOutputLines { + require.Contains(t, line, expectedPrefix) + } +} diff --git a/proto/pocket/application/application.proto b/proto/pocket/application/application.proto index 4ccb1940a..e5763d697 100644 --- a/proto/pocket/application/application.proto +++ b/proto/pocket/application/application.proto @@ -1,17 +1,18 @@ syntax = "proto3"; package pocket.application; -option go_package = "pocket/x/application/types"; +option go_package = "github.com/pokt-network/poktroll/x/application/types"; import "cosmos_proto/cosmos.proto"; import "cosmos/base/v1beta1/coin.proto"; +import "gogoproto/gogo.proto"; + import "pocket/shared/service.proto"; // Application defines the type used to store an on-chain definition and state for an application message Application { string address = 1 [(cosmos_proto.scalar) = "cosmos.AddressString"]; // The Bech32 address of the application using cosmos' ScalarDescriptor to ensure deterministic encoding cosmos.base.v1beta1.Coin stake = 2; // The total amount of uPOKT the application has staked - // TODO(@olshansk): Change this to `shared.ApplicationServiceConfig` in #95 - repeated shared.ServiceId service_ids = 3; // The ID of the service this session is servicing + repeated shared.ApplicationServiceConfig service_configs = 3; // The ID of the service this session is servicing + repeated string delegatee_gateway_addresses = 4 [(cosmos_proto.scalar) = "cosmos.AddressString", (gogoproto.nullable) = false]; // The Bech32 encoded addresses for all delegatee Gateways, in a non-nullable slice } - diff --git a/proto/pocket/application/genesis.proto b/proto/pocket/application/genesis.proto index 6598d6636..e2741972a 100644 --- a/proto/pocket/application/genesis.proto +++ b/proto/pocket/application/genesis.proto @@ -6,7 +6,7 @@ import "gogoproto/gogo.proto"; import "pocket/application/params.proto"; import "pocket/application/application.proto"; -option go_package = "pocket/x/application/types"; +option go_package = "github.com/pokt-network/poktroll/x/application/types"; // GenesisState defines the application module's genesis state. message GenesisState { diff --git a/proto/pocket/application/params.proto b/proto/pocket/application/params.proto index 608ca124d..18390d1d2 100644 --- a/proto/pocket/application/params.proto +++ b/proto/pocket/application/params.proto @@ -3,10 +3,11 @@ package pocket.application; import "gogoproto/gogo.proto"; -option go_package = "pocket/x/application/types"; +option go_package = "github.com/pokt-network/poktroll/x/application/types"; // Params defines the parameters for the module. message Params { option (gogoproto.goproto_stringer) = false; - + + int64 max_delegated_gateways = 1 [(gogoproto.jsontag) = "max_delegated_gateways"]; // The maximum number of gateways an application can delegate trust to } diff --git a/proto/pocket/application/query.proto b/proto/pocket/application/query.proto index fd25f232d..28a48fb99 100644 --- a/proto/pocket/application/query.proto +++ b/proto/pocket/application/query.proto @@ -8,25 +8,25 @@ import "cosmos/base/query/v1beta1/pagination.proto"; import "pocket/application/params.proto"; import "pocket/application/application.proto"; -option go_package = "pocket/x/application/types"; +option go_package = "github.com/pokt-network/poktroll/x/application/types"; // Query defines the gRPC querier service. service Query { - + // Parameters queries the parameters of the module. rpc Params (QueryParamsRequest) returns (QueryParamsResponse) { option (google.api.http).get = "/pocket/application/params"; - + } - + // Queries a list of Application items. rpc Application (QueryGetApplicationRequest) returns (QueryGetApplicationResponse) { option (google.api.http).get = "/pocket/application/application/{address}"; - + } rpc ApplicationAll (QueryAllApplicationRequest) returns (QueryAllApplicationResponse) { option (google.api.http).get = "/pocket/application/application"; - + } } // QueryParamsRequest is request type for the Query/Params RPC method. @@ -34,7 +34,7 @@ message QueryParamsRequest {} // QueryParamsResponse is response type for the Query/Params RPC method. message QueryParamsResponse { - + // params holds all the parameters of this module. Params params = 1 [(gogoproto.nullable) = false]; } diff --git a/proto/pocket/application/tx.proto b/proto/pocket/application/tx.proto index 4bf9eb789..1f4b9674c 100644 --- a/proto/pocket/application/tx.proto +++ b/proto/pocket/application/tx.proto @@ -5,8 +5,9 @@ package pocket.application; import "cosmos_proto/cosmos.proto"; import "cosmos/base/v1beta1/coin.proto"; import "cosmos/msg/v1/msg.proto"; +import "pocket/shared/service.proto"; -option go_package = "pocket/x/application/types"; +option go_package = "github.com/pokt-network/poktroll/x/application/types"; // Msg defines the Msg service. service Msg { @@ -19,9 +20,8 @@ message MsgStakeApplication { option (cosmos.msg.v1.signer) = "address"; // https://docs.cosmos.network/main/build/building-modules/messages-and-queries string address = 1 [(cosmos_proto.scalar) = "cosmos.AddressString"]; // The Bech32 address of the application using cosmos' ScalarDescriptor to ensure deterministic encoding - cosmos.base.v1beta1.Coin stake = 2; // The total amount of uPOKT the application has staked. Must be ≥ to the current amount that the application has staked (if any) - // TODO(@Olshansk): Update the tx flow to add support for `services` - // repeated service.ApplicationServiceConfig services = 3; // The list of services this application is staked to request service for + cosmos.base.v1beta1.Coin stake = 2; // The total amount of uPOKT the application has staked. Must be ≥ to the current amount that the application has staked (if any) + repeated shared.ApplicationServiceConfig services = 3; // The list of services this application is staked to request service for } message MsgStakeApplicationResponse {} @@ -34,13 +34,17 @@ message MsgUnstakeApplication { message MsgUnstakeApplicationResponse {} message MsgDelegateToGateway { - string address = 1; + option (cosmos.msg.v1.signer) = "app_address"; // https://docs.cosmos.network/main/build/building-modules/messages-and-queries + string app_address = 1 [(cosmos_proto.scalar) = "cosmos.AddressString"]; // The Bech32 address of the application using cosmos' ScalarDescriptor to ensure deterministic deterministic encoding using cosmos' ScalarDescriptor to ensure deterministic deterministic encoding + string gateway_address = 2 [(cosmos_proto.scalar) = "cosmos.AddressString"]; // The Bech32 address of the gateway the application wants to delegate to using cosmos' ScalarDescriptor to ensure deterministic deterministic encoding using cosmos' ScalarDescriptor to ensure deterministic deterministic encoding } message MsgDelegateToGatewayResponse {} message MsgUndelegateFromGateway { - string address = 1; + option (cosmos.msg.v1.signer) = "appAddress"; // https://docs.cosmos.network/main/build/building-modules/messages-and-queries + string appAddress = 1 [(cosmos_proto.scalar) = "cosmos.AddressString"]; // The Bech32 address of the application using cosmos' ScalarDescriptor to ensure deterministic deterministic encoding using cosmos' ScalarDescriptor to ensure deterministic deterministic encoding + string gatewayAddress = 2 [(cosmos_proto.scalar) = "cosmos.AddressString"]; // The Bech32 address of the gateway the application wants to undelegate from using cosmos' ScalarDescriptor to ensure deterministic deterministic encoding using cosmos' ScalarDescriptor to ensure deterministic deterministic encoding } message MsgUndelegateFromGatewayResponse {} diff --git a/proto/pocket/gateway/gateway.proto b/proto/pocket/gateway/gateway.proto index f2a450cb3..ed7b08751 100644 --- a/proto/pocket/gateway/gateway.proto +++ b/proto/pocket/gateway/gateway.proto @@ -1,7 +1,7 @@ syntax = "proto3"; package pocket.gateway; -option go_package = "pocket/x/gateway/types"; +option go_package = "github.com/pokt-network/poktroll/x/gateway/types"; import "cosmos_proto/cosmos.proto"; import "cosmos/base/v1beta1/coin.proto"; diff --git a/proto/pocket/gateway/genesis.proto b/proto/pocket/gateway/genesis.proto index 1f1b97cf8..85e6fb8a5 100644 --- a/proto/pocket/gateway/genesis.proto +++ b/proto/pocket/gateway/genesis.proto @@ -6,7 +6,7 @@ import "gogoproto/gogo.proto"; import "pocket/gateway/params.proto"; import "pocket/gateway/gateway.proto"; -option go_package = "pocket/x/gateway/types"; +option go_package = "github.com/pokt-network/poktroll/x/gateway/types"; // GenesisState defines the gateway module's genesis state. message GenesisState { diff --git a/proto/pocket/gateway/params.proto b/proto/pocket/gateway/params.proto index 8d5f42acd..040f5630d 100644 --- a/proto/pocket/gateway/params.proto +++ b/proto/pocket/gateway/params.proto @@ -3,10 +3,10 @@ package pocket.gateway; import "gogoproto/gogo.proto"; -option go_package = "pocket/x/gateway/types"; +option go_package = "github.com/pokt-network/poktroll/x/gateway/types"; // Params defines the parameters for the module. message Params { option (gogoproto.goproto_stringer) = false; - + } diff --git a/proto/pocket/gateway/query.proto b/proto/pocket/gateway/query.proto index 2076a32c0..48bd62f98 100644 --- a/proto/pocket/gateway/query.proto +++ b/proto/pocket/gateway/query.proto @@ -8,25 +8,25 @@ import "cosmos/base/query/v1beta1/pagination.proto"; import "pocket/gateway/params.proto"; import "pocket/gateway/gateway.proto"; -option go_package = "pocket/x/gateway/types"; +option go_package = "github.com/pokt-network/poktroll/x/gateway/types"; // Query defines the gRPC querier service. service Query { - + // Parameters queries the parameters of the module. rpc Params (QueryParamsRequest) returns (QueryParamsResponse) { option (google.api.http).get = "/pocket/gateway/params"; - + } - + // Queries a list of Gateway items. rpc Gateway (QueryGetGatewayRequest) returns (QueryGetGatewayResponse) { option (google.api.http).get = "/pocket/gateway/gateway/{address}"; - + } rpc GatewayAll (QueryAllGatewayRequest) returns (QueryAllGatewayResponse) { option (google.api.http).get = "/pocket/gateway/gateway"; - + } } // QueryParamsRequest is request type for the Query/Params RPC method. @@ -34,7 +34,7 @@ message QueryParamsRequest {} // QueryParamsResponse is response type for the Query/Params RPC method. message QueryParamsResponse { - + // params holds all the parameters of this module. Params params = 1 [(gogoproto.nullable) = false]; } diff --git a/proto/pocket/gateway/tx.proto b/proto/pocket/gateway/tx.proto index 43b4b3245..6b0814add 100644 --- a/proto/pocket/gateway/tx.proto +++ b/proto/pocket/gateway/tx.proto @@ -2,7 +2,7 @@ syntax = "proto3"; package pocket.gateway; -option go_package = "pocket/x/gateway/types"; +option go_package = "github.com/pokt-network/poktroll/x/gateway/types"; import "cosmos/msg/v1/msg.proto"; import "cosmos_proto/cosmos.proto"; diff --git a/proto/pocket/pocket/genesis.proto b/proto/pocket/pocket/genesis.proto index e35b3ae9b..52d21410c 100644 --- a/proto/pocket/pocket/genesis.proto +++ b/proto/pocket/pocket/genesis.proto @@ -4,7 +4,7 @@ package pocket.pocket; import "gogoproto/gogo.proto"; import "pocket/pocket/params.proto"; -option go_package = "pocket/x/pocket/types"; +option go_package = "github.com/pokt-network/poktroll/x/pocket/types"; // GenesisState defines the pocket module's genesis state. message GenesisState { diff --git a/proto/pocket/pocket/params.proto b/proto/pocket/pocket/params.proto index 61db3feb2..a760a6fb6 100644 --- a/proto/pocket/pocket/params.proto +++ b/proto/pocket/pocket/params.proto @@ -3,10 +3,10 @@ package pocket.pocket; import "gogoproto/gogo.proto"; -option go_package = "pocket/x/pocket/types"; +option go_package = "github.com/pokt-network/poktroll/x/pocket/types"; // Params defines the parameters for the module. message Params { option (gogoproto.goproto_stringer) = false; - + } diff --git a/proto/pocket/pocket/query.proto b/proto/pocket/pocket/query.proto index 3e983730d..55c4471c0 100644 --- a/proto/pocket/pocket/query.proto +++ b/proto/pocket/pocket/query.proto @@ -5,7 +5,7 @@ import "gogoproto/gogo.proto"; import "google/api/annotations.proto"; import "pocket/pocket/params.proto"; -option go_package = "pocket/x/pocket/types"; +option go_package = "github.com/pokt-network/poktroll/x/pocket/types"; // Query defines the gRPC querier service. service Query { diff --git a/proto/pocket/pocket/tx.proto b/proto/pocket/pocket/tx.proto index ba08384d3..a0e741350 100644 --- a/proto/pocket/pocket/tx.proto +++ b/proto/pocket/pocket/tx.proto @@ -1,7 +1,7 @@ syntax = "proto3"; package pocket.pocket; -option go_package = "pocket/x/pocket/types"; +option go_package = "github.com/pokt-network/poktroll/x/pocket/types"; // Msg defines the Msg service. service Msg {} \ No newline at end of file diff --git a/proto/pocket/service/genesis.proto b/proto/pocket/service/genesis.proto index 50f61d258..5ca50c9f0 100644 --- a/proto/pocket/service/genesis.proto +++ b/proto/pocket/service/genesis.proto @@ -4,7 +4,7 @@ package pocket.service; import "gogoproto/gogo.proto"; import "pocket/service/params.proto"; -option go_package = "pocket/x/service/types"; +option go_package = "github.com/pokt-network/poktroll/x/service/types"; // GenesisState defines the service module's genesis state. message GenesisState { diff --git a/proto/pocket/service/params.proto b/proto/pocket/service/params.proto index 54cc15bd6..9b7fe8363 100644 --- a/proto/pocket/service/params.proto +++ b/proto/pocket/service/params.proto @@ -3,10 +3,10 @@ package pocket.service; import "gogoproto/gogo.proto"; -option go_package = "pocket/x/service/types"; +option go_package = "github.com/pokt-network/poktroll/x/service/types"; // Params defines the parameters for the module. message Params { option (gogoproto.goproto_stringer) = false; - + } diff --git a/proto/pocket/service/query.proto b/proto/pocket/service/query.proto index cd7c836fd..4abf2a13e 100644 --- a/proto/pocket/service/query.proto +++ b/proto/pocket/service/query.proto @@ -6,7 +6,7 @@ import "google/api/annotations.proto"; import "cosmos/base/query/v1beta1/pagination.proto"; import "pocket/service/params.proto"; -option go_package = "pocket/x/service/types"; +option go_package = "github.com/pokt-network/poktroll/x/service/types"; // Query defines the gRPC querier service. service Query { diff --git a/proto/pocket/service/relay.proto b/proto/pocket/service/relay.proto index 70c5854dd..3450a1e40 100644 --- a/proto/pocket/service/relay.proto +++ b/proto/pocket/service/relay.proto @@ -1,7 +1,7 @@ syntax = "proto3"; package pocket.service; -option go_package = "pocket/x/service/types"; +option go_package = "github.com/pokt-network/poktroll/x/service/types"; import "cosmos_proto/cosmos.proto"; // TODO(@Olshansk): Uncomment the line below once the `service.proto` is added. diff --git a/proto/pocket/service/tx.proto b/proto/pocket/service/tx.proto index 21f556e08..70aef03b7 100644 --- a/proto/pocket/service/tx.proto +++ b/proto/pocket/service/tx.proto @@ -1,7 +1,7 @@ syntax = "proto3"; package pocket.service; -option go_package = "pocket/x/service/types"; +option go_package = "github.com/pokt-network/poktroll/x/service/types"; // Msg defines the Msg service. service Msg {} \ No newline at end of file diff --git a/proto/pocket/session/genesis.proto b/proto/pocket/session/genesis.proto index d0dc85aeb..2ee8ed8ff 100644 --- a/proto/pocket/session/genesis.proto +++ b/proto/pocket/session/genesis.proto @@ -4,7 +4,7 @@ package pocket.session; import "gogoproto/gogo.proto"; import "pocket/session/params.proto"; -option go_package = "pocket/x/session/types"; +option go_package = "github.com/pokt-network/poktroll/x/session/types"; // GenesisState defines the session module's genesis state. message GenesisState { diff --git a/proto/pocket/session/params.proto b/proto/pocket/session/params.proto index 391c43b5f..428f2999e 100644 --- a/proto/pocket/session/params.proto +++ b/proto/pocket/session/params.proto @@ -3,10 +3,10 @@ package pocket.session; import "gogoproto/gogo.proto"; -option go_package = "pocket/x/session/types"; +option go_package = "github.com/pokt-network/poktroll/x/session/types"; // Params defines the parameters for the module. message Params { option (gogoproto.goproto_stringer) = false; - + } diff --git a/proto/pocket/session/query.proto b/proto/pocket/session/query.proto index cd3ef8380..f8b1c7187 100644 --- a/proto/pocket/session/query.proto +++ b/proto/pocket/session/query.proto @@ -9,7 +9,7 @@ import "pocket/session/params.proto"; import "pocket/session/session.proto"; import "pocket/shared/service.proto"; -option go_package = "pocket/x/session/types"; +option go_package = "github.com/pokt-network/poktroll/x/session/types"; // Query defines the gRPC querier service. service Query { diff --git a/proto/pocket/session/session.proto b/proto/pocket/session/session.proto index 7d864e1b1..e8f14b35e 100644 --- a/proto/pocket/session/session.proto +++ b/proto/pocket/session/session.proto @@ -1,7 +1,7 @@ syntax = "proto3"; package pocket.session; -option go_package = "pocket/x/session/types"; +option go_package = "github.com/pokt-network/poktroll/x/session/types"; import "cosmos_proto/cosmos.proto"; import "pocket/shared/service.proto"; diff --git a/proto/pocket/session/tx.proto b/proto/pocket/session/tx.proto index 0590fcee9..6d793ffdb 100644 --- a/proto/pocket/session/tx.proto +++ b/proto/pocket/session/tx.proto @@ -1,7 +1,7 @@ syntax = "proto3"; package pocket.session; -option go_package = "pocket/x/session/types"; +option go_package = "github.com/pokt-network/poktroll/x/session/types"; // Msg defines the Msg service. service Msg {} \ No newline at end of file diff --git a/proto/pocket/shared/service.proto b/proto/pocket/shared/service.proto index cff126d35..c911bad1f 100644 --- a/proto/pocket/shared/service.proto +++ b/proto/pocket/shared/service.proto @@ -4,24 +4,30 @@ syntax = "proto3"; // but rather a manually created package to resolve circular dependencies. package pocket.shared; -option go_package = "pocket/x/shared/types"; +option go_package = "github.com/pokt-network/poktroll/x/shared/types"; // TODO_CLEANUP(@Olshansk): Add native optional identifiers once its supported; https://github.com/ignite/cli/issues/3698 // ServiceId message to encapsulate unique and semantic identifiers for a service on the network message ServiceId { + // NOTE: `ServiceId.Id` may seem redundant but was desigtned created to enable more complex service identification + // For example, what if we want to request a session for a certain service but with some additional configs that identify it? string id = 1; // Unique identifier for the service + + // TODO_TECHDEBT: Name is currently unused but acts as a reminder than an optional onchain representation of the service is necessary string name = 2; // (Optional) Semantic human readable name for the service + // NOTE: `ServiceId.Id` may seem redundant but was designed to enable more complex service identification. // For example, what if we want to request a session for a certain service but with some additional configs that identify it? } -// SupplierServiceConfig holds the service configuration the application stakes for +// ApplicationServiceConfig holds the service configuration the application stakes for message ApplicationServiceConfig { - repeated ServiceId service_id = 1; // Unique and semantic identifier for the service + ServiceId service_id = 1; // Unique and semantic identifier for the service + // TODO_RESEARCH: There is an opportunity for applications to advertise the max // they're willing to pay for a certain configuration/price, but this is outside of scope. - // repeated RPCConfig rpc_configs = 2; // List of endpoints for the service + // RPCConfig rpc_configs = 2; // List of endpoints for the service } // SupplierServiceConfig holds the service configuration the supplier stakes for @@ -32,7 +38,7 @@ message SupplierServiceConfig { // they're willing to earn for a certain configuration/price, but this is outside of scope. } -// Endpoint message to hold service configuration details +// SupplierEndpoint message to hold service configuration details message SupplierEndpoint { string url = 1; // URL of the endpoint RPCType rpc_type = 2; // Type of RPC exposed on the url above diff --git a/proto/pocket/shared/supplier.proto b/proto/pocket/shared/supplier.proto index 1aa626b16..90e6999b3 100644 --- a/proto/pocket/shared/supplier.proto +++ b/proto/pocket/shared/supplier.proto @@ -4,7 +4,7 @@ package pocket.shared; // NOTE that the `shared` package is not a Cosmos module, // but rather a manually created package to resolve circular dependencies. -option go_package = "pocket/x/shared/types"; +option go_package = "github.com/pokt-network/poktroll/x/shared/types"; import "cosmos_proto/cosmos.proto"; import "cosmos/base/v1beta1/coin.proto"; @@ -16,4 +16,3 @@ message Supplier { cosmos.base.v1beta1.Coin stake = 2; // The total amount of uPOKT the supplier has staked repeated SupplierServiceConfig services = 3; // The service configs this supplier can support } - diff --git a/proto/pocket/supplier/genesis.proto b/proto/pocket/supplier/genesis.proto index 186ef81b8..81d3550d0 100644 --- a/proto/pocket/supplier/genesis.proto +++ b/proto/pocket/supplier/genesis.proto @@ -6,7 +6,7 @@ import "gogoproto/gogo.proto"; import "pocket/supplier/params.proto"; import "pocket/shared/supplier.proto"; -option go_package = "pocket/x/supplier/types"; +option go_package = "github.com/pokt-network/poktroll/x/supplier/types"; // GenesisState defines the supplier module's genesis state. message GenesisState { diff --git a/proto/pocket/supplier/params.proto b/proto/pocket/supplier/params.proto index 8e0d4c79b..6623b4b82 100644 --- a/proto/pocket/supplier/params.proto +++ b/proto/pocket/supplier/params.proto @@ -3,10 +3,10 @@ package pocket.supplier; import "gogoproto/gogo.proto"; -option go_package = "pocket/x/supplier/types"; +option go_package = "github.com/pokt-network/poktroll/x/supplier/types"; // Params defines the parameters for the module. message Params { option (gogoproto.goproto_stringer) = false; - + } diff --git a/proto/pocket/supplier/query.proto b/proto/pocket/supplier/query.proto index 339f03307..6ed7eb194 100644 --- a/proto/pocket/supplier/query.proto +++ b/proto/pocket/supplier/query.proto @@ -8,25 +8,25 @@ import "cosmos/base/query/v1beta1/pagination.proto"; import "pocket/supplier/params.proto"; import "pocket/shared/supplier.proto"; -option go_package = "pocket/x/supplier/types"; +option go_package = "github.com/pokt-network/poktroll/x/supplier/types"; // Query defines the gRPC querier service. service Query { - + // Parameters queries the parameters of the module. rpc Params (QueryParamsRequest) returns (QueryParamsResponse) { option (google.api.http).get = "/pocket/supplier/params"; - + } - + // Queries a list of Supplier items. rpc Supplier (QueryGetSupplierRequest) returns (QueryGetSupplierResponse) { option (google.api.http).get = "/pocket/supplier/supplier/{address}"; - + } rpc SupplierAll (QueryAllSupplierRequest) returns (QueryAllSupplierResponse) { option (google.api.http).get = "/pocket/supplier/supplier"; - + } } // QueryParamsRequest is request type for the Query/Params RPC method. @@ -34,7 +34,7 @@ message QueryParamsRequest {} // QueryParamsResponse is response type for the Query/Params RPC method. message QueryParamsResponse { - + // params holds all the parameters of this module. Params params = 1 [(gogoproto.nullable) = false]; } diff --git a/proto/pocket/supplier/tx.proto b/proto/pocket/supplier/tx.proto index d994b03aa..2671b83a9 100644 --- a/proto/pocket/supplier/tx.proto +++ b/proto/pocket/supplier/tx.proto @@ -5,9 +5,11 @@ package pocket.supplier; import "cosmos_proto/cosmos.proto"; import "cosmos/base/v1beta1/coin.proto"; import "cosmos/msg/v1/msg.proto"; + import "pocket/session/session.proto"; +import "pocket/shared/service.proto"; -option go_package = "pocket/x/supplier/types"; +option go_package = "github.com/pokt-network/poktroll/x/supplier/types"; // Msg defines the Msg service. service Msg { @@ -22,8 +24,7 @@ message MsgStakeSupplier { string address = 1 [(cosmos_proto.scalar) = "cosmos.AddressString"]; // The Bech32 address of the supplier using cosmos' ScalarDescriptor to ensure deterministic deterministic encoding using cosmos' ScalarDescriptor to ensure deterministic deterministic encoding cosmos.base.v1beta1.Coin stake = 2; // The total amount of uPOKT the supplier has staked. Must be ≥ to the current amount that the supplier has staked (if any) - // TODO(@Olshansk): Update the tx flow to add support for `services` - // repeated service.SupplierServiceConfig services = 3; // The list of services this supplier is staked to provide service for + repeated shared.SupplierServiceConfig services = 3; // The list of services this supplier is staked to provide service for } message MsgStakeSupplierResponse {} diff --git a/testutil/application/mocks/mocks.go b/testutil/application/mocks/mocks.go index 4ccc3e251..595954e65 100644 --- a/testutil/application/mocks/mocks.go +++ b/testutil/application/mocks/mocks.go @@ -1,6 +1,11 @@ package mocks // This file is in place to declare the package for dynamically generated structs. +// // Note that this does not follow the Cosmos SDK pattern of committing Mocks to main. // For example, they commit auto-generate code to main: https://github.com/cosmos/cosmos-sdk/blob/main/x/gov/testutil/expected_keepers_mocks.go // Documentation on how Cosmos uses mockgen can be found here: https://docs.cosmos.network/main/build/building-modules/testing#unit-tests +// +// IMPORTANT: We have attempted to use `.gitkeep` files instead, but it causes a circular dependency issue with protobuf and mock generation +// since we are leveraging `ignite` to compile `.proto` files which runs `go mod tidy` before generating, requiring the entire dependency tree +// to be valid before mock implementations have been generated. diff --git a/testutil/gateway/mocks/mocks.go b/testutil/gateway/mocks/mocks.go index 16355b5a1..595954e65 100644 --- a/testutil/gateway/mocks/mocks.go +++ b/testutil/gateway/mocks/mocks.go @@ -1,3 +1,11 @@ package mocks -// This file is in place to declare the package for dynamically generated mocks +// This file is in place to declare the package for dynamically generated structs. +// +// Note that this does not follow the Cosmos SDK pattern of committing Mocks to main. +// For example, they commit auto-generate code to main: https://github.com/cosmos/cosmos-sdk/blob/main/x/gov/testutil/expected_keepers_mocks.go +// Documentation on how Cosmos uses mockgen can be found here: https://docs.cosmos.network/main/build/building-modules/testing#unit-tests +// +// IMPORTANT: We have attempted to use `.gitkeep` files instead, but it causes a circular dependency issue with protobuf and mock generation +// since we are leveraging `ignite` to compile `.proto` files which runs `go mod tidy` before generating, requiring the entire dependency tree +// to be valid before mock implementations have been generated. diff --git a/testutil/keeper/application.go b/testutil/keeper/application.go index 08bf27bb7..7a92cd2f1 100644 --- a/testutil/keeper/application.go +++ b/testutil/keeper/application.go @@ -15,11 +15,18 @@ import ( "github.com/golang/mock/gomock" "github.com/stretchr/testify/require" - mocks "pocket/testutil/application/mocks" - "pocket/x/application/keeper" - "pocket/x/application/types" + mocks "github.com/pokt-network/poktroll/testutil/application/mocks" + "github.com/pokt-network/poktroll/x/application/keeper" + "github.com/pokt-network/poktroll/x/application/types" + gatewaytypes "github.com/pokt-network/poktroll/x/gateway/types" ) +// StakedGatewayMap is used to mock whether a gateway is staked or not for use +// in the application's mocked gateway keeper. This enables the tester to +// control whether a gateway is "staked" or not and whether it can be delegated to +// WARNING: Using this map may cause issues if running multiple tests in parallel +var StakedGatewayMap = make(map[string]struct{}) + func ApplicationKeeper(t testing.TB) (*keeper.Keeper, sdk.Context) { storeKey := sdk.NewKVStoreKey(types.StoreKey) memStoreKey := storetypes.NewMemoryStoreKey(types.MemStoreKey) @@ -38,6 +45,23 @@ func ApplicationKeeper(t testing.TB) (*keeper.Keeper, sdk.Context) { mockBankKeeper.EXPECT().DelegateCoinsFromAccountToModule(gomock.Any(), gomock.Any(), types.ModuleName, gomock.Any()).AnyTimes() mockBankKeeper.EXPECT().UndelegateCoinsFromModuleToAccount(gomock.Any(), types.ModuleName, gomock.Any(), gomock.Any()).AnyTimes() + mockAccountKeeper := mocks.NewMockAccountKeeper(ctrl) + mockAccountKeeper.EXPECT().GetAccount(gomock.Any(), gomock.Any()).AnyTimes() + + mockGatewayKeeper := mocks.NewMockGatewayKeeper(ctrl) + mockGatewayKeeper.EXPECT().GetGateway(gomock.Any(), gomock.Any()).DoAndReturn( + func(_ sdk.Context, addr string) (gatewaytypes.Gateway, bool) { + if _, ok := StakedGatewayMap[addr]; !ok { + return gatewaytypes.Gateway{}, false + } + stake := sdk.NewCoin("upokt", sdk.NewInt(10000)) + return gatewaytypes.Gateway{ + Address: addr, + Stake: &stake, + }, true + }, + ).AnyTimes() + paramsSubspace := typesparams.NewSubspace(cdc, types.Amino, storeKey, @@ -50,6 +74,8 @@ func ApplicationKeeper(t testing.TB) (*keeper.Keeper, sdk.Context) { memStoreKey, paramsSubspace, mockBankKeeper, + mockAccountKeeper, + mockGatewayKeeper, ) ctx := sdk.NewContext(stateStore, tmproto.Header{}, false, log.NewNopLogger()) diff --git a/testutil/keeper/gateway.go b/testutil/keeper/gateway.go index cd2e3a3eb..7a7fa84bb 100644 --- a/testutil/keeper/gateway.go +++ b/testutil/keeper/gateway.go @@ -3,11 +3,6 @@ package keeper import ( "testing" - "pocket/x/gateway/keeper" - "pocket/x/gateway/types" - - mocks "pocket/testutil/gateway/mocks" - tmdb "github.com/cometbft/cometbft-db" "github.com/cometbft/cometbft/libs/log" tmproto "github.com/cometbft/cometbft/proto/tendermint/types" @@ -19,6 +14,10 @@ import ( typesparams "github.com/cosmos/cosmos-sdk/x/params/types" "github.com/golang/mock/gomock" "github.com/stretchr/testify/require" + + mocks "github.com/pokt-network/poktroll/testutil/gateway/mocks" + "github.com/pokt-network/poktroll/x/gateway/keeper" + "github.com/pokt-network/poktroll/x/gateway/types" ) func GatewayKeeper(t testing.TB) (*keeper.Keeper, sdk.Context) { @@ -51,7 +50,6 @@ func GatewayKeeper(t testing.TB) (*keeper.Keeper, sdk.Context) { memStoreKey, paramsSubspace, mockBankKeeper, - nil, ) ctx := sdk.NewContext(stateStore, tmproto.Header{}, false, log.NewNopLogger()) diff --git a/testutil/keeper/pocket.go b/testutil/keeper/pocket.go index 096be904f..cebf1b0bf 100644 --- a/testutil/keeper/pocket.go +++ b/testutil/keeper/pocket.go @@ -13,8 +13,9 @@ import ( sdk "github.com/cosmos/cosmos-sdk/types" typesparams "github.com/cosmos/cosmos-sdk/x/params/types" "github.com/stretchr/testify/require" - "pocket/x/pocket/keeper" - "pocket/x/pocket/types" + + "github.com/pokt-network/poktroll/x/pocket/keeper" + "github.com/pokt-network/poktroll/x/pocket/types" ) func PocketKeeper(t testing.TB) (*keeper.Keeper, sdk.Context) { diff --git a/testutil/keeper/service.go b/testutil/keeper/service.go index 404e2e7fc..3bdb9f219 100644 --- a/testutil/keeper/service.go +++ b/testutil/keeper/service.go @@ -13,8 +13,9 @@ import ( sdk "github.com/cosmos/cosmos-sdk/types" typesparams "github.com/cosmos/cosmos-sdk/x/params/types" "github.com/stretchr/testify/require" - "pocket/x/service/keeper" - "pocket/x/service/types" + + "github.com/pokt-network/poktroll/x/service/keeper" + "github.com/pokt-network/poktroll/x/service/types" ) func ServiceKeeper(t testing.TB) (*keeper.Keeper, sdk.Context) { diff --git a/testutil/keeper/session.go b/testutil/keeper/session.go index cd3ea868f..e4be2537f 100644 --- a/testutil/keeper/session.go +++ b/testutil/keeper/session.go @@ -16,12 +16,12 @@ import ( "github.com/golang/mock/gomock" "github.com/stretchr/testify/require" - "pocket/testutil/sample" - mocks "pocket/testutil/session/mocks" - apptypes "pocket/x/application/types" - "pocket/x/session/keeper" - "pocket/x/session/types" - sharedtypes "pocket/x/shared/types" + "github.com/pokt-network/poktroll/testutil/sample" + "github.com/pokt-network/poktroll/testutil/session/mocks" + apptypes "github.com/pokt-network/poktroll/x/application/types" + "github.com/pokt-network/poktroll/x/session/keeper" + "github.com/pokt-network/poktroll/x/session/types" + sharedtypes "github.com/pokt-network/poktroll/x/shared/types" ) type option[V any] func(k *keeper.Keeper) @@ -34,12 +34,12 @@ var ( TestApp1 = apptypes.Application{ Address: TestApp1Address, Stake: &sdk.Coin{Denom: "upokt", Amount: sdk.NewInt(100)}, - ServiceIds: []*sharedtypes.ServiceId{ + ServiceConfigs: []*sharedtypes.ApplicationServiceConfig{ { - Id: TestServiceId1, + ServiceId: &sharedtypes.ServiceId{Id: TestServiceId1}, }, { - Id: TestServiceId2, + ServiceId: &sharedtypes.ServiceId{Id: TestServiceId2}, }, }, } @@ -48,12 +48,12 @@ var ( TestApp2 = apptypes.Application{ Address: TestApp1Address, Stake: &sdk.Coin{Denom: "upokt", Amount: sdk.NewInt(100)}, - ServiceIds: []*sharedtypes.ServiceId{ + ServiceConfigs: []*sharedtypes.ApplicationServiceConfig{ { - Id: TestServiceId1, + ServiceId: &sharedtypes.ServiceId{Id: TestServiceId1}, }, { - Id: TestServiceId2, + ServiceId: &sharedtypes.ServiceId{Id: TestServiceId2}, }, }, } @@ -70,6 +70,7 @@ var ( { Url: TestSupplierUrl, RpcType: sharedtypes.RPCType_JSON_RPC, + Configs: make([]*sharedtypes.ConfigOption, 0), }, }, }, @@ -79,6 +80,7 @@ var ( { Url: TestSupplierUrl, RpcType: sharedtypes.RPCType_GRPC, + Configs: make([]*sharedtypes.ConfigOption, 0), }, }, }, diff --git a/testutil/keeper/supplier.go b/testutil/keeper/supplier.go index 769d314c2..d54095fd6 100644 --- a/testutil/keeper/supplier.go +++ b/testutil/keeper/supplier.go @@ -15,9 +15,9 @@ import ( "github.com/golang/mock/gomock" "github.com/stretchr/testify/require" - mocks "pocket/testutil/supplier/mocks" - "pocket/x/supplier/keeper" - "pocket/x/supplier/types" + mocks "github.com/pokt-network/poktroll/testutil/supplier/mocks" + "github.com/pokt-network/poktroll/x/supplier/keeper" + "github.com/pokt-network/poktroll/x/supplier/types" ) func SupplierKeeper(t testing.TB) (*keeper.Keeper, sdk.Context) { @@ -49,6 +49,7 @@ func SupplierKeeper(t testing.TB) (*keeper.Keeper, sdk.Context) { storeKey, memStoreKey, paramsSubspace, + mockBankKeeper, ) diff --git a/testutil/network/network.go b/testutil/network/network.go index 1b5f3022d..c8ab3efcb 100644 --- a/testutil/network/network.go +++ b/testutil/network/network.go @@ -2,7 +2,6 @@ package network import ( "fmt" - "strconv" "testing" "time" @@ -22,13 +21,12 @@ import ( authtypes "github.com/cosmos/cosmos-sdk/x/auth/types" "github.com/stretchr/testify/require" - "pocket/app" - "pocket/testutil/nullify" - "pocket/testutil/sample" - app_types "pocket/x/application/types" - gateway_types "pocket/x/gateway/types" - shared_types "pocket/x/shared/types" - supplier_types "pocket/x/supplier/types" + "github.com/pokt-network/poktroll/app" + "github.com/pokt-network/poktroll/testutil/sample" + apptypes "github.com/pokt-network/poktroll/x/application/types" + gatewaytypes "github.com/pokt-network/poktroll/x/gateway/types" + sharedtypes "github.com/pokt-network/poktroll/x/shared/types" + suppliertypes "github.com/pokt-network/poktroll/x/supplier/types" ) type ( @@ -103,16 +101,22 @@ func DefaultConfig() network.Config { // DefaultApplicationModuleGenesisState generates a GenesisState object with a given number of applications. // It returns the populated GenesisState object. -func DefaultApplicationModuleGenesisState(t *testing.T, n int) *app_types.GenesisState { +func DefaultApplicationModuleGenesisState(t *testing.T, n int) *apptypes.GenesisState { t.Helper() - state := app_types.DefaultGenesis() + state := apptypes.DefaultGenesis() for i := 0; i < n; i++ { stake := sdk.NewCoin("upokt", sdk.NewInt(int64(i+1))) - application := app_types.Application{ + application := apptypes.Application{ Address: sample.AccAddress(), Stake: &stake, + ServiceConfigs: []*sharedtypes.ApplicationServiceConfig{ + { + ServiceId: &sharedtypes.ServiceId{Id: fmt.Sprintf("svc%d", i)}, + }, + }, } - nullify.Fill(&application) + // TODO_CONSIDERATION: Evaluate whether we need `nullify.Fill` or if we should enforce `(gogoproto.nullable) = false` everywhere + // nullify.Fill(&application) state.ApplicationList = append(state.ApplicationList, application) } return state @@ -120,16 +124,17 @@ func DefaultApplicationModuleGenesisState(t *testing.T, n int) *app_types.Genesi // DefaultGatewayModuleGenesisState generates a GenesisState object with a given number of gateways. // It returns the populated GenesisState object. -func DefaultGatewayModuleGenesisState(t *testing.T, n int) *gateway_types.GenesisState { +func DefaultGatewayModuleGenesisState(t *testing.T, n int) *gatewaytypes.GenesisState { t.Helper() - state := gateway_types.DefaultGenesis() + state := gatewaytypes.DefaultGenesis() for i := 0; i < n; i++ { stake := sdk.NewCoin("upokt", sdk.NewInt(int64(i))) - gateway := gateway_types.Gateway{ - Address: strconv.Itoa(i), + gateway := gatewaytypes.Gateway{ + Address: sample.AccAddress(), Stake: &stake, } - nullify.Fill(&gateway) + // TODO_CONSIDERATION: Evaluate whether we need `nullify.Fill` or if we should enforce `(gogoproto.nullable) = false` everywhere + // nullify.Fill(&gateway) state.GatewayList = append(state.GatewayList, gateway) } return state @@ -137,17 +142,29 @@ func DefaultGatewayModuleGenesisState(t *testing.T, n int) *gateway_types.Genesi // DefaultSupplierModuleGenesisState generates a GenesisState object with a given number of suppliers. // It returns the populated GenesisState object. -func DefaultSupplierModuleGenesisState(t *testing.T, n int) *supplier_types.GenesisState { +func DefaultSupplierModuleGenesisState(t *testing.T, n int) *suppliertypes.GenesisState { t.Helper() - state := supplier_types.DefaultGenesis() + state := suppliertypes.DefaultGenesis() for i := 0; i < n; i++ { stake := sdk.NewCoin("upokt", sdk.NewInt(int64(i))) - gateway := shared_types.Supplier{ - Address: strconv.Itoa(i), + supplier := sharedtypes.Supplier{ + Address: sample.AccAddress(), Stake: &stake, + Services: []*sharedtypes.SupplierServiceConfig{ + { + ServiceId: &sharedtypes.ServiceId{Id: fmt.Sprintf("svc%d", i)}, + Endpoints: []*sharedtypes.SupplierEndpoint{ + { + Url: fmt.Sprintf("http://localhost:%d", i), + RpcType: sharedtypes.RPCType_JSON_RPC, + }, + }, + }, + }, } - nullify.Fill(&gateway) - state.SupplierList = append(state.SupplierList, gateway) + // TODO_CONSIDERATION: Evaluate whether we need `nullify.Fill` or if we should enforce `(gogoproto.nullable) = false` everywhere + // nullify.Fill(&supplier) + state.SupplierList = append(state.SupplierList, supplier) } return state } diff --git a/testutil/session/mocks/mocks.go b/testutil/session/mocks/mocks.go index 4ccc3e251..423f63d3e 100644 --- a/testutil/session/mocks/mocks.go +++ b/testutil/session/mocks/mocks.go @@ -1,6 +1,10 @@ package mocks // This file is in place to declare the package for dynamically generated structs. +// // Note that this does not follow the Cosmos SDK pattern of committing Mocks to main. // For example, they commit auto-generate code to main: https://github.com/cosmos/cosmos-sdk/blob/main/x/gov/testutil/expected_keepers_mocks.go // Documentation on how Cosmos uses mockgen can be found here: https://docs.cosmos.network/main/build/building-modules/testing#unit-tests +// +// IMPORTANT: We have attempted to use `.gitkeep` files instead, but it causes a circular dependency issue with protobuf and mock generation +// since we are leveraging `ignite` to compile `.proto` files which requires `.go` files to compile. diff --git a/testutil/supplier/mocks/mocks.go b/testutil/supplier/mocks/mocks.go index 4ccc3e251..595954e65 100644 --- a/testutil/supplier/mocks/mocks.go +++ b/testutil/supplier/mocks/mocks.go @@ -1,6 +1,11 @@ package mocks // This file is in place to declare the package for dynamically generated structs. +// // Note that this does not follow the Cosmos SDK pattern of committing Mocks to main. // For example, they commit auto-generate code to main: https://github.com/cosmos/cosmos-sdk/blob/main/x/gov/testutil/expected_keepers_mocks.go // Documentation on how Cosmos uses mockgen can be found here: https://docs.cosmos.network/main/build/building-modules/testing#unit-tests +// +// IMPORTANT: We have attempted to use `.gitkeep` files instead, but it causes a circular dependency issue with protobuf and mock generation +// since we are leveraging `ignite` to compile `.proto` files which runs `go mod tidy` before generating, requiring the entire dependency tree +// to be valid before mock implementations have been generated. diff --git a/tools/scripts/goimports/filters/filters.go b/tools/scripts/goimports/filters/filters.go new file mode 100644 index 000000000..67a048a51 --- /dev/null +++ b/tools/scripts/goimports/filters/filters.go @@ -0,0 +1,103 @@ +// The filters package contains functions that can be used to filter file paths. + +package filters + +import ( + "bufio" + "bytes" + "os" + "path/filepath" + "strings" +) + +const igniteScaffoldComment = "// this line is used by starport scaffolding" + +var ( + importStart = []byte("import (") + importEnd = []byte(")") +) + +// FilterFn is a function that returns true if the given path matches the +// filter's criteria. +type FilterFn func(path string) (bool, error) + +// PathMatchesGoExtension matches go source files. +func PathMatchesGoExtension(path string) (bool, error) { + return filepath.Ext(path) == ".go", nil +} + +// PathMatchesProtobufGo matches generated protobuf go source files. +func PathMatchesProtobufGo(path string) (bool, error) { + return strings.HasSuffix(path, ".pb.go"), nil +} + +// PathMatchesProtobufGatewayGo matches generated protobuf gateway go source files. +func PathMatchesProtobufGatewayGo(path string) (bool, error) { + return strings.HasSuffix(path, ".pb.gw.go"), nil +} + +// PathMatchesMockGo matches generated mock go source files. +func PathMatchesMockGo(path string) (bool, error) { + return strings.HasSuffix(path, "_mock.go"), nil +} + +// PathMatchesTestGo matches go test files. +func PathMatchesTestGo(path string) (bool, error) { + return strings.HasSuffix(path, "_test.go"), nil +} + +// ContentMatchesEmptyImportScaffold matches files that can't be goimport'd due +// to ignite incompatibility. +func ContentMatchesEmptyImportScaffold(path string) (bool, error) { + return containsEmptyImportScaffold(path) +} + +// containsEmptyImportScaffold checks if the go file at goSrcPath contains an +// import statement like the following: +// +// import ( +// // this line is used by starport scaffolding ... +// ) +func containsEmptyImportScaffold(goSrcPath string) (isEmptyImport bool, _ error) { + file, err := os.Open(goSrcPath) + if err != nil { + return false, err + } + defer file.Close() + + scanner := bufio.NewScanner(file) + scanner.Split(importBlockSplit) + + for scanner.Scan() { + trimmedImportBlock := strings.Trim(scanner.Text(), "\n\t") + if strings.HasPrefix(trimmedImportBlock, igniteScaffoldComment) { + return true, nil + } + } + + if scanner.Err() != nil { + return false, scanner.Err() + } + + return false, nil +} + +// importBlockSplit is a split function intended to be used with bufio.Scanner +// to extract the contents of a multi-line go import block. +func importBlockSplit(data []byte, _ bool) (advance int, token []byte, err error) { + // Search for the beginning of the import block + startIdx := bytes.Index(data, importStart) + if startIdx == -1 { + return 0, nil, nil + } + + // Search for the end of the import block from the start index + endIdx := bytes.Index(data[startIdx:], importEnd) + if endIdx == -1 { + return 0, nil, nil + } + + // Return the entire import block, including "import (" and ")" + importBlock := data[startIdx+len(importStart) : startIdx-len(importEnd)+endIdx+1] + return startIdx + endIdx + 1, importBlock, nil +} diff --git a/tools/scripts/goimports/main.go b/tools/scripts/goimports/main.go new file mode 100644 index 000000000..ca5c26648 --- /dev/null +++ b/tools/scripts/goimports/main.go @@ -0,0 +1,120 @@ +package main + +import ( + "fmt" + "os" + "os/exec" + "path/filepath" + "strings" + + "github.com/pokt-network/poktroll/tools/scripts/goimports/filters" +) + +// defaultArgs are always passed to goimports. +// -w: write result to (source) file instead of stdout +// -local: put imports beginning with this string after 3rd-party packages (comma-separated list) +// (see: goimports -h for more info) +var ( + defaultArgs = []string{"-w", "-local", "github.com/pokt-network/poktroll"} + defaultIncludeFilters = []filters.FilterFn{ + filters.PathMatchesGoExtension, + } + defaultExcludeFilters = []filters.FilterFn{ + filters.PathMatchesProtobufGo, + filters.PathMatchesProtobufGatewayGo, + filters.PathMatchesMockGo, + filters.PathMatchesTestGo, + filters.ContentMatchesEmptyImportScaffold, + } +) + +func main() { + root := "." + var filesToProcess []string + + // Walk the file system and accumulate matching files + err := filepath.Walk(root, walkRepoRootFn( + root, + defaultIncludeFilters, + defaultExcludeFilters, + &filesToProcess, + )) + if err != nil { + fmt.Printf("Error processing files: %s\n", err) + return + } + + // Run goimports on all accumulated files + if len(filesToProcess) > 0 { + cmd := exec.Command("goimports", append(defaultArgs, filesToProcess...)...) + if err := cmd.Run(); err != nil { + fmt.Printf("Failed running goimports: %v\n", err) + } + } +} + +func walkRepoRootFn( + rootPath string, + includeFilters []filters.FilterFn, + excludeFilters []filters.FilterFn, + filesToProcess *[]string, +) filepath.WalkFunc { + return func(path string, info os.FileInfo, err error) error { + if err != nil { + return err + } + + // Don't process the root directory but don't skip it either; that would + // exclude everything. + if info.Name() == rootPath { + return nil + } + + // No need to process directories + if info.IsDir() { + // Skip directories that start with a period + if strings.HasPrefix(info.Name(), ".") { + return filepath.SkipDir + } + return nil + } + + // Don't process paths which don't match any include filter. + var shouldIncludePath bool + for _, includeFilter := range includeFilters { + pathMatches, err := includeFilter(path) + if err != nil { + panic(err) + } + + if pathMatches { + shouldIncludePath = true + break + } + } + if !shouldIncludePath { + return nil + } + + // Don't process paths which match any exclude filter. + var shouldExcludePath bool + for _, excludeFilter := range excludeFilters { + pathMatches, err := excludeFilter(path) + if err != nil { + panic(err) + } + + if pathMatches { + shouldExcludePath = true + break + } + } + if shouldExcludePath { + return nil + } + + *filesToProcess = append(*filesToProcess, path) + + return nil + } +} diff --git a/tools/scripts/itest.sh b/tools/scripts/itest.sh index 642cdbfe8..323db3d71 100755 --- a/tools/scripts/itest.sh +++ b/tools/scripts/itest.sh @@ -44,7 +44,7 @@ itest() { # If go test fails, exit the loop. if [[ $test_exit_status -ne 0 ]]; then - echo "go test failed on iteration $i. Exiting early." + echo "go test failed on iteration $i; exiting early. Total tests run: $total_tests_run" return 1 fi done diff --git a/x/application/client/cli/helpers_test.go b/x/application/client/cli/helpers_test.go index aa60d57ed..46f1e9440 100644 --- a/x/application/client/cli/helpers_test.go +++ b/x/application/client/cli/helpers_test.go @@ -5,11 +5,11 @@ import ( "strconv" "testing" - "pocket/cmd/pocketd/cmd" - "pocket/testutil/network" - "pocket/x/application/types" - "github.com/stretchr/testify/require" + + "github.com/pokt-network/poktroll/cmd/pocketd/cmd" + "github.com/pokt-network/poktroll/testutil/network" + "github.com/pokt-network/poktroll/x/application/types" ) // Dummy variable to avoid unused import error. diff --git a/x/application/client/cli/query.go b/x/application/client/cli/query.go index 2c42f0de6..86cd73b25 100644 --- a/x/application/client/cli/query.go +++ b/x/application/client/cli/query.go @@ -10,7 +10,7 @@ import ( // "github.com/cosmos/cosmos-sdk/client/flags" // sdk "github.com/cosmos/cosmos-sdk/types" - "pocket/x/application/types" + "github.com/pokt-network/poktroll/x/application/types" ) // GetQueryCmd returns the cli query commands for this module diff --git a/x/application/client/cli/query_application.go b/x/application/client/cli/query_application.go index a11a3059e..61a5eb35b 100644 --- a/x/application/client/cli/query_application.go +++ b/x/application/client/cli/query_application.go @@ -5,7 +5,7 @@ import ( "github.com/cosmos/cosmos-sdk/client/flags" "github.com/spf13/cobra" - "pocket/x/application/types" + "github.com/pokt-network/poktroll/x/application/types" ) func CmdListApplication() *cobra.Command { diff --git a/x/application/client/cli/query_application_test.go b/x/application/client/cli/query_application_test.go index 376f63a62..10a8b77c6 100644 --- a/x/application/client/cli/query_application_test.go +++ b/x/application/client/cli/query_application_test.go @@ -12,9 +12,9 @@ import ( "google.golang.org/grpc/codes" "google.golang.org/grpc/status" - "pocket/testutil/nullify" - "pocket/x/application/client/cli" - "pocket/x/application/types" + "github.com/pokt-network/poktroll/testutil/nullify" + "github.com/pokt-network/poktroll/x/application/client/cli" + "github.com/pokt-network/poktroll/x/application/types" ) func TestShowApplication(t *testing.T) { diff --git a/x/application/client/cli/query_params.go b/x/application/client/cli/query_params.go index 1c14c41b5..7a47d705c 100644 --- a/x/application/client/cli/query_params.go +++ b/x/application/client/cli/query_params.go @@ -5,7 +5,7 @@ import ( "github.com/cosmos/cosmos-sdk/client/flags" "github.com/spf13/cobra" - "pocket/x/application/types" + "github.com/pokt-network/poktroll/x/application/types" ) func CmdQueryParams() *cobra.Command { diff --git a/x/application/client/cli/tx.go b/x/application/client/cli/tx.go index be1ead388..cb09f9b3f 100644 --- a/x/application/client/cli/tx.go +++ b/x/application/client/cli/tx.go @@ -8,7 +8,7 @@ import ( "github.com/cosmos/cosmos-sdk/client" // "github.com/cosmos/cosmos-sdk/client/flags" - "pocket/x/application/types" + "github.com/pokt-network/poktroll/x/application/types" ) var ( diff --git a/x/application/client/cli/tx_delegate_to_gateway.go b/x/application/client/cli/tx_delegate_to_gateway.go index 8291df1a5..ea251e6cd 100644 --- a/x/application/client/cli/tx_delegate_to_gateway.go +++ b/x/application/client/cli/tx_delegate_to_gateway.go @@ -7,18 +7,25 @@ import ( "github.com/cosmos/cosmos-sdk/client/flags" "github.com/cosmos/cosmos-sdk/client/tx" "github.com/spf13/cobra" - "pocket/x/application/types" + + "github.com/pokt-network/poktroll/x/application/types" ) var _ = strconv.Itoa(0) func CmdDelegateToGateway() *cobra.Command { cmd := &cobra.Command{ - Use: "delegate-to-gateway", - Short: "Broadcast message delegate-to-gateway", - Args: cobra.ExactArgs(0), + Use: "delegate-to-gateway [gateway address]", + Short: "Delegate an application to a gateway", + Long: `Delegate an application to the gateway with the provided address. This is a broadcast operation +that delegates authority to the gateway specified to sign relays requests for the application, allowing the gateway +act on the behalf of the application during a session. + +Example: +$ poktrolld --home=$(POCKETD_HOME) tx application delegate-to-gateway $(GATEWAY_ADDR) --keyring-backend test --from $(APP) --node $(POCKET_NODE)`, + Args: cobra.ExactArgs(1), RunE: func(cmd *cobra.Command, args []string) (err error) { - + gatewayAddress := args[0] clientCtx, err := client.GetClientTxContext(cmd) if err != nil { return err @@ -26,10 +33,12 @@ func CmdDelegateToGateway() *cobra.Command { msg := types.NewMsgDelegateToGateway( clientCtx.GetFromAddress().String(), + gatewayAddress, ) if err := msg.ValidateBasic(); err != nil { return err } + return tx.GenerateOrBroadcastTxCLI(clientCtx, cmd.Flags(), msg) }, } diff --git a/x/application/client/cli/tx_delegate_to_gateway_test.go b/x/application/client/cli/tx_delegate_to_gateway_test.go new file mode 100644 index 000000000..97ad29a91 --- /dev/null +++ b/x/application/client/cli/tx_delegate_to_gateway_test.go @@ -0,0 +1,117 @@ +package cli_test + +import ( + "fmt" + "testing" + + sdkerrors "cosmossdk.io/errors" + sdkmath "cosmossdk.io/math" + "github.com/cosmos/cosmos-sdk/client/flags" + "github.com/cosmos/cosmos-sdk/testutil" + clitestutil "github.com/cosmos/cosmos-sdk/testutil/cli" + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/stretchr/testify/require" + "google.golang.org/grpc/status" + + "github.com/pokt-network/poktroll/testutil/network" + "github.com/pokt-network/poktroll/x/application/client/cli" + "github.com/pokt-network/poktroll/x/application/types" +) + +func TestCLI_DelegateToGateway(t *testing.T) { + net, _ := networkWithApplicationObjects(t, 2) + val := net.Validators[0] + ctx := val.ClientCtx + + // Create a keyring and add an account for the application to be delegated + // and the gateway to be delegated to + kr := ctx.Keyring + accounts := testutil.CreateKeyringAccounts(t, kr, 2) + appAccount := accounts[0] + gatewayAccount := accounts[1] + + // Update the context with the new keyring + ctx = ctx.WithKeyring(kr) + + // Common args used for all requests + commonArgs := []string{ + fmt.Sprintf("--%s=true", flags.FlagSkipConfirmation), + fmt.Sprintf("--%s=%s", flags.FlagBroadcastMode, flags.BroadcastSync), + fmt.Sprintf("--%s=%s", flags.FlagFees, sdk.NewCoins(sdk.NewCoin(net.Config.BondDenom, sdkmath.NewInt(10))).String()), + } + + tests := []struct { + desc string + appAddress string + gatewayAddress string + err *sdkerrors.Error + }{ + { + desc: "delegate to gateway: valid", + appAddress: appAccount.Address.String(), + gatewayAddress: gatewayAccount.Address.String(), + }, + { + desc: "invalid - missing app address", + // appAddress: appAccount.Address.String(), + gatewayAddress: gatewayAccount.Address.String(), + err: types.ErrAppInvalidAddress, + }, + { + desc: "invalid - invalid app address", + appAddress: "invalid address", + gatewayAddress: gatewayAccount.Address.String(), + err: types.ErrAppInvalidAddress, + }, + { + desc: "invalid - missing gateway address", + appAddress: appAccount.Address.String(), + // gatewayAddress: gatewayAccount.Address.String(), + err: types.ErrAppInvalidGatewayAddress, + }, + { + desc: "invalid - invalid gateway address", + appAddress: appAccount.Address.String(), + gatewayAddress: "invalid address", + err: types.ErrAppInvalidGatewayAddress, + }, + } + + // Initialize the App and Gateway Accounts by sending it some funds from the validator account that is part of genesis + network.InitAccount(t, net, appAccount.Address) + network.InitAccount(t, net, gatewayAccount.Address) + + // Run the tests + for _, tt := range tests { + t.Run(tt.desc, func(t *testing.T) { + // Wait for a new block to be committed + require.NoError(t, net.WaitForNextBlock()) + + // Prepare the arguments for the CLI command + args := []string{ + tt.gatewayAddress, + fmt.Sprintf("--%s=%s", flags.FlagFrom, tt.appAddress), + } + args = append(args, commonArgs...) + + // Execute the command + delegateOutput, err := clitestutil.ExecTestCLICmd(ctx, cli.CmdDelegateToGateway(), args) + + // Validate the error if one is expected + if tt.err != nil { + stat, ok := status.FromError(tt.err) + require.True(t, ok) + require.Contains(t, stat.Message(), tt.err.Error()) + return + } + require.NoError(t, err) + + // Check the response + var resp sdk.TxResponse + require.NoError(t, net.Config.Codec.UnmarshalJSON(delegateOutput.Bytes(), &resp)) + require.NotNil(t, resp) + require.NotNil(t, resp.TxHash) + require.Equal(t, uint32(0), resp.Code) + }) + } +} diff --git a/x/application/client/cli/tx_stake_application.go b/x/application/client/cli/tx_stake_application.go index 8a20f533c..510cfd648 100644 --- a/x/application/client/cli/tx_stake_application.go +++ b/x/application/client/cli/tx_stake_application.go @@ -2,6 +2,7 @@ package cli import ( "strconv" + "strings" "github.com/cosmos/cosmos-sdk/client" "github.com/cosmos/cosmos-sdk/client/flags" @@ -9,7 +10,7 @@ import ( sdk "github.com/cosmos/cosmos-sdk/types" "github.com/spf13/cobra" - "pocket/x/application/types" + "github.com/pokt-network/poktroll/x/application/types" ) var _ = strconv.Itoa(0) @@ -17,16 +18,20 @@ var _ = strconv.Itoa(0) func CmdStakeApplication() *cobra.Command { // fromAddress & signature is retrieved via `flags.FlagFrom` in the `clientCtx` cmd := &cobra.Command{ - Use: "stake-application [amount]", + // TODO_HACK: For now we are only specifying the service IDs as a list of of strings separated by commas. + // This needs to be expand to specify the full ApplicationServiceConfig. Furthermore, providing a flag to + // a file where ApplicationServiceConfig specifying full service configurations in the CLI by providing a flag that accepts a JSON string + Use: "stake-application [amount] [svcId1,svcId2,...,svcIdN]", Short: "Stake an application", Long: `Stake an application with the provided parameters. This is a broadcast operation that -will stake the tokens and associate them with the application specified by the 'from' address. +will stake the tokens and serviceIds and associate them with the application specified by the 'from' address. Example: -$ pocketd --home=$(POCKETD_HOME) tx application stake-application 1000upokt --keyring-backend test --from $(APP) --node $(POCKET_NODE)`, - Args: cobra.ExactArgs(1), +$ poktrolld --home=$(POCKETD_HOME) tx application stake-application 1000upokt svc1,svc2,svc3 --keyring-backend test --from $(APP) --node $(POCKET_NODE)`, + Args: cobra.ExactArgs(2), RunE: func(cmd *cobra.Command, args []string) (err error) { stakeString := args[0] + serviceIdsString := args[1] clientCtx, err := client.GetClientTxContext(cmd) if err != nil { @@ -38,9 +43,12 @@ $ pocketd --home=$(POCKETD_HOME) tx application stake-application 1000upokt --ke return err } + serviceIds := strings.Split(serviceIdsString, ",") + msg := types.NewMsgStakeApplication( clientCtx.GetFromAddress().String(), stake, + serviceIds, ) if err := msg.ValidateBasic(); err != nil { diff --git a/x/application/client/cli/tx_stake_application_test.go b/x/application/client/cli/tx_stake_application_test.go index 8a0dac4b6..f88b02fea 100644 --- a/x/application/client/cli/tx_stake_application_test.go +++ b/x/application/client/cli/tx_stake_application_test.go @@ -13,9 +13,9 @@ import ( "github.com/stretchr/testify/require" "google.golang.org/grpc/status" - "pocket/testutil/network" - "pocket/x/application/client/cli" - "pocket/x/application/types" + "github.com/pokt-network/poktroll/testutil/network" + "github.com/pokt-network/poktroll/x/application/client/cli" + "github.com/pokt-network/poktroll/x/application/types" ) func TestCLI_StakeApplication(t *testing.T) { @@ -39,51 +39,95 @@ func TestCLI_StakeApplication(t *testing.T) { } tests := []struct { - desc string - address string - stakeString string - err *sdkerrors.Error + desc string + address string + stakeString string + serviceIdsString string + err *sdkerrors.Error }{ + // Happy Paths { - desc: "stake application: valid", - address: appAccount.Address.String(), - stakeString: "1000upokt", + desc: "valid", + address: appAccount.Address.String(), + stakeString: "1000upokt", + serviceIdsString: "svc1,svc2,svc3", + err: nil, }, + + // Error Paths - Address Related { - desc: "stake application: missing address", + desc: "address_test: missing address", // address: "explicitly missing", - stakeString: "1000upokt", - err: types.ErrAppInvalidAddress, + stakeString: "1000upokt", + serviceIdsString: "svc1,svc2,svc3", + err: types.ErrAppInvalidAddress, }, { - desc: "stake application: invalid address", - address: "invalid", - stakeString: "1000upokt", - err: types.ErrAppInvalidAddress, + desc: "stake application: invalid address", + address: "invalid", + stakeString: "1000upokt", + serviceIdsString: "svc1,svc2,svc3", + err: types.ErrAppInvalidAddress, }, + + // Error Paths - Stake Related { - desc: "stake application: missing stake", + desc: "address_test: missing stake", address: appAccount.Address.String(), // stakeString: "explicitly missing", - err: types.ErrAppInvalidStake, + serviceIdsString: "svc1,svc2,svc3", + err: types.ErrAppInvalidStake, + }, + { + desc: "address_test: invalid stake denom", + address: appAccount.Address.String(), + stakeString: "1000invalid", + serviceIdsString: "svc1,svc2,svc3", + err: types.ErrAppInvalidStake, + }, + { + desc: "address_test: invalid stake amount (zero)", + address: appAccount.Address.String(), + stakeString: "0upokt", + serviceIdsString: "svc1,svc2,svc3", + err: types.ErrAppInvalidStake, + }, + { + desc: "address_test: invalid stake amount (negative)", + address: appAccount.Address.String(), + stakeString: "-1000upokt", + serviceIdsString: "svc1,svc2,svc3", + err: types.ErrAppInvalidStake, + }, + + // Error Paths - Service Related + { + desc: "services_test: invalid services (empty string)", + address: appAccount.Address.String(), + stakeString: "1000upokt", + serviceIdsString: "", + err: types.ErrAppInvalidServiceConfigs, }, { - desc: "stake application: invalid stake denom", - address: appAccount.Address.String(), - stakeString: "1000invalid", - err: types.ErrAppInvalidStake, + desc: "services_test: single invalid service contains spaces", + address: appAccount.Address.String(), + stakeString: "1000upokt", + serviceIdsString: "svc1 svc1_part2 svc1_part3", + err: types.ErrAppInvalidServiceConfigs, }, { - desc: "stake application: invalid stake amount (zero)", - address: appAccount.Address.String(), - stakeString: "0upokt", - err: types.ErrAppInvalidStake, + desc: "services_test: one of two services is invalid because it contains spaces", + address: appAccount.Address.String(), + stakeString: "1000upokt", + serviceIdsString: "svc1 svc1_part2,svc2", + err: types.ErrAppInvalidServiceConfigs, }, { - desc: "stake application: invalid stake amount (negative)", - address: appAccount.Address.String(), - stakeString: "-1000upokt", - err: types.ErrAppInvalidStake, + desc: "services_test: service ID is too long (8 chars is the max)", + address: appAccount.Address.String(), + stakeString: "1000upokt", + serviceIdsString: "svc1,abcdefghi", + err: types.ErrAppInvalidServiceConfigs, }, } @@ -99,6 +143,7 @@ func TestCLI_StakeApplication(t *testing.T) { // Prepare the arguments for the CLI command args := []string{ tt.stakeString, + tt.serviceIdsString, fmt.Sprintf("--%s=%s", flags.FlagFrom, tt.address), } args = append(args, commonArgs...) diff --git a/x/application/client/cli/tx_undelegate_from_gateway.go b/x/application/client/cli/tx_undelegate_from_gateway.go index 6c6bdf2a1..308a5d8a0 100644 --- a/x/application/client/cli/tx_undelegate_from_gateway.go +++ b/x/application/client/cli/tx_undelegate_from_gateway.go @@ -3,22 +3,29 @@ package cli import ( "strconv" + "github.com/pokt-network/poktroll/x/application/types" + "github.com/cosmos/cosmos-sdk/client" "github.com/cosmos/cosmos-sdk/client/flags" "github.com/cosmos/cosmos-sdk/client/tx" "github.com/spf13/cobra" - "pocket/x/application/types" ) var _ = strconv.Itoa(0) func CmdUndelegateFromGateway() *cobra.Command { cmd := &cobra.Command{ - Use: "undelegate-from-gateway", - Short: "Broadcast message undelegate-from-gateway", - Args: cobra.ExactArgs(0), + Use: "undelegate-from-gateway [gateway address]", + Short: "Undelegate an application from a gateway", + Long: `Undelegate an application from the gateway with the provided address. This is a broadcast operation +that removes the authority from the gateway specified to sign relays requests for the application, disallowing the gateway +act on the behalf of the application during a session. + +Example: +$ poktrolld --home=$(POCKETD_HOME) tx application undelegate-from-gateway $(GATEWAY_ADDR) --keyring-backend test --from $(APP) --node $(POCKET_NODE)`, + Args: cobra.ExactArgs(1), RunE: func(cmd *cobra.Command, args []string) (err error) { - + gatewayAddress := args[0] clientCtx, err := client.GetClientTxContext(cmd) if err != nil { return err @@ -26,10 +33,12 @@ func CmdUndelegateFromGateway() *cobra.Command { msg := types.NewMsgUndelegateFromGateway( clientCtx.GetFromAddress().String(), + gatewayAddress, ) if err := msg.ValidateBasic(); err != nil { return err } + return tx.GenerateOrBroadcastTxCLI(clientCtx, cmd.Flags(), msg) }, } diff --git a/x/application/client/cli/tx_undelegate_from_gateway_test.go b/x/application/client/cli/tx_undelegate_from_gateway_test.go new file mode 100644 index 000000000..a1c57973d --- /dev/null +++ b/x/application/client/cli/tx_undelegate_from_gateway_test.go @@ -0,0 +1,117 @@ +package cli_test + +import ( + "fmt" + "testing" + + sdkerrors "cosmossdk.io/errors" + sdkmath "cosmossdk.io/math" + "github.com/cosmos/cosmos-sdk/client/flags" + "github.com/cosmos/cosmos-sdk/testutil" + clitestutil "github.com/cosmos/cosmos-sdk/testutil/cli" + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/stretchr/testify/require" + "google.golang.org/grpc/status" + + "github.com/pokt-network/poktroll/testutil/network" + "github.com/pokt-network/poktroll/x/application/client/cli" + "github.com/pokt-network/poktroll/x/application/types" +) + +func TestCLI_UndelegateFromGateway(t *testing.T) { + net, _ := networkWithApplicationObjects(t, 2) + val := net.Validators[0] + ctx := val.ClientCtx + + // Create a keyring and add an account for the application to be delegated + // and the gateway to be delegated to + kr := ctx.Keyring + accounts := testutil.CreateKeyringAccounts(t, kr, 2) + appAccount := accounts[0] + gatewayAccount := accounts[1] + + // Update the context with the new keyring + ctx = ctx.WithKeyring(kr) + + // Common args used for all requests + commonArgs := []string{ + fmt.Sprintf("--%s=true", flags.FlagSkipConfirmation), + fmt.Sprintf("--%s=%s", flags.FlagBroadcastMode, flags.BroadcastSync), + fmt.Sprintf("--%s=%s", flags.FlagFees, sdk.NewCoins(sdk.NewCoin(net.Config.BondDenom, sdkmath.NewInt(10))).String()), + } + + tests := []struct { + desc string + appAddress string + gatewayAddress string + err *sdkerrors.Error + }{ + { + desc: "undelegate from gateway: valid", + appAddress: appAccount.Address.String(), + gatewayAddress: gatewayAccount.Address.String(), + }, + { + desc: "invalid - missing app address", + // appAddress: appAccount.Address.String(), + gatewayAddress: gatewayAccount.Address.String(), + err: types.ErrAppInvalidAddress, + }, + { + desc: "invalid - invalid app address", + appAddress: "invalid address", + gatewayAddress: gatewayAccount.Address.String(), + err: types.ErrAppInvalidAddress, + }, + { + desc: "invalid - missing gateway address", + appAddress: appAccount.Address.String(), + // gatewayAddress: gatewayAccount.Address.String(), + err: types.ErrAppInvalidGatewayAddress, + }, + { + desc: "invalid - invalid gateway address", + appAddress: appAccount.Address.String(), + gatewayAddress: "invalid address", + err: types.ErrAppInvalidGatewayAddress, + }, + } + + // Initialize the App and Gateway Accounts by sending it some funds from the validator account that is part of genesis + network.InitAccount(t, net, appAccount.Address) + network.InitAccount(t, net, gatewayAccount.Address) + + // Run the tests + for _, tt := range tests { + t.Run(tt.desc, func(t *testing.T) { + // Wait for a new block to be committed + require.NoError(t, net.WaitForNextBlock()) + + // Prepare the arguments for the CLI command + args := []string{ + tt.gatewayAddress, + fmt.Sprintf("--%s=%s", flags.FlagFrom, tt.appAddress), + } + args = append(args, commonArgs...) + + // Execute the command + undelegateOutput, err := clitestutil.ExecTestCLICmd(ctx, cli.CmdUndelegateFromGateway(), args) + + // Validate the error if one is expected + if tt.err != nil { + stat, ok := status.FromError(tt.err) + require.True(t, ok) + require.Contains(t, stat.Message(), tt.err.Error()) + return + } + require.NoError(t, err) + + // Check the response + var resp sdk.TxResponse + require.NoError(t, net.Config.Codec.UnmarshalJSON(undelegateOutput.Bytes(), &resp)) + require.NotNil(t, resp) + require.NotNil(t, resp.TxHash) + require.Equal(t, uint32(0), resp.Code) + }) + } +} diff --git a/x/application/client/cli/tx_unstake_application.go b/x/application/client/cli/tx_unstake_application.go index a6bb6a689..bfbf10e32 100644 --- a/x/application/client/cli/tx_unstake_application.go +++ b/x/application/client/cli/tx_unstake_application.go @@ -8,7 +8,7 @@ import ( "github.com/cosmos/cosmos-sdk/client/tx" "github.com/spf13/cobra" - "pocket/x/application/types" + "github.com/pokt-network/poktroll/x/application/types" ) var _ = strconv.Itoa(0) @@ -22,7 +22,7 @@ func CmdUnstakeApplication() *cobra.Command { the application specified by the 'from' address. Example: -$ pocketd --home=$(POCKETD_HOME) tx application unstake-application --keyring-backend test --from $(APP) --node $(POCKET_NODE)`, +$ poktrolld --home=$(POCKETD_HOME) tx application unstake-application --keyring-backend test --from $(APP) --node $(POCKET_NODE)`, Args: cobra.ExactArgs(0), RunE: func(cmd *cobra.Command, args []string) (err error) { diff --git a/x/application/client/cli/tx_unstake_application_test.go b/x/application/client/cli/tx_unstake_application_test.go index b2906e9df..2681a7d77 100644 --- a/x/application/client/cli/tx_unstake_application_test.go +++ b/x/application/client/cli/tx_unstake_application_test.go @@ -13,9 +13,9 @@ import ( "github.com/stretchr/testify/require" "google.golang.org/grpc/status" - "pocket/testutil/network" - "pocket/x/application/client/cli" - "pocket/x/application/types" + "github.com/pokt-network/poktroll/testutil/network" + "github.com/pokt-network/poktroll/x/application/client/cli" + "github.com/pokt-network/poktroll/x/application/types" ) func TestCLI_UnstakeApplication(t *testing.T) { diff --git a/x/application/genesis.go b/x/application/genesis.go index 89e0ccac9..713c9da61 100644 --- a/x/application/genesis.go +++ b/x/application/genesis.go @@ -3,8 +3,8 @@ package application import ( sdk "github.com/cosmos/cosmos-sdk/types" - "pocket/x/application/keeper" - "pocket/x/application/types" + "github.com/pokt-network/poktroll/x/application/keeper" + "github.com/pokt-network/poktroll/x/application/types" ) // InitGenesis initializes the module's state from a provided genesis state. diff --git a/x/application/genesis_test.go b/x/application/genesis_test.go index 50b2d432c..51d1f7ec9 100644 --- a/x/application/genesis_test.go +++ b/x/application/genesis_test.go @@ -6,11 +6,12 @@ import ( sdk "github.com/cosmos/cosmos-sdk/types" "github.com/stretchr/testify/require" - keepertest "pocket/testutil/keeper" - "pocket/testutil/nullify" - "pocket/testutil/sample" - "pocket/x/application" - "pocket/x/application/types" + keepertest "github.com/pokt-network/poktroll/testutil/keeper" + "github.com/pokt-network/poktroll/testutil/nullify" + "github.com/pokt-network/poktroll/testutil/sample" + "github.com/pokt-network/poktroll/x/application" + "github.com/pokt-network/poktroll/x/application/types" + sharedtypes "github.com/pokt-network/poktroll/x/shared/types" ) // Please see `x/application/types/genesis_test.go` for extensive tests related to the validity of the genesis state. @@ -21,10 +22,20 @@ func TestGenesis(t *testing.T) { { Address: sample.AccAddress(), Stake: &sdk.Coin{Denom: "upokt", Amount: sdk.NewInt(100)}, + ServiceConfigs: []*sharedtypes.ApplicationServiceConfig{ + { + ServiceId: &sharedtypes.ServiceId{Id: "svc1"}, + }, + }, }, { Address: sample.AccAddress(), Stake: &sdk.Coin{Denom: "upokt", Amount: sdk.NewInt(100)}, + ServiceConfigs: []*sharedtypes.ApplicationServiceConfig{ + { + ServiceId: &sharedtypes.ServiceId{Id: "svc2"}, + }, + }, }, }, // this line is used by starport scaffolding # genesis/test/state diff --git a/x/application/keeper/application.go b/x/application/keeper/application.go index 256328744..b95b77f9e 100644 --- a/x/application/keeper/application.go +++ b/x/application/keeper/application.go @@ -4,7 +4,7 @@ import ( "github.com/cosmos/cosmos-sdk/store/prefix" sdk "github.com/cosmos/cosmos-sdk/types" - "pocket/x/application/types" + "github.com/pokt-network/poktroll/x/application/types" ) // SetApplication set a specific application in the store from its index @@ -19,36 +19,36 @@ func (k Keeper) SetApplication(ctx sdk.Context, application types.Application) { // GetApplication returns a application from its index func (k Keeper) GetApplication( ctx sdk.Context, - address string, + appAddr string, -) (val types.Application, found bool) { +) (app types.Application, found bool) { store := prefix.NewStore(ctx.KVStore(k.storeKey), types.KeyPrefix(types.ApplicationKeyPrefix)) b := store.Get(types.ApplicationKey( - address, + appAddr, )) if b == nil { - return val, false + return app, false } - k.cdc.MustUnmarshal(b, &val) - return val, true + k.cdc.MustUnmarshal(b, &app) + return app, true } // RemoveApplication removes a application from the store func (k Keeper) RemoveApplication( ctx sdk.Context, - address string, + appAddr string, ) { store := prefix.NewStore(ctx.KVStore(k.storeKey), types.KeyPrefix(types.ApplicationKeyPrefix)) store.Delete(types.ApplicationKey( - address, + appAddr, )) } // GetAllApplication returns all application -func (k Keeper) GetAllApplication(ctx sdk.Context) (list []types.Application) { +func (k Keeper) GetAllApplication(ctx sdk.Context) (apps []types.Application) { store := prefix.NewStore(ctx.KVStore(k.storeKey), types.KeyPrefix(types.ApplicationKeyPrefix)) iterator := sdk.KVStorePrefixIterator(store, []byte{}) @@ -57,7 +57,7 @@ func (k Keeper) GetAllApplication(ctx sdk.Context) (list []types.Application) { for ; iterator.Valid(); iterator.Next() { var val types.Application k.cdc.MustUnmarshal(iterator.Value(), &val) - list = append(list, val) + apps = append(apps, val) } return diff --git a/x/application/keeper/application_test.go b/x/application/keeper/application_test.go index 76f5ad1a3..0fd7d7ea1 100644 --- a/x/application/keeper/application_test.go +++ b/x/application/keeper/application_test.go @@ -1,6 +1,7 @@ package keeper_test import ( + "fmt" "strconv" "testing" @@ -8,11 +9,13 @@ import ( authtypes "github.com/cosmos/cosmos-sdk/x/auth/types" "github.com/stretchr/testify/require" - "pocket/cmd/pocketd/cmd" - keepertest "pocket/testutil/keeper" - "pocket/testutil/nullify" - "pocket/x/application/keeper" - "pocket/x/application/types" + "github.com/pokt-network/poktroll/cmd/pocketd/cmd" + keepertest "github.com/pokt-network/poktroll/testutil/keeper" + "github.com/pokt-network/poktroll/testutil/nullify" + "github.com/pokt-network/poktroll/testutil/sample" + "github.com/pokt-network/poktroll/x/application/keeper" + "github.com/pokt-network/poktroll/x/application/types" + sharedtypes "github.com/pokt-network/poktroll/x/shared/types" ) // Prevent strconv unused error @@ -23,38 +26,44 @@ func init() { } func createNApplication(keeper *keeper.Keeper, ctx sdk.Context, n int) []types.Application { - items := make([]types.Application, n) - for i := range items { - items[i].Address = strconv.Itoa(i) - - keeper.SetApplication(ctx, items[i]) + apps := make([]types.Application, n) + for i := range apps { + app := &apps[i] + app.Address = sample.AccAddress() + app.Stake = &sdk.Coin{Denom: "upokt", Amount: sdk.NewInt(int64(i))} + app.ServiceConfigs = []*sharedtypes.ApplicationServiceConfig{ + { + ServiceId: &sharedtypes.ServiceId{Id: fmt.Sprintf("svc%d", i)}, + }, + } + keeper.SetApplication(ctx, *app) } - return items + return apps } func TestApplicationGet(t *testing.T) { keeper, ctx := keepertest.ApplicationKeeper(t) - items := createNApplication(keeper, ctx, 10) - for _, item := range items { - rst, found := keeper.GetApplication(ctx, - item.Address, + apps := createNApplication(keeper, ctx, 10) + for _, app := range apps { + appFound, isAppFound := keeper.GetApplication(ctx, + app.Address, ) - require.True(t, found) + require.True(t, isAppFound) require.Equal(t, - nullify.Fill(&item), - nullify.Fill(&rst), + nullify.Fill(&app), + nullify.Fill(&appFound), ) } } func TestApplicationRemove(t *testing.T) { keeper, ctx := keepertest.ApplicationKeeper(t) - items := createNApplication(keeper, ctx, 10) - for _, item := range items { + apps := createNApplication(keeper, ctx, 10) + for _, app := range apps { keeper.RemoveApplication(ctx, - item.Address, + app.Address, ) _, found := keeper.GetApplication(ctx, - item.Address, + app.Address, ) require.False(t, found) } @@ -62,9 +71,9 @@ func TestApplicationRemove(t *testing.T) { func TestApplicationGetAll(t *testing.T) { keeper, ctx := keepertest.ApplicationKeeper(t) - items := createNApplication(keeper, ctx, 10) + apps := createNApplication(keeper, ctx, 10) require.ElementsMatch(t, - nullify.Fill(items), + nullify.Fill(apps), nullify.Fill(keeper.GetAllApplication(ctx)), ) } diff --git a/x/application/keeper/keeper.go b/x/application/keeper/keeper.go index d115dddd5..38ac34db4 100644 --- a/x/application/keeper/keeper.go +++ b/x/application/keeper/keeper.go @@ -9,7 +9,7 @@ import ( sdk "github.com/cosmos/cosmos-sdk/types" paramtypes "github.com/cosmos/cosmos-sdk/x/params/types" - "pocket/x/application/types" + "github.com/pokt-network/poktroll/x/application/types" ) type ( @@ -19,7 +19,9 @@ type ( memKey storetypes.StoreKey paramstore paramtypes.Subspace - bankKeeper types.BankKeeper + bankKeeper types.BankKeeper + accountKeeper types.AccountKeeper + gatewayKeeper types.GatewayKeeper } ) @@ -30,6 +32,8 @@ func NewKeeper( ps paramtypes.Subspace, bankKeeper types.BankKeeper, + accountKeeper types.AccountKeeper, + gatewayKeeper types.GatewayKeeper, ) *Keeper { // set KeyTable if it has not already been set if !ps.HasKeyTable() { @@ -42,7 +46,9 @@ func NewKeeper( memKey: memKey, paramstore: ps, - bankKeeper: bankKeeper, + bankKeeper: bankKeeper, + accountKeeper: accountKeeper, + gatewayKeeper: gatewayKeeper, } } diff --git a/x/application/keeper/msg_server.go b/x/application/keeper/msg_server.go index 4772c9c72..8f740e15c 100644 --- a/x/application/keeper/msg_server.go +++ b/x/application/keeper/msg_server.go @@ -1,7 +1,7 @@ package keeper import ( - "pocket/x/application/types" + "github.com/pokt-network/poktroll/x/application/types" ) type msgServer struct { diff --git a/x/application/keeper/msg_server_delegate_to_gateway.go b/x/application/keeper/msg_server_delegate_to_gateway.go index 732bd5a4e..a50523905 100644 --- a/x/application/keeper/msg_server_delegate_to_gateway.go +++ b/x/application/keeper/msg_server_delegate_to_gateway.go @@ -3,20 +3,59 @@ package keeper import ( "context" - sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/pokt-network/poktroll/x/application/types" - "pocket/x/application/types" + sdkerrors "cosmossdk.io/errors" + sdk "github.com/cosmos/cosmos-sdk/types" ) func (k msgServer) DelegateToGateway(goCtx context.Context, msg *types.MsgDelegateToGateway) (*types.MsgDelegateToGatewayResponse, error) { ctx := sdk.UnwrapSDKContext(goCtx) + logger := k.Logger(ctx).With("method", "DelegateToGateway") + logger.Info("About to delegate application to gateway with msg: %v", msg) + if err := msg.ValidateBasic(); err != nil { + logger.Error("Delegation Message failed basic validation: %v", err) return nil, err } - // TODO: Handling the message - _ = ctx + // Retrieve the application from the store + app, found := k.GetApplication(ctx, msg.AppAddress) + if !found { + logger.Info("Application not found with address [%s]", msg.AppAddress) + return nil, sdkerrors.Wrapf(types.ErrAppNotFound, "application not found with address: %s", msg.AppAddress) + } + logger.Info("Application found with address [%s]", msg.AppAddress) + + // Check if the gateway is staked + if _, found := k.gatewayKeeper.GetGateway(ctx, msg.GatewayAddress); !found { + logger.Info("Gateway not found with address [%s]", msg.GatewayAddress) + return nil, sdkerrors.Wrapf(types.ErrAppGatewayNotFound, "gateway not found with address: %s", msg.GatewayAddress) + } + + // Ensure the application is not already delegated to the maximum number of gateways + maxDelegatedParam := k.GetParams(ctx).MaxDelegatedGateways + if int64(len(app.DelegateeGatewayAddresses)) >= maxDelegatedParam { + logger.Info("Application already delegated to maximum number of gateways: %d", maxDelegatedParam) + return nil, sdkerrors.Wrapf(types.ErrAppMaxDelegatedGateways, "application already delegated to %d gateways", maxDelegatedParam) + } + + // Check if the application is already delegated to the gateway + for _, gatewayAddr := range app.DelegateeGatewayAddresses { + if gatewayAddr == msg.GatewayAddress { + logger.Info("Application already delegated to gateway with address [%s]", msg.GatewayAddress) + return nil, sdkerrors.Wrapf(types.ErrAppAlreadyDelegated, "application already delegated to gateway with address: %s", msg.GatewayAddress) + } + } + + // Update the application with the new delegatee public key + app.DelegateeGatewayAddresses = append(app.DelegateeGatewayAddresses, msg.GatewayAddress) + logger.Info("Successfully added delegatee public key to application") + + // Update the application store with the new delegation + k.SetApplication(ctx, app) + logger.Info("Successfully delegated application to gateway for app: %+v", app) return &types.MsgDelegateToGatewayResponse{}, nil } diff --git a/x/application/keeper/msg_server_delegate_to_gateway_test.go b/x/application/keeper/msg_server_delegate_to_gateway_test.go new file mode 100644 index 000000000..2e48edf5f --- /dev/null +++ b/x/application/keeper/msg_server_delegate_to_gateway_test.go @@ -0,0 +1,252 @@ +package keeper_test + +import ( + "testing" + + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/stretchr/testify/require" + + keepertest "github.com/pokt-network/poktroll/testutil/keeper" + "github.com/pokt-network/poktroll/testutil/sample" + "github.com/pokt-network/poktroll/x/application/keeper" + "github.com/pokt-network/poktroll/x/application/types" + sharedtypes "github.com/pokt-network/poktroll/x/shared/types" +) + +func TestMsgServer_DelegateToGateway_SuccessfullyDelegate(t *testing.T) { + k, ctx := keepertest.ApplicationKeeper(t) + srv := keeper.NewMsgServerImpl(*k) + wctx := sdk.WrapSDKContext(ctx) + + // Generate an address for the application and gateways + appAddr := sample.AccAddress() + gatewayAddr1 := sample.AccAddress() + gatewayAddr2 := sample.AccAddress() + // Mock the gateway being staked via the staked gateway map + keepertest.StakedGatewayMap[gatewayAddr1] = struct{}{} + keepertest.StakedGatewayMap[gatewayAddr2] = struct{}{} + t.Cleanup(func() { + delete(keepertest.StakedGatewayMap, gatewayAddr1) + delete(keepertest.StakedGatewayMap, gatewayAddr2) + }) + + // Prepare the application + stakeMsg := &types.MsgStakeApplication{ + Address: appAddr, + Stake: &sdk.Coin{Denom: "upokt", Amount: sdk.NewInt(100)}, + Services: []*sharedtypes.ApplicationServiceConfig{ + { + ServiceId: &sharedtypes.ServiceId{Id: "svc1"}, + }, + }, + } + + // Stake the application & verify that the application exists + _, err := srv.StakeApplication(wctx, stakeMsg) + require.NoError(t, err) + _, isAppFound := k.GetApplication(ctx, appAddr) + require.True(t, isAppFound) + + // Prepare the delegation message + delegateMsg := &types.MsgDelegateToGateway{ + AppAddress: appAddr, + GatewayAddress: gatewayAddr1, + } + + // Delegate the application to the gateway + _, err = srv.DelegateToGateway(wctx, delegateMsg) + require.NoError(t, err) + + // Verify that the application exists + foundApp, isAppFound := k.GetApplication(ctx, appAddr) + require.True(t, isAppFound) + require.Equal(t, appAddr, foundApp.Address) + require.Equal(t, 1, len(foundApp.DelegateeGatewayAddresses)) + require.Equal(t, gatewayAddr1, foundApp.DelegateeGatewayAddresses[0]) + + // Prepare a second delegation message + delegateMsg2 := &types.MsgDelegateToGateway{ + AppAddress: appAddr, + GatewayAddress: gatewayAddr2, + } + + // Delegate the application to the second gateway + _, err = srv.DelegateToGateway(wctx, delegateMsg2) + require.NoError(t, err) + foundApp, isAppFound = k.GetApplication(ctx, appAddr) + require.True(t, isAppFound) + require.Equal(t, 2, len(foundApp.DelegateeGatewayAddresses)) + require.Equal(t, gatewayAddr1, foundApp.DelegateeGatewayAddresses[0]) + require.Equal(t, gatewayAddr2, foundApp.DelegateeGatewayAddresses[1]) +} + +func TestMsgServer_DelegateToGateway_FailDuplicate(t *testing.T) { + k, ctx := keepertest.ApplicationKeeper(t) + srv := keeper.NewMsgServerImpl(*k) + wctx := sdk.WrapSDKContext(ctx) + + // Generate an address for the application and gateway + appAddr := sample.AccAddress() + gatewayAddr := sample.AccAddress() + // Mock the gateway being staked via the staked gateway map + keepertest.StakedGatewayMap[gatewayAddr] = struct{}{} + t.Cleanup(func() { + delete(keepertest.StakedGatewayMap, gatewayAddr) + }) + + // Prepare the application + stakeMsg := &types.MsgStakeApplication{ + Address: appAddr, + Stake: &sdk.Coin{Denom: "upokt", Amount: sdk.NewInt(100)}, + Services: []*sharedtypes.ApplicationServiceConfig{ + { + ServiceId: &sharedtypes.ServiceId{Id: "svc1"}, + }, + }, + } + + // Stake the application & verify that the application exists + _, err := srv.StakeApplication(wctx, stakeMsg) + require.NoError(t, err) + _, isAppFound := k.GetApplication(ctx, appAddr) + require.True(t, isAppFound) + + // Prepare the delegation message + delegateMsg := &types.MsgDelegateToGateway{ + AppAddress: appAddr, + GatewayAddress: gatewayAddr, + } + + // Delegate the application to the gateway + _, err = srv.DelegateToGateway(wctx, delegateMsg) + require.NoError(t, err) + + // Verify that the application exists + foundApp, isAppFound := k.GetApplication(ctx, appAddr) + require.True(t, isAppFound) + require.Equal(t, appAddr, foundApp.Address) + require.Equal(t, 1, len(foundApp.DelegateeGatewayAddresses)) + require.Equal(t, gatewayAddr, foundApp.DelegateeGatewayAddresses[0]) + + // Prepare a second delegation message + delegateMsg2 := &types.MsgDelegateToGateway{ + AppAddress: appAddr, + GatewayAddress: gatewayAddr, + } + + // Attempt to delegate the application to the gateway again + _, err = srv.DelegateToGateway(wctx, delegateMsg2) + require.ErrorIs(t, err, types.ErrAppAlreadyDelegated) + foundApp, isAppFound = k.GetApplication(ctx, appAddr) + require.True(t, isAppFound) + require.Equal(t, 1, len(foundApp.DelegateeGatewayAddresses)) + require.Equal(t, gatewayAddr, foundApp.DelegateeGatewayAddresses[0]) +} + +func TestMsgServer_DelegateToGateway_FailGatewayNotStaked(t *testing.T) { + k, ctx := keepertest.ApplicationKeeper(t) + srv := keeper.NewMsgServerImpl(*k) + wctx := sdk.WrapSDKContext(ctx) + + // Generate an address for the application and gateway + appAddr := sample.AccAddress() + gatewayAddr := sample.AccAddress() + + // Prepare the application + stakeMsg := &types.MsgStakeApplication{ + Address: appAddr, + Stake: &sdk.Coin{Denom: "upokt", Amount: sdk.NewInt(100)}, + Services: []*sharedtypes.ApplicationServiceConfig{ + { + ServiceId: &sharedtypes.ServiceId{Id: "svc1"}, + }, + }, + } + + // Stake the application & verify that the application exists + _, err := srv.StakeApplication(wctx, stakeMsg) + require.NoError(t, err) + _, isAppFound := k.GetApplication(ctx, appAddr) + require.True(t, isAppFound) + + // Prepare the delegation message + delegateMsg := &types.MsgDelegateToGateway{ + AppAddress: appAddr, + GatewayAddress: gatewayAddr, + } + + // Attempt to delegate the application to the unstaked gateway + _, err = srv.DelegateToGateway(wctx, delegateMsg) + require.ErrorIs(t, err, types.ErrAppGatewayNotFound) + foundApp, isAppFound := k.GetApplication(ctx, appAddr) + require.True(t, isAppFound) + require.Equal(t, 0, len(foundApp.DelegateeGatewayAddresses)) +} + +func TestMsgServer_DelegateToGateway_FailMaxReached(t *testing.T) { + k, ctx := keepertest.ApplicationKeeper(t) + srv := keeper.NewMsgServerImpl(*k) + wctx := sdk.WrapSDKContext(ctx) + + // Generate an address for the application and gateway + appAddr := sample.AccAddress() + gatewayAddr := sample.AccAddress() + // Mock the gateway being staked via the staked gateway map + keepertest.StakedGatewayMap[gatewayAddr] = struct{}{} + t.Cleanup(func() { + delete(keepertest.StakedGatewayMap, gatewayAddr) + }) + + // Prepare the application + stakeMsg := &types.MsgStakeApplication{ + Address: appAddr, + Stake: &sdk.Coin{Denom: "upokt", Amount: sdk.NewInt(100)}, + Services: []*sharedtypes.ApplicationServiceConfig{ + { + ServiceId: &sharedtypes.ServiceId{Id: "svc1"}, + }, + }, + } + + // Stake the application & verify that the application exists + _, err := srv.StakeApplication(wctx, stakeMsg) + require.NoError(t, err) + _, isAppFound := k.GetApplication(ctx, appAddr) + require.True(t, isAppFound) + + // Prepare the delegation message + delegateMsg := &types.MsgDelegateToGateway{ + AppAddress: appAddr, + GatewayAddress: gatewayAddr, + } + + // Delegate the application to the max number of gateways + maxDelegatedParam := k.GetParams(ctx).MaxDelegatedGateways + for i := int64(0); i < k.GetParams(ctx).MaxDelegatedGateways; i++ { + // Prepare the delegation message + gatewayAddr := sample.AccAddress() + // Mock the gateway being staked via the staked gateway map + keepertest.StakedGatewayMap[gatewayAddr] = struct{}{} + t.Cleanup(func() { + delete(keepertest.StakedGatewayMap, gatewayAddr) + }) + delegateMsg := &types.MsgDelegateToGateway{ + AppAddress: appAddr, + GatewayAddress: gatewayAddr, + } + // Delegate the application to the gateway + _, err = srv.DelegateToGateway(wctx, delegateMsg) + require.NoError(t, err) + // Check number of gateways delegated to is correct + foundApp, isAppFound := k.GetApplication(ctx, appAddr) + require.True(t, isAppFound) + require.Equal(t, int(i+1), len(foundApp.DelegateeGatewayAddresses)) + } + + // Attempt to delegate the application when the max is already reached + _, err = srv.DelegateToGateway(wctx, delegateMsg) + require.ErrorIs(t, err, types.ErrAppMaxDelegatedGateways) + foundApp, isAppFound := k.GetApplication(ctx, appAddr) + require.True(t, isAppFound) + require.Equal(t, maxDelegatedParam, int64(len(foundApp.DelegateeGatewayAddresses))) +} diff --git a/x/application/keeper/msg_server_stake_application.go b/x/application/keeper/msg_server_stake_application.go index 7fd06a529..cc735919b 100644 --- a/x/application/keeper/msg_server_stake_application.go +++ b/x/application/keeper/msg_server_stake_application.go @@ -6,7 +6,7 @@ import ( sdkerrors "cosmossdk.io/errors" sdk "github.com/cosmos/cosmos-sdk/types" - "pocket/x/application/types" + "github.com/pokt-network/poktroll/x/application/types" ) func (k msgServer) StakeApplication( @@ -19,6 +19,7 @@ func (k msgServer) StakeApplication( logger.Info("About to stake application with msg: %v", msg) if err := msg.ValidateBasic(); err != nil { + logger.Error("invalid MsgStakeApplication: %v", err) return nil, err } @@ -46,6 +47,7 @@ func (k msgServer) StakeApplication( return nil, err } + // TODO_IMPROVE: Should we avoid making this call if `coinsToDelegate` = 0? // Send the coins from the application to the staked application pool err = k.bankKeeper.DelegateCoinsFromAccountToModule(ctx, appAddress, types.ModuleName, []sdk.Coin{coinsToDelegate}) if err != nil { @@ -65,8 +67,10 @@ func (k msgServer) createApplication( msg *types.MsgStakeApplication, ) types.Application { return types.Application{ - Address: msg.Address, - Stake: msg.Stake, + Address: msg.Address, + Stake: msg.Stake, + ServiceConfigs: msg.Services, + DelegateeGatewayAddresses: make([]string, 0), } } @@ -80,16 +84,21 @@ func (k msgServer) updateApplication( return sdkerrors.Wrapf(types.ErrAppUnauthorized, "msg Address (%s) != application address (%s)", msg.Address, app.Address) } + // Validate that the stake is not being lowered if msg.Stake == nil { return sdkerrors.Wrapf(types.ErrAppInvalidStake, "stake amount cannot be nil") } - if msg.Stake.IsLTE(*app.Stake) { - return sdkerrors.Wrapf(types.ErrAppInvalidStake, "stake amount %v must be higher than previous stake amount %v", msg.Stake, app.Stake) } - app.Stake = msg.Stake + // Validate that the service configs maintain at least one service. + // Additional validation is done in `msg.ValidateBasic` above. + if len(msg.Services) == 0 { + return sdkerrors.Wrapf(types.ErrAppInvalidServiceConfigs, "must have at least one service") + } + app.ServiceConfigs = msg.Services + return nil } diff --git a/x/application/keeper/msg_server_stake_application_test.go b/x/application/keeper/msg_server_stake_application_test.go index 0a4747957..3b67cd20c 100644 --- a/x/application/keeper/msg_server_stake_application_test.go +++ b/x/application/keeper/msg_server_stake_application_test.go @@ -6,10 +6,11 @@ import ( sdk "github.com/cosmos/cosmos-sdk/types" "github.com/stretchr/testify/require" - keepertest "pocket/testutil/keeper" - "pocket/testutil/sample" - "pocket/x/application/keeper" - "pocket/x/application/types" + keepertest "github.com/pokt-network/poktroll/testutil/keeper" + "github.com/pokt-network/poktroll/testutil/sample" + "github.com/pokt-network/poktroll/x/application/keeper" + "github.com/pokt-network/poktroll/x/application/types" + sharedtypes "github.com/pokt-network/poktroll/x/shared/types" ) func TestMsgServer_StakeApplication_SuccessfulCreateAndUpdate(t *testing.T) { @@ -28,6 +29,11 @@ func TestMsgServer_StakeApplication_SuccessfulCreateAndUpdate(t *testing.T) { stakeMsg := &types.MsgStakeApplication{ Address: addr, Stake: &sdk.Coin{Denom: "upokt", Amount: sdk.NewInt(100)}, + Services: []*sharedtypes.ApplicationServiceConfig{ + { + ServiceId: &sharedtypes.ServiceId{Id: "svc1"}, + }, + }, } // Stake the application @@ -35,23 +41,99 @@ func TestMsgServer_StakeApplication_SuccessfulCreateAndUpdate(t *testing.T) { require.NoError(t, err) // Verify that the application exists - foundApp, isAppFound := k.GetApplication(ctx, addr) + appFound, isAppFound := k.GetApplication(ctx, addr) require.True(t, isAppFound) - require.Equal(t, addr, foundApp.Address) - require.Equal(t, int64(100), foundApp.Stake.Amount.Int64()) + require.Equal(t, addr, appFound.Address) + require.Equal(t, int64(100), appFound.Stake.Amount.Int64()) + require.Len(t, appFound.ServiceConfigs, 1) + require.Equal(t, "svc1", appFound.ServiceConfigs[0].ServiceId.Id) - // Prepare an updated application with a higher stake + // Prepare an updated application with a higher stake and another service updateStakeMsg := &types.MsgStakeApplication{ Address: addr, Stake: &sdk.Coin{Denom: "upokt", Amount: sdk.NewInt(200)}, + Services: []*sharedtypes.ApplicationServiceConfig{ + { + ServiceId: &sharedtypes.ServiceId{Id: "svc1"}, + }, + { + ServiceId: &sharedtypes.ServiceId{Id: "svc2"}, + }, + }, } // Update the staked application _, err = srv.StakeApplication(wctx, updateStakeMsg) require.NoError(t, err) - foundApp, isAppFound = k.GetApplication(ctx, addr) + appFound, isAppFound = k.GetApplication(ctx, addr) + require.True(t, isAppFound) + require.Equal(t, int64(200), appFound.Stake.Amount.Int64()) + require.Len(t, appFound.ServiceConfigs, 2) + require.Equal(t, "svc1", appFound.ServiceConfigs[0].ServiceId.Id) + require.Equal(t, "svc2", appFound.ServiceConfigs[1].ServiceId.Id) +} + +func TestMsgServer_StakeApplication_FailRestakingDueToInvalidServices(t *testing.T) { + k, ctx := keepertest.ApplicationKeeper(t) + srv := keeper.NewMsgServerImpl(*k) + wctx := sdk.WrapSDKContext(ctx) + + appAddr := sample.AccAddress() + + // Prepare the application stake message + stakeMsg := &types.MsgStakeApplication{ + Address: appAddr, + Stake: &sdk.Coin{Denom: "upokt", Amount: sdk.NewInt(100)}, + Services: []*sharedtypes.ApplicationServiceConfig{ + { + ServiceId: &sharedtypes.ServiceId{Id: "svc1"}, + }, + }, + } + + // Stake the application + _, err := srv.StakeApplication(wctx, stakeMsg) + require.NoError(t, err) + + // Prepare the application stake message without any services + updateStakeMsg := &types.MsgStakeApplication{ + Address: appAddr, + Stake: &sdk.Coin{Denom: "upokt", Amount: sdk.NewInt(100)}, + Services: []*sharedtypes.ApplicationServiceConfig{}, + } + + // Fail updating the application when the list of services is empty + _, err = srv.StakeApplication(wctx, updateStakeMsg) + require.Error(t, err) + + // Verify the app still exists and is staked for svc1 + app, isAppFound := k.GetApplication(ctx, appAddr) + require.True(t, isAppFound) + require.Equal(t, appAddr, app.Address) + require.Len(t, app.ServiceConfigs, 1) + require.Equal(t, "svc1", app.ServiceConfigs[0].ServiceId.Id) + + // Prepare the application stake message with an invalid service ID + updateStakeMsg = &types.MsgStakeApplication{ + Address: appAddr, + Stake: &sdk.Coin{Denom: "upokt", Amount: sdk.NewInt(100)}, + Services: []*sharedtypes.ApplicationServiceConfig{ + { + ServiceId: &sharedtypes.ServiceId{Id: "svc1 INVALID ! & *"}, + }, + }, + } + + // Fail updating the application when the list of services is empty + _, err = srv.StakeApplication(wctx, updateStakeMsg) + require.Error(t, err) + + // Verify the app still exists and is staked for svc1 + app, isAppFound = k.GetApplication(ctx, appAddr) require.True(t, isAppFound) - require.Equal(t, int64(200), foundApp.Stake.Amount.Int64()) + require.Equal(t, appAddr, app.Address) + require.Len(t, app.ServiceConfigs, 1) + require.Equal(t, "svc1", app.ServiceConfigs[0].ServiceId.Id) } func TestMsgServer_StakeApplication_FailLoweringStake(t *testing.T) { @@ -64,6 +146,11 @@ func TestMsgServer_StakeApplication_FailLoweringStake(t *testing.T) { stakeMsg := &types.MsgStakeApplication{ Address: addr, Stake: &sdk.Coin{Denom: "upokt", Amount: sdk.NewInt(100)}, + Services: []*sharedtypes.ApplicationServiceConfig{ + { + ServiceId: &sharedtypes.ServiceId{Id: "svc1"}, + }, + }, } // Stake the application & verify that the application exists @@ -76,6 +163,11 @@ func TestMsgServer_StakeApplication_FailLoweringStake(t *testing.T) { updateMsg := &types.MsgStakeApplication{ Address: addr, Stake: &sdk.Coin{Denom: "upokt", Amount: sdk.NewInt(50)}, + Services: []*sharedtypes.ApplicationServiceConfig{ + { + ServiceId: &sharedtypes.ServiceId{Id: "svc1"}, + }, + }, } // Verify that it fails diff --git a/x/application/keeper/msg_server_test.go b/x/application/keeper/msg_server_test.go index a3a8d787d..cc6a8f16f 100644 --- a/x/application/keeper/msg_server_test.go +++ b/x/application/keeper/msg_server_test.go @@ -7,9 +7,9 @@ import ( sdk "github.com/cosmos/cosmos-sdk/types" "github.com/stretchr/testify/require" - keepertest "pocket/testutil/keeper" - "pocket/x/application/keeper" - "pocket/x/application/types" + keepertest "github.com/pokt-network/poktroll/testutil/keeper" + "github.com/pokt-network/poktroll/x/application/keeper" + "github.com/pokt-network/poktroll/x/application/types" ) func setupMsgServer(t testing.TB) (types.MsgServer, context.Context) { diff --git a/x/application/keeper/msg_server_undelegate_from_gateway.go b/x/application/keeper/msg_server_undelegate_from_gateway.go index f4b1d45d9..a1239d7ef 100644 --- a/x/application/keeper/msg_server_undelegate_from_gateway.go +++ b/x/application/keeper/msg_server_undelegate_from_gateway.go @@ -3,20 +3,49 @@ package keeper import ( "context" + sdkerrors "cosmossdk.io/errors" sdk "github.com/cosmos/cosmos-sdk/types" - "pocket/x/application/types" + "github.com/pokt-network/poktroll/x/application/types" ) func (k msgServer) UndelegateFromGateway(goCtx context.Context, msg *types.MsgUndelegateFromGateway) (*types.MsgUndelegateFromGatewayResponse, error) { ctx := sdk.UnwrapSDKContext(goCtx) + logger := k.Logger(ctx).With("method", "UndelegateFromGateway") + logger.Info("About to undelegate application from gateway with msg: %v", msg) + if err := msg.ValidateBasic(); err != nil { + logger.Error("Undelegation Message failed basic validation: %v", err) return nil, err } - // TODO: Handling the message - _ = ctx + // Retrieve the application from the store + app, found := k.GetApplication(ctx, msg.AppAddress) + if !found { + logger.Info("Application not found with address [%s]", msg.AppAddress) + return nil, sdkerrors.Wrapf(types.ErrAppNotFound, "application not found with address: %s", msg.AppAddress) + } + logger.Info("Application found with address [%s]", msg.AppAddress) + + // Check if the application is already delegated to the gateway + foundIdx := -1 + for i, gatewayAddr := range app.DelegateeGatewayAddresses { + if gatewayAddr == msg.GatewayAddress { + foundIdx = i + } + } + if foundIdx == -1 { + logger.Info("Application not delegated to gateway with address [%s]", msg.GatewayAddress) + return nil, sdkerrors.Wrapf(types.ErrAppNotDelegated, "application not delegated to gateway with address: %s", msg.GatewayAddress) + } + + // Remove the gateway from the application's delegatee gateway public keys + app.DelegateeGatewayAddresses = append(app.DelegateeGatewayAddresses[:foundIdx], app.DelegateeGatewayAddresses[foundIdx+1:]...) + + // Update the application store with the new delegation + k.SetApplication(ctx, app) + logger.Info("Successfully undelegated application from gateway for app: %+v", app) return &types.MsgUndelegateFromGatewayResponse{}, nil } diff --git a/x/application/keeper/msg_server_undelegate_from_gateway_test.go b/x/application/keeper/msg_server_undelegate_from_gateway_test.go new file mode 100644 index 000000000..30824a6d2 --- /dev/null +++ b/x/application/keeper/msg_server_undelegate_from_gateway_test.go @@ -0,0 +1,220 @@ +package keeper_test + +import ( + "testing" + + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/stretchr/testify/require" + + keepertest "github.com/pokt-network/poktroll/testutil/keeper" + "github.com/pokt-network/poktroll/testutil/sample" + "github.com/pokt-network/poktroll/x/application/keeper" + "github.com/pokt-network/poktroll/x/application/types" + sharedtypes "github.com/pokt-network/poktroll/x/shared/types" +) + +func TestMsgServer_UndelegateFromGateway_SuccessfullyUndelegate(t *testing.T) { + k, ctx := keepertest.ApplicationKeeper(t) + srv := keeper.NewMsgServerImpl(*k) + wctx := sdk.WrapSDKContext(ctx) + + // Generate an address for the application and gateways + appAddr := sample.AccAddress() + gatewayAddresses := make([]string, int(k.GetParams(ctx).MaxDelegatedGateways)) + for i := 0; i < len(gatewayAddresses); i++ { + gatewayAddr := sample.AccAddress() + // Mock the gateway being staked via the staked gateway map + keepertest.StakedGatewayMap[gatewayAddr] = struct{}{} + gatewayAddresses[i] = gatewayAddr + } + t.Cleanup(func() { + for _, gatewayAddr := range gatewayAddresses { + delete(keepertest.StakedGatewayMap, gatewayAddr) + } + }) + + // Prepare the application + stakeMsg := &types.MsgStakeApplication{ + Address: appAddr, + Stake: &sdk.Coin{Denom: "upokt", Amount: sdk.NewInt(100)}, + Services: []*sharedtypes.ApplicationServiceConfig{ + { + ServiceId: &sharedtypes.ServiceId{Id: "svc1"}, + }, + }, + } + + // Stake the application & verify that the application exists + _, err := srv.StakeApplication(wctx, stakeMsg) + require.NoError(t, err) + _, isAppFound := k.GetApplication(ctx, appAddr) + require.True(t, isAppFound) + + // Prepare the delegation messages and delegate the application to the gateways + for _, gatewayAddr := range gatewayAddresses { + delegateMsg := &types.MsgDelegateToGateway{ + AppAddress: appAddr, + GatewayAddress: gatewayAddr, + } + // Delegate the application to the gateway + _, err = srv.DelegateToGateway(wctx, delegateMsg) + require.NoError(t, err) + } + + // Verify that the application exists + maxDelegatedGateways := k.GetParams(ctx).MaxDelegatedGateways + foundApp, isAppFound := k.GetApplication(ctx, appAddr) + require.True(t, isAppFound) + require.Equal(t, appAddr, foundApp.Address) + require.Equal(t, maxDelegatedGateways, int64(len(foundApp.DelegateeGatewayAddresses))) + for i, gatewayAddr := range gatewayAddresses { + require.Equal(t, gatewayAddr, foundApp.DelegateeGatewayAddresses[i]) + } + + // Prepare an undelegation message + undelegateMsg := &types.MsgUndelegateFromGateway{ + AppAddress: appAddr, + GatewayAddress: gatewayAddresses[3], + } + + // Undelegate the application from the gateway + _, err = srv.UndelegateFromGateway(wctx, undelegateMsg) + require.NoError(t, err) + foundApp, isAppFound = k.GetApplication(ctx, appAddr) + require.True(t, isAppFound) + require.Equal(t, appAddr, foundApp.Address) + require.Equal(t, maxDelegatedGateways-1, int64(len(foundApp.DelegateeGatewayAddresses))) + gatewayAddresses = append(gatewayAddresses[:3], gatewayAddresses[4:]...) + for i, gatewayAddr := range gatewayAddresses { + require.Equal(t, gatewayAddr, foundApp.DelegateeGatewayAddresses[i]) + } +} + +func TestMsgServer_UndelegateFromGateway_FailNotDelegated(t *testing.T) { + k, ctx := keepertest.ApplicationKeeper(t) + srv := keeper.NewMsgServerImpl(*k) + wctx := sdk.WrapSDKContext(ctx) + + // Generate an address for the application and gateway + appAddr := sample.AccAddress() + gatewayAddr1 := sample.AccAddress() + gatewayAddr2 := sample.AccAddress() + // Mock the gateway being staked via the staked gateway map + keepertest.StakedGatewayMap[gatewayAddr1] = struct{}{} + keepertest.StakedGatewayMap[gatewayAddr2] = struct{}{} + t.Cleanup(func() { + delete(keepertest.StakedGatewayMap, gatewayAddr1) + delete(keepertest.StakedGatewayMap, gatewayAddr2) + }) + + // Prepare the application + stakeMsg := &types.MsgStakeApplication{ + Address: appAddr, + Stake: &sdk.Coin{Denom: "upokt", Amount: sdk.NewInt(100)}, + Services: []*sharedtypes.ApplicationServiceConfig{ + { + ServiceId: &sharedtypes.ServiceId{Id: "svc1"}, + }, + }, + } + + // Stake the application & verify that the application exists + _, err := srv.StakeApplication(wctx, stakeMsg) + require.NoError(t, err) + _, isAppFound := k.GetApplication(ctx, appAddr) + require.True(t, isAppFound) + + // Prepare the undelegation message + undelegateMsg := &types.MsgUndelegateFromGateway{ + AppAddress: appAddr, + GatewayAddress: gatewayAddr1, + } + + // Attempt to undelgate the application from the gateway + _, err = srv.UndelegateFromGateway(wctx, undelegateMsg) + require.ErrorIs(t, err, types.ErrAppNotDelegated) + foundApp, isAppFound := k.GetApplication(ctx, appAddr) + require.True(t, isAppFound) + require.Equal(t, appAddr, foundApp.Address) + require.Equal(t, 0, len(foundApp.DelegateeGatewayAddresses)) + + // Prepare a delegation message + delegateMsg := &types.MsgDelegateToGateway{ + AppAddress: appAddr, + GatewayAddress: gatewayAddr2, + } + + // Delegate the application to the gateway + _, err = srv.DelegateToGateway(wctx, delegateMsg) + require.NoError(t, err) + + // Ensure the failed undelegation did not affect the application + _, err = srv.UndelegateFromGateway(wctx, undelegateMsg) + require.ErrorIs(t, err, types.ErrAppNotDelegated) + foundApp, isAppFound = k.GetApplication(ctx, appAddr) + require.True(t, isAppFound) + require.Equal(t, 1, len(foundApp.DelegateeGatewayAddresses)) + require.Equal(t, gatewayAddr2, foundApp.DelegateeGatewayAddresses[0]) +} + +func TestMsgServer_UndelegateFromGateway_SuccessfullyUndelegateFromUnstakedGateway(t *testing.T) { + k, ctx := keepertest.ApplicationKeeper(t) + srv := keeper.NewMsgServerImpl(*k) + wctx := sdk.WrapSDKContext(ctx) + + // Generate an address for the application and gateways + appAddr := sample.AccAddress() + gatewayAddr := sample.AccAddress() + // Mock the gateway being staked via the staked gateway map + keepertest.StakedGatewayMap[gatewayAddr] = struct{}{} + + // Prepare the application + stakeMsg := &types.MsgStakeApplication{ + Address: appAddr, + Stake: &sdk.Coin{Denom: "upokt", Amount: sdk.NewInt(100)}, + Services: []*sharedtypes.ApplicationServiceConfig{ + { + ServiceId: &sharedtypes.ServiceId{Id: "svc1"}, + }, + }, + } + + // Stake the application & verify that the application exists + _, err := srv.StakeApplication(wctx, stakeMsg) + require.NoError(t, err) + _, isAppFound := k.GetApplication(ctx, appAddr) + require.True(t, isAppFound) + + // Prepare the delegation message and delegate the application to the gateway + delegateMsg := &types.MsgDelegateToGateway{ + AppAddress: appAddr, + GatewayAddress: gatewayAddr, + } + // Delegate the application to the gateway + _, err = srv.DelegateToGateway(wctx, delegateMsg) + require.NoError(t, err) + + // Verify that the application exists + foundApp, isAppFound := k.GetApplication(ctx, appAddr) + require.True(t, isAppFound) + require.Equal(t, appAddr, foundApp.Address) + require.Equal(t, 1, len(foundApp.DelegateeGatewayAddresses)) + require.Equal(t, gatewayAddr, foundApp.DelegateeGatewayAddresses[0]) + + // Mock unstaking the gateway + delete(keepertest.StakedGatewayMap, gatewayAddr) + + // Prepare an undelegation message + undelegateMsg := &types.MsgUndelegateFromGateway{ + AppAddress: appAddr, + GatewayAddress: gatewayAddr, + } + + // Undelegate the application from the gateway + _, err = srv.UndelegateFromGateway(wctx, undelegateMsg) + require.NoError(t, err) + foundApp, isAppFound = k.GetApplication(ctx, appAddr) + require.True(t, isAppFound) + require.Equal(t, appAddr, foundApp.Address) + require.Equal(t, 0, len(foundApp.DelegateeGatewayAddresses)) +} diff --git a/x/application/keeper/msg_server_unstake_application.go b/x/application/keeper/msg_server_unstake_application.go index 376ea45fb..8f062487b 100644 --- a/x/application/keeper/msg_server_unstake_application.go +++ b/x/application/keeper/msg_server_unstake_application.go @@ -5,7 +5,7 @@ import ( sdk "github.com/cosmos/cosmos-sdk/types" - "pocket/x/application/types" + "github.com/pokt-network/poktroll/x/application/types" ) // TODO(#73): Determine if an application needs an unbonding period after unstaking. diff --git a/x/application/keeper/msg_server_unstake_application_test.go b/x/application/keeper/msg_server_unstake_application_test.go index 7d2a2799c..fece27bd0 100644 --- a/x/application/keeper/msg_server_unstake_application_test.go +++ b/x/application/keeper/msg_server_unstake_application_test.go @@ -6,10 +6,11 @@ import ( sdk "github.com/cosmos/cosmos-sdk/types" "github.com/stretchr/testify/require" - keepertest "pocket/testutil/keeper" - "pocket/testutil/sample" - "pocket/x/application/keeper" - "pocket/x/application/types" + keepertest "github.com/pokt-network/poktroll/testutil/keeper" + "github.com/pokt-network/poktroll/testutil/sample" + "github.com/pokt-network/poktroll/x/application/keeper" + "github.com/pokt-network/poktroll/x/application/types" + sharedtypes "github.com/pokt-network/poktroll/x/shared/types" ) func TestMsgServer_UnstakeApplication_Success(t *testing.T) { @@ -29,6 +30,11 @@ func TestMsgServer_UnstakeApplication_Success(t *testing.T) { stakeMsg := &types.MsgStakeApplication{ Address: addr, Stake: &initialStake, + Services: []*sharedtypes.ApplicationServiceConfig{ + { + ServiceId: &sharedtypes.ServiceId{Id: "svc1"}, + }, + }, } // Stake the application @@ -36,10 +42,11 @@ func TestMsgServer_UnstakeApplication_Success(t *testing.T) { require.NoError(t, err) // Verify that the application exists - foundApp, isAppFound := k.GetApplication(ctx, addr) + appFound, isAppFound := k.GetApplication(ctx, addr) require.True(t, isAppFound) - require.Equal(t, addr, foundApp.Address) - require.Equal(t, initialStake.Amount, foundApp.Stake.Amount) + require.Equal(t, addr, appFound.Address) + require.Equal(t, initialStake.Amount, appFound.Stake.Amount) + require.Len(t, appFound.ServiceConfigs, 1) // Unstake the application unstakeMsg := &types.MsgUnstakeApplication{Address: addr} diff --git a/x/application/keeper/params.go b/x/application/keeper/params.go index c7dab2f49..566999f58 100644 --- a/x/application/keeper/params.go +++ b/x/application/keeper/params.go @@ -2,7 +2,8 @@ package keeper import ( sdk "github.com/cosmos/cosmos-sdk/types" - "pocket/x/application/types" + + "github.com/pokt-network/poktroll/x/application/types" ) // GetParams get all parameters as types.Params diff --git a/x/application/keeper/params_test.go b/x/application/keeper/params_test.go index 36d9af4ff..fb0b40921 100644 --- a/x/application/keeper/params_test.go +++ b/x/application/keeper/params_test.go @@ -4,8 +4,9 @@ import ( "testing" "github.com/stretchr/testify/require" - testkeeper "pocket/testutil/keeper" - "pocket/x/application/types" + + testkeeper "github.com/pokt-network/poktroll/testutil/keeper" + "github.com/pokt-network/poktroll/x/application/types" ) func TestGetParams(t *testing.T) { diff --git a/x/application/keeper/query.go b/x/application/keeper/query.go index 9b386f4ce..288fcb527 100644 --- a/x/application/keeper/query.go +++ b/x/application/keeper/query.go @@ -1,7 +1,7 @@ package keeper import ( - "pocket/x/application/types" + "github.com/pokt-network/poktroll/x/application/types" ) var _ types.QueryServer = Keeper{} diff --git a/x/application/keeper/query_application.go b/x/application/keeper/query_application.go index 9b2aa7be5..1a6b2a1b7 100644 --- a/x/application/keeper/query_application.go +++ b/x/application/keeper/query_application.go @@ -9,7 +9,7 @@ import ( "google.golang.org/grpc/codes" "google.golang.org/grpc/status" - "pocket/x/application/types" + "github.com/pokt-network/poktroll/x/application/types" ) func (k Keeper) ApplicationAll(goCtx context.Context, req *types.QueryAllApplicationRequest) (*types.QueryAllApplicationResponse, error) { diff --git a/x/application/keeper/query_application_test.go b/x/application/keeper/query_application_test.go index 714191885..feb0fef55 100644 --- a/x/application/keeper/query_application_test.go +++ b/x/application/keeper/query_application_test.go @@ -10,9 +10,9 @@ import ( "google.golang.org/grpc/codes" "google.golang.org/grpc/status" - keepertest "pocket/testutil/keeper" - "pocket/testutil/nullify" - "pocket/x/application/types" + keepertest "github.com/pokt-network/poktroll/testutil/keeper" + "github.com/pokt-network/poktroll/testutil/nullify" + "github.com/pokt-network/poktroll/x/application/types" ) // Prevent strconv unused error diff --git a/x/application/keeper/query_params.go b/x/application/keeper/query_params.go index ad362722b..b0c717d4f 100644 --- a/x/application/keeper/query_params.go +++ b/x/application/keeper/query_params.go @@ -6,7 +6,8 @@ import ( sdk "github.com/cosmos/cosmos-sdk/types" "google.golang.org/grpc/codes" "google.golang.org/grpc/status" - "pocket/x/application/types" + + "github.com/pokt-network/poktroll/x/application/types" ) func (k Keeper) Params(goCtx context.Context, req *types.QueryParamsRequest) (*types.QueryParamsResponse, error) { diff --git a/x/application/keeper/query_params_test.go b/x/application/keeper/query_params_test.go index 8ea47b150..6463c3764 100644 --- a/x/application/keeper/query_params_test.go +++ b/x/application/keeper/query_params_test.go @@ -5,8 +5,9 @@ import ( sdk "github.com/cosmos/cosmos-sdk/types" "github.com/stretchr/testify/require" - testkeeper "pocket/testutil/keeper" - "pocket/x/application/types" + + testkeeper "github.com/pokt-network/poktroll/testutil/keeper" + "github.com/pokt-network/poktroll/x/application/types" ) func TestParamsQuery(t *testing.T) { diff --git a/x/application/module.go b/x/application/module.go index 3ebe43d9e..44ba01150 100644 --- a/x/application/module.go +++ b/x/application/module.go @@ -4,6 +4,7 @@ import ( "context" "encoding/json" "fmt" + // this line is used by starport scaffolding # 1 "github.com/grpc-ecosystem/grpc-gateway/runtime" @@ -16,9 +17,10 @@ import ( cdctypes "github.com/cosmos/cosmos-sdk/codec/types" sdk "github.com/cosmos/cosmos-sdk/types" "github.com/cosmos/cosmos-sdk/types/module" - "pocket/x/application/client/cli" - "pocket/x/application/keeper" - "pocket/x/application/types" + + "github.com/pokt-network/poktroll/x/application/client/cli" + "github.com/pokt-network/poktroll/x/application/keeper" + "github.com/pokt-network/poktroll/x/application/types" ) var ( diff --git a/x/application/module_simulation.go b/x/application/module_simulation.go index 40f47fe42..9982f2db9 100644 --- a/x/application/module_simulation.go +++ b/x/application/module_simulation.go @@ -9,9 +9,9 @@ import ( simtypes "github.com/cosmos/cosmos-sdk/types/simulation" "github.com/cosmos/cosmos-sdk/x/simulation" - "pocket/testutil/sample" - applicationsimulation "pocket/x/application/simulation" - "pocket/x/application/types" + "github.com/pokt-network/poktroll/testutil/sample" + applicationsimulation "github.com/pokt-network/poktroll/x/application/simulation" + "github.com/pokt-network/poktroll/x/application/types" ) // avoid unused import issue diff --git a/x/application/simulation/delegate_to_gateway.go b/x/application/simulation/delegate_to_gateway.go index 35ea46bee..37070e16e 100644 --- a/x/application/simulation/delegate_to_gateway.go +++ b/x/application/simulation/delegate_to_gateway.go @@ -3,11 +3,12 @@ package simulation import ( "math/rand" + "github.com/pokt-network/poktroll/x/application/keeper" + "github.com/pokt-network/poktroll/x/application/types" + "github.com/cosmos/cosmos-sdk/baseapp" sdk "github.com/cosmos/cosmos-sdk/types" simtypes "github.com/cosmos/cosmos-sdk/types/simulation" - "pocket/x/application/keeper" - "pocket/x/application/types" ) func SimulateMsgDelegateToGateway( @@ -17,9 +18,11 @@ func SimulateMsgDelegateToGateway( ) simtypes.Operation { return func(r *rand.Rand, app *baseapp.BaseApp, ctx sdk.Context, accs []simtypes.Account, chainID string, ) (simtypes.OperationMsg, []simtypes.FutureOperation, error) { - simAccount, _ := simtypes.RandomAcc(r, accs) + simAppAccount, _ := simtypes.RandomAcc(r, accs) + simGatewayAccount, _ := simtypes.RandomAcc(r, accs) msg := &types.MsgDelegateToGateway{ - Address: simAccount.Address.String(), + AppAddress: simAppAccount.Address.String(), + GatewayAddress: simGatewayAccount.Address.String(), } // TODO: Handling the DelegateToGateway simulation diff --git a/x/application/simulation/stake_application.go b/x/application/simulation/stake_application.go index 107fc0b6a..d17afd14e 100644 --- a/x/application/simulation/stake_application.go +++ b/x/application/simulation/stake_application.go @@ -7,8 +7,8 @@ import ( sdk "github.com/cosmos/cosmos-sdk/types" simtypes "github.com/cosmos/cosmos-sdk/types/simulation" - "pocket/x/application/keeper" - "pocket/x/application/types" + "github.com/pokt-network/poktroll/x/application/keeper" + "github.com/pokt-network/poktroll/x/application/types" ) // TODO(@Olshansk): Implement simulation for application staking diff --git a/x/application/simulation/undelegate_from_gateway.go b/x/application/simulation/undelegate_from_gateway.go index ae03b5927..9ada988fd 100644 --- a/x/application/simulation/undelegate_from_gateway.go +++ b/x/application/simulation/undelegate_from_gateway.go @@ -3,11 +3,12 @@ package simulation import ( "math/rand" + "github.com/pokt-network/poktroll/x/application/keeper" + "github.com/pokt-network/poktroll/x/application/types" + "github.com/cosmos/cosmos-sdk/baseapp" sdk "github.com/cosmos/cosmos-sdk/types" simtypes "github.com/cosmos/cosmos-sdk/types/simulation" - "pocket/x/application/keeper" - "pocket/x/application/types" ) func SimulateMsgUndelegateFromGateway( @@ -17,9 +18,11 @@ func SimulateMsgUndelegateFromGateway( ) simtypes.Operation { return func(r *rand.Rand, app *baseapp.BaseApp, ctx sdk.Context, accs []simtypes.Account, chainID string, ) (simtypes.OperationMsg, []simtypes.FutureOperation, error) { - simAccount, _ := simtypes.RandomAcc(r, accs) + simAppAccount, _ := simtypes.RandomAcc(r, accs) + simGatewayAccount, _ := simtypes.RandomAcc(r, accs) msg := &types.MsgUndelegateFromGateway{ - Address: simAccount.Address.String(), + AppAddress: simAppAccount.Address.String(), + GatewayAddress: simGatewayAccount.Address.String(), } // TODO: Handling the UndelegateFromGateway simulation diff --git a/x/application/simulation/unstake_application.go b/x/application/simulation/unstake_application.go index 51c35dc4a..3dcccd3b5 100644 --- a/x/application/simulation/unstake_application.go +++ b/x/application/simulation/unstake_application.go @@ -8,8 +8,8 @@ import ( sdk "github.com/cosmos/cosmos-sdk/types" simtypes "github.com/cosmos/cosmos-sdk/types/simulation" - "pocket/x/application/keeper" - "pocket/x/application/types" + "github.com/pokt-network/poktroll/x/application/keeper" + "github.com/pokt-network/poktroll/x/application/types" ) // TODO(@Olshansk): Implement simulation for application staking diff --git a/x/application/types/errors.go b/x/application/types/errors.go index 94d76ccad..ea89e77e1 100644 --- a/x/application/types/errors.go +++ b/x/application/types/errors.go @@ -8,8 +8,15 @@ import ( // x/application module sentinel errors var ( - ErrAppInvalidStake = sdkerrors.Register(ModuleName, 1, "invalid application stake") - ErrAppInvalidAddress = sdkerrors.Register(ModuleName, 2, "invalid application address") - ErrAppUnauthorized = sdkerrors.Register(ModuleName, 3, "unauthorized application signer") - ErrAppNotFound = sdkerrors.Register(ModuleName, 4, "application not found") + ErrAppInvalidStake = sdkerrors.Register(ModuleName, 1, "invalid application stake") + ErrAppInvalidAddress = sdkerrors.Register(ModuleName, 2, "invalid application address") + ErrAppUnauthorized = sdkerrors.Register(ModuleName, 3, "unauthorized application signer") + ErrAppNotFound = sdkerrors.Register(ModuleName, 4, "application not found") + ErrAppInvalidServiceConfigs = sdkerrors.Register(ModuleName, 6, "invalid service configs") + ErrAppGatewayNotFound = sdkerrors.Register(ModuleName, 7, "gateway not found") + ErrAppInvalidGatewayAddress = sdkerrors.Register(ModuleName, 8, "invalid gateway address") + ErrAppAlreadyDelegated = sdkerrors.Register(ModuleName, 9, "application already delegated to gateway") + ErrAppMaxDelegatedGateways = sdkerrors.Register(ModuleName, 10, "maximum number of delegated gateways reached") + ErrAppInvalidMaxDelegatedGateways = sdkerrors.Register(ModuleName, 11, "invalid MaxDelegatedGateways parameter") + ErrAppNotDelegated = sdkerrors.Register(ModuleName, 12, "application not delegated to gateway") ) diff --git a/x/application/types/expected_keepers.go b/x/application/types/expected_keepers.go index ff977bf18..55ab9222a 100644 --- a/x/application/types/expected_keepers.go +++ b/x/application/types/expected_keepers.go @@ -1,10 +1,12 @@ package types -//go:generate mockgen -destination ../../../testutil/application/mocks/expected_keepers_mock.go -package mocks . AccountKeeper,BankKeeper +//go:generate mockgen -destination ../../../testutil/application/mocks/expected_keepers_mock.go -package mocks . AccountKeeper,BankKeeper,GatewayKeeper import ( sdk "github.com/cosmos/cosmos-sdk/types" "github.com/cosmos/cosmos-sdk/x/auth/types" + + gatewaytypes "github.com/pokt-network/poktroll/x/gateway/types" ) // AccountKeeper defines the expected account keeper used for simulations (noalias) @@ -17,3 +19,8 @@ type BankKeeper interface { DelegateCoinsFromAccountToModule(ctx sdk.Context, senderAddr sdk.AccAddress, recipientModule string, amt sdk.Coins) error UndelegateCoinsFromModuleToAccount(ctx sdk.Context, senderModule string, recipientAddr sdk.AccAddress, amt sdk.Coins) error } + +// GatewayKeeper defines the expected interface needed to retrieve gateway information. +type GatewayKeeper interface { + GetGateway(ctx sdk.Context, addr string) (gatewaytypes.Gateway, bool) +} diff --git a/x/application/types/genesis.go b/x/application/types/genesis.go index b42c4070c..ce8046f53 100644 --- a/x/application/types/genesis.go +++ b/x/application/types/genesis.go @@ -5,6 +5,8 @@ import ( sdkerrors "cosmossdk.io/errors" sdk "github.com/cosmos/cosmos-sdk/types" + + servicehelpers "github.com/pokt-network/poktroll/x/shared/helpers" ) // DefaultIndex is the default global index @@ -32,8 +34,10 @@ func (gs GenesisState) Validate() error { applicationIndexMap[index] = struct{}{} } - // Check that the stake value for the apps is valid + // Check that the stake value for the apps is valid and that the delegatee addresses are valid for _, app := range gs.ApplicationList { + // TODO_TECHDEBT: Consider creating shared helpers across the board for stake validation, + // similar to how we have `ValidateAppServiceConfigs` below if app.Stake == nil { return sdkerrors.Wrapf(ErrAppInvalidStake, "nil stake amount for application") } @@ -50,6 +54,18 @@ func (gs GenesisState) Validate() error { if stake.Denom != "upokt" { return sdkerrors.Wrapf(ErrAppInvalidStake, "invalid stake amount denom for application %v", app.Stake) } + + // Check that the application's delegated gateway addresses are valid + for _, gatewayAddr := range app.DelegateeGatewayAddresses { + if _, err := sdk.AccAddressFromBech32(gatewayAddr); err != nil { + return sdkerrors.Wrapf(ErrAppInvalidGatewayAddress, "invalid gateway address %s; (%v)", gatewayAddr, err) + } + } + + // Validate the application service configs + if err := servicehelpers.ValidateAppServiceConfigs(app.ServiceConfigs); err != nil { + return sdkerrors.Wrapf(ErrAppInvalidServiceConfigs, err.Error()) + } } // this line is used by starport scaffolding # genesis/types/validate diff --git a/x/application/types/genesis_test.go b/x/application/types/genesis_test.go index 69bc318c2..01dd9c174 100644 --- a/x/application/types/genesis_test.go +++ b/x/application/types/genesis_test.go @@ -6,16 +6,27 @@ import ( sdk "github.com/cosmos/cosmos-sdk/types" "github.com/stretchr/testify/require" - "pocket/testutil/sample" - "pocket/x/application/types" + "github.com/pokt-network/poktroll/testutil/sample" + "github.com/pokt-network/poktroll/x/application/types" + sharedtypes "github.com/pokt-network/poktroll/x/shared/types" ) func TestGenesisState_Validate(t *testing.T) { addr1 := sample.AccAddress() stake1 := sdk.NewCoin("upokt", sdk.NewInt(100)) + svc1AppConfig := &sharedtypes.ApplicationServiceConfig{ + ServiceId: &sharedtypes.ServiceId{Id: "svc1"}, + } addr2 := sample.AccAddress() stake2 := sdk.NewCoin("upokt", sdk.NewInt(100)) + svc2AppConfig := &sharedtypes.ApplicationServiceConfig{ + ServiceId: &sharedtypes.ServiceId{Id: "svc2"}, + } + + emptyDelegatees := make([]string, 0) + gatewayAddr1 := sample.AccAddress() + gatewayAddr2 := sample.AccAddress() tests := []struct { desc string @@ -30,15 +41,21 @@ func TestGenesisState_Validate(t *testing.T) { { desc: "valid genesis state", genState: &types.GenesisState{ - + Params: types.Params{ + MaxDelegatedGateways: 7, + }, ApplicationList: []types.Application{ { - Address: addr1, - Stake: &stake1, + Address: addr1, + Stake: &stake1, + ServiceConfigs: []*sharedtypes.ApplicationServiceConfig{svc1AppConfig}, + DelegateeGatewayAddresses: []string{gatewayAddr1, gatewayAddr2}, }, { - Address: addr2, - Stake: &stake2, + Address: addr2, + Stake: &stake2, + ServiceConfigs: []*sharedtypes.ApplicationServiceConfig{svc2AppConfig}, + DelegateeGatewayAddresses: []string{gatewayAddr2, gatewayAddr1}, }, }, // this line is used by starport scaffolding # types/genesis/validField @@ -48,14 +65,21 @@ func TestGenesisState_Validate(t *testing.T) { { desc: "invalid - zero app stake", genState: &types.GenesisState{ + Params: types.Params{ + MaxDelegatedGateways: 7, + }, ApplicationList: []types.Application{ { - Address: addr1, - Stake: &stake1, + Address: addr1, + Stake: &stake1, + ServiceConfigs: []*sharedtypes.ApplicationServiceConfig{svc1AppConfig}, + DelegateeGatewayAddresses: emptyDelegatees, }, { - Address: addr2, - Stake: &sdk.Coin{Denom: "upokt", Amount: sdk.NewInt(0)}, + Address: addr2, + Stake: &sdk.Coin{Denom: "upokt", Amount: sdk.NewInt(0)}, + ServiceConfigs: []*sharedtypes.ApplicationServiceConfig{svc2AppConfig}, + DelegateeGatewayAddresses: emptyDelegatees, }, }, }, @@ -64,14 +88,21 @@ func TestGenesisState_Validate(t *testing.T) { { desc: "invalid - negative application stake", genState: &types.GenesisState{ + Params: types.Params{ + MaxDelegatedGateways: 7, + }, ApplicationList: []types.Application{ { - Address: addr1, - Stake: &stake1, + Address: addr1, + Stake: &stake1, + ServiceConfigs: []*sharedtypes.ApplicationServiceConfig{svc1AppConfig}, + DelegateeGatewayAddresses: emptyDelegatees, }, { - Address: addr2, - Stake: &sdk.Coin{Denom: "upokt", Amount: sdk.NewInt(-100)}, + Address: addr2, + Stake: &sdk.Coin{Denom: "upokt", Amount: sdk.NewInt(-100)}, + ServiceConfigs: []*sharedtypes.ApplicationServiceConfig{svc2AppConfig}, + DelegateeGatewayAddresses: emptyDelegatees, }, }, }, @@ -80,14 +111,21 @@ func TestGenesisState_Validate(t *testing.T) { { desc: "invalid - wrong stake denom", genState: &types.GenesisState{ + Params: types.Params{ + MaxDelegatedGateways: 7, + }, ApplicationList: []types.Application{ { - Address: addr1, - Stake: &stake1, + Address: addr1, + Stake: &stake1, + ServiceConfigs: []*sharedtypes.ApplicationServiceConfig{svc1AppConfig}, + DelegateeGatewayAddresses: emptyDelegatees, }, { - Address: addr2, - Stake: &sdk.Coin{Denom: "invalid", Amount: sdk.NewInt(100)}, + Address: addr2, + Stake: &sdk.Coin{Denom: "invalid", Amount: sdk.NewInt(100)}, + ServiceConfigs: []*sharedtypes.ApplicationServiceConfig{svc2AppConfig}, + DelegateeGatewayAddresses: emptyDelegatees, }, }, }, @@ -96,14 +134,21 @@ func TestGenesisState_Validate(t *testing.T) { { desc: "invalid - missing denom", genState: &types.GenesisState{ + Params: types.Params{ + MaxDelegatedGateways: 7, + }, ApplicationList: []types.Application{ { - Address: addr1, - Stake: &stake1, + Address: addr1, + Stake: &stake1, + ServiceConfigs: []*sharedtypes.ApplicationServiceConfig{svc1AppConfig}, + DelegateeGatewayAddresses: emptyDelegatees, }, { - Address: addr2, - Stake: &sdk.Coin{Denom: "", Amount: sdk.NewInt(100)}, + Address: addr2, + Stake: &sdk.Coin{Denom: "", Amount: sdk.NewInt(100)}, + ServiceConfigs: []*sharedtypes.ApplicationServiceConfig{svc2AppConfig}, + DelegateeGatewayAddresses: emptyDelegatees, }, }, }, @@ -112,14 +157,21 @@ func TestGenesisState_Validate(t *testing.T) { { desc: "invalid - due to duplicated app address", genState: &types.GenesisState{ + Params: types.Params{ + MaxDelegatedGateways: 7, + }, ApplicationList: []types.Application{ { - Address: addr1, - Stake: &stake1, + Address: addr1, + Stake: &stake1, + ServiceConfigs: []*sharedtypes.ApplicationServiceConfig{svc1AppConfig}, + DelegateeGatewayAddresses: emptyDelegatees, }, { - Address: addr1, - Stake: &stake2, + Address: addr1, + Stake: &stake2, + ServiceConfigs: []*sharedtypes.ApplicationServiceConfig{svc2AppConfig}, + DelegateeGatewayAddresses: emptyDelegatees, }, }, }, @@ -128,35 +180,199 @@ func TestGenesisState_Validate(t *testing.T) { { desc: "invalid - due to nil app stake", genState: &types.GenesisState{ + Params: types.Params{ + MaxDelegatedGateways: 7, + }, + ApplicationList: []types.Application{ + { + Address: addr1, + Stake: &stake1, + ServiceConfigs: []*sharedtypes.ApplicationServiceConfig{svc1AppConfig}, + DelegateeGatewayAddresses: emptyDelegatees, + }, + { + Address: addr2, + Stake: nil, + ServiceConfigs: []*sharedtypes.ApplicationServiceConfig{svc2AppConfig}, + DelegateeGatewayAddresses: emptyDelegatees, + }, + }, + }, + valid: false, + }, + { + desc: "invalid - due to missing app stake", + genState: &types.GenesisState{ + Params: types.Params{ + MaxDelegatedGateways: 7, + }, + ApplicationList: []types.Application{ + { + Address: addr1, + Stake: &stake1, + ServiceConfigs: []*sharedtypes.ApplicationServiceConfig{svc1AppConfig}, + DelegateeGatewayAddresses: emptyDelegatees, + }, + { + Address: addr2, + // Explicitly missing stake + ServiceConfigs: []*sharedtypes.ApplicationServiceConfig{svc2AppConfig}, + DelegateeGatewayAddresses: emptyDelegatees, + }, + }, + }, + valid: false, + }, + { + desc: "invalid - due to invalid delegatee pub key", + genState: &types.GenesisState{ + Params: types.Params{ + MaxDelegatedGateways: 7, + }, + ApplicationList: []types.Application{ + { + Address: addr1, + Stake: &stake1, + ServiceConfigs: []*sharedtypes.ApplicationServiceConfig{svc1AppConfig}, + DelegateeGatewayAddresses: emptyDelegatees, + }, + { + Address: addr2, + Stake: &stake2, + ServiceConfigs: []*sharedtypes.ApplicationServiceConfig{svc2AppConfig}, + DelegateeGatewayAddresses: []string{"invalid address"}, + }, + }, + }, + valid: false, + }, + { + desc: "invalid - due to invalid delegatee pub keys", + genState: &types.GenesisState{ + Params: types.Params{ + MaxDelegatedGateways: 7, + }, + ApplicationList: []types.Application{ + { + Address: addr1, + Stake: &stake1, + ServiceConfigs: []*sharedtypes.ApplicationServiceConfig{svc1AppConfig}, + DelegateeGatewayAddresses: []string{gatewayAddr1}, + }, + { + Address: addr2, + Stake: &stake2, + ServiceConfigs: []*sharedtypes.ApplicationServiceConfig{svc2AppConfig}, + DelegateeGatewayAddresses: []string{"invalid address", gatewayAddr2}, + }, + }, + }, + valid: false, + }, + { + desc: "invalid - service config not present", + genState: &types.GenesisState{ + Params: types.Params{ + MaxDelegatedGateways: 7, + }, ApplicationList: []types.Application{ { Address: addr1, Stake: &stake1, + // ServiceConfigs: omitted + DelegateeGatewayAddresses: emptyDelegatees, }, + }, + }, + valid: false, + }, + { + desc: "invalid - empty service config", + genState: &types.GenesisState{ + Params: types.Params{ + MaxDelegatedGateways: 7, + }, + ApplicationList: []types.Application{ { - Address: addr2, - Stake: nil, + Address: addr1, + Stake: &stake1, + ServiceConfigs: []*sharedtypes.ApplicationServiceConfig{}, + DelegateeGatewayAddresses: emptyDelegatees, }, }, }, valid: false, }, { - desc: "invalid - due to missing app stake", + desc: "invalid - service ID too long", genState: &types.GenesisState{ + Params: types.Params{ + MaxDelegatedGateways: 7, + }, ApplicationList: []types.Application{ { Address: addr1, Stake: &stake1, + ServiceConfigs: []*sharedtypes.ApplicationServiceConfig{ + {ServiceId: &sharedtypes.ServiceId{Id: "12345678901"}}, + }, + DelegateeGatewayAddresses: emptyDelegatees, }, + }, + }, + valid: false, + }, + { + desc: "invalid - service name too long", + genState: &types.GenesisState{ + Params: types.Params{ + MaxDelegatedGateways: 7, + }, + ApplicationList: []types.Application{ { - Address: addr2, - // Explicitly missing stake + Address: addr1, + Stake: &stake1, + ServiceConfigs: []*sharedtypes.ApplicationServiceConfig{ + {ServiceId: &sharedtypes.ServiceId{ + Id: "123", + Name: "abcdefghijklmnopqrstuvwxyzab-abcdefghijklmnopqrstuvwxyzab", + }}, + }, + DelegateeGatewayAddresses: emptyDelegatees, + }, + }, + }, + valid: false, + }, + { + desc: "invalid - service ID with invalid characters", + genState: &types.GenesisState{ + Params: types.Params{ + MaxDelegatedGateways: 7, + }, + ApplicationList: []types.Application{ + { + Address: addr1, + Stake: &stake1, + ServiceConfigs: []*sharedtypes.ApplicationServiceConfig{ + {ServiceId: &sharedtypes.ServiceId{Id: "12 45 !"}}, + }, + DelegateeGatewayAddresses: emptyDelegatees, }, }, }, valid: false, }, + { + desc: "invalid - MaxDelegatedGateways less than 1", + genState: &types.GenesisState{ + Params: types.Params{ + MaxDelegatedGateways: 0, + }, + }, + valid: false, + }, + // this line is used by starport scaffolding # types/genesis/testcase } for _, tc := range tests { diff --git a/x/application/types/message_delegate_to_gateway.go b/x/application/types/message_delegate_to_gateway.go index 232564310..652b5baa6 100644 --- a/x/application/types/message_delegate_to_gateway.go +++ b/x/application/types/message_delegate_to_gateway.go @@ -1,17 +1,18 @@ package types import ( + sdkerrors "cosmossdk.io/errors" sdk "github.com/cosmos/cosmos-sdk/types" - sdkerrors "github.com/cosmos/cosmos-sdk/types/errors" ) const TypeMsgDelegateToGateway = "delegate_to_gateway" var _ sdk.Msg = (*MsgDelegateToGateway)(nil) -func NewMsgDelegateToGateway(address string) *MsgDelegateToGateway { +func NewMsgDelegateToGateway(appAddress, gatewayAddress string) *MsgDelegateToGateway { return &MsgDelegateToGateway{ - Address: address, + AppAddress: appAddress, + GatewayAddress: gatewayAddress, } } @@ -24,7 +25,7 @@ func (msg *MsgDelegateToGateway) Type() string { } func (msg *MsgDelegateToGateway) GetSigners() []sdk.AccAddress { - address, err := sdk.AccAddressFromBech32(msg.Address) + address, err := sdk.AccAddressFromBech32(msg.AppAddress) if err != nil { panic(err) } @@ -37,9 +38,13 @@ func (msg *MsgDelegateToGateway) GetSignBytes() []byte { } func (msg *MsgDelegateToGateway) ValidateBasic() error { - _, err := sdk.AccAddressFromBech32(msg.Address) - if err != nil { - return sdkerrors.Wrapf(sdkerrors.ErrInvalidAddress, "invalid address address (%s)", err) + // Validate the application address + if _, err := sdk.AccAddressFromBech32(msg.AppAddress); err != nil { + return sdkerrors.Wrapf(ErrAppInvalidAddress, "invalid application address %s; (%v)", msg.AppAddress, err) + } + // Validate the gateway address + if _, err := sdk.AccAddressFromBech32(msg.GatewayAddress); err != nil { + return sdkerrors.Wrapf(ErrAppInvalidGatewayAddress, "invalid gateway address %s; (%v)", msg.GatewayAddress, err) } return nil } diff --git a/x/application/types/message_delegate_to_gateway_test.go b/x/application/types/message_delegate_to_gateway_test.go index 770d801b2..0769d3e65 100644 --- a/x/application/types/message_delegate_to_gateway_test.go +++ b/x/application/types/message_delegate_to_gateway_test.go @@ -3,9 +3,9 @@ package types import ( "testing" - sdkerrors "github.com/cosmos/cosmos-sdk/types/errors" + "github.com/pokt-network/poktroll/testutil/sample" + "github.com/stretchr/testify/require" - "pocket/testutil/sample" ) func TestMsgDelegateToGateway_ValidateBasic(t *testing.T) { @@ -15,15 +15,31 @@ func TestMsgDelegateToGateway_ValidateBasic(t *testing.T) { err error }{ { - name: "invalid address", + name: "invalid app address - no gateway address", + msg: MsgDelegateToGateway{ + AppAddress: "invalid_address", + // GatewayAddress: intentionally omitted, + }, + err: ErrAppInvalidAddress, + }, { + name: "valid app address - no gateway address", + msg: MsgDelegateToGateway{ + AppAddress: sample.AccAddress(), + // GatewayAddress: intentionally omitted, + }, + err: ErrAppInvalidGatewayAddress, + }, { + name: "valid app address - invalid gateway address", msg: MsgDelegateToGateway{ - Address: "invalid_address", + AppAddress: sample.AccAddress(), + GatewayAddress: "invalid_address", }, - err: sdkerrors.ErrInvalidAddress, + err: ErrAppInvalidGatewayAddress, }, { name: "valid address", msg: MsgDelegateToGateway{ - Address: sample.AccAddress(), + AppAddress: sample.AccAddress(), + GatewayAddress: sample.AccAddress(), }, }, } diff --git a/x/application/types/message_stake_application.go b/x/application/types/message_stake_application.go index 219a65701..9dba9eebf 100644 --- a/x/application/types/message_stake_application.go +++ b/x/application/types/message_stake_application.go @@ -4,20 +4,35 @@ import ( sdkerrors "cosmossdk.io/errors" sdk "github.com/cosmos/cosmos-sdk/types" types "github.com/cosmos/cosmos-sdk/types" + + servicehelpers "github.com/pokt-network/poktroll/x/shared/helpers" + sharedtypes "github.com/pokt-network/poktroll/x/shared/types" ) const TypeMsgStakeApplication = "stake_application" var _ sdk.Msg = (*MsgStakeApplication)(nil) +// TODO_TECHDEBT: See `NewMsgStakeSupplier` and follow the same pattern for the `Services` parameter func NewMsgStakeApplication( address string, stake types.Coin, - + serviceIds []string, ) *MsgStakeApplication { + // Convert the serviceIds to the proper ApplicationServiceConfig type (enables future expansion) + appServiceConfigs := make([]*sharedtypes.ApplicationServiceConfig, len(serviceIds)) + for idx, serviceId := range serviceIds { + appServiceConfigs[idx] = &sharedtypes.ApplicationServiceConfig{ + ServiceId: &sharedtypes.ServiceId{ + Id: serviceId, + }, + } + } + return &MsgStakeApplication{ - Address: address, - Stake: &stake, + Address: address, + Stake: &stake, + Services: appServiceConfigs, } } @@ -49,6 +64,7 @@ func (msg *MsgStakeApplication) ValidateBasic() error { return sdkerrors.Wrapf(ErrAppInvalidAddress, "invalid application address %s; (%v)", msg.Address, err) } + // TODO_TECHDEBT: Centralize stake related verification and share across different parts of the source code // Validate the stake amount if msg.Stake == nil { return sdkerrors.Wrapf(ErrAppInvalidStake, "nil application stake; (%v)", err) @@ -64,7 +80,12 @@ func (msg *MsgStakeApplication) ValidateBasic() error { return sdkerrors.Wrapf(ErrAppInvalidStake, "invalid stake amount for application: %v <= 0", msg.Stake) } if stake.Denom != "upokt" { - return sdkerrors.Wrapf(ErrAppInvalidStake, "invalid stake amount denom for application %v", msg.Stake) + return sdkerrors.Wrapf(ErrAppInvalidStake, "invalid stake amount denom for application: %v", msg.Stake) + } + + // Validate the application service configs + if err := servicehelpers.ValidateAppServiceConfigs(msg.Services); err != nil { + return sdkerrors.Wrapf(ErrAppInvalidServiceConfigs, err.Error()) } return nil diff --git a/x/application/types/message_stake_application_test.go b/x/application/types/message_stake_application_test.go index 3e36098a7..c14119898 100644 --- a/x/application/types/message_stake_application_test.go +++ b/x/application/types/message_stake_application_test.go @@ -6,7 +6,8 @@ import ( sdk "github.com/cosmos/cosmos-sdk/types" "github.com/stretchr/testify/require" - "pocket/testutil/sample" + "github.com/pokt-network/poktroll/testutil/sample" + sharedtypes "github.com/pokt-network/poktroll/x/shared/types" ) func TestMsgStakeApplication_ValidateBasic(t *testing.T) { @@ -15,18 +16,28 @@ func TestMsgStakeApplication_ValidateBasic(t *testing.T) { msg MsgStakeApplication err error }{ + // address related tests { name: "invalid address - nil stake", msg: MsgStakeApplication{ Address: "invalid_address", // Stake explicitly nil + Services: []*sharedtypes.ApplicationServiceConfig{ + {ServiceId: &sharedtypes.ServiceId{Id: "svc1"}}, + }, }, err: ErrAppInvalidAddress, - }, { + }, + + // stake related tests + { name: "valid address - nil stake", msg: MsgStakeApplication{ Address: sample.AccAddress(), // Stake explicitly nil + Services: []*sharedtypes.ApplicationServiceConfig{ + {ServiceId: &sharedtypes.ServiceId{Id: "svc1"}}, + }, }, err: ErrAppInvalidStake, }, { @@ -34,12 +45,18 @@ func TestMsgStakeApplication_ValidateBasic(t *testing.T) { msg: MsgStakeApplication{ Address: sample.AccAddress(), Stake: &sdk.Coin{Denom: "upokt", Amount: sdk.NewInt(100)}, + Services: []*sharedtypes.ApplicationServiceConfig{ + {ServiceId: &sharedtypes.ServiceId{Id: "svc1"}}, + }, }, }, { name: "valid address - zero stake", msg: MsgStakeApplication{ Address: sample.AccAddress(), Stake: &sdk.Coin{Denom: "upokt", Amount: sdk.NewInt(0)}, + Services: []*sharedtypes.ApplicationServiceConfig{ + {ServiceId: &sharedtypes.ServiceId{Id: "svc1"}}, + }, }, err: ErrAppInvalidStake, }, { @@ -47,6 +64,9 @@ func TestMsgStakeApplication_ValidateBasic(t *testing.T) { msg: MsgStakeApplication{ Address: sample.AccAddress(), Stake: &sdk.Coin{Denom: "upokt", Amount: sdk.NewInt(-100)}, + Services: []*sharedtypes.ApplicationServiceConfig{ + {ServiceId: &sharedtypes.ServiceId{Id: "svc1"}}, + }, }, err: ErrAppInvalidStake, }, { @@ -54,6 +74,9 @@ func TestMsgStakeApplication_ValidateBasic(t *testing.T) { msg: MsgStakeApplication{ Address: sample.AccAddress(), Stake: &sdk.Coin{Denom: "invalid", Amount: sdk.NewInt(100)}, + Services: []*sharedtypes.ApplicationServiceConfig{ + {ServiceId: &sharedtypes.ServiceId{Id: "svc1"}}, + }, }, err: ErrAppInvalidStake, }, { @@ -61,16 +84,86 @@ func TestMsgStakeApplication_ValidateBasic(t *testing.T) { msg: MsgStakeApplication{ Address: sample.AccAddress(), Stake: &sdk.Coin{Denom: "", Amount: sdk.NewInt(100)}, + Services: []*sharedtypes.ApplicationServiceConfig{ + {ServiceId: &sharedtypes.ServiceId{Id: "svc1"}}, + }, }, err: ErrAppInvalidStake, }, + + // service related tests + { + name: "valid service configs - multiple services", + msg: MsgStakeApplication{ + Address: sample.AccAddress(), + Stake: &sdk.Coin{Denom: "upokt", Amount: sdk.NewInt(100)}, + Services: []*sharedtypes.ApplicationServiceConfig{ + {ServiceId: &sharedtypes.ServiceId{Id: "svc1"}}, + {ServiceId: &sharedtypes.ServiceId{Id: "svc2"}}, + }, + }, + }, + { + name: "invalid service configs - not present", + msg: MsgStakeApplication{ + Address: sample.AccAddress(), + Stake: &sdk.Coin{Denom: "upokt", Amount: sdk.NewInt(100)}, + // Services: omitted + }, + err: ErrAppInvalidServiceConfigs, + }, + { + name: "invalid service configs - empty", + msg: MsgStakeApplication{ + Address: sample.AccAddress(), + Stake: &sdk.Coin{Denom: "upokt", Amount: sdk.NewInt(100)}, + Services: []*sharedtypes.ApplicationServiceConfig{}, + }, + err: ErrAppInvalidServiceConfigs, + }, + { + name: "invalid service configs - invalid service ID that's too long", + msg: MsgStakeApplication{ + Address: sample.AccAddress(), + Stake: &sdk.Coin{Denom: "upokt", Amount: sdk.NewInt(100)}, + Services: []*sharedtypes.ApplicationServiceConfig{ + {ServiceId: &sharedtypes.ServiceId{Id: "123456790"}}, + }, + }, + err: ErrAppInvalidServiceConfigs, + }, + { + name: "invalid service configs - invalid service Name that's too long", + msg: MsgStakeApplication{ + Address: sample.AccAddress(), + Stake: &sdk.Coin{Denom: "upokt", Amount: sdk.NewInt(100)}, + Services: []*sharedtypes.ApplicationServiceConfig{ + {ServiceId: &sharedtypes.ServiceId{ + Id: "123", + Name: "abcdefghijklmnopqrstuvwxyzab-abcdefghijklmnopqrstuvwxyzab", + }}, + }, + }, + err: ErrAppInvalidServiceConfigs, + }, + { + name: "invalid service configs - invalid service ID that contains invalid characters", + msg: MsgStakeApplication{ + Address: sample.AccAddress(), + Stake: &sdk.Coin{Denom: "upokt", Amount: sdk.NewInt(100)}, + Services: []*sharedtypes.ApplicationServiceConfig{ + {ServiceId: &sharedtypes.ServiceId{Id: "12 45 !"}}, + }, + }, + err: ErrAppInvalidServiceConfigs, + }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { err := tt.msg.ValidateBasic() if tt.err != nil { - require.ErrorIs(t, err, tt.err) + require.ErrorContains(t, err, tt.err.Error()) return } require.NoError(t, err) diff --git a/x/application/types/message_undelegate_from_gateway.go b/x/application/types/message_undelegate_from_gateway.go index 240605383..4d74748c1 100644 --- a/x/application/types/message_undelegate_from_gateway.go +++ b/x/application/types/message_undelegate_from_gateway.go @@ -9,9 +9,10 @@ const TypeMsgUndelegateFromGateway = "undelegate_from_gateway" var _ sdk.Msg = (*MsgUndelegateFromGateway)(nil) -func NewMsgUndelegateFromGateway(address string) *MsgUndelegateFromGateway { +func NewMsgUndelegateFromGateway(appAddress, gatewayAddress string) *MsgUndelegateFromGateway { return &MsgUndelegateFromGateway{ - Address: address, + AppAddress: appAddress, + GatewayAddress: gatewayAddress, } } @@ -24,7 +25,7 @@ func (msg *MsgUndelegateFromGateway) Type() string { } func (msg *MsgUndelegateFromGateway) GetSigners() []sdk.AccAddress { - address, err := sdk.AccAddressFromBech32(msg.Address) + address, err := sdk.AccAddressFromBech32(msg.AppAddress) if err != nil { panic(err) } @@ -37,9 +38,13 @@ func (msg *MsgUndelegateFromGateway) GetSignBytes() []byte { } func (msg *MsgUndelegateFromGateway) ValidateBasic() error { - _, err := sdk.AccAddressFromBech32(msg.Address) - if err != nil { - return sdkerrors.Wrapf(sdkerrors.ErrInvalidAddress, "invalid address address (%s)", err) + // Validate the application address + if _, err := sdk.AccAddressFromBech32(msg.AppAddress); err != nil { + return sdkerrors.Wrapf(ErrAppInvalidAddress, "invalid application address %s; (%v)", msg.AppAddress, err) + } + // Validate the gateway address + if _, err := sdk.AccAddressFromBech32(msg.GatewayAddress); err != nil { + return sdkerrors.Wrapf(ErrAppInvalidGatewayAddress, "invalid gateway address %s; (%v)", msg.GatewayAddress, err) } return nil } diff --git a/x/application/types/message_undelegate_from_gateway_test.go b/x/application/types/message_undelegate_from_gateway_test.go index 1781a887a..b40b520e9 100644 --- a/x/application/types/message_undelegate_from_gateway_test.go +++ b/x/application/types/message_undelegate_from_gateway_test.go @@ -3,9 +3,9 @@ package types import ( "testing" - sdkerrors "github.com/cosmos/cosmos-sdk/types/errors" + "github.com/pokt-network/poktroll/testutil/sample" + "github.com/stretchr/testify/require" - "pocket/testutil/sample" ) func TestMsgUndelegateFromGateway_ValidateBasic(t *testing.T) { @@ -15,15 +15,31 @@ func TestMsgUndelegateFromGateway_ValidateBasic(t *testing.T) { err error }{ { - name: "invalid address", + name: "invalid app address - no gateway address", + msg: MsgUndelegateFromGateway{ + AppAddress: "invalid_address", + // GatewayAddress: sample.AccAddress(), + }, + err: ErrAppInvalidAddress, + }, { + name: "valid app address - no gateway address", + msg: MsgUndelegateFromGateway{ + AppAddress: sample.AccAddress(), + // GatewayAddress: sample.AccAddress(), + }, + err: ErrAppInvalidGatewayAddress, + }, { + name: "valid app address - invalid gateway address", msg: MsgUndelegateFromGateway{ - Address: "invalid_address", + AppAddress: sample.AccAddress(), + GatewayAddress: "invalid_address", }, - err: sdkerrors.ErrInvalidAddress, + err: ErrAppInvalidGatewayAddress, }, { name: "valid address", msg: MsgUndelegateFromGateway{ - Address: sample.AccAddress(), + AppAddress: sample.AccAddress(), + GatewayAddress: sample.AccAddress(), }, }, } diff --git a/x/application/types/message_unstake_application_test.go b/x/application/types/message_unstake_application_test.go index ca31b6dd0..fdc9a5a91 100644 --- a/x/application/types/message_unstake_application_test.go +++ b/x/application/types/message_unstake_application_test.go @@ -5,7 +5,7 @@ import ( "github.com/stretchr/testify/require" - "pocket/testutil/sample" + "github.com/pokt-network/poktroll/testutil/sample" ) func TestMsgUnstakeApplication_ValidateBasic(t *testing.T) { diff --git a/x/application/types/params.go b/x/application/types/params.go index 357196ad6..f5ec7cd0c 100644 --- a/x/application/types/params.go +++ b/x/application/types/params.go @@ -1,10 +1,14 @@ package types import ( + sdkerrors "cosmossdk.io/errors" paramtypes "github.com/cosmos/cosmos-sdk/x/params/types" "gopkg.in/yaml.v2" ) +// TODO: Revisit default param values +const DefaultMaxDelegatedGateways int64 = 7 + var _ paramtypes.ParamSet = (*Params)(nil) // ParamKeyTable the param key table for launch module @@ -14,7 +18,7 @@ func ParamKeyTable() paramtypes.KeyTable { // NewParams creates a new Params instance func NewParams() Params { - return Params{} + return Params{MaxDelegatedGateways: DefaultMaxDelegatedGateways} } // DefaultParams returns a default set of parameters @@ -29,6 +33,9 @@ func (p *Params) ParamSetPairs() paramtypes.ParamSetPairs { // Validate validates the set of params func (p Params) Validate() error { + if p.MaxDelegatedGateways < 1 { + return sdkerrors.Wrapf(ErrAppInvalidMaxDelegatedGateways, "MaxDelegatedGateways param < 1: got %d", p.MaxDelegatedGateways) + } return nil } diff --git a/x/gateway/client/cli/helpers_test.go b/x/gateway/client/cli/helpers_test.go index 32f159e55..927212a46 100644 --- a/x/gateway/client/cli/helpers_test.go +++ b/x/gateway/client/cli/helpers_test.go @@ -3,8 +3,8 @@ package cli_test import ( "testing" - "pocket/testutil/network" - "pocket/x/gateway/types" + "github.com/pokt-network/poktroll/testutil/network" + "github.com/pokt-network/poktroll/x/gateway/types" "github.com/stretchr/testify/require" ) diff --git a/x/gateway/client/cli/query.go b/x/gateway/client/cli/query.go index 5bffe1840..b896f7583 100644 --- a/x/gateway/client/cli/query.go +++ b/x/gateway/client/cli/query.go @@ -10,7 +10,7 @@ import ( // "github.com/cosmos/cosmos-sdk/client/flags" // sdk "github.com/cosmos/cosmos-sdk/types" - "pocket/x/gateway/types" + "github.com/pokt-network/poktroll/x/gateway/types" ) // GetQueryCmd returns the cli query commands for this module diff --git a/x/gateway/client/cli/query_gateway.go b/x/gateway/client/cli/query_gateway.go index be9a3f964..30076ed22 100644 --- a/x/gateway/client/cli/query_gateway.go +++ b/x/gateway/client/cli/query_gateway.go @@ -5,7 +5,7 @@ import ( "github.com/cosmos/cosmos-sdk/client/flags" "github.com/spf13/cobra" - "pocket/x/gateway/types" + "github.com/pokt-network/poktroll/x/gateway/types" ) func CmdListGateway() *cobra.Command { diff --git a/x/gateway/client/cli/query_gateway_test.go b/x/gateway/client/cli/query_gateway_test.go index 37e0ec3b3..fba32713c 100644 --- a/x/gateway/client/cli/query_gateway_test.go +++ b/x/gateway/client/cli/query_gateway_test.go @@ -12,9 +12,9 @@ import ( "google.golang.org/grpc/codes" "google.golang.org/grpc/status" - "pocket/testutil/nullify" - "pocket/x/gateway/client/cli" - "pocket/x/gateway/types" + "github.com/pokt-network/poktroll/testutil/nullify" + "github.com/pokt-network/poktroll/x/gateway/client/cli" + "github.com/pokt-network/poktroll/x/gateway/types" ) // Prevent strconv unused error diff --git a/x/gateway/client/cli/query_params.go b/x/gateway/client/cli/query_params.go index b868cb116..447a64b79 100644 --- a/x/gateway/client/cli/query_params.go +++ b/x/gateway/client/cli/query_params.go @@ -5,7 +5,7 @@ import ( "github.com/cosmos/cosmos-sdk/client/flags" "github.com/spf13/cobra" - "pocket/x/gateway/types" + "github.com/pokt-network/poktroll/x/gateway/types" ) func CmdQueryParams() *cobra.Command { diff --git a/x/gateway/client/cli/tx.go b/x/gateway/client/cli/tx.go index 3d8c8a520..52be2fb88 100644 --- a/x/gateway/client/cli/tx.go +++ b/x/gateway/client/cli/tx.go @@ -8,7 +8,7 @@ import ( "github.com/cosmos/cosmos-sdk/client" // "github.com/cosmos/cosmos-sdk/client/flags" - "pocket/x/gateway/types" + "github.com/pokt-network/poktroll/x/gateway/types" ) var DefaultRelativePacketTimeoutTimestamp = uint64((time.Duration(10) * time.Minute).Nanoseconds()) diff --git a/x/gateway/client/cli/tx_stake_gateway.go b/x/gateway/client/cli/tx_stake_gateway.go index cabb4de31..2c363b43b 100644 --- a/x/gateway/client/cli/tx_stake_gateway.go +++ b/x/gateway/client/cli/tx_stake_gateway.go @@ -3,7 +3,7 @@ package cli import ( "strconv" - "pocket/x/gateway/types" + "github.com/pokt-network/poktroll/x/gateway/types" "github.com/cosmos/cosmos-sdk/client" "github.com/cosmos/cosmos-sdk/client/flags" @@ -21,7 +21,7 @@ func CmdStakeGateway() *cobra.Command { Long: `Stake a gateway with the provided parameters. This is a broadcast operation that will stake the tokens and associate them with the gateway specified by the 'from' address. Example: -$ pocketd --home=$(POCKETD_HOME) tx gateway stake-gateway 1000upokt --keyring-backend test --from $(GATEWAY) --node $(POCKET_NODE)`, +$ poktrolld --home=$(POCKETD_HOME) tx gateway stake-gateway 1000upokt --keyring-backend test --from $(GATEWAY) --node $(POCKET_NODE)`, Args: cobra.ExactArgs(1), RunE: func(cmd *cobra.Command, args []string) (err error) { clientCtx, err := client.GetClientTxContext(cmd) diff --git a/x/gateway/client/cli/tx_stake_gateway_test.go b/x/gateway/client/cli/tx_stake_gateway_test.go index 1d9d43e57..ce1fb39d0 100644 --- a/x/gateway/client/cli/tx_stake_gateway_test.go +++ b/x/gateway/client/cli/tx_stake_gateway_test.go @@ -13,9 +13,9 @@ import ( "github.com/stretchr/testify/require" "google.golang.org/grpc/status" - "pocket/testutil/network" - "pocket/x/gateway/client/cli" - "pocket/x/gateway/types" + "github.com/pokt-network/poktroll/testutil/network" + "github.com/pokt-network/poktroll/x/gateway/client/cli" + "github.com/pokt-network/poktroll/x/gateway/types" ) func TestCLI_StakeGateway(t *testing.T) { diff --git a/x/gateway/client/cli/tx_unstake_gateway.go b/x/gateway/client/cli/tx_unstake_gateway.go index 28bfa0623..e417b7540 100644 --- a/x/gateway/client/cli/tx_unstake_gateway.go +++ b/x/gateway/client/cli/tx_unstake_gateway.go @@ -8,7 +8,7 @@ import ( "github.com/cosmos/cosmos-sdk/client/tx" "github.com/spf13/cobra" - "pocket/x/gateway/types" + "github.com/pokt-network/poktroll/x/gateway/types" ) var _ = strconv.Itoa(0) @@ -21,7 +21,7 @@ func CmdUnstakeGateway() *cobra.Command { Long: `Unstake a gateway. This is a broadcast operation that will unstake the gateway specified by the 'from' address. Example: -$ pocketd --home=$(POCKETD_HOME) tx gateway unstake-gateway --keyring-backend test --from $(GATEWAY) --node $(POCKET_NODE)`, +$ poktrolld --home=$(POCKETD_HOME) tx gateway unstake-gateway --keyring-backend test --from $(GATEWAY) --node $(POCKET_NODE)`, Args: cobra.ExactArgs(0), RunE: func(cmd *cobra.Command, _ []string) (err error) { clientCtx, err := client.GetClientTxContext(cmd) diff --git a/x/gateway/client/cli/tx_unstake_gateway_test.go b/x/gateway/client/cli/tx_unstake_gateway_test.go index 2bd58b552..b0aa9fc16 100644 --- a/x/gateway/client/cli/tx_unstake_gateway_test.go +++ b/x/gateway/client/cli/tx_unstake_gateway_test.go @@ -13,9 +13,9 @@ import ( "github.com/stretchr/testify/require" "google.golang.org/grpc/status" - "pocket/testutil/network" - "pocket/x/gateway/client/cli" - "pocket/x/gateway/types" + "github.com/pokt-network/poktroll/testutil/network" + "github.com/pokt-network/poktroll/x/gateway/client/cli" + "github.com/pokt-network/poktroll/x/gateway/types" ) func TestCLI_UnstakeGateway(t *testing.T) { diff --git a/x/gateway/genesis.go b/x/gateway/genesis.go index 060183c3d..eb9791d31 100644 --- a/x/gateway/genesis.go +++ b/x/gateway/genesis.go @@ -3,8 +3,8 @@ package gateway import ( sdk "github.com/cosmos/cosmos-sdk/types" - "pocket/x/gateway/keeper" - "pocket/x/gateway/types" + "github.com/pokt-network/poktroll/x/gateway/keeper" + "github.com/pokt-network/poktroll/x/gateway/types" ) // InitGenesis initializes the module's state from a provided genesis state. diff --git a/x/gateway/genesis_test.go b/x/gateway/genesis_test.go index ad8ff9d51..2a020c7cb 100644 --- a/x/gateway/genesis_test.go +++ b/x/gateway/genesis_test.go @@ -4,10 +4,11 @@ import ( "testing" "github.com/stretchr/testify/require" - keepertest "pocket/testutil/keeper" - "pocket/testutil/nullify" - "pocket/x/gateway" - "pocket/x/gateway/types" + + keepertest "github.com/pokt-network/poktroll/testutil/keeper" + "github.com/pokt-network/poktroll/testutil/nullify" + "github.com/pokt-network/poktroll/x/gateway" + "github.com/pokt-network/poktroll/x/gateway/types" ) func TestGenesis(t *testing.T) { diff --git a/x/gateway/keeper/gateway.go b/x/gateway/keeper/gateway.go index 2e6dca3de..4cb1e5092 100644 --- a/x/gateway/keeper/gateway.go +++ b/x/gateway/keeper/gateway.go @@ -3,7 +3,8 @@ package keeper import ( "github.com/cosmos/cosmos-sdk/store/prefix" sdk "github.com/cosmos/cosmos-sdk/types" - "pocket/x/gateway/types" + + "github.com/pokt-network/poktroll/x/gateway/types" ) // SetGateway set a specific gateway in the store from its index diff --git a/x/gateway/keeper/gateway_test.go b/x/gateway/keeper/gateway_test.go index b61928b70..b343591a0 100644 --- a/x/gateway/keeper/gateway_test.go +++ b/x/gateway/keeper/gateway_test.go @@ -4,11 +4,11 @@ import ( "strconv" "testing" - "pocket/cmd/pocketd/cmd" - keepertest "pocket/testutil/keeper" - "pocket/testutil/nullify" - "pocket/x/gateway/keeper" - "pocket/x/gateway/types" + "github.com/pokt-network/poktroll/cmd/pocketd/cmd" + keepertest "github.com/pokt-network/poktroll/testutil/keeper" + "github.com/pokt-network/poktroll/testutil/nullify" + "github.com/pokt-network/poktroll/x/gateway/keeper" + "github.com/pokt-network/poktroll/x/gateway/types" sdk "github.com/cosmos/cosmos-sdk/types" authtypes "github.com/cosmos/cosmos-sdk/x/auth/types" diff --git a/x/gateway/keeper/keeper.go b/x/gateway/keeper/keeper.go index c132d3a48..fcc143222 100644 --- a/x/gateway/keeper/keeper.go +++ b/x/gateway/keeper/keeper.go @@ -9,7 +9,7 @@ import ( sdk "github.com/cosmos/cosmos-sdk/types" paramtypes "github.com/cosmos/cosmos-sdk/x/params/types" - "pocket/x/gateway/types" + "github.com/pokt-network/poktroll/x/gateway/types" ) type ( @@ -19,8 +19,7 @@ type ( memKey storetypes.StoreKey paramstore paramtypes.Subspace - bankKeeper types.BankKeeper - accountKeeper types.AccountKeeper + bankKeeper types.BankKeeper } ) @@ -31,7 +30,6 @@ func NewKeeper( ps paramtypes.Subspace, bankKeeper types.BankKeeper, - accountKeeper types.AccountKeeper, ) *Keeper { // set KeyTable if it has not already been set if !ps.HasKeyTable() { @@ -44,8 +42,7 @@ func NewKeeper( memKey: memKey, paramstore: ps, - bankKeeper: bankKeeper, - accountKeeper: accountKeeper, + bankKeeper: bankKeeper, } } diff --git a/x/gateway/keeper/msg_server.go b/x/gateway/keeper/msg_server.go index 9c9e6c757..fafeff27b 100644 --- a/x/gateway/keeper/msg_server.go +++ b/x/gateway/keeper/msg_server.go @@ -1,7 +1,7 @@ package keeper import ( - "pocket/x/gateway/types" + "github.com/pokt-network/poktroll/x/gateway/types" ) type msgServer struct { diff --git a/x/gateway/keeper/msg_server_stake_gateway.go b/x/gateway/keeper/msg_server_stake_gateway.go index a1a893d41..919e0f4dd 100644 --- a/x/gateway/keeper/msg_server_stake_gateway.go +++ b/x/gateway/keeper/msg_server_stake_gateway.go @@ -6,7 +6,7 @@ import ( sdkerrors "cosmossdk.io/errors" sdk "github.com/cosmos/cosmos-sdk/types" - "pocket/x/gateway/types" + "github.com/pokt-network/poktroll/x/gateway/types" ) func (k msgServer) StakeGateway( diff --git a/x/gateway/keeper/msg_server_stake_gateway_test.go b/x/gateway/keeper/msg_server_stake_gateway_test.go index 92c787aef..597cc76d2 100644 --- a/x/gateway/keeper/msg_server_stake_gateway_test.go +++ b/x/gateway/keeper/msg_server_stake_gateway_test.go @@ -6,10 +6,10 @@ import ( sdk "github.com/cosmos/cosmos-sdk/types" "github.com/stretchr/testify/require" - keepertest "pocket/testutil/keeper" - "pocket/testutil/sample" - "pocket/x/gateway/keeper" - "pocket/x/gateway/types" + keepertest "github.com/pokt-network/poktroll/testutil/keeper" + "github.com/pokt-network/poktroll/testutil/sample" + "github.com/pokt-network/poktroll/x/gateway/keeper" + "github.com/pokt-network/poktroll/x/gateway/types" ) func TestMsgServer_StakeGateway_SuccessfulCreateAndUpdate(t *testing.T) { diff --git a/x/gateway/keeper/msg_server_test.go b/x/gateway/keeper/msg_server_test.go index fc97c41e2..598dda0e5 100644 --- a/x/gateway/keeper/msg_server_test.go +++ b/x/gateway/keeper/msg_server_test.go @@ -6,9 +6,10 @@ import ( sdk "github.com/cosmos/cosmos-sdk/types" "github.com/stretchr/testify/require" - keepertest "pocket/testutil/keeper" - "pocket/x/gateway/keeper" - "pocket/x/gateway/types" + + keepertest "github.com/pokt-network/poktroll/testutil/keeper" + "github.com/pokt-network/poktroll/x/gateway/keeper" + "github.com/pokt-network/poktroll/x/gateway/types" ) func setupMsgServer(t testing.TB) (types.MsgServer, context.Context) { diff --git a/x/gateway/keeper/msg_server_unstake_gateway.go b/x/gateway/keeper/msg_server_unstake_gateway.go index b27f89dfd..16c913693 100644 --- a/x/gateway/keeper/msg_server_unstake_gateway.go +++ b/x/gateway/keeper/msg_server_unstake_gateway.go @@ -5,7 +5,7 @@ import ( sdk "github.com/cosmos/cosmos-sdk/types" - "pocket/x/gateway/types" + "github.com/pokt-network/poktroll/x/gateway/types" ) // TODO_TECHDEBT(#49): Add un-delegation from delegated apps diff --git a/x/gateway/keeper/msg_server_unstake_gateway_test.go b/x/gateway/keeper/msg_server_unstake_gateway_test.go index e4d7e5e4d..421316b78 100644 --- a/x/gateway/keeper/msg_server_unstake_gateway_test.go +++ b/x/gateway/keeper/msg_server_unstake_gateway_test.go @@ -6,10 +6,10 @@ import ( sdk "github.com/cosmos/cosmos-sdk/types" "github.com/stretchr/testify/require" - keepertest "pocket/testutil/keeper" - "pocket/testutil/sample" - "pocket/x/gateway/keeper" - "pocket/x/gateway/types" + keepertest "github.com/pokt-network/poktroll/testutil/keeper" + "github.com/pokt-network/poktroll/testutil/sample" + "github.com/pokt-network/poktroll/x/gateway/keeper" + "github.com/pokt-network/poktroll/x/gateway/types" ) func TestMsgServer_UnstakeGateway_Success(t *testing.T) { diff --git a/x/gateway/keeper/params.go b/x/gateway/keeper/params.go index d4fc473ed..e16780bc5 100644 --- a/x/gateway/keeper/params.go +++ b/x/gateway/keeper/params.go @@ -2,7 +2,8 @@ package keeper import ( sdk "github.com/cosmos/cosmos-sdk/types" - "pocket/x/gateway/types" + + "github.com/pokt-network/poktroll/x/gateway/types" ) // GetParams get all parameters as types.Params diff --git a/x/gateway/keeper/params_test.go b/x/gateway/keeper/params_test.go index 171bb5d30..748d837cf 100644 --- a/x/gateway/keeper/params_test.go +++ b/x/gateway/keeper/params_test.go @@ -4,8 +4,9 @@ import ( "testing" "github.com/stretchr/testify/require" - testkeeper "pocket/testutil/keeper" - "pocket/x/gateway/types" + + testkeeper "github.com/pokt-network/poktroll/testutil/keeper" + "github.com/pokt-network/poktroll/x/gateway/types" ) func TestGetParams(t *testing.T) { diff --git a/x/gateway/keeper/query.go b/x/gateway/keeper/query.go index 2b7887a31..ffa80d00d 100644 --- a/x/gateway/keeper/query.go +++ b/x/gateway/keeper/query.go @@ -1,7 +1,7 @@ package keeper import ( - "pocket/x/gateway/types" + "github.com/pokt-network/poktroll/x/gateway/types" ) var _ types.QueryServer = Keeper{} diff --git a/x/gateway/keeper/query_gateway.go b/x/gateway/keeper/query_gateway.go index 0cd5e651d..bd0576a2d 100644 --- a/x/gateway/keeper/query_gateway.go +++ b/x/gateway/keeper/query_gateway.go @@ -8,7 +8,8 @@ import ( "github.com/cosmos/cosmos-sdk/types/query" "google.golang.org/grpc/codes" "google.golang.org/grpc/status" - "pocket/x/gateway/types" + + "github.com/pokt-network/poktroll/x/gateway/types" ) func (k Keeper) GatewayAll(goCtx context.Context, req *types.QueryAllGatewayRequest) (*types.QueryAllGatewayResponse, error) { diff --git a/x/gateway/keeper/query_gateway_test.go b/x/gateway/keeper/query_gateway_test.go index 3597a6965..9d44de980 100644 --- a/x/gateway/keeper/query_gateway_test.go +++ b/x/gateway/keeper/query_gateway_test.go @@ -10,9 +10,9 @@ import ( "google.golang.org/grpc/codes" "google.golang.org/grpc/status" - keepertest "pocket/testutil/keeper" - "pocket/testutil/nullify" - "pocket/x/gateway/types" + keepertest "github.com/pokt-network/poktroll/testutil/keeper" + "github.com/pokt-network/poktroll/testutil/nullify" + "github.com/pokt-network/poktroll/x/gateway/types" ) // Prevent strconv unused error diff --git a/x/gateway/keeper/query_params.go b/x/gateway/keeper/query_params.go index 188c71c02..8d3afe26d 100644 --- a/x/gateway/keeper/query_params.go +++ b/x/gateway/keeper/query_params.go @@ -6,7 +6,8 @@ import ( sdk "github.com/cosmos/cosmos-sdk/types" "google.golang.org/grpc/codes" "google.golang.org/grpc/status" - "pocket/x/gateway/types" + + "github.com/pokt-network/poktroll/x/gateway/types" ) func (k Keeper) Params(goCtx context.Context, req *types.QueryParamsRequest) (*types.QueryParamsResponse, error) { diff --git a/x/gateway/keeper/query_params_test.go b/x/gateway/keeper/query_params_test.go index b2fa1f57d..062f049ff 100644 --- a/x/gateway/keeper/query_params_test.go +++ b/x/gateway/keeper/query_params_test.go @@ -5,8 +5,9 @@ import ( sdk "github.com/cosmos/cosmos-sdk/types" "github.com/stretchr/testify/require" - testkeeper "pocket/testutil/keeper" - "pocket/x/gateway/types" + + testkeeper "github.com/pokt-network/poktroll/testutil/keeper" + "github.com/pokt-network/poktroll/x/gateway/types" ) func TestParamsQuery(t *testing.T) { diff --git a/x/gateway/module.go b/x/gateway/module.go index e1aacb5d9..86f16fb7c 100644 --- a/x/gateway/module.go +++ b/x/gateway/module.go @@ -4,6 +4,7 @@ import ( "context" "encoding/json" "fmt" + // this line is used by starport scaffolding # 1 "github.com/grpc-ecosystem/grpc-gateway/runtime" @@ -16,9 +17,10 @@ import ( cdctypes "github.com/cosmos/cosmos-sdk/codec/types" sdk "github.com/cosmos/cosmos-sdk/types" "github.com/cosmos/cosmos-sdk/types/module" - "pocket/x/gateway/client/cli" - "pocket/x/gateway/keeper" - "pocket/x/gateway/types" + + "github.com/pokt-network/poktroll/x/gateway/client/cli" + "github.com/pokt-network/poktroll/x/gateway/keeper" + "github.com/pokt-network/poktroll/x/gateway/types" ) var ( diff --git a/x/gateway/module_simulation.go b/x/gateway/module_simulation.go index e6eac7056..0aab36ae9 100644 --- a/x/gateway/module_simulation.go +++ b/x/gateway/module_simulation.go @@ -3,9 +3,9 @@ package gateway import ( "math/rand" - "pocket/testutil/sample" - gatewaysimulation "pocket/x/gateway/simulation" - "pocket/x/gateway/types" + "github.com/pokt-network/poktroll/testutil/sample" + gatewaysimulation "github.com/pokt-network/poktroll/x/gateway/simulation" + "github.com/pokt-network/poktroll/x/gateway/types" "github.com/cosmos/cosmos-sdk/baseapp" sdk "github.com/cosmos/cosmos-sdk/types" diff --git a/x/gateway/simulation/stake_gateway.go b/x/gateway/simulation/stake_gateway.go index 986035de2..9a76e82fc 100644 --- a/x/gateway/simulation/stake_gateway.go +++ b/x/gateway/simulation/stake_gateway.go @@ -7,8 +7,8 @@ import ( sdk "github.com/cosmos/cosmos-sdk/types" simtypes "github.com/cosmos/cosmos-sdk/types/simulation" - "pocket/x/gateway/keeper" - "pocket/x/gateway/types" + "github.com/pokt-network/poktroll/x/gateway/keeper" + "github.com/pokt-network/poktroll/x/gateway/types" ) func SimulateMsgStakeGateway( diff --git a/x/gateway/simulation/unstake_gateway.go b/x/gateway/simulation/unstake_gateway.go index aaf9c3543..e82ebf748 100644 --- a/x/gateway/simulation/unstake_gateway.go +++ b/x/gateway/simulation/unstake_gateway.go @@ -6,8 +6,9 @@ import ( "github.com/cosmos/cosmos-sdk/baseapp" sdk "github.com/cosmos/cosmos-sdk/types" simtypes "github.com/cosmos/cosmos-sdk/types/simulation" - "pocket/x/gateway/keeper" - "pocket/x/gateway/types" + + "github.com/pokt-network/poktroll/x/gateway/keeper" + "github.com/pokt-network/poktroll/x/gateway/types" ) func SimulateMsgUnstakeGateway( diff --git a/x/gateway/types/genesis_test.go b/x/gateway/types/genesis_test.go index e08ce6f13..ea1e6cdf8 100644 --- a/x/gateway/types/genesis_test.go +++ b/x/gateway/types/genesis_test.go @@ -3,8 +3,8 @@ package types_test import ( "testing" - "pocket/testutil/sample" - "pocket/x/gateway/types" + "github.com/pokt-network/poktroll/testutil/sample" + "github.com/pokt-network/poktroll/x/gateway/types" sdk "github.com/cosmos/cosmos-sdk/types" "github.com/stretchr/testify/require" diff --git a/x/gateway/types/message_stake_gateway_test.go b/x/gateway/types/message_stake_gateway_test.go index 584a08ad7..94299afaa 100644 --- a/x/gateway/types/message_stake_gateway_test.go +++ b/x/gateway/types/message_stake_gateway_test.go @@ -6,7 +6,7 @@ import ( sdk "github.com/cosmos/cosmos-sdk/types" "github.com/stretchr/testify/require" - "pocket/testutil/sample" + "github.com/pokt-network/poktroll/testutil/sample" ) func TestMsgStakeGateway_ValidateBasic(t *testing.T) { diff --git a/x/gateway/types/message_unstake_gateway_test.go b/x/gateway/types/message_unstake_gateway_test.go index ded54f1a5..759aa11d7 100644 --- a/x/gateway/types/message_unstake_gateway_test.go +++ b/x/gateway/types/message_unstake_gateway_test.go @@ -5,7 +5,7 @@ import ( "github.com/stretchr/testify/require" - "pocket/testutil/sample" + "github.com/pokt-network/poktroll/testutil/sample" ) func TestMsgUnstakeGateway_ValidateBasic(t *testing.T) { diff --git a/x/pocket/client/cli/query.go b/x/pocket/client/cli/query.go index d4206d0e4..7fb94d626 100644 --- a/x/pocket/client/cli/query.go +++ b/x/pocket/client/cli/query.go @@ -10,7 +10,7 @@ import ( // "github.com/cosmos/cosmos-sdk/client/flags" // sdk "github.com/cosmos/cosmos-sdk/types" - "pocket/x/pocket/types" + "github.com/pokt-network/poktroll/x/pocket/types" ) // GetQueryCmd returns the cli query commands for this module diff --git a/x/pocket/client/cli/query_params.go b/x/pocket/client/cli/query_params.go index c1c31f62a..8461c3c60 100644 --- a/x/pocket/client/cli/query_params.go +++ b/x/pocket/client/cli/query_params.go @@ -5,7 +5,7 @@ import ( "github.com/cosmos/cosmos-sdk/client/flags" "github.com/spf13/cobra" - "pocket/x/pocket/types" + "github.com/pokt-network/poktroll/x/pocket/types" ) func CmdQueryParams() *cobra.Command { diff --git a/x/pocket/client/cli/tx.go b/x/pocket/client/cli/tx.go index 06b2b0eb0..70032b9b8 100644 --- a/x/pocket/client/cli/tx.go +++ b/x/pocket/client/cli/tx.go @@ -8,7 +8,7 @@ import ( "github.com/cosmos/cosmos-sdk/client" // "github.com/cosmos/cosmos-sdk/client/flags" - "pocket/x/pocket/types" + "github.com/pokt-network/poktroll/x/pocket/types" ) var ( diff --git a/x/pocket/genesis.go b/x/pocket/genesis.go index 23992c930..1a6b8738d 100644 --- a/x/pocket/genesis.go +++ b/x/pocket/genesis.go @@ -2,8 +2,9 @@ package pocket import ( sdk "github.com/cosmos/cosmos-sdk/types" - "pocket/x/pocket/keeper" - "pocket/x/pocket/types" + + "github.com/pokt-network/poktroll/x/pocket/keeper" + "github.com/pokt-network/poktroll/x/pocket/types" ) // InitGenesis initializes the module's state from a provided genesis state. diff --git a/x/pocket/genesis_test.go b/x/pocket/genesis_test.go index 8da506367..8e5f5ba18 100644 --- a/x/pocket/genesis_test.go +++ b/x/pocket/genesis_test.go @@ -4,10 +4,11 @@ import ( "testing" "github.com/stretchr/testify/require" - keepertest "pocket/testutil/keeper" - "pocket/testutil/nullify" - "pocket/x/pocket" - "pocket/x/pocket/types" + + keepertest "github.com/pokt-network/poktroll/testutil/keeper" + "github.com/pokt-network/poktroll/testutil/nullify" + "github.com/pokt-network/poktroll/x/pocket" + "github.com/pokt-network/poktroll/x/pocket/types" ) func TestGenesis(t *testing.T) { diff --git a/x/pocket/keeper/keeper.go b/x/pocket/keeper/keeper.go index b5fb8a2be..44114c392 100644 --- a/x/pocket/keeper/keeper.go +++ b/x/pocket/keeper/keeper.go @@ -9,7 +9,7 @@ import ( sdk "github.com/cosmos/cosmos-sdk/types" paramtypes "github.com/cosmos/cosmos-sdk/x/params/types" - "pocket/x/pocket/types" + "github.com/pokt-network/poktroll/x/pocket/types" ) type ( diff --git a/x/pocket/keeper/msg_server.go b/x/pocket/keeper/msg_server.go index 1916656f5..310ebb2e5 100644 --- a/x/pocket/keeper/msg_server.go +++ b/x/pocket/keeper/msg_server.go @@ -1,7 +1,7 @@ package keeper import ( - "pocket/x/pocket/types" + "github.com/pokt-network/poktroll/x/pocket/types" ) type msgServer struct { diff --git a/x/pocket/keeper/msg_server_test.go b/x/pocket/keeper/msg_server_test.go index cc89704f3..e27eda21b 100644 --- a/x/pocket/keeper/msg_server_test.go +++ b/x/pocket/keeper/msg_server_test.go @@ -6,9 +6,10 @@ import ( sdk "github.com/cosmos/cosmos-sdk/types" "github.com/stretchr/testify/require" - keepertest "pocket/testutil/keeper" - "pocket/x/pocket/keeper" - "pocket/x/pocket/types" + + keepertest "github.com/pokt-network/poktroll/testutil/keeper" + "github.com/pokt-network/poktroll/x/pocket/keeper" + "github.com/pokt-network/poktroll/x/pocket/types" ) func setupMsgServer(t testing.TB) (types.MsgServer, context.Context) { diff --git a/x/pocket/keeper/params.go b/x/pocket/keeper/params.go index 8527fa7c9..b4b197932 100644 --- a/x/pocket/keeper/params.go +++ b/x/pocket/keeper/params.go @@ -2,7 +2,8 @@ package keeper import ( sdk "github.com/cosmos/cosmos-sdk/types" - "pocket/x/pocket/types" + + "github.com/pokt-network/poktroll/x/pocket/types" ) // GetParams get all parameters as types.Params diff --git a/x/pocket/keeper/params_test.go b/x/pocket/keeper/params_test.go index feb90768d..06afe192c 100644 --- a/x/pocket/keeper/params_test.go +++ b/x/pocket/keeper/params_test.go @@ -4,8 +4,9 @@ import ( "testing" "github.com/stretchr/testify/require" - testkeeper "pocket/testutil/keeper" - "pocket/x/pocket/types" + + testkeeper "github.com/pokt-network/poktroll/testutil/keeper" + "github.com/pokt-network/poktroll/x/pocket/types" ) func TestGetParams(t *testing.T) { diff --git a/x/pocket/keeper/query.go b/x/pocket/keeper/query.go index dd2416549..24adda2bb 100644 --- a/x/pocket/keeper/query.go +++ b/x/pocket/keeper/query.go @@ -1,7 +1,7 @@ package keeper import ( - "pocket/x/pocket/types" + "github.com/pokt-network/poktroll/x/pocket/types" ) var _ types.QueryServer = Keeper{} diff --git a/x/pocket/keeper/query_params.go b/x/pocket/keeper/query_params.go index b3af6fbbd..3fdff1051 100644 --- a/x/pocket/keeper/query_params.go +++ b/x/pocket/keeper/query_params.go @@ -6,7 +6,8 @@ import ( sdk "github.com/cosmos/cosmos-sdk/types" "google.golang.org/grpc/codes" "google.golang.org/grpc/status" - "pocket/x/pocket/types" + + "github.com/pokt-network/poktroll/x/pocket/types" ) func (k Keeper) Params(goCtx context.Context, req *types.QueryParamsRequest) (*types.QueryParamsResponse, error) { diff --git a/x/pocket/keeper/query_params_test.go b/x/pocket/keeper/query_params_test.go index a91d8d93a..d40a517f4 100644 --- a/x/pocket/keeper/query_params_test.go +++ b/x/pocket/keeper/query_params_test.go @@ -5,8 +5,9 @@ import ( sdk "github.com/cosmos/cosmos-sdk/types" "github.com/stretchr/testify/require" - testkeeper "pocket/testutil/keeper" - "pocket/x/pocket/types" + + testkeeper "github.com/pokt-network/poktroll/testutil/keeper" + "github.com/pokt-network/poktroll/x/pocket/types" ) func TestParamsQuery(t *testing.T) { diff --git a/x/pocket/module.go b/x/pocket/module.go index 83daf8b91..684ea1d95 100644 --- a/x/pocket/module.go +++ b/x/pocket/module.go @@ -4,6 +4,7 @@ import ( "context" "encoding/json" "fmt" + // this line is used by starport scaffolding # 1 "github.com/grpc-ecosystem/grpc-gateway/runtime" @@ -16,9 +17,10 @@ import ( cdctypes "github.com/cosmos/cosmos-sdk/codec/types" sdk "github.com/cosmos/cosmos-sdk/types" "github.com/cosmos/cosmos-sdk/types/module" - "pocket/x/pocket/client/cli" - "pocket/x/pocket/keeper" - "pocket/x/pocket/types" + + "github.com/pokt-network/poktroll/x/pocket/client/cli" + "github.com/pokt-network/poktroll/x/pocket/keeper" + "github.com/pokt-network/poktroll/x/pocket/types" ) var ( diff --git a/x/pocket/module_simulation.go b/x/pocket/module_simulation.go index 0b5db9a81..5d5746526 100644 --- a/x/pocket/module_simulation.go +++ b/x/pocket/module_simulation.go @@ -8,9 +8,10 @@ import ( "github.com/cosmos/cosmos-sdk/types/module" simtypes "github.com/cosmos/cosmos-sdk/types/simulation" "github.com/cosmos/cosmos-sdk/x/simulation" - "pocket/testutil/sample" - pocketsimulation "pocket/x/pocket/simulation" - "pocket/x/pocket/types" + + "github.com/pokt-network/poktroll/testutil/sample" + pocketsimulation "github.com/pokt-network/poktroll/x/pocket/simulation" + "github.com/pokt-network/poktroll/x/pocket/types" ) // avoid unused import issue diff --git a/x/pocket/types/codec.go b/x/pocket/types/codec.go index 844157a87..72399f81e 100644 --- a/x/pocket/types/codec.go +++ b/x/pocket/types/codec.go @@ -3,8 +3,8 @@ package types import ( "github.com/cosmos/cosmos-sdk/codec" cdctypes "github.com/cosmos/cosmos-sdk/codec/types" - // this line is used by starport scaffolding # 1 "github.com/cosmos/cosmos-sdk/types/msgservice" + // this line is used by starport scaffolding # 1 ) func RegisterCodec(cdc *codec.LegacyAmino) { diff --git a/x/pocket/types/genesis_test.go b/x/pocket/types/genesis_test.go index d09e041a2..c2eb457a8 100644 --- a/x/pocket/types/genesis_test.go +++ b/x/pocket/types/genesis_test.go @@ -4,7 +4,8 @@ import ( "testing" "github.com/stretchr/testify/require" - "pocket/x/pocket/types" + + "github.com/pokt-network/poktroll/x/pocket/types" ) func TestGenesisState_Validate(t *testing.T) { diff --git a/x/service/client/cli/query.go b/x/service/client/cli/query.go index 9d7cd5323..f52a37c7a 100644 --- a/x/service/client/cli/query.go +++ b/x/service/client/cli/query.go @@ -10,7 +10,7 @@ import ( // "github.com/cosmos/cosmos-sdk/client/flags" // sdk "github.com/cosmos/cosmos-sdk/types" - "pocket/x/service/types" + "github.com/pokt-network/poktroll/x/service/types" ) // GetQueryCmd returns the cli query commands for this module diff --git a/x/service/client/cli/query_params.go b/x/service/client/cli/query_params.go index 18bf13450..cd99b673f 100644 --- a/x/service/client/cli/query_params.go +++ b/x/service/client/cli/query_params.go @@ -5,7 +5,7 @@ import ( "github.com/cosmos/cosmos-sdk/client/flags" "github.com/spf13/cobra" - "pocket/x/service/types" + "github.com/pokt-network/poktroll/x/service/types" ) func CmdQueryParams() *cobra.Command { diff --git a/x/service/client/cli/tx.go b/x/service/client/cli/tx.go index e5c1c6ef5..b4c63f360 100644 --- a/x/service/client/cli/tx.go +++ b/x/service/client/cli/tx.go @@ -8,7 +8,7 @@ import ( "github.com/cosmos/cosmos-sdk/client" // "github.com/cosmos/cosmos-sdk/client/flags" - "pocket/x/service/types" + "github.com/pokt-network/poktroll/x/service/types" ) var ( diff --git a/x/service/genesis.go b/x/service/genesis.go index d46f97436..a0c3211ba 100644 --- a/x/service/genesis.go +++ b/x/service/genesis.go @@ -2,8 +2,9 @@ package service import ( sdk "github.com/cosmos/cosmos-sdk/types" - "pocket/x/service/keeper" - "pocket/x/service/types" + + "github.com/pokt-network/poktroll/x/service/keeper" + "github.com/pokt-network/poktroll/x/service/types" ) // InitGenesis initializes the module's state from a provided genesis state. diff --git a/x/service/genesis_test.go b/x/service/genesis_test.go index a1ff0b888..4eed97699 100644 --- a/x/service/genesis_test.go +++ b/x/service/genesis_test.go @@ -4,10 +4,11 @@ import ( "testing" "github.com/stretchr/testify/require" - keepertest "pocket/testutil/keeper" - "pocket/testutil/nullify" - "pocket/x/service" - "pocket/x/service/types" + + keepertest "github.com/pokt-network/poktroll/testutil/keeper" + "github.com/pokt-network/poktroll/testutil/nullify" + "github.com/pokt-network/poktroll/x/service" + "github.com/pokt-network/poktroll/x/service/types" ) func TestGenesis(t *testing.T) { diff --git a/x/service/keeper/keeper.go b/x/service/keeper/keeper.go index f81d8d56c..fb4409d2f 100644 --- a/x/service/keeper/keeper.go +++ b/x/service/keeper/keeper.go @@ -9,7 +9,7 @@ import ( sdk "github.com/cosmos/cosmos-sdk/types" paramtypes "github.com/cosmos/cosmos-sdk/x/params/types" - "pocket/x/service/types" + "github.com/pokt-network/poktroll/x/service/types" ) type ( diff --git a/x/service/keeper/msg_server.go b/x/service/keeper/msg_server.go index 422d49a12..e5f891b01 100644 --- a/x/service/keeper/msg_server.go +++ b/x/service/keeper/msg_server.go @@ -1,7 +1,7 @@ package keeper import ( - "pocket/x/service/types" + "github.com/pokt-network/poktroll/x/service/types" ) type msgServer struct { diff --git a/x/service/keeper/msg_server_test.go b/x/service/keeper/msg_server_test.go index a9d15f912..78c80d8e6 100644 --- a/x/service/keeper/msg_server_test.go +++ b/x/service/keeper/msg_server_test.go @@ -6,9 +6,10 @@ import ( sdk "github.com/cosmos/cosmos-sdk/types" "github.com/stretchr/testify/require" - keepertest "pocket/testutil/keeper" - "pocket/x/service/keeper" - "pocket/x/service/types" + + keepertest "github.com/pokt-network/poktroll/testutil/keeper" + "github.com/pokt-network/poktroll/x/service/keeper" + "github.com/pokt-network/poktroll/x/service/types" ) func setupMsgServer(t testing.TB) (types.MsgServer, context.Context) { diff --git a/x/service/keeper/params.go b/x/service/keeper/params.go index 34a1a742c..948fec7a1 100644 --- a/x/service/keeper/params.go +++ b/x/service/keeper/params.go @@ -2,7 +2,8 @@ package keeper import ( sdk "github.com/cosmos/cosmos-sdk/types" - "pocket/x/service/types" + + "github.com/pokt-network/poktroll/x/service/types" ) // GetParams get all parameters as types.Params diff --git a/x/service/keeper/params_test.go b/x/service/keeper/params_test.go index 3a7b4aee3..097742cd1 100644 --- a/x/service/keeper/params_test.go +++ b/x/service/keeper/params_test.go @@ -4,8 +4,9 @@ import ( "testing" "github.com/stretchr/testify/require" - testkeeper "pocket/testutil/keeper" - "pocket/x/service/types" + + testkeeper "github.com/pokt-network/poktroll/testutil/keeper" + "github.com/pokt-network/poktroll/x/service/types" ) func TestGetParams(t *testing.T) { diff --git a/x/service/keeper/query.go b/x/service/keeper/query.go index a0bfe1122..ac3116f9a 100644 --- a/x/service/keeper/query.go +++ b/x/service/keeper/query.go @@ -1,7 +1,7 @@ package keeper import ( - "pocket/x/service/types" + "github.com/pokt-network/poktroll/x/service/types" ) var _ types.QueryServer = Keeper{} diff --git a/x/service/keeper/query_params.go b/x/service/keeper/query_params.go index 6068e16c6..c04f32dd9 100644 --- a/x/service/keeper/query_params.go +++ b/x/service/keeper/query_params.go @@ -6,7 +6,8 @@ import ( sdk "github.com/cosmos/cosmos-sdk/types" "google.golang.org/grpc/codes" "google.golang.org/grpc/status" - "pocket/x/service/types" + + "github.com/pokt-network/poktroll/x/service/types" ) func (k Keeper) Params(goCtx context.Context, req *types.QueryParamsRequest) (*types.QueryParamsResponse, error) { diff --git a/x/service/keeper/query_params_test.go b/x/service/keeper/query_params_test.go index 239a6799c..243a9ed80 100644 --- a/x/service/keeper/query_params_test.go +++ b/x/service/keeper/query_params_test.go @@ -5,8 +5,9 @@ import ( sdk "github.com/cosmos/cosmos-sdk/types" "github.com/stretchr/testify/require" - testkeeper "pocket/testutil/keeper" - "pocket/x/service/types" + + testkeeper "github.com/pokt-network/poktroll/testutil/keeper" + "github.com/pokt-network/poktroll/x/service/types" ) func TestParamsQuery(t *testing.T) { diff --git a/x/service/module.go b/x/service/module.go index 4a02e77d5..a7ebd35f2 100644 --- a/x/service/module.go +++ b/x/service/module.go @@ -4,6 +4,7 @@ import ( "context" "encoding/json" "fmt" + // this line is used by starport scaffolding # 1 "github.com/grpc-ecosystem/grpc-gateway/runtime" @@ -16,9 +17,10 @@ import ( cdctypes "github.com/cosmos/cosmos-sdk/codec/types" sdk "github.com/cosmos/cosmos-sdk/types" "github.com/cosmos/cosmos-sdk/types/module" - "pocket/x/service/client/cli" - "pocket/x/service/keeper" - "pocket/x/service/types" + + "github.com/pokt-network/poktroll/x/service/client/cli" + "github.com/pokt-network/poktroll/x/service/keeper" + "github.com/pokt-network/poktroll/x/service/types" ) var ( diff --git a/x/service/module_simulation.go b/x/service/module_simulation.go index e0d2b39e1..f449b4ce4 100644 --- a/x/service/module_simulation.go +++ b/x/service/module_simulation.go @@ -8,9 +8,10 @@ import ( "github.com/cosmos/cosmos-sdk/types/module" simtypes "github.com/cosmos/cosmos-sdk/types/simulation" "github.com/cosmos/cosmos-sdk/x/simulation" - "pocket/testutil/sample" - servicesimulation "pocket/x/service/simulation" - "pocket/x/service/types" + + "github.com/pokt-network/poktroll/testutil/sample" + servicesimulation "github.com/pokt-network/poktroll/x/service/simulation" + "github.com/pokt-network/poktroll/x/service/types" ) // avoid unused import issue diff --git a/x/service/types/codec.go b/x/service/types/codec.go index 844157a87..72399f81e 100644 --- a/x/service/types/codec.go +++ b/x/service/types/codec.go @@ -3,8 +3,8 @@ package types import ( "github.com/cosmos/cosmos-sdk/codec" cdctypes "github.com/cosmos/cosmos-sdk/codec/types" - // this line is used by starport scaffolding # 1 "github.com/cosmos/cosmos-sdk/types/msgservice" + // this line is used by starport scaffolding # 1 ) func RegisterCodec(cdc *codec.LegacyAmino) { diff --git a/x/service/types/genesis_test.go b/x/service/types/genesis_test.go index 99eafc91c..77e357f78 100644 --- a/x/service/types/genesis_test.go +++ b/x/service/types/genesis_test.go @@ -4,7 +4,8 @@ import ( "testing" "github.com/stretchr/testify/require" - "pocket/x/service/types" + + "github.com/pokt-network/poktroll/x/service/types" ) func TestGenesisState_Validate(t *testing.T) { diff --git a/x/session/client/cli/query.go b/x/session/client/cli/query.go index 3b4376f72..cd6bf6f70 100644 --- a/x/session/client/cli/query.go +++ b/x/session/client/cli/query.go @@ -10,7 +10,7 @@ import ( // "github.com/cosmos/cosmos-sdk/client/flags" // sdk "github.com/cosmos/cosmos-sdk/types" - "pocket/x/session/types" + "github.com/pokt-network/poktroll/x/session/types" ) // GetQueryCmd returns the cli query commands for this module diff --git a/x/session/client/cli/query_get_session.go b/x/session/client/cli/query_get_session.go index deebf1a31..1c21aa881 100644 --- a/x/session/client/cli/query_get_session.go +++ b/x/session/client/cli/query_get_session.go @@ -7,7 +7,7 @@ import ( "github.com/cosmos/cosmos-sdk/client/flags" "github.com/spf13/cobra" - "pocket/x/session/types" + "github.com/pokt-network/poktroll/x/session/types" ) var _ = strconv.Itoa(0) diff --git a/x/session/client/cli/query_params.go b/x/session/client/cli/query_params.go index 9139ba693..5f8aa0609 100644 --- a/x/session/client/cli/query_params.go +++ b/x/session/client/cli/query_params.go @@ -5,7 +5,7 @@ import ( "github.com/cosmos/cosmos-sdk/client/flags" "github.com/spf13/cobra" - "pocket/x/session/types" + "github.com/pokt-network/poktroll/x/session/types" ) func CmdQueryParams() *cobra.Command { diff --git a/x/session/client/cli/tx.go b/x/session/client/cli/tx.go index 5bd4f72aa..248ae5237 100644 --- a/x/session/client/cli/tx.go +++ b/x/session/client/cli/tx.go @@ -8,7 +8,7 @@ import ( "github.com/cosmos/cosmos-sdk/client" // "github.com/cosmos/cosmos-sdk/client/flags" - "pocket/x/session/types" + "github.com/pokt-network/poktroll/x/session/types" ) var ( diff --git a/x/session/genesis.go b/x/session/genesis.go index 6603b6d87..a14b12f31 100644 --- a/x/session/genesis.go +++ b/x/session/genesis.go @@ -2,8 +2,9 @@ package session import ( sdk "github.com/cosmos/cosmos-sdk/types" - "pocket/x/session/keeper" - "pocket/x/session/types" + + "github.com/pokt-network/poktroll/x/session/keeper" + "github.com/pokt-network/poktroll/x/session/types" ) // InitGenesis initializes the module's state from a provided genesis state. diff --git a/x/session/genesis_test.go b/x/session/genesis_test.go index afedb7434..50298ada9 100644 --- a/x/session/genesis_test.go +++ b/x/session/genesis_test.go @@ -4,10 +4,11 @@ import ( "testing" "github.com/stretchr/testify/require" - keepertest "pocket/testutil/keeper" - "pocket/testutil/nullify" - "pocket/x/session" - "pocket/x/session/types" + + keepertest "github.com/pokt-network/poktroll/testutil/keeper" + "github.com/pokt-network/poktroll/testutil/nullify" + "github.com/pokt-network/poktroll/x/session" + "github.com/pokt-network/poktroll/x/session/types" ) func TestGenesis(t *testing.T) { diff --git a/x/session/keeper/keeper.go b/x/session/keeper/keeper.go index 4515d5a1e..292890964 100644 --- a/x/session/keeper/keeper.go +++ b/x/session/keeper/keeper.go @@ -9,7 +9,7 @@ import ( sdk "github.com/cosmos/cosmos-sdk/types" paramtypes "github.com/cosmos/cosmos-sdk/x/params/types" - "pocket/x/session/types" + "github.com/pokt-network/poktroll/x/session/types" ) type ( diff --git a/x/session/keeper/msg_server.go b/x/session/keeper/msg_server.go index 7dcf714c9..6dbe55462 100644 --- a/x/session/keeper/msg_server.go +++ b/x/session/keeper/msg_server.go @@ -1,7 +1,7 @@ package keeper import ( - "pocket/x/session/types" + "github.com/pokt-network/poktroll/x/session/types" ) type msgServer struct { diff --git a/x/session/keeper/msg_server_test.go b/x/session/keeper/msg_server_test.go index 61c82603a..b00a56aba 100644 --- a/x/session/keeper/msg_server_test.go +++ b/x/session/keeper/msg_server_test.go @@ -7,9 +7,9 @@ import ( sdk "github.com/cosmos/cosmos-sdk/types" "github.com/stretchr/testify/require" - keepertest "pocket/testutil/keeper" - "pocket/x/session/keeper" - "pocket/x/session/types" + keepertest "github.com/pokt-network/poktroll/testutil/keeper" + "github.com/pokt-network/poktroll/x/session/keeper" + "github.com/pokt-network/poktroll/x/session/types" ) func setupMsgServer(t testing.TB) (types.MsgServer, context.Context) { diff --git a/x/session/keeper/params.go b/x/session/keeper/params.go index 37247b35e..142887657 100644 --- a/x/session/keeper/params.go +++ b/x/session/keeper/params.go @@ -2,7 +2,8 @@ package keeper import ( sdk "github.com/cosmos/cosmos-sdk/types" - "pocket/x/session/types" + + "github.com/pokt-network/poktroll/x/session/types" ) // GetParams get all parameters as types.Params diff --git a/x/session/keeper/params_test.go b/x/session/keeper/params_test.go index 562c4afd7..bf020e294 100644 --- a/x/session/keeper/params_test.go +++ b/x/session/keeper/params_test.go @@ -4,8 +4,9 @@ import ( "testing" "github.com/stretchr/testify/require" - testkeeper "pocket/testutil/keeper" - "pocket/x/session/types" + + testkeeper "github.com/pokt-network/poktroll/testutil/keeper" + "github.com/pokt-network/poktroll/x/session/types" ) func TestGetParams(t *testing.T) { diff --git a/x/session/keeper/query.go b/x/session/keeper/query.go index 7add9b6e4..700ec87e8 100644 --- a/x/session/keeper/query.go +++ b/x/session/keeper/query.go @@ -1,7 +1,7 @@ package keeper import ( - "pocket/x/session/types" + "github.com/pokt-network/poktroll/x/session/types" ) var _ types.QueryServer = Keeper{} diff --git a/x/session/keeper/query_get_session.go b/x/session/keeper/query_get_session.go index f937f5033..d9fd3deaf 100644 --- a/x/session/keeper/query_get_session.go +++ b/x/session/keeper/query_get_session.go @@ -7,7 +7,7 @@ import ( "google.golang.org/grpc/codes" "google.golang.org/grpc/status" - "pocket/x/session/types" + "github.com/pokt-network/poktroll/x/session/types" ) func (k Keeper) GetSession(goCtx context.Context, req *types.QueryGetSessionRequest) (*types.QueryGetSessionResponse, error) { diff --git a/x/session/keeper/query_get_session_test.go b/x/session/keeper/query_get_session_test.go index ec518bb8f..5f15a94e9 100644 --- a/x/session/keeper/query_get_session_test.go +++ b/x/session/keeper/query_get_session_test.go @@ -6,10 +6,10 @@ import ( sdk "github.com/cosmos/cosmos-sdk/types" "github.com/stretchr/testify/require" - "pocket/cmd/pocketd/cmd" - keepertest "pocket/testutil/keeper" - "pocket/x/session/types" - sharedtypes "pocket/x/shared/types" + "github.com/pokt-network/poktroll/cmd/pocketd/cmd" + keepertest "github.com/pokt-network/poktroll/testutil/keeper" + "github.com/pokt-network/poktroll/x/session/types" + sharedtypes "github.com/pokt-network/poktroll/x/shared/types" ) func init() { @@ -45,8 +45,8 @@ func TestSession_GetSession_Success(t *testing.T) { blockHeight: 1, // Intentionally only checking a subset of the session metadata returned - expectedSessionId: "6420ac467b6470fd377357a815c960870518dd25e6df3ae50e97ec49c08dddfe", - expectedSessionNumber: 1, + expectedSessionId: "e1e51d087e447525d7beb648711eb3deaf016a8089938a158e6a0f600979370c", + expectedSessionNumber: 0, expectedNumSuppliers: 1, }, } diff --git a/x/session/keeper/query_params.go b/x/session/keeper/query_params.go index 9a6775da2..75734ad7a 100644 --- a/x/session/keeper/query_params.go +++ b/x/session/keeper/query_params.go @@ -6,7 +6,8 @@ import ( sdk "github.com/cosmos/cosmos-sdk/types" "google.golang.org/grpc/codes" "google.golang.org/grpc/status" - "pocket/x/session/types" + + "github.com/pokt-network/poktroll/x/session/types" ) func (k Keeper) Params(goCtx context.Context, req *types.QueryParamsRequest) (*types.QueryParamsResponse, error) { diff --git a/x/session/keeper/query_params_test.go b/x/session/keeper/query_params_test.go index c7ff9b68a..85f0ddd9b 100644 --- a/x/session/keeper/query_params_test.go +++ b/x/session/keeper/query_params_test.go @@ -6,8 +6,8 @@ import ( sdk "github.com/cosmos/cosmos-sdk/types" "github.com/stretchr/testify/require" - testkeeper "pocket/testutil/keeper" - "pocket/x/session/types" + testkeeper "github.com/pokt-network/poktroll/testutil/keeper" + "github.com/pokt-network/poktroll/x/session/types" ) func TestParamsQuery(t *testing.T) { diff --git a/x/session/keeper/session_hydrator.go b/x/session/keeper/session_hydrator.go index a62cd3e36..b6e78e2c8 100644 --- a/x/session/keeper/session_hydrator.go +++ b/x/session/keeper/session_hydrator.go @@ -11,8 +11,8 @@ import ( sdk "github.com/cosmos/cosmos-sdk/types" _ "golang.org/x/crypto/sha3" - "pocket/x/session/types" - sharedtypes "pocket/x/shared/types" + "github.com/pokt-network/poktroll/x/session/types" + sharedtypes "github.com/pokt-network/poktroll/x/shared/types" ) var SHA3HashLen = crypto.SHA3_256.Size() @@ -91,15 +91,18 @@ func (k Keeper) hydrateSessionMetadata(ctx sdk.Context, sh *sessionHydrator) err // TODO_TECHDEBT: Add a test if `blockHeight` is ahead of the current chain or what this node is aware of sh.session.NumBlocksPerSession = NumBlocksPerSession - sh.session.SessionNumber = int64(sh.blockHeight/NumBlocksPerSession) + 1 + sh.session.SessionNumber = int64(sh.blockHeight / NumBlocksPerSession) sh.sessionHeader.SessionStartBlockHeight = sh.blockHeight - (sh.blockHeight % NumBlocksPerSession) return nil } // hydrateSessionID use both session and on-chain data to determine a unique session ID func (k Keeper) hydrateSessionID(ctx sdk.Context, sh *sessionHydrator) error { - // TODO_TECHDEBT: Need to retrieve the block hash at SessionStartBlockHeight, NOT THE CURRENT ONE - prevHashBz := ctx.HeaderHash() + // TODO_BLOCKER: Need to retrieve the block hash at SessionStartBlockHeight, but this requires + // a bit of work and the `ctx` only gives access to the current block/header. See this thread + // for more details: https://github.com/pokt-network/poktroll/pull/78/files#r1369215667 + // prevHashBz := ctx.HeaderHash() + prevHashBz := []byte("TODO_BLOCKER: See the comment above") appPubKeyBz := []byte(sh.sessionHeader.ApplicationAddress) // TODO_TECHDEBT: In the future, we will need to valid that the ServiceId is a valid service depending on whether @@ -130,10 +133,11 @@ func (k Keeper) hydrateSessionApplication(ctx sdk.Context, sh *sessionHydrator) func (k Keeper) hydrateSessionSuppliers(ctx sdk.Context, sh *sessionHydrator) error { logger := k.Logger(ctx).With("method", "hydrateSessionSuppliers") - // TODO_TECHDEBT(@Olshansk): Need to retrieve the suppliers at SessionStartBlockHeight, + // TODO_TECHDEBT(@Olshansk, @bryanchriswhite): Need to retrieve the suppliers at SessionStartBlockHeight, // NOT THE CURRENT ONE which is what's provided by the context. For now, for simplicity, // only retrieving the suppliers at the current block height which could create a discrepancy // if new suppliers were staked mid session. + // TODO(@bryanchriswhite): Investigate if `BlockClient` + `ReplayObservable` where `N = SessionLength` could be used here.` suppliers := k.supplierKeeper.GetAllSupplier(ctx) candidateSuppliers := make([]*sharedtypes.Supplier, 0) diff --git a/x/session/keeper/session_hydrator_test.go b/x/session/keeper/session_hydrator_test.go index 8ce629198..c50d653ab 100644 --- a/x/session/keeper/session_hydrator_test.go +++ b/x/session/keeper/session_hydrator_test.go @@ -5,10 +5,10 @@ import ( "github.com/stretchr/testify/require" - keepertest "pocket/testutil/keeper" - "pocket/testutil/sample" - "pocket/x/session/keeper" - "pocket/x/session/types" + keepertest "github.com/pokt-network/poktroll/testutil/keeper" + "github.com/pokt-network/poktroll/testutil/sample" + "github.com/pokt-network/poktroll/x/session/keeper" + "github.com/pokt-network/poktroll/x/session/types" ) func TestSession_HydrateSession_Success_BaseCase(t *testing.T) { @@ -25,17 +25,17 @@ func TestSession_HydrateSession_Success_BaseCase(t *testing.T) { require.Equal(t, keepertest.TestServiceId1, sessionHeader.ServiceId.Id) require.Equal(t, "", sessionHeader.ServiceId.Name) require.Equal(t, int64(8), sessionHeader.SessionStartBlockHeight) - require.Equal(t, "cb975874ae3c9c12e99be4b8bf76758a34f72f91478f6fc7d16175b7f02061c1", sessionHeader.SessionId) + require.Equal(t, "23f037a10f9d51d020d27763c42dd391d7e71765016d95d0d61f36c4a122efd0", sessionHeader.SessionId) // Check the session require.Equal(t, int64(4), session.NumBlocksPerSession) - require.Equal(t, "cb975874ae3c9c12e99be4b8bf76758a34f72f91478f6fc7d16175b7f02061c1", session.SessionId) - require.Equal(t, int64(3), session.SessionNumber) + require.Equal(t, "23f037a10f9d51d020d27763c42dd391d7e71765016d95d0d61f36c4a122efd0", session.SessionId) + require.Equal(t, int64(2), session.SessionNumber) // Check the application app := session.Application require.Equal(t, keepertest.TestApp1Address, app.Address) - require.Len(t, app.ServiceIds, 2) + require.Len(t, app.ServiceConfigs, 2) // Check the suppliers suppliers := session.Suppliers @@ -63,7 +63,7 @@ func TestSession_HydrateSession_Metadata(t *testing.T) { blockHeight: 0, expectedNumBlocksPerSession: 4, - expectedSessionNumber: 1, + expectedSessionNumber: 0, expectedSessionStartBlock: 0, }, { @@ -71,7 +71,7 @@ func TestSession_HydrateSession_Metadata(t *testing.T) { blockHeight: 1, expectedNumBlocksPerSession: 4, - expectedSessionNumber: 1, + expectedSessionNumber: 0, expectedSessionStartBlock: 0, }, { @@ -79,7 +79,7 @@ func TestSession_HydrateSession_Metadata(t *testing.T) { blockHeight: 4, expectedNumBlocksPerSession: 4, - expectedSessionNumber: 2, + expectedSessionNumber: 1, expectedSessionStartBlock: 4, }, { @@ -87,7 +87,7 @@ func TestSession_HydrateSession_Metadata(t *testing.T) { blockHeight: 5, expectedNumBlocksPerSession: 4, - expectedSessionNumber: 2, + expectedSessionNumber: 1, expectedSessionStartBlock: 4, }, } @@ -141,8 +141,8 @@ func TestSession_HydrateSession_SessionId(t *testing.T) { serviceId1: keepertest.TestServiceId1, // svc1 serviceId2: keepertest.TestServiceId1, // svc1 - expectedSessionId1: "945e8c698dd243ba7600ba254bb362b234ce32769c732ef88cbab81eff2a23ba", - expectedSessionId2: "cb975874ae3c9c12e99be4b8bf76758a34f72f91478f6fc7d16175b7f02061c1", + expectedSessionId1: "aabaa25668538f80395170be95ce1d1536d9228353ced71cc3b763171316fe39", + expectedSessionId2: "23f037a10f9d51d020d27763c42dd391d7e71765016d95d0d61f36c4a122efd0", }, { name: "app1: sessionId for svc1 != sessionId for svc2", @@ -156,8 +156,8 @@ func TestSession_HydrateSession_SessionId(t *testing.T) { serviceId1: keepertest.TestServiceId1, // svc1 serviceId2: keepertest.TestServiceId2, // svc2 - expectedSessionId1: "945e8c698dd243ba7600ba254bb362b234ce32769c732ef88cbab81eff2a23ba", - expectedSessionId2: "ae3a89594026cdb62700b9126e79540a1e342dea311075c3548f20b231c8deda", + expectedSessionId1: "aabaa25668538f80395170be95ce1d1536d9228353ced71cc3b763171316fe39", + expectedSessionId2: "478d005769e5edf38d9bf2d8828a56d78b17348bb2c4796dd6d85b5d736a908a", }, { name: "svc1: sessionId for app1 != sessionId for app2", @@ -171,8 +171,8 @@ func TestSession_HydrateSession_SessionId(t *testing.T) { serviceId1: keepertest.TestServiceId1, // svc1 serviceId2: keepertest.TestServiceId1, // svc1 - expectedSessionId1: "945e8c698dd243ba7600ba254bb362b234ce32769c732ef88cbab81eff2a23ba", - expectedSessionId2: "59e4334a9f3fb59ae7f60f6ed51823904a6bab08df443677a3773ae92b7d9198", + expectedSessionId1: "aabaa25668538f80395170be95ce1d1536d9228353ced71cc3b763171316fe39", + expectedSessionId2: "b4b0d8747b1cf67050a7bfefd7e93ebbad80c534fa14fb3c69339886f2ed7061", }, } diff --git a/x/session/module.go b/x/session/module.go index e6131b51f..990ada0c0 100644 --- a/x/session/module.go +++ b/x/session/module.go @@ -14,9 +14,9 @@ import ( "github.com/grpc-ecosystem/grpc-gateway/runtime" "github.com/spf13/cobra" - "pocket/x/session/client/cli" - "pocket/x/session/keeper" - "pocket/x/session/types" + "github.com/pokt-network/poktroll/x/session/client/cli" + "github.com/pokt-network/poktroll/x/session/keeper" + "github.com/pokt-network/poktroll/x/session/types" ) var ( diff --git a/x/session/module_simulation.go b/x/session/module_simulation.go index 9f75aea86..befedd421 100644 --- a/x/session/module_simulation.go +++ b/x/session/module_simulation.go @@ -8,9 +8,10 @@ import ( "github.com/cosmos/cosmos-sdk/types/module" simtypes "github.com/cosmos/cosmos-sdk/types/simulation" "github.com/cosmos/cosmos-sdk/x/simulation" - "pocket/testutil/sample" - sessionsimulation "pocket/x/session/simulation" - "pocket/x/session/types" + + "github.com/pokt-network/poktroll/testutil/sample" + sessionsimulation "github.com/pokt-network/poktroll/x/session/simulation" + "github.com/pokt-network/poktroll/x/session/types" ) // avoid unused import issue diff --git a/x/session/types/codec.go b/x/session/types/codec.go index 844157a87..72399f81e 100644 --- a/x/session/types/codec.go +++ b/x/session/types/codec.go @@ -3,8 +3,8 @@ package types import ( "github.com/cosmos/cosmos-sdk/codec" cdctypes "github.com/cosmos/cosmos-sdk/codec/types" - // this line is used by starport scaffolding # 1 "github.com/cosmos/cosmos-sdk/types/msgservice" + // this line is used by starport scaffolding # 1 ) func RegisterCodec(cdc *codec.LegacyAmino) { diff --git a/x/session/types/expected_keepers.go b/x/session/types/expected_keepers.go index 1bbae52a1..697528f21 100644 --- a/x/session/types/expected_keepers.go +++ b/x/session/types/expected_keepers.go @@ -6,8 +6,8 @@ import ( sdk "github.com/cosmos/cosmos-sdk/types" "github.com/cosmos/cosmos-sdk/x/auth/types" - apptypes "pocket/x/application/types" - sharedtypes "pocket/x/shared/types" + apptypes "github.com/pokt-network/poktroll/x/application/types" + sharedtypes "github.com/pokt-network/poktroll/x/shared/types" ) // AccountKeeper defines the expected account keeper used for simulations (noalias) diff --git a/x/session/types/genesis_test.go b/x/session/types/genesis_test.go index e435341ae..97252a21e 100644 --- a/x/session/types/genesis_test.go +++ b/x/session/types/genesis_test.go @@ -4,7 +4,8 @@ import ( "testing" "github.com/stretchr/testify/require" - "pocket/x/session/types" + + "github.com/pokt-network/poktroll/x/session/types" ) func TestGenesisState_Validate(t *testing.T) { diff --git a/x/shared/helpers/service.go b/x/shared/helpers/service.go new file mode 100644 index 000000000..3a4e56e5f --- /dev/null +++ b/x/shared/helpers/service.go @@ -0,0 +1,76 @@ +package helpers + +import ( + "net/url" + "regexp" +) + +const ( + maxServiceIdLength = 8 // Limiting all serviceIds to 8 characters + maxServiceIdName = 42 // Limit the the name of the + + regexServiceId = "^[a-zA-Z0-9_-]+$" // Define the regex pattern to match allowed characters + regexServiceName = "^[a-zA-Z0-9-_ ]+$" // Define the regex pattern to match allowed characters (allows spaces) +) + +var ( + regexExprServiceId *regexp.Regexp + regexExprServiceName *regexp.Regexp +) + +func init() { + // Compile the regex pattern + regexExprServiceId = regexp.MustCompile(regexServiceId) + regexExprServiceName = regexp.MustCompile(regexServiceName) + +} + +// IsValidServiceId checks if the input string is a valid serviceId +func IsValidServiceId(serviceId string) bool { + // ServiceId CANNOT be empty + if len(serviceId) == 0 { + return false + } + + if len(serviceId) > maxServiceIdLength { + return false + } + + // Use the regex to match against the input string + return regexExprServiceId.MatchString(serviceId) +} + +// IsValidServiceName checks if the input string is a valid serviceName +func IsValidServiceName(serviceName string) bool { + // ServiceName CAN be empty + if len(serviceName) == 0 { + return true + } + + if len(serviceName) > maxServiceIdName { + return false + } + + // Use the regex to match against the input string + return regexExprServiceName.MatchString(serviceName) +} + +// IsValidEndpointUrl checks if the provided string is a valid URL. +func IsValidEndpointUrl(endpoint string) bool { + u, err := url.Parse(endpoint) + if err != nil { + return false + } + + // Check if scheme is http or https + if u.Scheme != "http" && u.Scheme != "https" { + return false + } + + // Ensure the URL has a host + if u.Host == "" { + return false + } + + return true +} diff --git a/x/shared/helpers/service_configs.go b/x/shared/helpers/service_configs.go new file mode 100644 index 000000000..6884da7ae --- /dev/null +++ b/x/shared/helpers/service_configs.go @@ -0,0 +1,98 @@ +package helpers + +import ( + "fmt" + + sharedtypes "github.com/pokt-network/poktroll/x/shared/types" +) + +// ValidateAppServiceConfigs returns an error if any of the application service configs are invalid +func ValidateAppServiceConfigs(services []*sharedtypes.ApplicationServiceConfig) error { + if len(services) == 0 { + return fmt.Errorf("no services configs provided for application: %v", services) + } + for _, serviceConfig := range services { + if serviceConfig == nil { + return fmt.Errorf("serviceConfig cannot be nil: %v", services) + } + if serviceConfig.ServiceId == nil { + return fmt.Errorf("serviceId cannot be nil: %v", serviceConfig) + } + if serviceConfig.ServiceId.Id == "" { + return fmt.Errorf("serviceId.Id cannot be empty: %v", serviceConfig) + } + if !IsValidServiceId(serviceConfig.ServiceId.Id) { + return fmt.Errorf("invalid serviceId.Id: %v", serviceConfig) + } + if !IsValidServiceName(serviceConfig.ServiceId.Name) { + return fmt.Errorf("invalid serviceId.Name: %v", serviceConfig) + } + } + return nil +} + +// ValidateSupplierServiceConfigs returns an error if any of the supplier service configs are invalid +func ValidateSupplierServiceConfigs(services []*sharedtypes.SupplierServiceConfig) error { + if len(services) == 0 { + return fmt.Errorf("no services provided for supplier: %v", services) + } + for _, serviceConfig := range services { + if serviceConfig == nil { + return fmt.Errorf("serviceConfig cannot be nil: %v", services) + } + + // Check the ServiceId + if serviceConfig.ServiceId == nil { + return fmt.Errorf("serviceId cannot be nil: %v", serviceConfig) + } + if serviceConfig.ServiceId.Id == "" { + return fmt.Errorf("serviceId.Id cannot be empty: %v", serviceConfig) + } + if !IsValidServiceId(serviceConfig.ServiceId.Id) { + return fmt.Errorf("invalid serviceId.Id: %v", serviceConfig) + } + if !IsValidServiceName(serviceConfig.ServiceId.Name) { + return fmt.Errorf("invalid serviceId.Name: %v", serviceConfig) + } + + // Check the Endpoints + if serviceConfig.Endpoints == nil { + return fmt.Errorf("endpoints cannot be nil: %v", serviceConfig) + } + if len(serviceConfig.Endpoints) == 0 { + return fmt.Errorf("endpoints must have at least one entry: %v", serviceConfig) + } + + // Check each endpoint + for _, endpoint := range serviceConfig.Endpoints { + if endpoint == nil { + return fmt.Errorf("endpoint cannot be nil: %v", serviceConfig) + } + + // Validate the URL + if endpoint.Url == "" { + return fmt.Errorf("endpoint.Url cannot be empty: %v", serviceConfig) + } + if !IsValidEndpointUrl(endpoint.Url) { + return fmt.Errorf("invalid endpoint.Url: %v", serviceConfig) + } + + // Validate the RPC type + if endpoint.RpcType == sharedtypes.RPCType_UNKNOWN_RPC { + return fmt.Errorf("endpoint.RpcType cannot be UNKNOWN_RPC: %v", serviceConfig) + } + if _, ok := sharedtypes.RPCType_name[int32(endpoint.RpcType)]; !ok { + return fmt.Errorf("endpoint.RpcType is not a valid RPCType: %v", serviceConfig) + } + + // TODO: Validate configs once they are being used + // if endpoint.Configs == nil { + // return fmt.Errorf("endpoint.Configs cannot be nil: %v", serviceConfig) + // } + // if len(endpoint.Configs) == 0 { + // return fmt.Errorf("endpoint.Configs must have at least one entry: %v", serviceConfig) + // } + } + } + return nil +} diff --git a/x/shared/helpers/service_test.go b/x/shared/helpers/service_test.go new file mode 100644 index 000000000..335cbcd51 --- /dev/null +++ b/x/shared/helpers/service_test.go @@ -0,0 +1,82 @@ +package helpers + +import "testing" + +func TestIsValidServiceId(t *testing.T) { + tests := []struct { + input string + expected bool + }{ + {"Hello-1", true}, + {"Hello_2", true}, + {"hello-world", false}, // exceeds maxServiceIdLength + {"Hello@", false}, // contains invalid character '@' + {"HELLO", true}, + {"12345678", true}, // exactly maxServiceIdLength + {"123456789", false}, // exceeds maxServiceIdLength + {"Hello.World", false}, // contains invalid character '.' + {"", false}, // empty string + } + + for _, test := range tests { + t.Run(test.input, func(t *testing.T) { + result := IsValidServiceId(test.input) + if result != test.expected { + t.Errorf("For input %s, expected %v but got %v", test.input, test.expected, result) + } + }) + } +} + +func TestIsValidEndpointUrl(t *testing.T) { + tests := []struct { + name string + input string + expected bool + }{ + { + name: "valid http URL", + input: "http://example.com", + expected: true, + }, + { + name: "valid https URL", + input: "https://example.com/path?query=value#fragment", + expected: true, + }, + { + name: "valid localhost URL with scheme", + input: "https://localhost:8081", + expected: true, + }, + { + name: "valid loopback URL with scheme", + input: "http://127.0.0.1:8081", + expected: true, + }, + { + name: "invalid scheme", + input: "ftp://example.com", + expected: false, + }, + { + name: "missing scheme", + input: "example.com", + expected: false, + }, + { + name: "invalid URL", + input: "not-a-valid-url", + expected: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := IsValidEndpointUrl(tt.input) + if got != tt.expected { + t.Errorf("IsValidEndpointUrl(%q) = %v, want %v", tt.input, got, tt.expected) + } + }) + } +} diff --git a/x/supplier/client/cli/helpers_test.go b/x/supplier/client/cli/helpers_test.go index 09f193920..d6066c28a 100644 --- a/x/supplier/client/cli/helpers_test.go +++ b/x/supplier/client/cli/helpers_test.go @@ -7,10 +7,10 @@ import ( "github.com/stretchr/testify/require" - "pocket/cmd/pocketd/cmd" - "pocket/testutil/network" - sharedtypes "pocket/x/shared/types" - "pocket/x/supplier/types" + "github.com/pokt-network/poktroll/cmd/pocketd/cmd" + "github.com/pokt-network/poktroll/testutil/network" + sharedtypes "github.com/pokt-network/poktroll/x/shared/types" + "github.com/pokt-network/poktroll/x/supplier/types" ) // Dummy variable to avoid unused import error. diff --git a/x/supplier/client/cli/query.go b/x/supplier/client/cli/query.go index b5e1e4bf4..da1e3de17 100644 --- a/x/supplier/client/cli/query.go +++ b/x/supplier/client/cli/query.go @@ -10,7 +10,7 @@ import ( // "github.com/cosmos/cosmos-sdk/client/flags" // sdk "github.com/cosmos/cosmos-sdk/types" - "pocket/x/supplier/types" + "github.com/pokt-network/poktroll/x/supplier/types" ) // GetQueryCmd returns the cli query commands for this module diff --git a/x/supplier/client/cli/query_params.go b/x/supplier/client/cli/query_params.go index d308b3e96..339dbcf00 100644 --- a/x/supplier/client/cli/query_params.go +++ b/x/supplier/client/cli/query_params.go @@ -5,7 +5,7 @@ import ( "github.com/cosmos/cosmos-sdk/client/flags" "github.com/spf13/cobra" - "pocket/x/supplier/types" + "github.com/pokt-network/poktroll/x/supplier/types" ) func CmdQueryParams() *cobra.Command { diff --git a/x/supplier/client/cli/query_supplier.go b/x/supplier/client/cli/query_supplier.go index 44adb2c8e..cfbc6b4ec 100644 --- a/x/supplier/client/cli/query_supplier.go +++ b/x/supplier/client/cli/query_supplier.go @@ -5,7 +5,7 @@ import ( "github.com/cosmos/cosmos-sdk/client/flags" "github.com/spf13/cobra" - "pocket/x/supplier/types" + "github.com/pokt-network/poktroll/x/supplier/types" ) func CmdListSupplier() *cobra.Command { diff --git a/x/supplier/client/cli/query_supplier_test.go b/x/supplier/client/cli/query_supplier_test.go index 4dbc5ed36..378811a2f 100644 --- a/x/supplier/client/cli/query_supplier_test.go +++ b/x/supplier/client/cli/query_supplier_test.go @@ -12,10 +12,10 @@ import ( "google.golang.org/grpc/codes" "google.golang.org/grpc/status" - "pocket/testutil/nullify" - sharedtypes "pocket/x/shared/types" - "pocket/x/supplier/client/cli" - "pocket/x/supplier/types" + "github.com/pokt-network/poktroll/testutil/nullify" + sharedtypes "github.com/pokt-network/poktroll/x/shared/types" + "github.com/pokt-network/poktroll/x/supplier/client/cli" + "github.com/pokt-network/poktroll/x/supplier/types" ) func TestShowSupplier(t *testing.T) { diff --git a/x/supplier/client/cli/tx.go b/x/supplier/client/cli/tx.go index c1059e51d..c76c24dae 100644 --- a/x/supplier/client/cli/tx.go +++ b/x/supplier/client/cli/tx.go @@ -7,7 +7,7 @@ import ( "github.com/cosmos/cosmos-sdk/client" "github.com/spf13/cobra" - "pocket/x/supplier/types" + "github.com/pokt-network/poktroll/x/supplier/types" ) var ( diff --git a/x/supplier/client/cli/tx_create_claim.go b/x/supplier/client/cli/tx_create_claim.go index b611e9795..db951891f 100644 --- a/x/supplier/client/cli/tx_create_claim.go +++ b/x/supplier/client/cli/tx_create_claim.go @@ -5,13 +5,14 @@ import ( "strconv" "encoding/json" + "github.com/cosmos/cosmos-sdk/client" "github.com/cosmos/cosmos-sdk/client/flags" "github.com/cosmos/cosmos-sdk/client/tx" "github.com/spf13/cobra" - sessiontypes "pocket/x/session/types" - "pocket/x/supplier/types" + sessiontypes "github.com/pokt-network/poktroll/x/session/types" + "github.com/pokt-network/poktroll/x/supplier/types" ) // TODO(@bryanchriswhite): Add unit tests for the CLI command when implementing the business logic. diff --git a/x/supplier/client/cli/tx_stake_supplier.go b/x/supplier/client/cli/tx_stake_supplier.go index a372f9772..eac4b4044 100644 --- a/x/supplier/client/cli/tx_stake_supplier.go +++ b/x/supplier/client/cli/tx_stake_supplier.go @@ -1,7 +1,9 @@ package cli import ( + "fmt" "strconv" + "strings" "github.com/cosmos/cosmos-sdk/client" "github.com/cosmos/cosmos-sdk/client/flags" @@ -9,7 +11,8 @@ import ( sdk "github.com/cosmos/cosmos-sdk/types" "github.com/spf13/cobra" - "pocket/x/supplier/types" + sharedtypes "github.com/pokt-network/poktroll/x/shared/types" + "github.com/pokt-network/poktroll/x/supplier/types" ) var _ = strconv.Itoa(0) @@ -17,15 +20,23 @@ var _ = strconv.Itoa(0) func CmdStakeSupplier() *cobra.Command { // fromAddress & signature is retrieved via `flags.FlagFrom` in the `clientCtx` cmd := &cobra.Command{ - Use: "stake-supplier [amount]", + // TODO_HACK: For now we are only specifying the service IDs as a list of of strings separated by commas. + // This needs to be expand to specify the full SupplierServiceConfig. Furthermore, providing a flag to + // a file where SupplierServiceConfig specifying full service configurations in the CLI by providing a flag that accepts a JSON string + Use: "stake-supplier [amount] [svcId1;url1,svcId2;url2,...,svcIdN;urlN]", Short: "Stake a supplier", Long: `Stake an supplier with the provided parameters. This is a broadcast operation that will stake the tokens and associate them with the supplier specified by the 'from' address. +TODO_HACK: Until proper service configuration files are supported, suppliers must specify the services as a single string +of comma separated values of the form 'service;url' where 'service' is the service ID and 'url' is the service URL. +For example, an application that stakes for 'anvil' could be matched with a supplier staking for 'anvil;http://anvil:8547'. + Example: -$ pocketd --home=$(POCKETD_HOME) tx supplier stake-supplier 1000upokt --keyring-backend test --from $(APP) --node $(POCKET_NODE)`, - Args: cobra.ExactArgs(1), +$ poktrolld --home=$(POCKETD_HOME) tx supplier stake-supplier 1000upokt anvil;http://anvil:8547 --keyring-backend test --from $(APP) --node $(POCKET_NODE)`, + Args: cobra.ExactArgs(2), RunE: func(cmd *cobra.Command, args []string) (err error) { stakeString := args[0] + servicesArg := args[1] clientCtx, err := client.GetClientTxContext(cmd) if err != nil { @@ -37,9 +48,15 @@ $ pocketd --home=$(POCKETD_HOME) tx supplier stake-supplier 1000upokt --keyring- return err } + services, err := hackStringToServices(servicesArg) + if err != nil { + return err + } + msg := types.NewMsgStakeSupplier( clientCtx.GetFromAddress().String(), stake, + services, ) if err := msg.ValidateBasic(); err != nil { @@ -54,3 +71,31 @@ $ pocketd --home=$(POCKETD_HOME) tx supplier stake-supplier 1000upokt --keyring- return cmd } + +// TODO_BLOCKER, TODO_HACK: The supplier stake command should take an argument +// or flag that points to a file containing all the services configurations & specifications. +// As a quick workaround, we just need the service & url to get things working for now. +func hackStringToServices(servicesArg string) ([]*sharedtypes.SupplierServiceConfig, error) { + supplierServiceConfig := make([]*sharedtypes.SupplierServiceConfig, 0) + serviceStrings := strings.Split(servicesArg, ",") + for _, serviceString := range serviceStrings { + serviceParts := strings.Split(serviceString, ";") + if len(serviceParts) != 2 { + return nil, fmt.Errorf("invalid service string: %s. Expected it to be of the form 'service;url'", serviceString) + } + service := &sharedtypes.SupplierServiceConfig{ + ServiceId: &sharedtypes.ServiceId{ + Id: serviceParts[0], + }, + Endpoints: []*sharedtypes.SupplierEndpoint{ + { + Url: serviceParts[1], + RpcType: sharedtypes.RPCType_JSON_RPC, + Configs: make([]*sharedtypes.ConfigOption, 0), + }, + }, + } + supplierServiceConfig = append(supplierServiceConfig, service) + } + return supplierServiceConfig, nil +} diff --git a/x/supplier/client/cli/tx_stake_supplier_test.go b/x/supplier/client/cli/tx_stake_supplier_test.go index 3997a8a5a..f61d41fc9 100644 --- a/x/supplier/client/cli/tx_stake_supplier_test.go +++ b/x/supplier/client/cli/tx_stake_supplier_test.go @@ -13,9 +13,9 @@ import ( "github.com/stretchr/testify/require" "google.golang.org/grpc/status" - "pocket/testutil/network" - "pocket/x/supplier/client/cli" - "pocket/x/supplier/types" + "github.com/pokt-network/poktroll/testutil/network" + "github.com/pokt-network/poktroll/x/supplier/client/cli" + "github.com/pokt-network/poktroll/x/supplier/types" ) func TestCLI_StakeSupplier(t *testing.T) { @@ -39,51 +39,134 @@ func TestCLI_StakeSupplier(t *testing.T) { } tests := []struct { - desc string - address string - stakeString string - err *sdkerrors.Error + desc string + address string + stakeString string + servicesString string + err *sdkerrors.Error }{ + // Happy Paths { - desc: "stake supplier: valid", - address: supplierAccount.Address.String(), - stakeString: "1000upokt", + desc: "stake supplier: valid", + address: supplierAccount.Address.String(), + stakeString: "1000upokt", + servicesString: "svc1;http://pokt.network:8081", }, + + // Error Paths - Address Related { desc: "stake supplier: missing address", // address: "explicitly missing", - stakeString: "1000upokt", - err: types.ErrSupplierInvalidAddress, + stakeString: "1000upokt", + servicesString: "svc1;http://pokt.network:8081", + err: types.ErrSupplierInvalidAddress, }, { - desc: "stake supplier: invalid address", - address: "invalid", - stakeString: "1000upokt", - err: types.ErrSupplierInvalidAddress, + desc: "stake supplier: invalid address", + address: "invalid", + stakeString: "1000upokt", + servicesString: "svc1;http://pokt.network:8081", + err: types.ErrSupplierInvalidAddress, }, + + // Error Paths - Stake Related { desc: "stake supplier: missing stake", address: supplierAccount.Address.String(), // stakeString: "explicitly missing", - err: types.ErrSupplierInvalidStake, + servicesString: "svc1;http://pokt.network:8081", + err: types.ErrSupplierInvalidStake, }, { - desc: "stake supplier: invalid stake denom", - address: supplierAccount.Address.String(), - stakeString: "1000invalid", - err: types.ErrSupplierInvalidStake, + desc: "stake supplier: invalid stake denom", + address: supplierAccount.Address.String(), + stakeString: "1000invalid", + servicesString: "svc1;http://pokt.network:8081", + err: types.ErrSupplierInvalidStake, }, { - desc: "stake supplier: invalid stake amount (zero)", - address: supplierAccount.Address.String(), - stakeString: "0upokt", - err: types.ErrSupplierInvalidStake, + desc: "stake supplier: invalid stake amount (zero)", + address: supplierAccount.Address.String(), + stakeString: "0upokt", + servicesString: "svc1;http://pokt.network:8081", + err: types.ErrSupplierInvalidStake, }, { - desc: "stake supplier: invalid stake amount (negative)", + desc: "stake supplier: invalid stake amount (negative)", + address: supplierAccount.Address.String(), + stakeString: "-1000upokt", + servicesString: "svc1;http://pokt.network:8081", + err: types.ErrSupplierInvalidStake, + }, + + // Happy Paths - Service Related + { + desc: "services_test: valid multiple services", + address: supplierAccount.Address.String(), + stakeString: "1000upokt", + servicesString: "svc1;http://pokt.network:8081,svc2;http://pokt.network:8082", + }, + { + desc: "services_test: valid localhost", + address: supplierAccount.Address.String(), + stakeString: "1000upokt", + servicesString: "scv1;http://127.0.0.1:8082", + }, + { + desc: "services_test: valid loopback", + address: supplierAccount.Address.String(), + stakeString: "1000upokt", + servicesString: "scv1;http://localhost:8082", + }, + { + desc: "services_test: valid without a pork", + address: supplierAccount.Address.String(), + stakeString: "1000upokt", + servicesString: "scv1;http://pokt.network", + }, + + // Error Paths - Service Related + { + desc: "services_test: invalid services (missing argument)", address: supplierAccount.Address.String(), - stakeString: "-1000upokt", - err: types.ErrSupplierInvalidStake, + stakeString: "1000upokt", + // servicesString: "explicitly omitted", + err: types.ErrSupplierInvalidServiceConfig, + }, + { + desc: "services_test: invalid services (empty string)", + address: supplierAccount.Address.String(), + stakeString: "1000upokt", + servicesString: "", + err: types.ErrSupplierInvalidServiceConfig, + }, + { + desc: "services_test: invalid because contains a space", + address: supplierAccount.Address.String(), + stakeString: "1000upokt", + servicesString: "scv1 http://127.0.0.1:8082", + err: types.ErrSupplierInvalidServiceConfig, + }, + { + desc: "services_test: invalid URL", + address: supplierAccount.Address.String(), + stakeString: "1000upokt", + servicesString: "svc1;bad_url", + err: types.ErrSupplierInvalidServiceConfig, + }, + { + desc: "services_test: missing URLs", + address: supplierAccount.Address.String(), + stakeString: "1000upokt", + servicesString: "svc1,svc2;", + err: types.ErrSupplierInvalidServiceConfig, + }, + { + desc: "services_test: missing service IDs", + address: supplierAccount.Address.String(), + stakeString: "1000upokt", + servicesString: "localhost:8081,;localhost:8082", + err: types.ErrSupplierInvalidServiceConfig, }, } @@ -99,6 +182,7 @@ func TestCLI_StakeSupplier(t *testing.T) { // Prepare the arguments for the CLI command args := []string{ tt.stakeString, + tt.servicesString, fmt.Sprintf("--%s=%s", flags.FlagFrom, tt.address), } args = append(args, commonArgs...) diff --git a/x/supplier/client/cli/tx_submit_proof.go b/x/supplier/client/cli/tx_submit_proof.go index 798d67492..64c498026 100644 --- a/x/supplier/client/cli/tx_submit_proof.go +++ b/x/supplier/client/cli/tx_submit_proof.go @@ -5,13 +5,14 @@ import ( "strconv" "encoding/json" + "github.com/cosmos/cosmos-sdk/client" "github.com/cosmos/cosmos-sdk/client/flags" "github.com/cosmos/cosmos-sdk/client/tx" "github.com/spf13/cobra" - sessiontypes "pocket/x/session/types" - "pocket/x/supplier/types" + sessiontypes "github.com/pokt-network/poktroll/x/session/types" + "github.com/pokt-network/poktroll/x/supplier/types" ) var _ = strconv.Itoa(0) diff --git a/x/supplier/client/cli/tx_unstake_supplier.go b/x/supplier/client/cli/tx_unstake_supplier.go index 7f3ea3003..2daf7c00a 100644 --- a/x/supplier/client/cli/tx_unstake_supplier.go +++ b/x/supplier/client/cli/tx_unstake_supplier.go @@ -6,7 +6,7 @@ import ( "github.com/cosmos/cosmos-sdk/client/tx" "github.com/spf13/cobra" - "pocket/x/supplier/types" + "github.com/pokt-network/poktroll/x/supplier/types" ) func CmdUnstakeSupplier() *cobra.Command { @@ -17,7 +17,7 @@ func CmdUnstakeSupplier() *cobra.Command { Long: `Unstake an supplier with the provided parameters. This is a broadcast operation that will unstake the supplier specified by the 'from' address. Example: -$ pocketd --home=$(POCKETD_HOME) tx supplier unstake-supplier --keyring-backend test --from $(SUPPLIER) --node $(POCKET_NODE)`, +$ poktrolld --home=$(POCKETD_HOME) tx supplier unstake-supplier --keyring-backend test --from $(SUPPLIER) --node $(POCKET_NODE)`, Args: cobra.ExactArgs(0), RunE: func(cmd *cobra.Command, args []string) (err error) { diff --git a/x/supplier/client/cli/tx_unstake_supplier_test.go b/x/supplier/client/cli/tx_unstake_supplier_test.go index 9956be635..179e5dce5 100644 --- a/x/supplier/client/cli/tx_unstake_supplier_test.go +++ b/x/supplier/client/cli/tx_unstake_supplier_test.go @@ -13,9 +13,9 @@ import ( "github.com/stretchr/testify/require" "google.golang.org/grpc/status" - "pocket/testutil/network" - "pocket/x/supplier/client/cli" - "pocket/x/supplier/types" + "github.com/pokt-network/poktroll/testutil/network" + "github.com/pokt-network/poktroll/x/supplier/client/cli" + "github.com/pokt-network/poktroll/x/supplier/types" ) func TestCLI_UnstakeSupplier(t *testing.T) { diff --git a/x/supplier/genesis.go b/x/supplier/genesis.go index 889f6d197..fb7d59806 100644 --- a/x/supplier/genesis.go +++ b/x/supplier/genesis.go @@ -3,8 +3,8 @@ package supplier import ( sdk "github.com/cosmos/cosmos-sdk/types" - "pocket/x/supplier/keeper" - "pocket/x/supplier/types" + "github.com/pokt-network/poktroll/x/supplier/keeper" + "github.com/pokt-network/poktroll/x/supplier/types" ) // InitGenesis initializes the module's state from a provided genesis state. diff --git a/x/supplier/genesis_test.go b/x/supplier/genesis_test.go index a0d3ccfcb..b6af0545c 100644 --- a/x/supplier/genesis_test.go +++ b/x/supplier/genesis_test.go @@ -6,14 +6,15 @@ import ( sdk "github.com/cosmos/cosmos-sdk/types" "github.com/stretchr/testify/require" - keepertest "pocket/testutil/keeper" - "pocket/testutil/nullify" - "pocket/testutil/sample" - sharedtypes "pocket/x/shared/types" - "pocket/x/supplier" - "pocket/x/supplier/types" + keepertest "github.com/pokt-network/poktroll/testutil/keeper" + "github.com/pokt-network/poktroll/testutil/nullify" + "github.com/pokt-network/poktroll/testutil/sample" + sharedtypes "github.com/pokt-network/poktroll/x/shared/types" + "github.com/pokt-network/poktroll/x/supplier" + "github.com/pokt-network/poktroll/x/supplier/types" ) +// Please see `x/supplier/types/genesis_test.go` for extensive tests related to the validity of the genesis state. func TestGenesis(t *testing.T) { genesisState := types.GenesisState{ Params: types.DefaultParams(), @@ -21,10 +22,38 @@ func TestGenesis(t *testing.T) { { Address: sample.AccAddress(), Stake: &sdk.Coin{Denom: "upokt", Amount: sdk.NewInt(100)}, + Services: []*sharedtypes.SupplierServiceConfig{ + { + ServiceId: &sharedtypes.ServiceId{ + Id: "svcId1", + }, + Endpoints: []*sharedtypes.SupplierEndpoint{ + { + Url: "http://localhost:8081", + RpcType: sharedtypes.RPCType_JSON_RPC, + Configs: make([]*sharedtypes.ConfigOption, 0), + }, + }, + }, + }, }, { Address: sample.AccAddress(), Stake: &sdk.Coin{Denom: "upokt", Amount: sdk.NewInt(100)}, + Services: []*sharedtypes.SupplierServiceConfig{ + { + ServiceId: &sharedtypes.ServiceId{ + Id: "svcId2", + }, + Endpoints: []*sharedtypes.SupplierEndpoint{ + { + Url: "http://localhost:8082", + RpcType: sharedtypes.RPCType_GRPC, + Configs: make([]*sharedtypes.ConfigOption, 0), + }, + }, + }, + }, }, }, // this line is used by starport scaffolding # genesis/test/state diff --git a/x/supplier/keeper/keeper.go b/x/supplier/keeper/keeper.go index cde9ba5e3..d77218231 100644 --- a/x/supplier/keeper/keeper.go +++ b/x/supplier/keeper/keeper.go @@ -9,7 +9,7 @@ import ( sdk "github.com/cosmos/cosmos-sdk/types" paramtypes "github.com/cosmos/cosmos-sdk/x/params/types" - "pocket/x/supplier/types" + "github.com/pokt-network/poktroll/x/supplier/types" ) type ( diff --git a/x/supplier/keeper/msg_server.go b/x/supplier/keeper/msg_server.go index e117ef1d9..83879071c 100644 --- a/x/supplier/keeper/msg_server.go +++ b/x/supplier/keeper/msg_server.go @@ -1,7 +1,7 @@ package keeper import ( - "pocket/x/supplier/types" + "github.com/pokt-network/poktroll/x/supplier/types" ) type msgServer struct { diff --git a/x/supplier/keeper/msg_server_create_claim.go b/x/supplier/keeper/msg_server_create_claim.go index 25a1ea1e8..752da7343 100644 --- a/x/supplier/keeper/msg_server_create_claim.go +++ b/x/supplier/keeper/msg_server_create_claim.go @@ -5,7 +5,7 @@ import ( sdk "github.com/cosmos/cosmos-sdk/types" - "pocket/x/supplier/types" + "github.com/pokt-network/poktroll/x/supplier/types" ) func (k msgServer) CreateClaim(goCtx context.Context, msg *types.MsgCreateClaim) (*types.MsgCreateClaimResponse, error) { diff --git a/x/supplier/keeper/msg_server_stake_supplier.go b/x/supplier/keeper/msg_server_stake_supplier.go index 3359b7d77..5fea47135 100644 --- a/x/supplier/keeper/msg_server_stake_supplier.go +++ b/x/supplier/keeper/msg_server_stake_supplier.go @@ -6,8 +6,8 @@ import ( sdkerrors "cosmossdk.io/errors" sdk "github.com/cosmos/cosmos-sdk/types" - sharedtypes "pocket/x/shared/types" - "pocket/x/supplier/types" + sharedtypes "github.com/pokt-network/poktroll/x/shared/types" + "github.com/pokt-network/poktroll/x/supplier/types" ) func (k msgServer) StakeSupplier( @@ -20,6 +20,7 @@ func (k msgServer) StakeSupplier( logger.Info("About to stake supplier with msg: %v", msg) if err := msg.ValidateBasic(); err != nil { + logger.Error("invalid MsgStakeSupplier: %v", msg) return nil, err } @@ -47,6 +48,7 @@ func (k msgServer) StakeSupplier( return nil, err } + // TODO_IMPROVE: Should we avoid making this call if `coinsToDelegate` = 0? // Send the coins from the supplier to the staked supplier pool err = k.bankKeeper.DelegateCoinsFromAccountToModule(ctx, supplierAddress, types.ModuleName, []sdk.Coin{coinsToDelegate}) if err != nil { @@ -66,8 +68,9 @@ func (k msgServer) createSupplier( msg *types.MsgStakeSupplier, ) sharedtypes.Supplier { return sharedtypes.Supplier{ - Address: msg.Address, - Stake: msg.Stake, + Address: msg.Address, + Stake: msg.Stake, + Services: msg.Services, } } @@ -81,16 +84,22 @@ func (k msgServer) updateSupplier( return sdkerrors.Wrapf(types.ErrSupplierUnauthorized, "msg Address (%s) != supplier address (%s)", msg.Address, supplier.Address) } + // Validate that the stake is not being lowered if msg.Stake == nil { return sdkerrors.Wrapf(types.ErrSupplierInvalidStake, "stake amount cannot be nil") } - if msg.Stake.IsLTE(*supplier.Stake) { return sdkerrors.Wrapf(types.ErrSupplierInvalidStake, "stake amount %v must be higher than previous stake amount %v", msg.Stake, supplier.Stake) } - supplier.Stake = msg.Stake + // Validate that the service configs maintain at least one service. + // Additional validation is done in `msg.ValidateBasic` above. + if len(msg.Services) == 0 { + return sdkerrors.Wrapf(types.ErrSupplierInvalidServiceConfig, "must have at least one service") + } + supplier.Services = msg.Services + return nil } diff --git a/x/supplier/keeper/msg_server_stake_supplier_test.go b/x/supplier/keeper/msg_server_stake_supplier_test.go index dfc6b3880..7ac0ee031 100644 --- a/x/supplier/keeper/msg_server_stake_supplier_test.go +++ b/x/supplier/keeper/msg_server_stake_supplier_test.go @@ -6,10 +6,11 @@ import ( sdk "github.com/cosmos/cosmos-sdk/types" "github.com/stretchr/testify/require" - keepertest "pocket/testutil/keeper" - "pocket/testutil/sample" - "pocket/x/supplier/keeper" - "pocket/x/supplier/types" + keepertest "github.com/pokt-network/poktroll/testutil/keeper" + "github.com/pokt-network/poktroll/testutil/sample" + sharedtypes "github.com/pokt-network/poktroll/x/shared/types" + "github.com/pokt-network/poktroll/x/supplier/keeper" + "github.com/pokt-network/poktroll/x/supplier/types" ) func TestMsgServer_StakeSupplier_SuccessfulCreateAndUpdate(t *testing.T) { @@ -28,6 +29,20 @@ func TestMsgServer_StakeSupplier_SuccessfulCreateAndUpdate(t *testing.T) { stakeMsg := &types.MsgStakeSupplier{ Address: addr, Stake: &sdk.Coin{Denom: "upokt", Amount: sdk.NewInt(100)}, + Services: []*sharedtypes.SupplierServiceConfig{ + { + ServiceId: &sharedtypes.ServiceId{ + Id: "svcId", + }, + Endpoints: []*sharedtypes.SupplierEndpoint{ + { + Url: "http://localhost:8080", + RpcType: sharedtypes.RPCType_JSON_RPC, + Configs: make([]*sharedtypes.ConfigOption, 0), + }, + }, + }, + }, } // Stake the supplier @@ -35,23 +50,126 @@ func TestMsgServer_StakeSupplier_SuccessfulCreateAndUpdate(t *testing.T) { require.NoError(t, err) // Verify that the supplier exists - foundSupplier, isSupplierFound := k.GetSupplier(ctx, addr) + supplierFound, isSupplierFound := k.GetSupplier(ctx, addr) require.True(t, isSupplierFound) - require.Equal(t, addr, foundSupplier.Address) - require.Equal(t, int64(100), foundSupplier.Stake.Amount.Int64()) + require.Equal(t, addr, supplierFound.Address) + require.Equal(t, int64(100), supplierFound.Stake.Amount.Int64()) + require.Len(t, supplierFound.Services, 1) + require.Equal(t, "svcId", supplierFound.Services[0].ServiceId.Id) + require.Len(t, supplierFound.Services[0].Endpoints, 1) + require.Equal(t, "http://localhost:8080", supplierFound.Services[0].Endpoints[0].Url) - // Prepare an updated supplier with a higher stake + // Prepare an updated supplier with a higher stake and a different URL for the service updateMsg := &types.MsgStakeSupplier{ Address: addr, Stake: &sdk.Coin{Denom: "upokt", Amount: sdk.NewInt(200)}, + Services: []*sharedtypes.SupplierServiceConfig{ + { + ServiceId: &sharedtypes.ServiceId{ + Id: "svcId2", + }, + Endpoints: []*sharedtypes.SupplierEndpoint{ + { + Url: "http://localhost:8082", + RpcType: sharedtypes.RPCType_JSON_RPC, + Configs: make([]*sharedtypes.ConfigOption, 0), + }, + }, + }, + }, } // Update the staked supplier _, err = srv.StakeSupplier(wctx, updateMsg) require.NoError(t, err) - foundSupplier, isSupplierFound = k.GetSupplier(ctx, addr) + supplierFound, isSupplierFound = k.GetSupplier(ctx, addr) + require.True(t, isSupplierFound) + require.Equal(t, int64(200), supplierFound.Stake.Amount.Int64()) + require.Len(t, supplierFound.Services, 1) + require.Equal(t, "svcId2", supplierFound.Services[0].ServiceId.Id) + require.Len(t, supplierFound.Services[0].Endpoints, 1) + require.Equal(t, "http://localhost:8082", supplierFound.Services[0].Endpoints[0].Url) +} + +func TestMsgServer_StakeSupplier_FailRestakingDueToInvalidServices(t *testing.T) { + k, ctx := keepertest.SupplierKeeper(t) + srv := keeper.NewMsgServerImpl(*k) + wctx := sdk.WrapSDKContext(ctx) + + supplierAddr := sample.AccAddress() + + // Prepare the supplier stake message + stakeMsg := &types.MsgStakeSupplier{ + Address: supplierAddr, + Stake: &sdk.Coin{Denom: "upokt", Amount: sdk.NewInt(100)}, + Services: []*sharedtypes.SupplierServiceConfig{ + { + ServiceId: &sharedtypes.ServiceId{ + Id: "svcId", + }, + Endpoints: []*sharedtypes.SupplierEndpoint{ + { + Url: "http://localhost:8080", + RpcType: sharedtypes.RPCType_JSON_RPC, + Configs: make([]*sharedtypes.ConfigOption, 0), + }, + }, + }, + }, + } + + // Stake the supplier + _, err := srv.StakeSupplier(wctx, stakeMsg) + require.NoError(t, err) + + // Prepare the supplier stake message without any service endpoints + updateStakeMsg := &types.MsgStakeSupplier{ + Address: supplierAddr, + Stake: &sdk.Coin{Denom: "upokt", Amount: sdk.NewInt(100)}, + Services: []*sharedtypes.SupplierServiceConfig{ + { + ServiceId: &sharedtypes.ServiceId{Id: "svcId"}, + Endpoints: []*sharedtypes.SupplierEndpoint{}, + }, + }, + } + + // Fail updating the supplier when the list of service endpoints is empty + _, err = srv.StakeSupplier(wctx, updateStakeMsg) + require.Error(t, err) + + // Verify the supplierFound still exists and is staked for svc1 + supplierFound, isSupplierFound := k.GetSupplier(ctx, supplierAddr) + require.True(t, isSupplierFound) + require.Equal(t, supplierAddr, supplierFound.Address) + require.Len(t, supplierFound.Services, 1) + require.Equal(t, "svcId", supplierFound.Services[0].ServiceId.Id) + require.Len(t, supplierFound.Services[0].Endpoints, 1) + require.Equal(t, "http://localhost:8080", supplierFound.Services[0].Endpoints[0].Url) + + // Prepare the supplier stake message with an invalid service ID + updateStakeMsg = &types.MsgStakeSupplier{ + Address: supplierAddr, + Stake: &sdk.Coin{Denom: "upokt", Amount: sdk.NewInt(100)}, + Services: []*sharedtypes.SupplierServiceConfig{ + { + ServiceId: &sharedtypes.ServiceId{Id: "svc1 INVALID ! & *"}, + }, + }, + } + + // Fail updating the supplier when the list of services is empty + _, err = srv.StakeSupplier(wctx, updateStakeMsg) + require.Error(t, err) + + // Verify the supplier still exists and is staked for svc1 + supplierFound, isSupplierFound = k.GetSupplier(ctx, supplierAddr) require.True(t, isSupplierFound) - require.Equal(t, int64(200), foundSupplier.Stake.Amount.Int64()) + require.Equal(t, supplierAddr, supplierFound.Address) + require.Len(t, supplierFound.Services, 1) + require.Equal(t, "svcId", supplierFound.Services[0].ServiceId.Id) + require.Len(t, supplierFound.Services[0].Endpoints, 1) + require.Equal(t, "http://localhost:8080", supplierFound.Services[0].Endpoints[0].Url) } func TestMsgServer_StakeSupplier_FailLoweringStake(t *testing.T) { @@ -64,6 +182,20 @@ func TestMsgServer_StakeSupplier_FailLoweringStake(t *testing.T) { stakeMsg := &types.MsgStakeSupplier{ Address: addr, Stake: &sdk.Coin{Denom: "upokt", Amount: sdk.NewInt(100)}, + Services: []*sharedtypes.SupplierServiceConfig{ + { + ServiceId: &sharedtypes.ServiceId{ + Id: "svcId", + }, + Endpoints: []*sharedtypes.SupplierEndpoint{ + { + Url: "http://localhost:8080", + RpcType: sharedtypes.RPCType_JSON_RPC, + Configs: make([]*sharedtypes.ConfigOption, 0), + }, + }, + }, + }, } // Stake the supplier & verify that the supplier exists @@ -76,6 +208,20 @@ func TestMsgServer_StakeSupplier_FailLoweringStake(t *testing.T) { updateMsg := &types.MsgStakeSupplier{ Address: addr, Stake: &sdk.Coin{Denom: "upokt", Amount: sdk.NewInt(50)}, + Services: []*sharedtypes.SupplierServiceConfig{ + { + ServiceId: &sharedtypes.ServiceId{ + Id: "svcId", + }, + Endpoints: []*sharedtypes.SupplierEndpoint{ + { + Url: "http://localhost:8080", + RpcType: sharedtypes.RPCType_JSON_RPC, + Configs: make([]*sharedtypes.ConfigOption, 0), + }, + }, + }, + }, } // Verify that it fails @@ -86,4 +232,5 @@ func TestMsgServer_StakeSupplier_FailLoweringStake(t *testing.T) { supplierFound, isSupplierFound := k.GetSupplier(ctx, addr) require.True(t, isSupplierFound) require.Equal(t, int64(100), supplierFound.Stake.Amount.Int64()) + require.Len(t, supplierFound.Services, 1) } diff --git a/x/supplier/keeper/msg_server_submit_proof.go b/x/supplier/keeper/msg_server_submit_proof.go index cd8f104d1..2715b7c8d 100644 --- a/x/supplier/keeper/msg_server_submit_proof.go +++ b/x/supplier/keeper/msg_server_submit_proof.go @@ -5,7 +5,7 @@ import ( sdk "github.com/cosmos/cosmos-sdk/types" - "pocket/x/supplier/types" + "github.com/pokt-network/poktroll/x/supplier/types" ) func (k msgServer) SubmitProof(goCtx context.Context, msg *types.MsgSubmitProof) (*types.MsgSubmitProofResponse, error) { diff --git a/x/supplier/keeper/msg_server_test.go b/x/supplier/keeper/msg_server_test.go index 2ca2981e7..7e4d01f27 100644 --- a/x/supplier/keeper/msg_server_test.go +++ b/x/supplier/keeper/msg_server_test.go @@ -6,9 +6,10 @@ import ( sdk "github.com/cosmos/cosmos-sdk/types" "github.com/stretchr/testify/require" - keepertest "pocket/testutil/keeper" - "pocket/x/supplier/keeper" - "pocket/x/supplier/types" + + keepertest "github.com/pokt-network/poktroll/testutil/keeper" + "github.com/pokt-network/poktroll/x/supplier/keeper" + "github.com/pokt-network/poktroll/x/supplier/types" ) func setupMsgServer(t testing.TB) (types.MsgServer, context.Context) { diff --git a/x/supplier/keeper/msg_server_unstake_supplier.go b/x/supplier/keeper/msg_server_unstake_supplier.go index b1028f278..830a4c37a 100644 --- a/x/supplier/keeper/msg_server_unstake_supplier.go +++ b/x/supplier/keeper/msg_server_unstake_supplier.go @@ -5,7 +5,7 @@ import ( sdk "github.com/cosmos/cosmos-sdk/types" - "pocket/x/supplier/types" + "github.com/pokt-network/poktroll/x/supplier/types" ) // TODO(#73): Determine if an application needs an unbonding period after unstaking. diff --git a/x/supplier/keeper/msg_server_unstake_supplier_test.go b/x/supplier/keeper/msg_server_unstake_supplier_test.go index b083aac2b..163cbe5ae 100644 --- a/x/supplier/keeper/msg_server_unstake_supplier_test.go +++ b/x/supplier/keeper/msg_server_unstake_supplier_test.go @@ -6,10 +6,11 @@ import ( sdk "github.com/cosmos/cosmos-sdk/types" "github.com/stretchr/testify/require" - keepertest "pocket/testutil/keeper" - "pocket/testutil/sample" - "pocket/x/supplier/keeper" - "pocket/x/supplier/types" + keepertest "github.com/pokt-network/poktroll/testutil/keeper" + "github.com/pokt-network/poktroll/testutil/sample" + sharedtypes "github.com/pokt-network/poktroll/x/shared/types" + "github.com/pokt-network/poktroll/x/supplier/keeper" + "github.com/pokt-network/poktroll/x/supplier/types" ) func TestMsgServer_UnstakeSupplier_Success(t *testing.T) { @@ -29,6 +30,20 @@ func TestMsgServer_UnstakeSupplier_Success(t *testing.T) { stakeMsg := &types.MsgStakeSupplier{ Address: addr, Stake: &initialStake, + Services: []*sharedtypes.SupplierServiceConfig{ + { + ServiceId: &sharedtypes.ServiceId{ + Id: "svcId", + }, + Endpoints: []*sharedtypes.SupplierEndpoint{ + { + Url: "http://localhost:8080", + RpcType: sharedtypes.RPCType_JSON_RPC, + Configs: make([]*sharedtypes.ConfigOption, 0), + }, + }, + }, + }, } // Stake the supplier @@ -40,6 +55,7 @@ func TestMsgServer_UnstakeSupplier_Success(t *testing.T) { require.True(t, isSupplierFound) require.Equal(t, addr, foundSupplier.Address) require.Equal(t, initialStake.Amount, foundSupplier.Stake.Amount) + require.Len(t, foundSupplier.Services, 1) // Unstake the supplier unstakeMsg := &types.MsgUnstakeSupplier{Address: addr} diff --git a/x/supplier/keeper/params.go b/x/supplier/keeper/params.go index 9c24f3acc..86ca01cbe 100644 --- a/x/supplier/keeper/params.go +++ b/x/supplier/keeper/params.go @@ -2,7 +2,8 @@ package keeper import ( sdk "github.com/cosmos/cosmos-sdk/types" - "pocket/x/supplier/types" + + "github.com/pokt-network/poktroll/x/supplier/types" ) // GetParams get all parameters as types.Params diff --git a/x/supplier/keeper/params_test.go b/x/supplier/keeper/params_test.go index 80fb79d25..5a7e866fe 100644 --- a/x/supplier/keeper/params_test.go +++ b/x/supplier/keeper/params_test.go @@ -4,8 +4,9 @@ import ( "testing" "github.com/stretchr/testify/require" - testkeeper "pocket/testutil/keeper" - "pocket/x/supplier/types" + + testkeeper "github.com/pokt-network/poktroll/testutil/keeper" + "github.com/pokt-network/poktroll/x/supplier/types" ) func TestGetParams(t *testing.T) { diff --git a/x/supplier/keeper/query.go b/x/supplier/keeper/query.go index 87e49fbc5..2d1d5c18e 100644 --- a/x/supplier/keeper/query.go +++ b/x/supplier/keeper/query.go @@ -1,7 +1,7 @@ package keeper import ( - "pocket/x/supplier/types" + "github.com/pokt-network/poktroll/x/supplier/types" ) var _ types.QueryServer = Keeper{} diff --git a/x/supplier/keeper/query_params.go b/x/supplier/keeper/query_params.go index dbadba386..67e2a17f4 100644 --- a/x/supplier/keeper/query_params.go +++ b/x/supplier/keeper/query_params.go @@ -6,7 +6,8 @@ import ( sdk "github.com/cosmos/cosmos-sdk/types" "google.golang.org/grpc/codes" "google.golang.org/grpc/status" - "pocket/x/supplier/types" + + "github.com/pokt-network/poktroll/x/supplier/types" ) func (k Keeper) Params(goCtx context.Context, req *types.QueryParamsRequest) (*types.QueryParamsResponse, error) { diff --git a/x/supplier/keeper/query_params_test.go b/x/supplier/keeper/query_params_test.go index 831196046..d9b909305 100644 --- a/x/supplier/keeper/query_params_test.go +++ b/x/supplier/keeper/query_params_test.go @@ -5,8 +5,9 @@ import ( sdk "github.com/cosmos/cosmos-sdk/types" "github.com/stretchr/testify/require" - testkeeper "pocket/testutil/keeper" - "pocket/x/supplier/types" + + testkeeper "github.com/pokt-network/poktroll/testutil/keeper" + "github.com/pokt-network/poktroll/x/supplier/types" ) func TestParamsQuery(t *testing.T) { diff --git a/x/supplier/keeper/query_supplier.go b/x/supplier/keeper/query_supplier.go index 193b0fb5e..14d5afab5 100644 --- a/x/supplier/keeper/query_supplier.go +++ b/x/supplier/keeper/query_supplier.go @@ -9,8 +9,8 @@ import ( "google.golang.org/grpc/codes" "google.golang.org/grpc/status" - sharedtypes "pocket/x/shared/types" - "pocket/x/supplier/types" + sharedtypes "github.com/pokt-network/poktroll/x/shared/types" + "github.com/pokt-network/poktroll/x/supplier/types" ) func (k Keeper) SupplierAll(goCtx context.Context, req *types.QueryAllSupplierRequest) (*types.QueryAllSupplierResponse, error) { diff --git a/x/supplier/keeper/query_supplier_test.go b/x/supplier/keeper/query_supplier_test.go index aa9095f79..6690e3f75 100644 --- a/x/supplier/keeper/query_supplier_test.go +++ b/x/supplier/keeper/query_supplier_test.go @@ -10,9 +10,9 @@ import ( "google.golang.org/grpc/codes" "google.golang.org/grpc/status" - keepertest "pocket/testutil/keeper" - "pocket/testutil/nullify" - "pocket/x/supplier/types" + keepertest "github.com/pokt-network/poktroll/testutil/keeper" + "github.com/pokt-network/poktroll/testutil/nullify" + "github.com/pokt-network/poktroll/x/supplier/types" ) // Prevent strconv unused error diff --git a/x/supplier/keeper/supplier.go b/x/supplier/keeper/supplier.go index f3ae6a310..0c31db2be 100644 --- a/x/supplier/keeper/supplier.go +++ b/x/supplier/keeper/supplier.go @@ -4,8 +4,8 @@ import ( "github.com/cosmos/cosmos-sdk/store/prefix" sdk "github.com/cosmos/cosmos-sdk/types" - sharedtypes "pocket/x/shared/types" - "pocket/x/supplier/types" + sharedtypes "github.com/pokt-network/poktroll/x/shared/types" + "github.com/pokt-network/poktroll/x/supplier/types" ) // SetSupplier set a specific supplier in the store from its index @@ -20,49 +20,49 @@ func (k Keeper) SetSupplier(ctx sdk.Context, supplier sharedtypes.Supplier) { // GetSupplier returns a supplier from its index func (k Keeper) GetSupplier( ctx sdk.Context, - address string, + supplierAddr string, -) (val sharedtypes.Supplier, found bool) { +) (supplier sharedtypes.Supplier, found bool) { store := prefix.NewStore(ctx.KVStore(k.storeKey), types.KeyPrefix(types.SupplierKeyPrefix)) b := store.Get(types.SupplierKey( - address, + supplierAddr, )) if b == nil { - return val, false + return supplier, false } - k.cdc.MustUnmarshal(b, &val) - return val, true + k.cdc.MustUnmarshal(b, &supplier) + return supplier, true } // RemoveSupplier removes a supplier from the store func (k Keeper) RemoveSupplier( ctx sdk.Context, - address string, + supplierAddr string, ) { store := prefix.NewStore(ctx.KVStore(k.storeKey), types.KeyPrefix(types.SupplierKeyPrefix)) store.Delete(types.SupplierKey( - address, + supplierAddr, )) } // GetAllSupplier returns all supplier -func (k Keeper) GetAllSupplier(ctx sdk.Context) (list []sharedtypes.Supplier) { +func (k Keeper) GetAllSupplier(ctx sdk.Context) (suppliers []sharedtypes.Supplier) { store := prefix.NewStore(ctx.KVStore(k.storeKey), types.KeyPrefix(types.SupplierKeyPrefix)) iterator := sdk.KVStorePrefixIterator(store, []byte{}) defer iterator.Close() for ; iterator.Valid(); iterator.Next() { - var val sharedtypes.Supplier - k.cdc.MustUnmarshal(iterator.Value(), &val) - list = append(list, val) + var supplier sharedtypes.Supplier + k.cdc.MustUnmarshal(iterator.Value(), &supplier) + suppliers = append(suppliers, supplier) } return } // TODO_OPTIMIZE: Index suppliers by serviceId so we can easily query `k.GetAllSupplier(ctx, ServiceId)` -// func (k Keeper) GetAllSupplier(ctx, sdkContext, serviceId string) (list []sharedtypes.Supplier) {} +// func (k Keeper) GetAllSupplier(ctx, sdkContext, serviceId string) (suppliers []sharedtypes.Supplier) {} diff --git a/x/supplier/keeper/supplier_test.go b/x/supplier/keeper/supplier_test.go index a2d072f79..c02c5758e 100644 --- a/x/supplier/keeper/supplier_test.go +++ b/x/supplier/keeper/supplier_test.go @@ -1,64 +1,94 @@ package keeper_test import ( + "fmt" "strconv" "testing" sdk "github.com/cosmos/cosmos-sdk/types" + authtypes "github.com/cosmos/cosmos-sdk/x/auth/types" "github.com/stretchr/testify/require" - keepertest "pocket/testutil/keeper" - "pocket/testutil/nullify" - sharedtypes "pocket/x/shared/types" - "pocket/x/supplier/keeper" + "github.com/pokt-network/poktroll/cmd/pocketd/cmd" + keepertest "github.com/pokt-network/poktroll/testutil/keeper" + "github.com/pokt-network/poktroll/testutil/nullify" + "github.com/pokt-network/poktroll/testutil/sample" + sharedtypes "github.com/pokt-network/poktroll/x/shared/types" + "github.com/pokt-network/poktroll/x/supplier/keeper" + "github.com/pokt-network/poktroll/x/supplier/types" ) // Prevent strconv unused error var _ = strconv.IntSize -func createNSupplier(keeper *keeper.Keeper, ctx sdk.Context, n int) []sharedtypes.Supplier { - items := make([]sharedtypes.Supplier, n) - for i := range items { - items[i].Address = strconv.Itoa(i) +func init() { + cmd.InitSDKConfig() +} - keeper.SetSupplier(ctx, items[i]) +func createNSupplier(keeper *keeper.Keeper, ctx sdk.Context, n int) []sharedtypes.Supplier { + suppliers := make([]sharedtypes.Supplier, n) + for i := range suppliers { + supplier := &suppliers[i] + supplier.Address = sample.AccAddress() + supplier.Stake = &sdk.Coin{Denom: "upokt", Amount: sdk.NewInt(int64(i))} + supplier.Services = []*sharedtypes.SupplierServiceConfig{ + { + ServiceId: &sharedtypes.ServiceId{Id: fmt.Sprintf("svc%d", i)}, + Endpoints: []*sharedtypes.SupplierEndpoint{ + { + Url: fmt.Sprintf("http://localhost:%d", i), + RpcType: sharedtypes.RPCType_JSON_RPC, + Configs: make([]*sharedtypes.ConfigOption, 0), + }, + }, + }, + } + keeper.SetSupplier(ctx, *supplier) } - return items + + return suppliers } func TestSupplierGet(t *testing.T) { keeper, ctx := keepertest.SupplierKeeper(t) - items := createNSupplier(keeper, ctx, 10) - for _, item := range items { - rst, found := keeper.GetSupplier(ctx, - item.Address, + suppliers := createNSupplier(keeper, ctx, 10) + for _, supplier := range suppliers { + supplierFound, isSupplierFound := keeper.GetSupplier(ctx, + supplier.Address, ) - require.True(t, found) + require.True(t, isSupplierFound) require.Equal(t, - nullify.Fill(&item), - nullify.Fill(&rst), + nullify.Fill(&supplier), + nullify.Fill(&supplierFound), ) } } func TestSupplierRemove(t *testing.T) { keeper, ctx := keepertest.SupplierKeeper(t) - items := createNSupplier(keeper, ctx, 10) - for _, item := range items { + suppliers := createNSupplier(keeper, ctx, 10) + for _, supplier := range suppliers { keeper.RemoveSupplier(ctx, - item.Address, + supplier.Address, ) - _, found := keeper.GetSupplier(ctx, - item.Address, + _, isSupplierFound := keeper.GetSupplier(ctx, + supplier.Address, ) - require.False(t, found) + require.False(t, isSupplierFound) } } func TestSupplierGetAll(t *testing.T) { keeper, ctx := keepertest.SupplierKeeper(t) - items := createNSupplier(keeper, ctx, 10) + suppliers := createNSupplier(keeper, ctx, 10) require.ElementsMatch(t, - nullify.Fill(items), + nullify.Fill(suppliers), nullify.Fill(keeper.GetAllSupplier(ctx)), ) } + +// The application module address is derived off of its semantic name. +// This test is a helper for us to easily identify the underlying address. +func TestApplicationModuleAddress(t *testing.T) { + moduleAddress := authtypes.NewModuleAddress(types.ModuleName) + require.Equal(t, "pokt1j40dzzmn6cn9kxku7a5tjnud6hv37vesr5ccaa", moduleAddress.String()) +} diff --git a/x/supplier/module.go b/x/supplier/module.go index 421271406..a39dc9303 100644 --- a/x/supplier/module.go +++ b/x/supplier/module.go @@ -4,6 +4,7 @@ import ( "context" "encoding/json" "fmt" + // this line is used by starport scaffolding # 1 "github.com/grpc-ecosystem/grpc-gateway/runtime" @@ -16,9 +17,10 @@ import ( cdctypes "github.com/cosmos/cosmos-sdk/codec/types" sdk "github.com/cosmos/cosmos-sdk/types" "github.com/cosmos/cosmos-sdk/types/module" - "pocket/x/supplier/client/cli" - "pocket/x/supplier/keeper" - "pocket/x/supplier/types" + + "github.com/pokt-network/poktroll/x/supplier/client/cli" + "github.com/pokt-network/poktroll/x/supplier/keeper" + "github.com/pokt-network/poktroll/x/supplier/types" ) var ( diff --git a/x/supplier/module_simulation.go b/x/supplier/module_simulation.go index 6f0e01ba5..eb30e7dd3 100644 --- a/x/supplier/module_simulation.go +++ b/x/supplier/module_simulation.go @@ -3,9 +3,9 @@ package supplier import ( "math/rand" - "pocket/testutil/sample" - suppliersimulation "pocket/x/supplier/simulation" - "pocket/x/supplier/types" + "github.com/pokt-network/poktroll/testutil/sample" + suppliersimulation "github.com/pokt-network/poktroll/x/supplier/simulation" + "github.com/pokt-network/poktroll/x/supplier/types" "github.com/cosmos/cosmos-sdk/baseapp" sdk "github.com/cosmos/cosmos-sdk/types" diff --git a/x/supplier/simulation/create_claim.go b/x/supplier/simulation/create_claim.go index 60d3ecff0..cae471c06 100644 --- a/x/supplier/simulation/create_claim.go +++ b/x/supplier/simulation/create_claim.go @@ -6,8 +6,9 @@ import ( "github.com/cosmos/cosmos-sdk/baseapp" sdk "github.com/cosmos/cosmos-sdk/types" simtypes "github.com/cosmos/cosmos-sdk/types/simulation" - "pocket/x/supplier/keeper" - "pocket/x/supplier/types" + + "github.com/pokt-network/poktroll/x/supplier/keeper" + "github.com/pokt-network/poktroll/x/supplier/types" ) func SimulateMsgCreateClaim( diff --git a/x/supplier/simulation/stake_supplier.go b/x/supplier/simulation/stake_supplier.go index 159035ee3..95fb7e8d6 100644 --- a/x/supplier/simulation/stake_supplier.go +++ b/x/supplier/simulation/stake_supplier.go @@ -7,8 +7,8 @@ import ( sdk "github.com/cosmos/cosmos-sdk/types" simtypes "github.com/cosmos/cosmos-sdk/types/simulation" - "pocket/x/supplier/keeper" - "pocket/x/supplier/types" + "github.com/pokt-network/poktroll/x/supplier/keeper" + "github.com/pokt-network/poktroll/x/supplier/types" ) func SimulateMsgStakeSupplier( @@ -21,6 +21,7 @@ func SimulateMsgStakeSupplier( simAccount, _ := simtypes.RandomAcc(r, accs) stakeMsg := &types.MsgStakeSupplier{ Address: simAccount.Address.String(), + // TODO: Update all stake message fields } // TODO: Handling the StakeSupplier simulation diff --git a/x/supplier/simulation/submit_proof.go b/x/supplier/simulation/submit_proof.go index 8996997d6..3473f1c7b 100644 --- a/x/supplier/simulation/submit_proof.go +++ b/x/supplier/simulation/submit_proof.go @@ -6,8 +6,9 @@ import ( "github.com/cosmos/cosmos-sdk/baseapp" sdk "github.com/cosmos/cosmos-sdk/types" simtypes "github.com/cosmos/cosmos-sdk/types/simulation" - "pocket/x/supplier/keeper" - "pocket/x/supplier/types" + + "github.com/pokt-network/poktroll/x/supplier/keeper" + "github.com/pokt-network/poktroll/x/supplier/types" ) func SimulateMsgSubmitProof( diff --git a/x/supplier/simulation/unstake_supplier.go b/x/supplier/simulation/unstake_supplier.go index c37290835..3955b25db 100644 --- a/x/supplier/simulation/unstake_supplier.go +++ b/x/supplier/simulation/unstake_supplier.go @@ -6,8 +6,9 @@ import ( "github.com/cosmos/cosmos-sdk/baseapp" sdk "github.com/cosmos/cosmos-sdk/types" simtypes "github.com/cosmos/cosmos-sdk/types/simulation" - "pocket/x/supplier/keeper" - "pocket/x/supplier/types" + + "github.com/pokt-network/poktroll/x/supplier/keeper" + "github.com/pokt-network/poktroll/x/supplier/types" ) func SimulateMsgUnstakeSupplier( diff --git a/x/supplier/types/errors.go b/x/supplier/types/errors.go index a345e67cc..02a76a573 100644 --- a/x/supplier/types/errors.go +++ b/x/supplier/types/errors.go @@ -8,8 +8,9 @@ import ( // x/supplier module sentinel errors var ( - ErrSupplierInvalidStake = sdkerrors.Register(ModuleName, 1, "invalid supplier stake") - ErrSupplierInvalidAddress = sdkerrors.Register(ModuleName, 2, "invalid supplier address") - ErrSupplierUnauthorized = sdkerrors.Register(ModuleName, 3, "unauthorized supplier signer") - ErrSupplierNotFound = sdkerrors.Register(ModuleName, 4, "supplier not found") + ErrSupplierInvalidStake = sdkerrors.Register(ModuleName, 1, "invalid supplier stake") + ErrSupplierInvalidAddress = sdkerrors.Register(ModuleName, 2, "invalid supplier address") + ErrSupplierUnauthorized = sdkerrors.Register(ModuleName, 3, "unauthorized supplier signer") + ErrSupplierNotFound = sdkerrors.Register(ModuleName, 4, "supplier not found") + ErrSupplierInvalidServiceConfig = sdkerrors.Register(ModuleName, 5, "invalid service config") ) diff --git a/x/supplier/types/genesis.go b/x/supplier/types/genesis.go index 1573a1269..2f4ff1872 100644 --- a/x/supplier/types/genesis.go +++ b/x/supplier/types/genesis.go @@ -6,7 +6,8 @@ import ( sdkerrors "cosmossdk.io/errors" sdk "github.com/cosmos/cosmos-sdk/types" - sharedtypes "pocket/x/shared/types" + servicehelpers "github.com/pokt-network/poktroll/x/shared/helpers" + sharedtypes "github.com/pokt-network/poktroll/x/shared/types" ) // DefaultIndex is the default global index @@ -36,6 +37,8 @@ func (gs GenesisState) Validate() error { // Check that the stake value for the suppliers is valid for _, supplier := range gs.SupplierList { + // TODO_TECHDEBT: Consider creating shared helpers across the board for stake validation, + // similar to how we have `ValidateAppServiceConfigs` below if supplier.Stake == nil { return sdkerrors.Wrapf(ErrSupplierInvalidStake, "nil stake amount for supplier") } @@ -52,6 +55,12 @@ func (gs GenesisState) Validate() error { if stake.Denom != "upokt" { return sdkerrors.Wrapf(ErrSupplierInvalidStake, "invalid stake amount denom for supplier %v", supplier.Stake) } + + // Valid the application service configs + // Validate the application service configs + if err := servicehelpers.ValidateSupplierServiceConfigs(supplier.Services); err != nil { + return sdkerrors.Wrapf(ErrSupplierInvalidServiceConfig, err.Error()) + } } // this line is used by starport scaffolding # genesis/types/validate diff --git a/x/supplier/types/genesis_test.go b/x/supplier/types/genesis_test.go index 2f33966e5..ba806c98b 100644 --- a/x/supplier/types/genesis_test.go +++ b/x/supplier/types/genesis_test.go @@ -6,17 +6,43 @@ import ( sdk "github.com/cosmos/cosmos-sdk/types" "github.com/stretchr/testify/require" - "pocket/testutil/sample" - sharedtypes "pocket/x/shared/types" - "pocket/x/supplier/types" + "github.com/pokt-network/poktroll/testutil/sample" + sharedtypes "github.com/pokt-network/poktroll/x/shared/types" + "github.com/pokt-network/poktroll/x/supplier/types" ) func TestGenesisState_Validate(t *testing.T) { addr1 := sample.AccAddress() stake1 := sdk.NewCoin("upokt", sdk.NewInt(100)) + serviceConfig1 := &sharedtypes.SupplierServiceConfig{ + ServiceId: &sharedtypes.ServiceId{ + Id: "svcId1", + }, + Endpoints: []*sharedtypes.SupplierEndpoint{ + { + Url: "http://localhost:8081", + RpcType: sharedtypes.RPCType_JSON_RPC, + Configs: make([]*sharedtypes.ConfigOption, 0), + }, + }, + } + serviceList1 := []*sharedtypes.SupplierServiceConfig{serviceConfig1} addr2 := sample.AccAddress() stake2 := sdk.NewCoin("upokt", sdk.NewInt(100)) + serviceConfig2 := &sharedtypes.SupplierServiceConfig{ + ServiceId: &sharedtypes.ServiceId{ + Id: "svcId2", + }, + Endpoints: []*sharedtypes.SupplierEndpoint{ + { + Url: "http://localhost:8082", + RpcType: sharedtypes.RPCType_GRPC, + Configs: make([]*sharedtypes.ConfigOption, 0), + }, + }, + } + serviceList2 := []*sharedtypes.SupplierServiceConfig{serviceConfig2} tests := []struct { desc string @@ -34,12 +60,14 @@ func TestGenesisState_Validate(t *testing.T) { SupplierList: []sharedtypes.Supplier{ { - Address: addr1, - Stake: &stake1, + Address: addr1, + Stake: &stake1, + Services: serviceList1, }, { - Address: addr2, - Stake: &stake2, + Address: addr2, + Stake: &stake2, + Services: serviceList2, }, }, // this line is used by starport scaffolding # types/genesis/validField @@ -51,12 +79,14 @@ func TestGenesisState_Validate(t *testing.T) { genState: &types.GenesisState{ SupplierList: []sharedtypes.Supplier{ { - Address: addr1, - Stake: &stake1, + Address: addr1, + Stake: &stake1, + Services: serviceList1, }, { - Address: addr2, - Stake: &sdk.Coin{Denom: "upokt", Amount: sdk.NewInt(0)}, + Address: addr2, + Stake: &sdk.Coin{Denom: "upokt", Amount: sdk.NewInt(0)}, + Services: serviceList2, }, }, }, @@ -67,12 +97,14 @@ func TestGenesisState_Validate(t *testing.T) { genState: &types.GenesisState{ SupplierList: []sharedtypes.Supplier{ { - Address: addr1, - Stake: &stake1, + Address: addr1, + Stake: &stake1, + Services: serviceList1, }, { - Address: addr2, - Stake: &sdk.Coin{Denom: "upokt", Amount: sdk.NewInt(-100)}, + Address: addr2, + Stake: &sdk.Coin{Denom: "upokt", Amount: sdk.NewInt(-100)}, + Services: serviceList2, }, }, }, @@ -83,12 +115,14 @@ func TestGenesisState_Validate(t *testing.T) { genState: &types.GenesisState{ SupplierList: []sharedtypes.Supplier{ { - Address: addr1, - Stake: &stake1, + Address: addr1, + Stake: &stake1, + Services: serviceList1, }, { - Address: addr2, - Stake: &sdk.Coin{Denom: "invalid", Amount: sdk.NewInt(100)}, + Address: addr2, + Stake: &sdk.Coin{Denom: "invalid", Amount: sdk.NewInt(100)}, + Services: serviceList2, }, }, }, @@ -99,12 +133,14 @@ func TestGenesisState_Validate(t *testing.T) { genState: &types.GenesisState{ SupplierList: []sharedtypes.Supplier{ { - Address: addr1, - Stake: &stake1, + Address: addr1, + Stake: &stake1, + Services: serviceList1, }, { - Address: addr2, - Stake: &sdk.Coin{Denom: "", Amount: sdk.NewInt(100)}, + Address: addr2, + Stake: &sdk.Coin{Denom: "", Amount: sdk.NewInt(100)}, + Services: serviceList2, }, }, }, @@ -115,12 +151,14 @@ func TestGenesisState_Validate(t *testing.T) { genState: &types.GenesisState{ SupplierList: []sharedtypes.Supplier{ { - Address: addr1, - Stake: &stake1, + Address: addr1, + Stake: &stake1, + Services: serviceList1, }, { - Address: addr1, - Stake: &stake2, + Address: addr1, + Stake: &stake2, + Services: serviceList2, }, }, }, @@ -131,12 +169,14 @@ func TestGenesisState_Validate(t *testing.T) { genState: &types.GenesisState{ SupplierList: []sharedtypes.Supplier{ { - Address: addr1, - Stake: &stake1, + Address: addr1, + Stake: &stake1, + Services: serviceList1, }, { - Address: addr2, - Stake: nil, + Address: addr2, + Stake: nil, + Services: serviceList2, }, }, }, @@ -147,12 +187,112 @@ func TestGenesisState_Validate(t *testing.T) { genState: &types.GenesisState{ SupplierList: []sharedtypes.Supplier{ { - Address: addr1, - Stake: &stake1, + Address: addr1, + Stake: &stake1, + Services: serviceList1, }, { Address: addr2, // Explicitly missing stake + Services: serviceList2, + }, + }, + }, + valid: false, + }, + { + desc: "invalid - missing services list", + genState: &types.GenesisState{ + SupplierList: []sharedtypes.Supplier{ + { + Address: addr1, + Stake: &stake1, + Services: serviceList1, + }, + { + Address: addr2, + Stake: &stake2, + // Services: intentionally omitted + }, + }, + }, + valid: false, + }, + { + desc: "invalid - empty services list", + genState: &types.GenesisState{ + SupplierList: []sharedtypes.Supplier{ + { + Address: addr1, + Stake: &stake1, + Services: serviceList1, + }, + { + Address: addr2, + Stake: &stake2, + Services: []*sharedtypes.SupplierServiceConfig{}, + }, + }, + }, + valid: false, + }, + { + desc: "invalid - invalid URL", + genState: &types.GenesisState{ + SupplierList: []sharedtypes.Supplier{ + { + Address: addr1, + Stake: &stake1, + Services: serviceList1, + }, + { + Address: addr2, + Stake: &stake2, + Services: []*sharedtypes.SupplierServiceConfig{ + { + ServiceId: &sharedtypes.ServiceId{ + Id: "svcId1", + }, + Endpoints: []*sharedtypes.SupplierEndpoint{ + { + Url: "invalid URL", + RpcType: sharedtypes.RPCType_JSON_RPC, + Configs: make([]*sharedtypes.ConfigOption, 0), + }, + }, + }, + }, + }, + }, + }, + valid: false, + }, + { + desc: "invalid - invalid RPC Type", + genState: &types.GenesisState{ + SupplierList: []sharedtypes.Supplier{ + { + Address: addr1, + Stake: &stake1, + Services: serviceList1, + }, + { + Address: addr2, + Stake: &stake2, + Services: []*sharedtypes.SupplierServiceConfig{ + { + ServiceId: &sharedtypes.ServiceId{ + Id: "svcId1", + }, + Endpoints: []*sharedtypes.SupplierEndpoint{ + { + Url: "http://localhost:8081", + RpcType: sharedtypes.RPCType_UNKNOWN_RPC, + Configs: make([]*sharedtypes.ConfigOption, 0), + }, + }, + }, + }, }, }, }, diff --git a/x/supplier/types/message_create_claim.go b/x/supplier/types/message_create_claim.go index 4bcfada3b..7d68c6a94 100644 --- a/x/supplier/types/message_create_claim.go +++ b/x/supplier/types/message_create_claim.go @@ -4,7 +4,7 @@ import ( sdkerrors "cosmossdk.io/errors" sdk "github.com/cosmos/cosmos-sdk/types" - sessiontypes "pocket/x/session/types" + sessiontypes "github.com/pokt-network/poktroll/x/session/types" ) const TypeMsgCreateClaim = "create_claim" diff --git a/x/supplier/types/message_create_claim_test.go b/x/supplier/types/message_create_claim_test.go index c10e3a4c9..8401c0d3d 100644 --- a/x/supplier/types/message_create_claim_test.go +++ b/x/supplier/types/message_create_claim_test.go @@ -5,7 +5,7 @@ import ( "github.com/stretchr/testify/require" - "pocket/testutil/sample" + "github.com/pokt-network/poktroll/testutil/sample" ) // TODO(@bryanchriswhite): Add unit tests for message validation when adding the business logic. diff --git a/x/supplier/types/message_stake_supplier.go b/x/supplier/types/message_stake_supplier.go index 9d4ce5cdc..7d1dbde07 100644 --- a/x/supplier/types/message_stake_supplier.go +++ b/x/supplier/types/message_stake_supplier.go @@ -4,6 +4,9 @@ import ( sdkerrors "cosmossdk.io/errors" sdk "github.com/cosmos/cosmos-sdk/types" types "github.com/cosmos/cosmos-sdk/types" + + servicehelpers "github.com/pokt-network/poktroll/x/shared/helpers" + sharedtypes "github.com/pokt-network/poktroll/x/shared/types" ) const TypeMsgStakeSupplier = "stake_supplier" @@ -13,11 +16,12 @@ var _ sdk.Msg = (*MsgStakeSupplier)(nil) func NewMsgStakeSupplier( address string, stake types.Coin, - + services []*sharedtypes.SupplierServiceConfig, ) *MsgStakeSupplier { return &MsgStakeSupplier{ - Address: address, - Stake: &stake, + Address: address, + Stake: &stake, + Services: services, } } @@ -49,6 +53,7 @@ func (msg *MsgStakeSupplier) ValidateBasic() error { return sdkerrors.Wrapf(ErrSupplierInvalidAddress, "invalid supplier address %s; (%v)", msg.Address, err) } + // TODO_TECHDEBT: Centralize stake related verification and share across different parts of the source code // Validate the stake amount if msg.Stake == nil { return sdkerrors.Wrapf(ErrSupplierInvalidStake, "nil supplier stake; (%v)", err) @@ -67,5 +72,10 @@ func (msg *MsgStakeSupplier) ValidateBasic() error { return sdkerrors.Wrapf(ErrSupplierInvalidStake, "invalid stake amount denom for supplier %v", msg.Stake) } + // Validate the supplier service configs + if err := servicehelpers.ValidateSupplierServiceConfigs(msg.Services); err != nil { + return sdkerrors.Wrapf(ErrSupplierInvalidServiceConfig, err.Error()) + } + return nil } diff --git a/x/supplier/types/message_stake_supplier_test.go b/x/supplier/types/message_stake_supplier_test.go index 3aa725c0b..57ce3e4fd 100644 --- a/x/supplier/types/message_stake_supplier_test.go +++ b/x/supplier/types/message_stake_supplier_test.go @@ -6,64 +6,284 @@ import ( sdk "github.com/cosmos/cosmos-sdk/types" "github.com/stretchr/testify/require" - "pocket/testutil/sample" + "github.com/pokt-network/poktroll/testutil/sample" + sharedtypes "github.com/pokt-network/poktroll/x/shared/types" ) +// TODO_CLEANUP: This test has a lot of copy-pasted code from test to test. +// It can be simplified by splitting it into smaller tests where the common +// fields don't need to be explicitly specified from test to test. func TestMsgStakeSupplier_ValidateBasic(t *testing.T) { + + defaultServicesList := []*sharedtypes.SupplierServiceConfig{ + { + ServiceId: &sharedtypes.ServiceId{ + Id: "svcId1", + }, + Endpoints: []*sharedtypes.SupplierEndpoint{ + { + Url: "http://localhost:8081", + RpcType: sharedtypes.RPCType_JSON_RPC, + Configs: make([]*sharedtypes.ConfigOption, 0), + }, + }, + }} + tests := []struct { name string msg MsgStakeSupplier err error }{ + // address related tests { name: "invalid address - nil stake", msg: MsgStakeSupplier{ Address: "invalid_address", // Stake explicitly nil + Services: defaultServicesList, }, err: ErrSupplierInvalidAddress, - }, { + }, + + // stake related tests + { name: "valid address - nil stake", msg: MsgStakeSupplier{ Address: sample.AccAddress(), // Stake explicitly nil + Services: defaultServicesList, }, err: ErrSupplierInvalidStake, }, { name: "valid address - valid stake", msg: MsgStakeSupplier{ - Address: sample.AccAddress(), - Stake: &sdk.Coin{Denom: "upokt", Amount: sdk.NewInt(100)}, + Address: sample.AccAddress(), + Stake: &sdk.Coin{Denom: "upokt", Amount: sdk.NewInt(100)}, + Services: defaultServicesList, }, }, { name: "valid address - zero stake", msg: MsgStakeSupplier{ - Address: sample.AccAddress(), - Stake: &sdk.Coin{Denom: "upokt", Amount: sdk.NewInt(0)}, + Address: sample.AccAddress(), + Stake: &sdk.Coin{Denom: "upokt", Amount: sdk.NewInt(0)}, + Services: defaultServicesList, }, err: ErrSupplierInvalidStake, }, { name: "valid address - negative stake", msg: MsgStakeSupplier{ - Address: sample.AccAddress(), - Stake: &sdk.Coin{Denom: "upokt", Amount: sdk.NewInt(-100)}, + Address: sample.AccAddress(), + Stake: &sdk.Coin{Denom: "upokt", Amount: sdk.NewInt(-100)}, + Services: defaultServicesList, }, err: ErrSupplierInvalidStake, }, { name: "valid address - invalid stake denom", msg: MsgStakeSupplier{ - Address: sample.AccAddress(), - Stake: &sdk.Coin{Denom: "invalid", Amount: sdk.NewInt(100)}, + Address: sample.AccAddress(), + Stake: &sdk.Coin{Denom: "invalid", Amount: sdk.NewInt(100)}, + Services: defaultServicesList, }, err: ErrSupplierInvalidStake, }, { name: "valid address - invalid stake missing denom", msg: MsgStakeSupplier{ - Address: sample.AccAddress(), - Stake: &sdk.Coin{Denom: "", Amount: sdk.NewInt(100)}, + Address: sample.AccAddress(), + Stake: &sdk.Coin{Denom: "", Amount: sdk.NewInt(100)}, + Services: defaultServicesList, }, err: ErrSupplierInvalidStake, }, + + // service related tests + { + name: "valid service configs - multiple services", + msg: MsgStakeSupplier{ + Address: sample.AccAddress(), + Stake: &sdk.Coin{Denom: "upokt", Amount: sdk.NewInt(100)}, + Services: []*sharedtypes.SupplierServiceConfig{ + { + ServiceId: &sharedtypes.ServiceId{ + Id: "svcId1", + }, + Endpoints: []*sharedtypes.SupplierEndpoint{ + { + Url: "http://localhost:8081", + RpcType: sharedtypes.RPCType_JSON_RPC, + Configs: make([]*sharedtypes.ConfigOption, 0), + }, + }, + }, + { + ServiceId: &sharedtypes.ServiceId{ + Id: "svcId2", + }, + Endpoints: []*sharedtypes.SupplierEndpoint{ + { + Url: "http://localhost:8082", + RpcType: sharedtypes.RPCType_GRPC, + Configs: make([]*sharedtypes.ConfigOption, 0), + }, + }, + }, + }, + }, + }, + { + name: "invalid service configs - omitted", + msg: MsgStakeSupplier{ + Address: sample.AccAddress(), + Stake: &sdk.Coin{Denom: "upokt", Amount: sdk.NewInt(100)}, + // Services: intentionally omitted + }, + err: ErrSupplierInvalidServiceConfig, + }, + { + name: "invalid service configs - empty", + msg: MsgStakeSupplier{ + Address: sample.AccAddress(), + Stake: &sdk.Coin{Denom: "upokt", Amount: sdk.NewInt(100)}, + Services: []*sharedtypes.SupplierServiceConfig{}, + }, + err: ErrSupplierInvalidServiceConfig, + }, + { + name: "invalid service configs - invalid service ID that's too long", + msg: MsgStakeSupplier{ + Address: sample.AccAddress(), + Stake: &sdk.Coin{Denom: "upokt", Amount: sdk.NewInt(100)}, + Services: []*sharedtypes.SupplierServiceConfig{ + { + ServiceId: &sharedtypes.ServiceId{ + Id: "123456790", + }, + Endpoints: []*sharedtypes.SupplierEndpoint{ + { + Url: "http://localhost:8080", + RpcType: sharedtypes.RPCType_JSON_RPC, + Configs: make([]*sharedtypes.ConfigOption, 0), + }, + }, + }, + }, + }, + err: ErrSupplierInvalidServiceConfig, + }, + { + name: "invalid service configs - invalid service Name that's too long", + msg: MsgStakeSupplier{ + Address: sample.AccAddress(), + Stake: &sdk.Coin{Denom: "upokt", Amount: sdk.NewInt(100)}, + Services: []*sharedtypes.SupplierServiceConfig{ + { + ServiceId: &sharedtypes.ServiceId{ + Id: "123", + Name: "abcdefghijklmnopqrstuvwxyzab-abcdefghijklmnopqrstuvwxyzab", + }, + Endpoints: []*sharedtypes.SupplierEndpoint{ + { + Url: "http://localhost:8080", + RpcType: sharedtypes.RPCType_JSON_RPC, + Configs: make([]*sharedtypes.ConfigOption, 0), + }, + }, + }, + }, + }, + err: ErrSupplierInvalidServiceConfig, + }, + { + name: "invalid service configs - invalid service ID that contains invalid characters", + msg: MsgStakeSupplier{ + Address: sample.AccAddress(), + Stake: &sdk.Coin{Denom: "upokt", Amount: sdk.NewInt(100)}, + Services: []*sharedtypes.SupplierServiceConfig{ + { + ServiceId: &sharedtypes.ServiceId{ + Id: "12 45 !", + }, + Endpoints: []*sharedtypes.SupplierEndpoint{ + { + Url: "http://localhost:8080", + RpcType: sharedtypes.RPCType_JSON_RPC, + Configs: make([]*sharedtypes.ConfigOption, 0), + }, + }, + }, + }, + }, + err: ErrSupplierInvalidServiceConfig, + }, + { + name: "invalid service configs - missing url", + msg: MsgStakeSupplier{ + Address: sample.AccAddress(), + Stake: &sdk.Coin{Denom: "upokt", Amount: sdk.NewInt(100)}, + Services: []*sharedtypes.SupplierServiceConfig{ + { + ServiceId: &sharedtypes.ServiceId{ + Id: "svcId", + Name: "name", + }, + Endpoints: []*sharedtypes.SupplierEndpoint{ + { + // Url: intentionally omitted + RpcType: sharedtypes.RPCType_JSON_RPC, + Configs: make([]*sharedtypes.ConfigOption, 0), + }, + }, + }, + }, + }, + err: ErrSupplierInvalidServiceConfig, + }, + { + name: "invalid service configs - invalid url", + msg: MsgStakeSupplier{ + Address: sample.AccAddress(), + Stake: &sdk.Coin{Denom: "upokt", Amount: sdk.NewInt(100)}, + Services: []*sharedtypes.SupplierServiceConfig{ + { + ServiceId: &sharedtypes.ServiceId{ + Id: "svcId", + Name: "name", + }, + Endpoints: []*sharedtypes.SupplierEndpoint{ + { + Url: "I am not a valid URL", + RpcType: sharedtypes.RPCType_JSON_RPC, + Configs: make([]*sharedtypes.ConfigOption, 0), + }, + }, + }, + }, + }, + err: ErrSupplierInvalidServiceConfig, + }, + { + name: "invalid service configs - missing rpc type", + msg: MsgStakeSupplier{ + Address: sample.AccAddress(), + Stake: &sdk.Coin{Denom: "upokt", Amount: sdk.NewInt(100)}, + Services: []*sharedtypes.SupplierServiceConfig{ + { + ServiceId: &sharedtypes.ServiceId{ + Id: "svcId", + Name: "name", + }, + Endpoints: []*sharedtypes.SupplierEndpoint{ + { + Url: "http://localhost:8080", + // RpcType: intentionally omitted, + Configs: make([]*sharedtypes.ConfigOption, 0), + }, + }, + }, + }, + }, + err: ErrSupplierInvalidServiceConfig, + }, + // TODO_TEST: Need to add more tests around config types } for _, tt := range tests { diff --git a/x/supplier/types/message_submit_proof.go b/x/supplier/types/message_submit_proof.go index 32faa796b..ad00eb225 100644 --- a/x/supplier/types/message_submit_proof.go +++ b/x/supplier/types/message_submit_proof.go @@ -4,7 +4,7 @@ import ( sdk "github.com/cosmos/cosmos-sdk/types" sdkerrors "github.com/cosmos/cosmos-sdk/types/errors" - sessiontypes "pocket/x/session/types" + sessiontypes "github.com/pokt-network/poktroll/x/session/types" ) const TypeMsgSubmitProof = "submit_proof" diff --git a/x/supplier/types/message_submit_proof_test.go b/x/supplier/types/message_submit_proof_test.go index 7f7a15862..8479db05d 100644 --- a/x/supplier/types/message_submit_proof_test.go +++ b/x/supplier/types/message_submit_proof_test.go @@ -5,7 +5,8 @@ import ( sdkerrors "github.com/cosmos/cosmos-sdk/types/errors" "github.com/stretchr/testify/require" - "pocket/testutil/sample" + + "github.com/pokt-network/poktroll/testutil/sample" ) // TODO(@bryanchriswhite): Add unit tests for message validation when adding the business logic. diff --git a/x/supplier/types/message_unstake_supplier_test.go b/x/supplier/types/message_unstake_supplier_test.go index cc2481bbb..b447397ed 100644 --- a/x/supplier/types/message_unstake_supplier_test.go +++ b/x/supplier/types/message_unstake_supplier_test.go @@ -5,7 +5,7 @@ import ( "github.com/stretchr/testify/require" - "pocket/testutil/sample" + "github.com/pokt-network/poktroll/testutil/sample" ) func TestMsgUnstakeSupplier_ValidateBasic(t *testing.T) {