├── .dockerignore ├── .env.sample ├── .github ├── issue_template.md ├── pull_request_template.md └── workflows │ ├── build-images.yml │ └── go.yml ├── .gitignore ├── Dockerfile ├── LICENSE ├── Makefile ├── README.md ├── cmd └── gateway_server │ ├── internal │ ├── .DS_Store │ ├── .gitkeep │ ├── common │ │ ├── response-json.go │ │ └── response-json_ffjson.go │ ├── config │ │ ├── dot_env_config_provider.go │ │ └── provider.go │ ├── controllers │ │ ├── pokt_apps.go │ │ ├── qos_nodes.go │ │ ├── relay.go │ │ └── relay_test.go │ ├── middleware │ │ └── x-api-key.go │ ├── models │ │ ├── pokt_application.go │ │ └── qos_node.go │ └── transform │ │ ├── pokt_application.go │ │ └── qos_node.go │ └── main.go ├── db_migrations ├── 000001_initial.down.sql ├── 000001_initial.up.sql ├── 000002_chain_configurations.down.sql └── 000002_chain_configurations.up.sql ├── docker-compose.yml.sample ├── docs ├── altruist-chain-configuration.md ├── api-endpoints.md ├── benchmarks │ └── 03_2024 │ │ ├── 03-2024-benchmark.md │ │ └── resources │ │ ├── cpu-03-2024.png │ │ ├── memory-03-2024.png │ │ └── node-selection-overhead-03-2024.png ├── docker-compose.md ├── node-selection.md ├── overview.md ├── performance-optimizations.md ├── pokt-primer.md ├── pokt-relay-specification.md ├── quick-onboarding-guide.md ├── resources │ ├── gateway-server-architecture.png │ ├── gateway-server-logo.jpg │ ├── gateway-server-node-selection-system.png │ └── gateway-server-session-cache.png └── system-architecture.md ├── go.mod ├── go.sum ├── internal ├── .gitkeep ├── apps_registry │ ├── app_registry_service.go │ ├── cached_app_registry.go │ ├── cached_app_registry_test.go │ └── models │ │ └── application.go ├── chain_configurations_registry │ ├── cached_chain_configurations_registry.go │ └── chain_configurations_registry_service.go ├── chain_network │ └── chain_network.go ├── db_query │ ├── db.go │ ├── queries.sql │ └── queries.sql.go ├── global_config │ └── config_provider.go ├── logging │ └── logger.go ├── node_selector_service │ ├── checks │ │ ├── async_relay_handler.go │ │ ├── chain_config_handler.go │ │ ├── data_integrity_handler.go │ │ ├── error_handler.go │ │ ├── evm_data_integrity_check │ │ │ └── evm_data_integrity_check.go │ │ ├── evm_height_check │ │ │ └── evm_height_check.go │ │ ├── height_check_handler.go │ │ ├── pokt_data_integrity_check │ │ │ └── pokt_data_integrity_check.go │ │ ├── pokt_height_check │ │ │ └── pokt_height_check.go │ │ ├── qos_check.go │ │ ├── solana_data_integrity_check │ │ │ └── solana_data_integrity_check.go │ │ └── solana_height_check │ │ │ └── solana_height_check.go │ ├── models │ │ └── qos_node.go │ └── node_selector_service.go ├── relayer │ ├── http_requester.go │ ├── relayer.go │ └── relayer_test.go └── session_registry │ ├── cached_session_registry_service.go │ └── session_registry_service.go ├── migrationfs.go ├── mocks ├── apps_registry │ └── app_registry_mock.go ├── chain_configurations_registry │ └── chain_configurations_registry_mock.go ├── global_config │ └── config_provider.go ├── node_selector │ └── node_selector_mock.go ├── pocket_service │ └── pocket_service_mock.go ├── session_registry │ └── session_registry_mock.go └── ttl_cache_service │ └── ttl_cache_service_mock.go ├── pkg ├── common │ ├── crypto.go │ ├── crypto_test.go │ ├── http.go │ ├── http_test.go │ └── slices.go ├── pokt │ ├── common │ │ ├── json-rpc.go │ │ └── json-rpc_ffjson.go │ └── pokt_v0 │ │ ├── basic_client.go │ │ ├── generate_proof_bytes.go │ │ ├── generate_proof_bytes_test.go │ │ ├── get_node_from_request.go │ │ ├── get_node_from_request_test.go │ │ ├── get_session_from_request.go │ │ ├── get_session_from_request_test.go │ │ ├── models │ │ ├── aat.go │ │ ├── aat_ffjson.go │ │ ├── application.go │ │ ├── application_ffjson.go │ │ ├── block.go │ │ ├── block_ffjson.go │ │ ├── client_errors.go │ │ ├── ed25519-account.go │ │ ├── ed25519-account_ffjson.go │ │ ├── ed25519-account_test.go │ │ ├── pokt_errors.go │ │ ├── pokt_errors_ffjson.go │ │ ├── relay.go │ │ ├── relay_ffjson.go │ │ ├── session.go │ │ └── session_ffjson.go │ │ └── service.go └── ttl_cache │ └── ttl_cache.go └── scripts ├── migration.sh ├── mockgen.sh └── querygen.sh /.dockerignore: -------------------------------------------------------------------------------- 1 | # Ignore build artifacts and binaries 2 | bin/ 3 | *.exe 4 | *.exe~ 5 | *.dll 6 | *.so 7 | *.dylib 8 | 9 | # Ignore files and directories generated by the go tool 10 | /vendor/ 11 | /Godeps/ 12 | /.glide/ 13 | /.idea/ 14 | 15 | # Ignore local configuration files 16 | .env 17 | *.env 18 | *.env.* 19 | !.env.example 20 | 21 | # Ignore editor-specific files 22 | .vscode/ 23 | .idea/ 24 | 25 | # Ignore test and coverage files 26 | *.test 27 | *.out 28 | *.prof 29 | 30 | # Ignore common version control directories 31 | .git/ 32 | .svn/ 33 | .hg/ 34 | 35 | # Ignore files and directories generated during development or testing 36 | /dev/ 37 | /tmp/ 38 | *.log -------------------------------------------------------------------------------- /.env.sample: -------------------------------------------------------------------------------- 1 | # Pocket RPC Configuration 2 | POKT_RPC_FULL_HOST= 3 | POKT_RPC_TIMEOUT=5s 4 | 5 | # Gateway Deployment Configuration 6 | HTTP_SERVER_PORT=8080 7 | ENVIRONMENT_STAGE=development 8 | EMIT_SERVICE_URL_PROM_METRICS=false 9 | 10 | # Pocket Business Logic 11 | CHAIN_NETWORK=morse_mainnet 12 | SESSION_CACHE_TTL=75m 13 | ALTRUIST_REQUEST_TIMEOUT=10s 14 | API_KEY= 15 | 16 | # App Stake Management 17 | DB_CONNECTION_URL=postgres://myuser:mypassword@postgres:5433/postgres?sslmode=disable 18 | POKT_APPLICATIONS_ENCRYPTION_KEY= 19 | POKT_APPLICATION_PRIVATE_KEY= 20 | -------------------------------------------------------------------------------- /.github/issue_template.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug Report 3 | about: Create a report to help us improve 4 | title: '[BUG] ' 5 | 6 | --- 7 | 8 | ## Describe the Bug 9 | 10 | A clear and concise description of what the bug is. 11 | 12 | ## Expected Behavior 13 | 14 | A clear and concise description of what you expected to happen. 15 | 16 | ## Steps to Reproduce 17 | 18 | 1. Step 1 19 | 2. Step 2 20 | 3. ... 21 | 22 | ## Screenshots 23 | 24 | If applicable, add screenshots to help explain your problem. 25 | 26 | ## Environment 27 | 28 | - OS: [e.g., Windows 10, macOS] 29 | - Version: [e.g., 22] 30 | 31 | ## Additional Context 32 | 33 | Add any other context about the problem here. 34 | -------------------------------------------------------------------------------- /.github/pull_request_template.md: -------------------------------------------------------------------------------- 1 | ## Github issue 2 | 3 | Add the correlated Github Issue here 4 | 5 | ## Description 6 | 7 | A few sentences describing the overall goals of the pull request's commits. 8 | 9 | ## Type of change 10 | 11 | Please delete option that is not relevant. 12 | 13 | - [ ] Bug fix (non-breaking change which fixes an issue) 14 | - [ ] New feature (non-breaking change which adds functionality) 15 | 16 | ## Related PRs 17 | 18 | List related PRs below 19 | 20 | | branch | PR | 21 | | -------- | -------- | 22 | | other_pr | [link]() | 23 | -------------------------------------------------------------------------------- /.github/workflows/build-images.yml: -------------------------------------------------------------------------------- 1 | # This workflow only handles build & push of images to GitHub Container Registry. 2 | # We have other pipepines in CircleCI, such as tests, that are not migrated to GitHub Actions. 3 | 4 | name: Build container and push images 5 | 6 | on: 7 | workflow_dispatch: 8 | push: 9 | tags: ['*'] 10 | 11 | jobs: 12 | build-images: 13 | runs-on: ubuntu-latest 14 | steps: 15 | - uses: actions/checkout@v3 16 | - name: Docker Setup QEMU 17 | uses: docker/setup-qemu-action@v2 18 | - name: Docker Setup Buildx 19 | uses: docker/setup-buildx-action@v2 20 | - name: Docker Metadata action 21 | id: meta 22 | uses: docker/metadata-action@v4 23 | env: 24 | DOCKER_METADATA_PR_HEAD_SHA: "true" 25 | with: 26 | images: | 27 | ghcr.io/pokt-network/pocket-gateway-server 28 | tags: | 29 | type=schedule 30 | type=ref,event=tag 31 | type=ref,event=branch 32 | type=ref,event=pr 33 | type=sha 34 | type=sha,format=long 35 | - name: Login to GitHub Container Registry 36 | uses: docker/login-action@v2 37 | with: 38 | registry: ghcr.io 39 | username: ${{ github.actor }} 40 | password: ${{ secrets.GITHUB_TOKEN }} 41 | - name: Build and push Docker image 42 | uses: docker/build-push-action@v3 43 | with: 44 | push: true 45 | tags: ${{ steps.meta.outputs.tags }} 46 | labels: ${{ steps.meta.outputs.labels }} 47 | platforms: linux/amd64,linux/arm64 48 | file: Dockerfile 49 | cache-from: type=gha 50 | cache-to: type=gha,mode=max 51 | -------------------------------------------------------------------------------- /.github/workflows/go.yml: -------------------------------------------------------------------------------- 1 | # This workflow will build a golang project 2 | # For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-go 3 | 4 | name: Build and Run Tests 5 | 6 | on: 7 | push: 8 | branches: [ "main" ] 9 | pull_request: 10 | types: [opened, synchronize, reopened] 11 | 12 | jobs: 13 | build: 14 | runs-on: ubuntu-latest 15 | steps: 16 | - uses: actions/checkout@v3 17 | 18 | - name: Set up Go 19 | uses: actions/setup-go@v4 20 | with: 21 | go-version: '1.21.4' 22 | 23 | - name: Build 24 | run: go build -v ./... 25 | 26 | - name: Test 27 | run: go test -v ./... 28 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Binaries for programs and plugins 2 | *.exe 3 | *.exe~ 4 | *.dll 5 | *.so 6 | *.dylib 7 | 8 | # Test binary, built with `go test -c` 9 | *.test 10 | 11 | # Output of the go coverage tool, specifically when used with LiteIDE 12 | *.out 13 | 14 | # Project-specific Go related files 15 | /_test/ 16 | /.idea/ 17 | *.iml 18 | .idea 19 | 20 | # Compiled Go code 21 | *.a 22 | *.o 23 | 24 | # Dependency directories (remove the ones that you use) 25 | /vendor/ 26 | 27 | # The default coverage output directory 28 | /coverage/ 29 | 30 | # Temporary files generated by editors or build system 31 | *.stackdump 32 | *~ 33 | 34 | # GoLand specific files 35 | .idea/ 36 | *.iws 37 | *.ipr 38 | *.iws.iml 39 | 40 | # GoLand Workspace file 41 | /goland/ 42 | 43 | # Compiled GoLand files 44 | /out/ 45 | 46 | # SQLite database file 47 | /*.sqlite 48 | 49 | ## environment vars 50 | .env 51 | .env.testnet 52 | .env.mainnet 53 | 54 | ## docker compose file 55 | docker-compose.yml 56 | 57 | ## MAC 58 | .DS_Store 59 | 60 | ## VSCode 61 | .vscode 62 | 63 | ## Copmiled binary 64 | main 65 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | # Stage 1: Build stage 2 | FROM golang:1.21.4-alpine AS builder 3 | 4 | WORKDIR /app 5 | 6 | # Copy only the necessary files for Go module dependency resolution 7 | COPY go.mod go.sum ./ 8 | 9 | # Download Go dependencies 10 | RUN go mod download 11 | 12 | # Copy the entire application 13 | COPY . . 14 | 15 | # Build the Go application with optimizations 16 | RUN go build -o main cmd/gateway_server/main.go 17 | 18 | # Stage 2: Runtime stage 19 | FROM scratch 20 | 21 | WORKDIR /app 22 | 23 | # Copy only the necessary files from the builder stage 24 | COPY --from=builder /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/ 25 | COPY --from=builder /app/main . 26 | 27 | # Set default value for port exposed 28 | ENV HTTP_SERVER_PORT 8080 29 | 30 | EXPOSE $HTTP_SERVER_PORT 31 | 32 | CMD ["/app/main"] 33 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 baaspoolsllc 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | include .env 2 | 3 | ######################## 4 | ### Makefile Helpers ### 5 | ######################## 6 | 7 | .PHONY: prompt_user 8 | # Internal helper target - prompt the user before continuing 9 | prompt_user: 10 | @echo "Are you sure? [y/N] " && read ans && [ $${ans:-N} = y ] 11 | 12 | .PHONY: list 13 | list: ## List all make targets 14 | @${MAKE} -pRrn : -f $(MAKEFILE_LIST) 2>/dev/null | awk -v RS= -F: '/^# File/,/^# Finished Make data base/ {if ($$1 !~ "^[#.]") {print $$1}}' | egrep -v -e '^[^[:alnum:]]' -e '^$@$$' | sort 15 | 16 | .PHONY: help 17 | .DEFAULT_GOAL := help 18 | help: ## Prints all the targets in all the Makefiles 19 | @grep -h -E '^[a-zA-Z0-9_-]+:.*?## .*$$' $(MAKEFILE_LIST) | awk 'BEGIN {FS = ":.*?## "}; {printf "\033[36m%-60s\033[0m %s\n", $$1, $$2}' 20 | 21 | ######################## 22 | ### Database Helpers ### 23 | ######################## 24 | 25 | .PHONY: db_migrate 26 | db_migrate: ## Run database migrations 27 | @echo "Running database migrations..." 28 | ./scripts/db_migrate.sh -u 29 | 30 | 31 | PG_CMD := INSERT INTO pokt_applications (encrypted_private_key) VALUES (pgp_sym_encrypt('$(POKT_APPLICATION_PRIVATE_KEY)', '$(POKT_APPLICATIONS_ENCRYPTION_KEY)')); 32 | db_insert_app_private_key: ## Insert application private key into database 33 | @echo "Running SQL command..." 34 | @echo "$(PG_CMD)" 35 | @psql "$(DB_CONNECTION_URL)" -c "$(PG_CMD)" 36 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |
2 | POKT Gateway Server 3 |
4 | 5 | > [!WARNING] 6 | > 7 | > 🚧🚧🚧 This repository is `Archived`! As of November 13, 2024 this repository is `Archived`. We encourage users to migrate and adopt the [Path API and Toolkit Harness (PATH)](https://github.com/buildwithgrove/path) 🚧🚧🚧 8 | 9 | # What is POKT Gateway Server? 10 | 11 | _tl;dr Streamline access to POKT Network's decentralized supply network._ 12 | 13 | The POKT Gateway Server is a comprehensive solution designed to simplify the integration of applications with POKT Network. Its goal is to reduce the complexities associated with directly interfacing with the protocol, making it accessible to a wide range of users, including application developers, existing centralized RPC platforms, and future gateway operators. 14 | 15 | Learn more about the vision and overall architecture [overview](./docs/overview.md). 16 | 17 | - [Gateway Operator Quickstart Guide](#gateway-operator-quickstart-guide) 18 | - [Interested in learning more?](#interested-in-learning-more) 19 | - [Docker Image Releases](#docker-image-releases) 20 | - [Docker Compose](#docker-compose) 21 | - [Minimum Hardware Requirements](#minimum-hardware-requirements) 22 | - [Database Migrations](#database-migrations) 23 | - [Creating a DB Migration](#creating-a-db-migration) 24 | - [Applying a DB Migration](#applying-a-db-migration) 25 | - [DB Migration helpers](#db-migration-helpers) 26 | - [Applying Migrations](#applying-migrations) 27 | - [Migrations Rollbacks](#migrations-rollbacks) 28 | - [Unit Testing](#unit-testing) 29 | - [Generating Mocks](#generating-mocks) 30 | - [Running Tests](#running-tests) 31 | - [Generating DB Queries](#generating-db-queries) 32 | - [Contributing Guidelines](#contributing-guidelines) 33 | - [Project Structure](#project-structure) 34 | 35 | ## Gateway Operator Quickstart Guide 36 | 37 | To onboard the gateway server without having to dig deep, you can follow the [Quick Onboarding Guide](docs/quick-onboarding-guide.md). 38 | 39 | ### Interested in learning more? 40 | 41 | We have an abundance of information in the [docs](docs) section: 42 | 43 | 1. [Gateway Server Overview](docs/overview.md) 44 | 2. [Gateway Server API Endpoints](docs/api-endpoints.md) 45 | 3. [Gateway Server System Architecture](docs/system-architecture.md) 46 | 4. [Gateway Server Node Selection](docs/node-selection.md) 47 | 5. [POKT Primer](docs/pokt-primer.md) 48 | 6. [POKT's Relay Specification](docs/pokt-relay-specification.md) 49 | 50 | ## Docker Image Releases 51 | 52 | Every release candidate is published to [gateway-server/pkgs/container/pocket-gateway-server](https://github.com/pokt-network/gateway-server/pkgs/container/pocket-gateway-server). 53 | 54 | ## Docker Compose 55 | 56 | There is an all-inclusive docker-compose file available for development [docker-compose.yml](docker-compose.yml.sample) 57 | 58 | ## Minimum Hardware Requirements 59 | 60 | To run a Gateway Server, we recommend the following minimum hardware requirements: 61 | 62 | - 1GB of RAM 63 | - 1GB of storage 64 | - 4 vCPUs+ 65 | 66 | In production, we have observed memory usage increase to 4GB+. The memory footprint will be dependent on the number of app stakes/chains staked and total traffic throughput. 67 | 68 | ## Database Migrations 69 | 70 | 71 | 72 | ### Creating a DB Migration 73 | 74 | Migrations are like version control for your database, allowing your team to define and share the application's database schema definition. 75 | 76 | Before running a migration make sure to install the go lang migration cli on your machine. See [golang-migrate/migrate/tree/master/cmd/migrate](https://github.com/golang-migrate/migrate/tree/master/cmd/migrate) for reference. 77 | 78 | ```sh 79 | ./scripts/migration.sh -n {migration_name} 80 | ``` 81 | 82 | This command will generate a up and down migration in `db_migrations` 83 | 84 | ### Applying a DB Migration 85 | 86 | DB Migrations are applied upon server start, but as well, it can be applied manually through: 87 | 88 | ```sh 89 | ./scripts/migration.sh {--down or --up} {number_of_times} 90 | ``` 91 | 92 | ### DB Migration helpers 93 | 94 | #### Applying Migrations 95 | 96 | - To apply all migrations: 97 | 98 | ```sh 99 | ./scripts/migration.sh --up 100 | ``` 101 | 102 | - To apply a specific number of migrations: 103 | 104 | ```sh 105 | ./scripts/migration.sh --up 2 106 | ``` 107 | 108 | #### Migrations Rollbacks 109 | 110 | Make sure to provide either the number of migrations to rollback or the `--all` flag to rollback all migrations. 111 | 112 | - To roll back a specific number of migrations: 113 | 114 | ```sh 115 | ./scripts/migration.sh --down 2 116 | ``` 117 | 118 | - To roll back all migrations: 119 | 120 | ```sh 121 | ./scripts/migration.sh --down --all 122 | ``` 123 | 124 | ## Unit Testing 125 | 126 | ### Generating Mocks 127 | 128 | Install Mockery with 129 | 130 | ```bash 131 | go install github.com/vektra/mockery/v2@v2.40.1 132 | ``` 133 | 134 | You can generate the mock files through: 135 | 136 | ```sh 137 | ./scripts/mockgen.sh 138 | ``` 139 | 140 | By running this command, it will generate the mock files in `./mocks` folder. 141 | 142 | Reference for mocks can be found [here](https://vektra.github.io/mockery/latest). 143 | 144 | ### Running Tests 145 | 146 | Run this command to run tests: 147 | 148 | ```sh 149 | go test -v -count=1 ./... 150 | ``` 151 | 152 | ## Generating DB Queries 153 | 154 | Gateway server uses [PGGen](https://github.com/jschaf/pggen) to create autogenerated type-safe queries. 155 | Queries are added inside [queries.sql](./internal/Fdb_query/queries.sql) and re-generated via `./scripts/querygen.sh`. 156 | 157 | ## Contributing Guidelines 158 | 159 | 1. Create a Github Issue on the feature/issue you're working on. 160 | 2. Fork the project 161 | 3. Create new branch with `git checkout -b "branch_name"` where branch name describes the feature. 162 | - All branches should be based off `main` 163 | 4. Write your code 164 | 5. Make sure your code lints with `go fmt ./...` (This will Lint and Prettify) 165 | 6. Commit code to your branch and issue a pull request and wait for at least one review. 166 | - Always ensure changes are rebased on top of main branch. 167 | 168 | ## Project Structure 169 | 170 | A partial high-level view of the code structure (generated) 171 | 172 | ```bash 173 | . 174 | ├── cmd # Contains the entry point of the binaries 175 | │   └── gateway_server # HTTP Server for serving requests 176 | ├── internal # Shared internal folder for all binaries 177 | ├── pkg # Distributable dependencies 178 | └── scripts # Contains scripts for development 179 | ``` 180 | 181 | _Generate via `tree -L 2`_ 182 | 183 | --- 184 | -------------------------------------------------------------------------------- /cmd/gateway_server/internal/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pokt-network/gateway-server/9f5e8be1165ba6966e29e936a68674052ddf8cf5/cmd/gateway_server/internal/.DS_Store -------------------------------------------------------------------------------- /cmd/gateway_server/internal/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pokt-network/gateway-server/9f5e8be1165ba6966e29e936a68674052ddf8cf5/cmd/gateway_server/internal/.gitkeep -------------------------------------------------------------------------------- /cmd/gateway_server/internal/common/response-json.go: -------------------------------------------------------------------------------- 1 | // Package common provides common utilities and structures used across the application. 2 | 3 | //go:generate ffjson $GOFILE 4 | package common 5 | 6 | import ( 7 | "fmt" 8 | "github.com/pquerna/ffjson/ffjson" 9 | "github.com/valyala/fasthttp" 10 | ) 11 | 12 | // ErrorResponse represents a JSON-formatted error response. 13 | type ErrorResponse struct { 14 | Message string `json:"message"` 15 | Status int `json:"status"` 16 | Error error `json:"error"` 17 | } 18 | 19 | // JSONError creates a JSON-formatted error response and sends it to the client. 20 | // It takes the fasthttp.RequestCtx, error message, and HTTP status code as parameters. 21 | func JSONError(ctx *fasthttp.RequestCtx, message string, statusCode int, err error) { 22 | // Create an ErrorResponse instance with the provided message and status code. 23 | errorResponse := ErrorResponse{ 24 | Message: message, 25 | Status: statusCode, 26 | Error: err, 27 | } 28 | 29 | // Marshal the ErrorResponse instance into JSON format. 30 | jsonData, err := ffjson.Marshal(errorResponse) 31 | if err != nil { 32 | // If there's an error during JSON marshaling, log it and set a generic internal server error response. 33 | ctx.Error(fmt.Sprintf("Error marshaling JSON: %s", err), fasthttp.StatusInternalServerError) 34 | return 35 | } 36 | 37 | // Set the response headers and body with the JSON data. 38 | ctx.Response.Header.Set("Content-Type", "application/json") 39 | ctx.Response.SetBody(jsonData) 40 | // Set the HTTP status code for the response. 41 | ctx.SetStatusCode(statusCode) 42 | } 43 | 44 | // JSONError creates a JSON-formatted error response and sends it to the client. 45 | // It takes the fasthttp.RequestCtx, error message, and HTTP status code as parameters. 46 | func JSONSuccess(ctx *fasthttp.RequestCtx, data any, statusCode int) { 47 | 48 | // Marshal the ErrorResponse instance into JSON format. 49 | jsonData, err := ffjson.Marshal(data) 50 | if err != nil { 51 | // If there's an error during JSON marshaling, log it and set a generic internal server error response. 52 | ctx.Error(fmt.Sprintf("Error marshaling JSON: %s", err), fasthttp.StatusInternalServerError) 53 | return 54 | } 55 | 56 | // Set the response headers and body with the JSON data. 57 | ctx.Response.Header.Set("Content-Type", "application/json") 58 | ctx.Response.SetBody(jsonData) 59 | // Set the HTTP status code for the response. 60 | ctx.SetStatusCode(statusCode) 61 | } 62 | -------------------------------------------------------------------------------- /cmd/gateway_server/internal/config/dot_env_config_provider.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "fmt" 5 | "github.com/joho/godotenv" 6 | "github.com/pokt-network/gateway-server/internal/chain_network" 7 | "github.com/pokt-network/gateway-server/internal/global_config" 8 | "os" 9 | "strconv" 10 | "time" 11 | ) 12 | 13 | const ( 14 | defaultAltruistRequestTimeout = time.Second * 30 15 | ) 16 | 17 | // Environment variable names 18 | const ( 19 | chainNetworkEnv = "CHAIN_NETWORK" 20 | emitServiceUrlPromMetricsEnv = "EMIT_SERVICE_URL_PROM_METRICS" 21 | poktRPCFullHostEnv = "POKT_RPC_FULL_HOST" 22 | httpServerPortEnv = "HTTP_SERVER_PORT" 23 | poktRPCTimeoutEnv = "POKT_RPC_TIMEOUT" 24 | altruistRequestTimeoutEnv = "ALTRUIST_REQUEST_TIMEOUT" 25 | dbConnectionUrlEnv = "DB_CONNECTION_URL" 26 | sessionCacheTTLEnv = "SESSION_CACHE_TTL" 27 | environmentStageEnv = "ENVIRONMENT_STAGE" 28 | poktApplicationsEncryptionKeyEnv = "POKT_APPLICATIONS_ENCRYPTION_KEY" 29 | apiKey = "API_KEY" 30 | ) 31 | 32 | // DotEnvGlobalConfigProvider implements the GatewayServerProvider interface. 33 | type DotEnvGlobalConfigProvider struct { 34 | poktRPCFullHost string 35 | chainNetwork chain_network.ChainNetwork 36 | httpServerPort uint 37 | poktRPCRequestTimeout time.Duration 38 | sessionCacheTTL time.Duration 39 | environmentStage global_config.EnvironmentStage 40 | poktApplicationsEncryptionKey string 41 | databaseConnectionUrl string 42 | apiKey string 43 | emitServiceUrlPromMetrics bool 44 | altruistRequestTimeout time.Duration 45 | } 46 | 47 | func (c DotEnvGlobalConfigProvider) GetAPIKey() string { 48 | return c.apiKey 49 | } 50 | 51 | // GetPoktRPCFullHost returns the PoktRPCFullHost value. 52 | func (c DotEnvGlobalConfigProvider) GetPoktRPCFullHost() string { 53 | return c.poktRPCFullHost 54 | } 55 | 56 | // GetHTTPServerPort returns the HTTPServerPort value. 57 | func (c DotEnvGlobalConfigProvider) GetHTTPServerPort() uint { 58 | return c.httpServerPort 59 | } 60 | 61 | // GetPoktRPCTimeout returns the PoktRPCTimeout value. 62 | func (c DotEnvGlobalConfigProvider) GetPoktRPCRequestTimeout() time.Duration { 63 | return c.poktRPCRequestTimeout 64 | } 65 | 66 | // GetSessionCacheTTL returns the time value for session to expire in cache. 67 | func (c DotEnvGlobalConfigProvider) GetSessionCacheTTL() time.Duration { 68 | return c.sessionCacheTTL 69 | } 70 | 71 | // GetEnvironmentStage returns the EnvironmentStage value. 72 | func (c DotEnvGlobalConfigProvider) GetEnvironmentStage() global_config.EnvironmentStage { 73 | return c.environmentStage 74 | } 75 | 76 | // GetPoktApplicationsEncryptionKey: Key used to decrypt pokt applications private key. 77 | func (c DotEnvGlobalConfigProvider) GetPoktApplicationsEncryptionKey() string { 78 | return c.poktApplicationsEncryptionKey 79 | } 80 | 81 | // GetDatabaseConnectionUrl returns the PoktRPCFullHost value. 82 | func (c DotEnvGlobalConfigProvider) GetDatabaseConnectionUrl() string { 83 | return c.databaseConnectionUrl 84 | } 85 | 86 | // GetDatabaseConnectionUrl returns the PoktRPCFullHost value. 87 | func (c DotEnvGlobalConfigProvider) GetAltruistRequestTimeout() time.Duration { 88 | return c.altruistRequestTimeout 89 | } 90 | 91 | // ShouldEmitServiceUrl returns whether to emit service url tags as part of relay metrics. 92 | func (c DotEnvGlobalConfigProvider) ShouldEmitServiceUrlPromMetrics() bool { 93 | return c.emitServiceUrlPromMetrics 94 | } 95 | 96 | // GetChainNetwork returns the current network, this can be useful for identifying the correct chain ids dependent on testnet or mainnet. 97 | func (c DotEnvGlobalConfigProvider) GetChainNetwork() chain_network.ChainNetwork { 98 | return c.chainNetwork 99 | } 100 | 101 | // NewDotEnvConfigProvider creates a new instance of DotEnvGlobalConfigProvider. 102 | func NewDotEnvConfigProvider() *DotEnvGlobalConfigProvider { 103 | _ = godotenv.Load() 104 | 105 | poktRPCTimeout, err := time.ParseDuration(getEnvVar(poktRPCTimeoutEnv, "")) 106 | if err != nil { 107 | panic(fmt.Sprintf("Error parsing %s: %s", poktRPCTimeoutEnv, err)) 108 | } 109 | 110 | httpServerPort, err := strconv.ParseUint(getEnvVar(httpServerPortEnv, ""), 10, 64) 111 | if err != nil { 112 | panic(fmt.Sprintf("Error parsing %s: %s", httpServerPortEnv, err)) 113 | } 114 | 115 | sessionCacheTTLDuration, err := time.ParseDuration(getEnvVar(sessionCacheTTLEnv, "")) 116 | if err != nil { 117 | panic(fmt.Sprintf("Error parsing %s: %s", sessionCacheTTLDuration, err)) 118 | } 119 | 120 | altruistRequestTimeoutDuration, err := time.ParseDuration(getEnvVar(altruistRequestTimeoutEnv, defaultAltruistRequestTimeout.String())) 121 | if err != nil { 122 | // Provide a default to prevent any breaking changes with new env variable. 123 | altruistRequestTimeoutDuration = defaultAltruistRequestTimeout 124 | } 125 | 126 | emitServiceUrlPromMetrics, err := strconv.ParseBool(getEnvVar(emitServiceUrlPromMetricsEnv, "false")) 127 | 128 | if err != nil { 129 | emitServiceUrlPromMetrics = false 130 | } 131 | 132 | return &DotEnvGlobalConfigProvider{ 133 | emitServiceUrlPromMetrics: emitServiceUrlPromMetrics, 134 | poktRPCFullHost: getEnvVar(poktRPCFullHostEnv, ""), 135 | httpServerPort: uint(httpServerPort), 136 | poktRPCRequestTimeout: poktRPCTimeout, 137 | sessionCacheTTL: sessionCacheTTLDuration, 138 | databaseConnectionUrl: getEnvVar(dbConnectionUrlEnv, ""), 139 | environmentStage: global_config.EnvironmentStage(getEnvVar(environmentStageEnv, "")), 140 | poktApplicationsEncryptionKey: getEnvVar(poktApplicationsEncryptionKeyEnv, ""), 141 | apiKey: getEnvVar(apiKey, ""), 142 | chainNetwork: chain_network.ChainNetwork(getEnvVar(chainNetworkEnv, string(chain_network.MorseMainnet))), 143 | altruistRequestTimeout: altruistRequestTimeoutDuration, 144 | } 145 | } 146 | 147 | // getEnvVar retrieves the value of the environment variable with error handling. 148 | func getEnvVar(name string, defaultValue string) string { 149 | if value, exists := os.LookupEnv(name); exists { 150 | return value 151 | } 152 | if defaultValue != "" { 153 | return defaultValue 154 | } 155 | panic(fmt.Errorf("%s not set", name)) 156 | } 157 | -------------------------------------------------------------------------------- /cmd/gateway_server/internal/config/provider.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "github.com/pokt-network/gateway-server/internal/global_config" 5 | ) 6 | 7 | type GatewayServerProvider interface { 8 | GetHTTPServerPort() uint 9 | global_config.DBCredentialsProvider 10 | global_config.PoktNodeConfigProvider 11 | global_config.SecretProvider 12 | global_config.EnvironmentProvider 13 | } 14 | -------------------------------------------------------------------------------- /cmd/gateway_server/internal/controllers/pokt_apps.go: -------------------------------------------------------------------------------- 1 | package controllers 2 | 3 | import ( 4 | "context" 5 | "github.com/jackc/pgtype" 6 | "github.com/pokt-network/gateway-server/cmd/gateway_server/internal/common" 7 | "github.com/pokt-network/gateway-server/cmd/gateway_server/internal/models" 8 | "github.com/pokt-network/gateway-server/cmd/gateway_server/internal/transform" 9 | "github.com/pokt-network/gateway-server/internal/apps_registry" 10 | "github.com/pokt-network/gateway-server/internal/db_query" 11 | "github.com/pokt-network/gateway-server/internal/global_config" 12 | "github.com/pokt-network/gateway-server/pkg/pokt/pokt_v0" 13 | pokt_models "github.com/pokt-network/gateway-server/pkg/pokt/pokt_v0/models" 14 | "github.com/pquerna/ffjson/ffjson" 15 | "github.com/valyala/fasthttp" 16 | "go.uber.org/zap" 17 | ) 18 | 19 | type addApplicationBody struct { 20 | PrivateKey string `json:"private_key"` 21 | } 22 | 23 | // PoktAppsController handles requests for staked applications 24 | type PoktAppsController struct { 25 | logger *zap.Logger 26 | query db_query.Querier 27 | poktClient pokt_v0.PocketService 28 | appRegistry apps_registry.AppsRegistryService 29 | secretProvider global_config.SecretProvider 30 | } 31 | 32 | // NewPoktAppsController creates a new instance of PoktAppsController. 33 | func NewPoktAppsController(appRegistry apps_registry.AppsRegistryService, query db_query.Querier, secretProvider global_config.SecretProvider, logger *zap.Logger) *PoktAppsController { 34 | return &PoktAppsController{appRegistry: appRegistry, query: query, secretProvider: secretProvider, logger: logger} 35 | } 36 | 37 | // GetAll returns all the apps in the registry 38 | func (c *PoktAppsController) GetAll(ctx *fasthttp.RequestCtx) { 39 | applications := c.appRegistry.GetApplications() 40 | appsPublic := []*models.PublicPoktApplication{} 41 | for _, app := range applications { 42 | appsPublic = append(appsPublic, transform.ToPoktApplication(app)) 43 | } 44 | common.JSONSuccess(ctx, appsPublic, fasthttp.StatusOK) 45 | } 46 | 47 | // AddApplication - enables users to add an application programmatically. 48 | // Not recommended since it requires transmitting creds over wire and opens up to MITM (if not encrypted, or user error). 49 | func (c *PoktAppsController) AddApplication(ctx *fasthttp.RequestCtx) { 50 | var body addApplicationBody 51 | err := ffjson.Unmarshal(ctx.PostBody(), &body) 52 | if err != nil { 53 | common.JSONError(ctx, "Faiiled to unmarshal req", fasthttp.StatusInternalServerError, err) 54 | return 55 | } 56 | 57 | account, err := pokt_models.NewAccount(body.PrivateKey) 58 | if err != nil { 59 | common.JSONError(ctx, "Faiiled to convert to ed25519 account", fasthttp.StatusBadRequest, err) 60 | return 61 | } 62 | _, err = c.query.InsertPoktApplications(context.Background(), account.PrivateKey, c.secretProvider.GetPoktApplicationsEncryptionKey()) 63 | if err != nil { 64 | common.JSONError(ctx, "Something went wrong", fasthttp.StatusInternalServerError, err) 65 | return 66 | } 67 | ctx.SetStatusCode(fasthttp.StatusCreated) 68 | } 69 | 70 | // DeleteApplication - enables users to delete an application programmatically. 71 | // Not recommended since it requires transmitting creds over wire and opens up to MITM (if not encrypted, or user error). 72 | func (c *PoktAppsController) DeleteApplication(ctx *fasthttp.RequestCtx) { 73 | applicationId := ctx.UserValue("app_id") 74 | uuid := pgtype.UUID{} 75 | uuid.Set(applicationId) 76 | _, err := c.query.DeletePoktApplication(context.Background(), uuid) 77 | if err != nil { 78 | common.JSONError(ctx, "Something went wrong", fasthttp.StatusInternalServerError, err) 79 | return 80 | } 81 | ctx.SetStatusCode(fasthttp.StatusOK) 82 | } 83 | -------------------------------------------------------------------------------- /cmd/gateway_server/internal/controllers/qos_nodes.go: -------------------------------------------------------------------------------- 1 | package controllers 2 | 3 | import ( 4 | "github.com/pokt-network/gateway-server/cmd/gateway_server/internal/common" 5 | "github.com/pokt-network/gateway-server/cmd/gateway_server/internal/models" 6 | "github.com/pokt-network/gateway-server/cmd/gateway_server/internal/transform" 7 | "github.com/pokt-network/gateway-server/internal/session_registry" 8 | "github.com/valyala/fasthttp" 9 | "go.uber.org/zap" 10 | ) 11 | 12 | // QosNodeController handles requests for staked applications 13 | type QosNodeController struct { 14 | logger *zap.Logger 15 | sessionRegistry session_registry.SessionRegistryService 16 | } 17 | 18 | // NewQosNodeController creates a new instance of QosNodeController. 19 | func NewQosNodeController(sessionRegistry session_registry.SessionRegistryService, logger *zap.Logger) *QosNodeController { 20 | return &QosNodeController{sessionRegistry: sessionRegistry, logger: logger} 21 | } 22 | 23 | // GetAll returns all the qos nodes in the registry and exposes public information about them. 24 | func (c *QosNodeController) GetAll(ctx *fasthttp.RequestCtx) { 25 | qosNodes := []*models.PublicQosNode{} 26 | for _, nodes := range c.sessionRegistry.GetNodesMap() { 27 | for _, node := range nodes.Value() { 28 | qosNodes = append(qosNodes, transform.ToPublicQosNode(node)) 29 | } 30 | } 31 | common.JSONSuccess(ctx, qosNodes, fasthttp.StatusOK) 32 | } 33 | -------------------------------------------------------------------------------- /cmd/gateway_server/internal/controllers/relay.go: -------------------------------------------------------------------------------- 1 | package controllers 2 | 3 | import ( 4 | "fmt" 5 | "github.com/pokt-network/gateway-server/cmd/gateway_server/internal/common" 6 | "github.com/pokt-network/gateway-server/pkg/pokt/pokt_v0" 7 | "github.com/pokt-network/gateway-server/pkg/pokt/pokt_v0/models" 8 | "github.com/valyala/fasthttp" 9 | "go.uber.org/zap" 10 | "strings" 11 | ) 12 | 13 | // RelayController handles relay requests for a specific chain. 14 | type RelayController struct { 15 | logger *zap.Logger 16 | relayer pokt_v0.PocketRelayer 17 | } 18 | 19 | // NewRelayController creates a new instance of RelayController. 20 | func NewRelayController(relayer pokt_v0.PocketRelayer, logger *zap.Logger) *RelayController { 21 | return &RelayController{relayer: relayer, logger: logger} 22 | } 23 | 24 | // chainIdLength represents the expected length of chain IDs. 25 | const chainIdLength = 4 26 | 27 | // HandleRelay handles incoming relay requests. 28 | func (c *RelayController) HandleRelay(ctx *fasthttp.RequestCtx) { 29 | 30 | chainID, path := getPathSegmented(ctx.Path()) 31 | 32 | // Check if the chain ID is empty or has an incorrect length. 33 | if chainID == "" || len(chainID) != chainIdLength { 34 | common.JSONError(ctx, "Incorrect chain id", fasthttp.StatusBadRequest, nil) 35 | return 36 | } 37 | 38 | relay, err := c.relayer.SendRelay(&models.SendRelayRequest{ 39 | Payload: &models.Payload{ 40 | Data: string(ctx.PostBody()), 41 | Method: string(ctx.Method()), 42 | Path: path, 43 | }, 44 | Chain: chainID, 45 | }) 46 | 47 | if err != nil { 48 | c.logger.Error("Error relaying", zap.Error(err)) 49 | common.JSONError(ctx, fmt.Sprintf("Something went wrong %v", err), fasthttp.StatusInternalServerError, err) 50 | return 51 | } 52 | 53 | // Send a successful response back to the client. 54 | ctx.Response.SetStatusCode(fasthttp.StatusOK) 55 | ctx.Response.Header.Set("Content-Type", "application/json") 56 | ctx.Response.SetBodyString(relay.Response) 57 | return 58 | } 59 | 60 | // getPathSegmented: returns the chain being requested and other parts to be proxied to pokt nodes 61 | // Example: /relay/0001/v1/client, returns 0001, /v1/client 62 | func getPathSegmented(path []byte) (chain, otherParts string) { 63 | paths := strings.Split(string(path), "/") 64 | 65 | if len(paths) >= 3 { 66 | chain = paths[2] 67 | } 68 | 69 | if len(paths) > 3 { 70 | otherParts = "/" + strings.Join(paths[3:], "/") 71 | } 72 | 73 | return chain, otherParts 74 | } 75 | -------------------------------------------------------------------------------- /cmd/gateway_server/internal/controllers/relay_test.go: -------------------------------------------------------------------------------- 1 | package controllers 2 | 3 | // Basic imports 4 | import ( 5 | "errors" 6 | pocket_service_mock "github.com/pokt-network/gateway-server/mocks/pocket_service" 7 | "github.com/pokt-network/gateway-server/pkg/pokt/pokt_v0/models" 8 | "testing" 9 | 10 | "github.com/stretchr/testify/suite" 11 | "github.com/valyala/fasthttp" 12 | "go.uber.org/zap" 13 | ) 14 | 15 | type RelayTestSuite struct { 16 | suite.Suite 17 | mockPocketService *pocket_service_mock.PocketService 18 | 19 | mockRelayController *RelayController 20 | context *fasthttp.RequestCtx 21 | } 22 | 23 | func (suite *RelayTestSuite) SetupTest() { 24 | suite.mockPocketService = new(pocket_service_mock.PocketService) 25 | suite.mockRelayController = NewRelayController(suite.mockPocketService, zap.NewNop()) 26 | suite.context = &fasthttp.RequestCtx{} // mock the fasthttp.RequestCtx 27 | } 28 | 29 | // mock send relay request function 30 | func (suite *RelayTestSuite) mockSendRelayRequest() *models.SendRelayRequest { 31 | 32 | chainID, path := getPathSegmented(suite.context.Path()) // get the chainID and path from the request path 33 | 34 | return &models.SendRelayRequest{ 35 | Payload: &models.Payload{ 36 | Data: string(suite.context.PostBody()), 37 | Method: string(suite.context.Method()), 38 | Path: path, 39 | }, 40 | Chain: chainID, 41 | } 42 | } 43 | 44 | // test for the HandleRelay function in relay.go file using table driven tests to test different scenarios for the function 45 | func (suite *RelayTestSuite) TestHandleRelay() { 46 | 47 | var testResponse string = "test" 48 | 49 | tests := []struct { 50 | name string 51 | setupMocks func(*fasthttp.RequestCtx) 52 | path string 53 | expectedStatus int 54 | expectedResponse *string 55 | }{ 56 | { 57 | name: "EmptyChainID", 58 | setupMocks: func(ctx *fasthttp.RequestCtx) { 59 | }, 60 | path: "/relay/", 61 | expectedStatus: fasthttp.StatusBadRequest, 62 | expectedResponse: nil, 63 | }, 64 | { 65 | name: "ChainIdLengthInvalid", 66 | setupMocks: func(ctx *fasthttp.RequestCtx) { 67 | }, 68 | path: "/relay/1234555", 69 | expectedStatus: fasthttp.StatusBadRequest, 70 | expectedResponse: nil, 71 | }, 72 | { 73 | name: "ErrorSendingRelay", 74 | setupMocks: func(ctx *fasthttp.RequestCtx) { 75 | suite.mockPocketService.EXPECT().SendRelay(suite.mockSendRelayRequest()). 76 | Return(nil, errors.New("relay error")) 77 | }, 78 | path: "/relay/1234", 79 | expectedStatus: fasthttp.StatusInternalServerError, 80 | expectedResponse: nil, 81 | }, 82 | { 83 | name: "Success", 84 | setupMocks: func(ctx *fasthttp.RequestCtx) { 85 | suite.mockPocketService.EXPECT().SendRelay(suite.mockSendRelayRequest()). 86 | Return(&models.SendRelayResponse{ 87 | Response: testResponse, 88 | }, nil) 89 | 90 | }, 91 | path: "/relay/1234", 92 | expectedStatus: fasthttp.StatusOK, 93 | expectedResponse: &testResponse, 94 | }, 95 | } 96 | for _, test := range tests { 97 | suite.Run(test.name, func() { 98 | 99 | suite.SetupTest() // reset the test suite 100 | 101 | suite.context.Request.SetBody([]byte("test")) 102 | suite.context.Request.Header.SetMethod("POST") 103 | suite.context.Request.SetRequestURI(test.path) 104 | 105 | test.setupMocks(suite.context) // setup the mocks for the test 106 | 107 | suite.mockRelayController.HandleRelay(suite.context) 108 | 109 | suite.Equal(test.expectedStatus, suite.context.Response.StatusCode()) 110 | 111 | if test.expectedResponse != nil { 112 | suite.Equal(*test.expectedResponse, string(suite.context.Response.Body())) 113 | } 114 | 115 | }) 116 | } 117 | } 118 | 119 | // test for the getPathSegmented function in relay.go file using table driven tests to test different scenarios for the function 120 | func (suite *RelayTestSuite) TestGetPathSegmented() { 121 | 122 | tests := []struct { 123 | name string 124 | path string 125 | expectedPath string 126 | expectedRest string 127 | }{ 128 | { 129 | name: "EmptyPath", 130 | path: "", 131 | expectedPath: "", 132 | expectedRest: "", 133 | }, 134 | { 135 | name: "LessThanTwoSegments", 136 | path: "/segment1", 137 | expectedPath: "", 138 | expectedRest: "", 139 | }, 140 | { 141 | name: "TwoSegments", 142 | path: "/segment1/1234", 143 | expectedPath: "1234", 144 | expectedRest: "", 145 | }, 146 | { 147 | name: "MoreThanTwoSegments", 148 | path: "/segment1/1234/segment2", 149 | expectedPath: "1234", 150 | expectedRest: "/segment2", 151 | }, 152 | } 153 | 154 | for _, test := range tests { 155 | suite.Run(test.name, func() { 156 | 157 | path, rest := getPathSegmented([]byte(test.path)) 158 | 159 | suite.Equal(test.expectedPath, path) 160 | suite.Equal(test.expectedRest, rest) 161 | 162 | }) 163 | } 164 | 165 | } 166 | 167 | func TestRelayTestSuite(t *testing.T) { 168 | suite.Run(t, new(RelayTestSuite)) 169 | } 170 | -------------------------------------------------------------------------------- /cmd/gateway_server/internal/middleware/x-api-key.go: -------------------------------------------------------------------------------- 1 | package middleware 2 | 3 | import ( 4 | "github.com/pokt-network/gateway-server/cmd/gateway_server/internal/common" 5 | config2 "github.com/pokt-network/gateway-server/internal/global_config" 6 | "github.com/valyala/fasthttp" 7 | ) 8 | 9 | func retrieveAPIKey(ctx *fasthttp.RequestCtx) string { 10 | auth := ctx.Request.Header.Peek("x-api-key") 11 | if auth == nil { 12 | return "" 13 | } 14 | return string(auth) 15 | } 16 | 17 | // BasicAuth is the basic auth handler 18 | func XAPIKeyAuth(h fasthttp.RequestHandler, provider config2.SecretProvider) fasthttp.RequestHandler { 19 | return func(ctx *fasthttp.RequestCtx) { 20 | // Get the Basic Authentication credentials 21 | xAPIKey := retrieveAPIKey(ctx) 22 | 23 | if xAPIKey != "" && xAPIKey == provider.GetAPIKey() { 24 | h(ctx) 25 | return 26 | } 27 | // Request Basic Authentication otherwise 28 | common.JSONError(ctx, "Unauthorized, invalid x-api-key header", fasthttp.StatusUnauthorized, nil) 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /cmd/gateway_server/internal/models/pokt_application.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | type PublicPoktApplication struct { 4 | ID string `json:"id"` 5 | MaxRelays int `json:"max_relays"` 6 | Chains []string `json:"chain"` 7 | Address string `json:"address"` 8 | } 9 | -------------------------------------------------------------------------------- /cmd/gateway_server/internal/models/qos_node.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | import "time" 4 | 5 | type PublicQosNode struct { 6 | NodePublicKey string `json:"node_public_key"` 7 | ServiceUrl string `json:"service_url"` 8 | Chain string `json:"chain"` 9 | SessionHeight uint `json:"session_height"` 10 | AppPublicKey string `json:"app_public_key"` 11 | TimeoutUntil time.Time `json:"timeout_until"` 12 | TimeoutReason string `json:"timeout_reason"` 13 | LastKnownErr string `json:"last_known_err"` 14 | IsHealthy bool `json:"is_healthy"` 15 | IsSynced bool `json:"is_synced"` 16 | LastKnownHeight uint64 `json:"last_known_height"` 17 | P90Latency float64 `json:"p90_latency"` 18 | } 19 | -------------------------------------------------------------------------------- /cmd/gateway_server/internal/transform/pokt_application.go: -------------------------------------------------------------------------------- 1 | package transform 2 | 3 | import ( 4 | "github.com/pokt-network/gateway-server/cmd/gateway_server/internal/models" 5 | internal_model "github.com/pokt-network/gateway-server/internal/apps_registry/models" 6 | ) 7 | 8 | func ToPoktApplication(app *internal_model.PoktApplicationSigner) *models.PublicPoktApplication { 9 | return &models.PublicPoktApplication{ 10 | ID: app.ID, 11 | MaxRelays: int(app.NetworkApp.MaxRelays), 12 | Chains: app.NetworkApp.Chains, 13 | Address: app.NetworkApp.Address, 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /cmd/gateway_server/internal/transform/qos_node.go: -------------------------------------------------------------------------------- 1 | package transform 2 | 3 | import ( 4 | "math" 5 | 6 | "github.com/pokt-network/gateway-server/cmd/gateway_server/internal/models" 7 | internal_model "github.com/pokt-network/gateway-server/internal/node_selector_service/models" 8 | ) 9 | 10 | func ToPublicQosNode(node *internal_model.QosNode) *models.PublicQosNode { 11 | latency := node.LatencyTracker.GetP90Latency() 12 | if math.IsNaN(latency) { 13 | latency = 0.0 14 | } 15 | return &models.PublicQosNode{ 16 | NodePublicKey: node.MorseNode.PublicKey, 17 | ServiceUrl: node.MorseNode.ServiceUrl, 18 | Chain: node.GetChain(), 19 | SessionHeight: node.MorseSession.SessionHeader.SessionHeight, 20 | AppPublicKey: node.MorseSigner.PublicKey, 21 | TimeoutReason: string(node.GetTimeoutReason()), 22 | LastKnownErr: node.GetLastKnownErrorStr(), 23 | IsHealthy: node.IsHealthy(), 24 | IsSynced: node.IsSynced(), 25 | LastKnownHeight: node.GetLastKnownHeight(), 26 | TimeoutUntil: node.GetTimeoutUntil(), 27 | P90Latency: latency, 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /cmd/gateway_server/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "github.com/fasthttp/router" 6 | fasthttpprometheus "github.com/flf2ko/fasthttp-prometheus" 7 | "github.com/jellydator/ttlcache/v3" 8 | "github.com/pokt-network/gateway-server/cmd/gateway_server/internal/config" 9 | "github.com/pokt-network/gateway-server/cmd/gateway_server/internal/controllers" 10 | "github.com/pokt-network/gateway-server/cmd/gateway_server/internal/middleware" 11 | "github.com/pokt-network/gateway-server/internal/apps_registry" 12 | "github.com/pokt-network/gateway-server/internal/chain_configurations_registry" 13 | "github.com/pokt-network/gateway-server/internal/db_query" 14 | "github.com/pokt-network/gateway-server/internal/logging" 15 | "github.com/pokt-network/gateway-server/internal/node_selector_service" 16 | qos_models "github.com/pokt-network/gateway-server/internal/node_selector_service/models" 17 | "github.com/pokt-network/gateway-server/internal/relayer" 18 | "github.com/pokt-network/gateway-server/internal/session_registry" 19 | "github.com/pokt-network/gateway-server/pkg/pokt/pokt_v0" 20 | "github.com/valyala/fasthttp" 21 | ) 22 | 23 | const ( 24 | userAgent = "pokt-gw-server" 25 | // Maximum amount of DB connections opened at a time. This should not have to be modified 26 | // as most of our database queries are periodic and not ran concurrently. 27 | maxDbConns = 50 28 | ) 29 | 30 | func main() { 31 | // Initialize configuration provider from environment variables 32 | gatewayConfigProvider := config.NewDotEnvConfigProvider() 33 | 34 | // Initialize logger using the configured settings 35 | logger, err := logging.NewLogger(gatewayConfigProvider) 36 | if err != nil { 37 | // If logger initialization fails, panic with the error 38 | panic(err) 39 | } 40 | 41 | querier, pool, err := db_query.InitDB(logger, gatewayConfigProvider, maxDbConns) 42 | if err != nil { 43 | logger.Sugar().Fatal(err) 44 | return 45 | } 46 | 47 | // Close connection to pool afterward 48 | defer pool.Close() 49 | 50 | // Initialize a POKT client using the configured POKT RPC host and timeout 51 | client, err := pokt_v0.NewBasicClient(gatewayConfigProvider.GetPoktRPCFullHost(), userAgent, gatewayConfigProvider.GetPoktRPCRequestTimeout()) 52 | if err != nil { 53 | // If POKT client initialization fails, log the error and exit 54 | logger.Sugar().Fatal(err) 55 | return 56 | } 57 | 58 | // Initialize a TTL cache for session caching 59 | sessionCache := ttlcache.New[string, *session_registry.Session]( 60 | ttlcache.WithTTL[string, *session_registry.Session](gatewayConfigProvider.GetSessionCacheTTL()), 61 | ) 62 | 63 | nodeCache := ttlcache.New[qos_models.SessionChainKey, []*qos_models.QosNode]( 64 | ttlcache.WithTTL[qos_models.SessionChainKey, []*qos_models.QosNode](gatewayConfigProvider.GetSessionCacheTTL()), 65 | ) 66 | 67 | poktApplicationRegistry := apps_registry.NewCachedAppsRegistry(client, querier, gatewayConfigProvider, logger.Named("pokt_application_registry")) 68 | chainConfigurationRegistry := chain_configurations_registry.NewCachedChainConfigurationRegistry(querier, logger.Named("chain_configurations_registry")) 69 | sessionRegistry := session_registry.NewCachedSessionRegistryService(client, poktApplicationRegistry, sessionCache, nodeCache, logger.Named("session_registry")) 70 | nodeSelectorService := node_selector_service.NewNodeSelectorService(sessionRegistry, client, chainConfigurationRegistry, gatewayConfigProvider, logger.Named("node_selector")) 71 | 72 | relayer := relayer.NewRelayer(client, sessionRegistry, poktApplicationRegistry, nodeSelectorService, chainConfigurationRegistry, userAgent, gatewayConfigProvider, logger.Named("relayer")) 73 | 74 | // Define routers 75 | r := router.New() 76 | 77 | // Create a relay controller with the necessary dependencies (logger, registry, cached relayer) 78 | relayController := controllers.NewRelayController(relayer, logger.Named("relay_controller")) 79 | 80 | relayRouter := r.Group("/relay") 81 | relayRouter.POST("/{catchAll:*}", relayController.HandleRelay) 82 | 83 | poktAppsController := controllers.NewPoktAppsController(poktApplicationRegistry, querier, gatewayConfigProvider, logger.Named("pokt_apps_controller")) 84 | poktAppsRouter := r.Group("/poktapps") 85 | 86 | poktAppsRouter.GET("/", middleware.XAPIKeyAuth(poktAppsController.GetAll, gatewayConfigProvider)) 87 | poktAppsRouter.POST("/", middleware.XAPIKeyAuth(poktAppsController.AddApplication, gatewayConfigProvider)) 88 | poktAppsRouter.DELETE("/{app_id}", middleware.XAPIKeyAuth(poktAppsController.DeleteApplication, gatewayConfigProvider)) 89 | 90 | // Create qos controller for debugging purposes 91 | qosNodeController := controllers.NewQosNodeController(sessionRegistry, logger.Named("qos_node_controller")) 92 | qosNodeRouter := r.Group("/qosnodes") 93 | qosNodeRouter.GET("/", middleware.XAPIKeyAuth(qosNodeController.GetAll, gatewayConfigProvider)) 94 | 95 | // Add Middleware for Generic E2E Prom Tracking 96 | p := fasthttpprometheus.NewPrometheus("fasthttp") 97 | fastpHandler := p.WrapHandler(r) 98 | 99 | logger.Info("Gateway Server Started") 100 | // Start the fasthttp server and listen on the configured server port 101 | if err := fasthttp.ListenAndServe(fmt.Sprintf(":%d", gatewayConfigProvider.GetHTTPServerPort()), fastpHandler); err != nil { 102 | // If an error occurs during server startup, log the error and exit 103 | logger.Sugar().Fatalw("Error in ListenAndServe", "err", err) 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /db_migrations/000001_initial.down.sql: -------------------------------------------------------------------------------- 1 | -- Drop the table 'pokt_applications' and its dependencies 2 | DROP TABLE IF EXISTS pokt_applications; 3 | 4 | -- Drop the table 'base_model' 5 | DROP TABLE IF EXISTS base_model; 6 | 7 | -- Drop the extensions 'pgcrypto' and 'uuid-ossp' 8 | DROP EXTENSION IF EXISTS pgcrypto; 9 | DROP EXTENSION IF EXISTS "uuid-ossp"; 10 | -------------------------------------------------------------------------------- /db_migrations/000001_initial.up.sql: -------------------------------------------------------------------------------- 1 | CREATE EXTENSION IF NOT EXISTS "uuid-ossp"; 2 | CREATE EXTENSION IF NOT EXISTS pgcrypto; 3 | 4 | CREATE TABLE base_model 5 | ( 6 | created_at TIMESTAMP NOT NULL DEFAULT NOW(), 7 | updated_at TIMESTAMP, 8 | deleted_at TIMESTAMP 9 | ); 10 | 11 | CREATE TABLE pokt_applications 12 | ( 13 | id UUID PRIMARY KEY NOT NULL DEFAULT uuid_generate_v4(), 14 | encrypted_private_key BYTEA NOT NULL, 15 | CONSTRAINT unique_encrypted_private_key UNIQUE (encrypted_private_key) 16 | ) INHERITS (base_model); 17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /db_migrations/000002_chain_configurations.down.sql: -------------------------------------------------------------------------------- 1 | -- Drop the table 'base_model' 2 | DROP TABLE IF EXISTS chain_configurations; -------------------------------------------------------------------------------- /db_migrations/000002_chain_configurations.up.sql: -------------------------------------------------------------------------------- 1 | CREATE TABLE chain_configurations 2 | ( 3 | id UUID PRIMARY KEY NOT NULL DEFAULT uuid_generate_v4(), 4 | chain_id VARCHAR NOT NULL UNIQUE, 5 | pocket_request_timeout_duration VARCHAR NOT NULL, 6 | altruist_url VARCHAR NOT NULL, 7 | altruist_request_timeout_duration VARCHAR NOT NULL, 8 | top_bucket_p90latency_duration VARCHAR NOT NULL, 9 | height_check_block_tolerance INT NOT NULL, 10 | data_integrity_check_lookback_height INT NOT NULL 11 | ) INHERITS (base_model); 12 | 13 | -- Insert an example configuration for Ethereum -- 14 | INSERT INTO chain_configurations (chain_id, pocket_request_timeout_duration, altruist_url, altruist_request_timeout_duration, top_bucket_p90latency_duration, height_check_block_tolerance, data_integrity_check_lookback_height) VALUES ('0000', '15s', 'example.com', '30s', '150ms', 100, 25); -------------------------------------------------------------------------------- /docker-compose.yml.sample: -------------------------------------------------------------------------------- 1 | version: '3' 2 | 3 | services: 4 | gateway-server: 5 | build: . 6 | volumes: 7 | - .env:/app/.env 8 | ports: 9 | - "${HTTP_SERVER_PORT}:${HTTP_SERVER_PORT}" 10 | env_file: 11 | - ./.env 12 | networks: 13 | - bridged_network 14 | # depends_on: 15 | # - postgres 16 | restart: on-failure 17 | logging: 18 | driver: json-file 19 | options: 20 | max-size: 10m 21 | 22 | # this postgres database is only to be used for testing. It should not be used in production systems 23 | # Leverage a production ready postgres database with HA/replicas in prod. 24 | # postgres: 25 | # image: postgres:latest 26 | # environment: 27 | # POSTGRES_DB: postgres 28 | # POSTGRES_USER: myuser 29 | # POSTGRES_PASSWORD: mypassword 30 | # ports: 31 | # - "5433:5432" 32 | # volumes: 33 | # - postgres_data:/var/lib/postgresql/data 34 | # networks: 35 | # - bridged_network 36 | 37 | volumes: 38 | postgres_data: 39 | 40 | networks: 41 | bridged_network: 42 | driver: bridge -------------------------------------------------------------------------------- /docs/altruist-chain-configuration.md: -------------------------------------------------------------------------------- 1 | # Chain & Altruist Configurations 2 | 3 | ## Altruist (Failover) Request 4 | 5 | - [Altruist (Failover) Request](#altruist-failover-request) 6 | - [Chain Configuration](#chain-configuration) 7 | - [Inserting a custom chain configuration](#inserting-a-custom-chain-configuration) 8 | 9 | In rare situations, a relay cannot be served from POKT Network. Some sample scenarios for when this can happen: 10 | 11 | 1. Dispatcher Outages - Gateway server cannot retrieve the necessary information to send a relay 12 | 2. Bad overall QoS - Majority of Node operators may have their nodes misconfigured improperly or there is a lack of node operators supporting the chain with respect to load. 13 | 3. Chain halts - In extreme conditions, if the chain halts, node operators may stop responding to relay requests 14 | 15 | So the Gateway Server will attempt to route the traffic to a backup chain node. This could be any source, for example: 16 | 17 | 1. Other gateway operators chain urls 18 | 2. Centralized Nodes 19 | 20 | ## Chain Configuration 21 | 22 | Given that every chain has differences and have different sources for fail over, the gateway server allows for optional customization for request timeouts, failover relay, and QoS checks. 23 | The data is stored inside the `chain_configuration` table and is accessed via the [chain_configurations_registry_service.go](../internal/chain_configurations_registry/chain_configurations_registry_service.go). 24 | 25 | _While it is **recommended** that you provide a chain configuration, the gateway server will assume defaults provided from the specified [config_provider.go](../internal/global_config/config_provider.go) and the provided QoS [checks](../internal/node_selector_service/checks)_ if not provided. 26 | 27 | ## Inserting a custom chain configuration 28 | 29 | ```sql 30 | -- Insert an example configuration for Ethereum -- 31 | INSERT INTO chain_configurations (chain_id, pocket_request_timeout_duration, altruist_url, altruist_request_timeout_duration, top_bucket_p90latency_duration, height_check_block_tolerance, data_integrity_check_lookback_height) VALUES ('0000', '15s', 'https://example.com', '30s', '150ms', 100, 25); 32 | ``` 33 | 34 | - `chain_id` - id of the Pocket Network Chain 35 | - `pocket_request_time` - duration of the maximum amount of time for a network relay to respond 36 | - `altruist_url` - source of the relay in the event that a network request fails 37 | - `altruist_request_timeout_duration` - duration of the maximum amount of time for a backup request to respond 38 | - `top_bucket_p90latency_duration` - maximum amount of latency for nodes to be favored 0 <= x <= `top_bucket_p90latency_duration` 39 | - `height_check_block_tolerance` - number of blocks a node is allowed to be behind (some chains may have node operators moving faster than others) 40 | - `data_integrity_check_lookback_height` - number of blocks data integrity will look behind for source of truth block for other node operators to attest too 41 | -------------------------------------------------------------------------------- /docs/api-endpoints.md: -------------------------------------------------------------------------------- 1 | # POKT Gateway Server API Endpoints 2 | 3 | The Gateway Server currently exposes all its API endpoints in form of HTTP endpoints. 4 | 5 | Postman collection can be found [here](https://www.postman.com/dark-shadow-851601/workspace/os-gateway/collection/27302708-537f3ba3-3193-4290-98d0-0d5836988a2f) 6 | 7 | `x-api-key` is an api key set by the gateway operator to transmit internal private data 8 | 9 | _TODO_IMPROVE: Move this to Swagger in the future if our API endpoints become more complex._ 10 | _TODO_IMPROVE: Add an OpenAPI spec if the admin endpoints are kept and/or expanded on.._ 11 | 12 | - [API Endpoints](#api-endpoints) 13 | - [Examples](#examples) 14 | - [Relay](#relay) 15 | - [Metrics](#metrics) 16 | - [PoktApps](#poktapps) 17 | - [List](#list) 18 | - [Add](#add) 19 | - [Delete](#delete) 20 | - [QoS Noes](#qos-noes) 21 | 22 | ## API Endpoints 23 | 24 | | Endpoint | HTTP METHOD | Description | HEADERS | Request Parameters | 25 | | -------------------- | ----------- | ------------------------------------------------------------------------------------------------------------------------------------------------ | ----------- | ---------------------------------------- | 26 | | `/relay/{chain_id}` | ANY | The main endpoint to send relays to | ANY | `{chain_id}` - Network identifier | 27 | | `/metrics` | GET | Gateway metadata related to server performance and observability | N/A | N/A | 28 | | `/poktapps` | GET | List all the available app stakes | `x-api-key` | N/A | 29 | | `/poktapps` | POST | Add an existing app stake to the appstake database (not recommended due to security) | `x-api-key` | `private_key` - private key of app stake | 30 | | `/poktapps/{app_id}` | DELETE | Remove an existing app stake from the appstake database (not recommended due to security) | `x-api-key` | `app_id` - id of the appstake | 31 | | `/qosnodes` | GET | List of nodes and public QoS state such as healthiness and last known error. This can be used to expose to node operators to improve visibility. | `x-api-key` | N/A | 32 | 33 | ## Examples 34 | 35 | These examples assume gateway server is running locally. 36 | 37 | ### Relay 38 | 39 | ```bash 40 | curl -X POST -H "Content-Type: application/json" \ 41 | --data '{"jsonrpc":"2.0","method":"eth_blockNumber","params":[],"id":1}' \ 42 | http://localhost:8080/relay/0021 43 | ``` 44 | 45 | ### Metrics 46 | 47 | ```bash 48 | curl -X GET http://localhost:8080/metrics 49 | ``` 50 | 51 | ### PoktApps 52 | 53 | Make sure that the gateway server starts up with the `API_KEY` environment variable set. 54 | 55 | #### List 56 | 57 | ```bash 58 | curl -X GET http://localhost:8080/poktapps 59 | ``` 60 | 61 | #### Add 62 | 63 | ```bash 64 | curl -X POST -H "x-api-key: $API_KEY" https://localhost:8080/poktapps/{private_key} 65 | ``` 66 | 67 | #### Delete 68 | 69 | ```bash 70 | curl -X DELETE -H "x-api-key: $API_KEY" https://localhost:8080/poktapps/{app_id} 71 | ``` 72 | 73 | #### QoS Noes 74 | 75 | ```bash 76 | curl -X GET -H "x-api-key: $API_KEY" http://localhost:8080/qosnodes 77 | ``` 78 | -------------------------------------------------------------------------------- /docs/benchmarks/03_2024/03-2024-benchmark.md: -------------------------------------------------------------------------------- 1 | # March 2024 Benchmark (RC 0.2.0) 2 | 3 | ## Benchmark Purpose 4 | 5 | The purpose of this benchmark is to comprehensively assess the performance metrics, particularly CPU and Memory behaviors, incurred while serving requests through the gateway server. Specifically, this evaluation aims to gauge the efficiency of various operations involved with sending a relay such as JSON serialization, IO handling, cryptographic signing, and asynchronous background processes (QoS checks). 6 | 7 | ## Benchmark Environment 8 | 9 | - **POKT Testnet**: The benchmark is conducted within the environment of the POKT Testnet to simulate real-world conditions accurately. 10 | - **RPC Method web3_clientVersion**: The benchmark uses a consistent time RPC method, web3_clientVersion, chosen deliberately to isolate the impact of the gateway server overhead. It's noteworthy that the computational overhead of sending a request to the POKT Network remains independent of the specific RPC Method employed. 11 | - **Gateway Server Hardware**: The gateway server is deployed on a dedicated DigitalOcean droplet instance (16 GB Memory / 8 Prem. Intel vCPUs / 100 GB Disk / FRA1), ensuring controlled conditions for performance evaluation. 12 | - **Tooling**: Utilizes [Vegeta](https://github.com/tsenart/vegeta), a versatile HTTP load testing too 13 | - **Vegeta Server Hardware**: The load tester is deployed on a seperate dedicated DigitalOcean droplet instance (8 GB Memory / 50 GB Disk / FRA1) to prevent any thrashing with the gateway server. 14 | - **Grafana**: Used to visualize Gateway server internal metrics. 15 | 16 | ## Scripts 17 | 18 | Load Testing Command 19 | 20 | ```sh 21 | vegeta attack -duration=180s -rate=100/1s -targets=gateway_server.config | tee results.bin | vegeta report 22 | ``` 23 | 24 | Vegeta Target 25 | 26 | ```sh 27 | POST http://{endpoint} 28 | Content-Type: application/json 29 | @payload.json 30 | ``` 31 | 32 | Payload.json 33 | 34 | ```json 35 | { "jsonrpc": "2.0", "method": "web3_clientVersion", "params": [], "id": 1 } 36 | ``` 37 | 38 | ## Load Test Results 39 | 40 | 100 RPS 41 | 42 | ```text 43 | Requests [total, rate, throughput] 18000, 100.01, 99.87 44 | Duration [total, attack, wait] 3m0s, 3m0s, 239.445ms 45 | Latencies [min, mean, 50, 90, 95, 99, max] 170.168ms, 191.573ms, 176.331ms, 200.473ms, 230.392ms, 283.411ms, 3.284s 46 | Bytes In [total, mean] 1260000, 70.00 47 | Bytes Out [total, mean] 1206000, 67.00 48 | Success [ratio] 100.00% 49 | Status Codes [code:count] 200:18000 50 | Error Set: 51 | ``` 52 | 53 | 500 RPS 54 | 55 | ```text 56 | Requests [total, rate, throughput] 90000, 500.01, 499.51 57 | Duration [total, attack, wait] 3m0s, 3m0s, 176.636ms 58 | Latencies [min, mean, 50, 90, 95, 99, max] 169.036ms, 182.464ms, 176.267ms, 197.629ms, 212.595ms, 263.593ms, 3.61s 59 | Bytes In [total, mean] 6300000, 70.00 60 | Bytes Out [total, mean] 6030000, 67.00 61 | Success [ratio] 100.00% 62 | Status Codes [code:count] 200:90000 63 | Error Set: 64 | ``` 65 | 66 | 1000 RPS 67 | 68 | ```text 69 | Requests [total, rate, throughput] 180000, 1000.00, 998.87 70 | Duration [total, attack, wait] 3m0s, 3m0s, 204.308ms 71 | Latencies [min, mean, 50, 90, 95, 99, max] 168.406ms, 183.103ms, 176.947ms, 190.737ms, 196.224ms, 216.507ms, 7.122s 72 | Bytes In [total, mean] 12600000, 70.00 73 | Bytes Out [total, mean] 12060000, 67.00 74 | Success [ratio] 100.00% 75 | Status Codes [code:count] 200:180000 76 | Error Set: 77 | ``` 78 | 79 | ## Analysis 80 | 81 | ### CPU Metrics 82 | 83 | ![cpu-03-2024.png](resources/cpu-03-2024.png) 84 | CPU metrics exhibit a slight uptick from approximately 10% to around 125% at peak load of 1,000 RPS. However, it's noteworthy that the gateway server did not reach full CPU utilization, with the maximum observed at 800%. 85 | 86 | ### Ram Metrics 87 | 88 | ![memory-03-2024.png](resources/memory-03-2024.png) 89 | RAM metrics show a similar pattern, with a slight increase from around 120MiB to approximately 280MiB at peak load. This increase is expected due to the opening of more network connections while serving traffic. 90 | 91 | ### Latency Analysis 92 | 93 | Upon closer inspection, despite the tenfold increase in load from 100 RPS to 1,000 RPS, the benchmark latency remained relatively consistent at ~150MS. Nodies, with the gateway server in production, has seen multiple node operators achieve lower latencies, typically ranging from 50ms to 70ms P90 latency at similar or higher request rates. Therefore, a consistent baseline latency of ~150ms at even 100 RPS in our benchmarking environment warranted further investigation to determine root cause. 94 | 95 | By analyzing Prometheus metrics emitted by the gateway server, specifically `pocket_relay_latency` (which measures the latency of creating a relay, including hashing, signing, and sending it to POKT Nodes) and `relay_latency` (providing an end-to-end latency metric including node selection), it was possible to identify the source of additional latency overhead. 96 | 97 | ![node-selection-overhead-03-2024.png](resources/node-selection-overhead-03-2024.png) 98 | 99 | This deep dive revealed that the bottleneck does not lie in QoS/Node selection, as the intersection of the two metrics indicated that node selection completes within fractions of a second, ruling out that the gateway server code has a bottleneck. 100 | 101 | The baseline latency overhead therefore is attributed to protocol requirements (hashing/signing a relay) and hardware specifications. To mitigate this latency, upgrading to more powerful CPUs or dedicated machines should decrease this latency. Nodies currently uses the AMD 5950X CPU for their gateway servers. 102 | 103 | ### Summary 104 | 105 | This benchmark provides a comprehensive quantitative assessment of the gateway server's performance under varying loads within the POKT Testnet environment. Analysis of CPU metrics reveals a slight uptick in CPU utilization from approximately 10% to around 125% at peak load of 1,000 RPS (max capacity of 800%). Memory metrics also show a similar pattern, with memory utilization increasing from around 120MiB to approximately 280MiB at peak load. 106 | 107 | Despite the increase in load, the gateway server demonstrates resilience, maintaining consistent latency across different request rates. 108 | 109 | In order to achieve better latency performance in production, gateway operators should strive for modern CPU's such as the AMD Ryzen 9 or EPYC processors. 110 | -------------------------------------------------------------------------------- /docs/benchmarks/03_2024/resources/cpu-03-2024.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pokt-network/gateway-server/9f5e8be1165ba6966e29e936a68674052ddf8cf5/docs/benchmarks/03_2024/resources/cpu-03-2024.png -------------------------------------------------------------------------------- /docs/benchmarks/03_2024/resources/memory-03-2024.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pokt-network/gateway-server/9f5e8be1165ba6966e29e936a68674052ddf8cf5/docs/benchmarks/03_2024/resources/memory-03-2024.png -------------------------------------------------------------------------------- /docs/benchmarks/03_2024/resources/node-selection-overhead-03-2024.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pokt-network/gateway-server/9f5e8be1165ba6966e29e936a68674052ddf8cf5/docs/benchmarks/03_2024/resources/node-selection-overhead-03-2024.png -------------------------------------------------------------------------------- /docs/docker-compose.md: -------------------------------------------------------------------------------- 1 | # [WIP] Docker Compose Example 2 | 3 | This is an example of the instructions to run the gateway server on a remote debian 4 | server using docker-compose from scratch. 5 | 6 | Locally 7 | 8 | ```bash 9 | 10 | # SSH into your server 11 | ssh user@your-remote-server 12 | 13 | # Install the necessary dependencies 14 | sudo apt update 15 | sudo apt install -y postgresql postgresql-contrib git docker.io 16 | sudo systemctl start docker 17 | sudo systemctl enable docker 18 | sudo curl -L "https://github.com/docker/compose/releases/download/1.29.2/docker-compose-$(uname -s)-$(uname -m)" -o /usr/local/bin/docker-compose 19 | 20 | # Make a workspace directory 21 | mkdir workspace 22 | cd workspace 23 | 24 | # Add the following to ~/.profile 25 | export PATH=$PATH:/usr/local/go/bin 26 | export GOPATH=$HOME/go 27 | export PATH=$PATH:$GOPATH/bin 28 | 29 | # Install go 30 | wget https://go.dev/dl/go1.21.12.linux-amd64.tar.gz 31 | sudo tar -C /usr/local -xzf go1.21.12.linux-amd64.tar.gz 32 | 33 | # Clone the gateway server repository 34 | git clone https://github.com/pokt-network/gateway-server.git 35 | cd gateway-server/ 36 | 37 | 38 | # Uncomment the postgres related lines in the docker-compose.yml file 39 | cp docker-compose.yml.sample docker-compose.yml 40 | 41 | # Update the .env accordingly 42 | cp .env.sample .env 43 | 44 | # Start the gateway server 45 | docker-compose up -d 46 | 47 | # OPTIONAL: Connect to the postgres container directly 48 | PGPASSWORD=mypassword psql -h localhost -U myuser -d postgres -p 5433 49 | ``` 50 | -------------------------------------------------------------------------------- /docs/node-selection.md: -------------------------------------------------------------------------------- 1 | # POKT Gateway Node Selection 2 | 3 | - [Node Selection System Architecture](#node-selection-system-architecture) 4 | - [QoS Controls](#qos-controls) 5 | - [Node Selector](#node-selector) 6 | - [Checks Framework](#checks-framework) 7 | - [Existing QoS checks](#existing-qos-checks) 8 | - [Height Check (i.e. sync checks)](#height-check-ie-sync-checks) 9 | - [Data Integrity Check (quorum checks)](#data-integrity-check-quorum-checks) 10 | - [Adding custom QoS checks](#adding-custom-qos-checks) 11 | - [Future Improvements](#future-improvements) 12 | 13 | ## Node Selection System Architecture 14 | 15 | ![gateway-server-node-selection-system.png](resources/gateway-server-node-selection-system.png) 16 | 17 | - `Session Registry` - responsible for "priming" sessions asynchronously, providing session metadata, and feeding the node to the `NodeSelectorService` 18 | - `Pocket Relayer` - responsible for sending a relay to the network 19 | - `NodeSelectorService` - responsible for running QoS checks and identifying healthy nodes by chain. 20 | - `ChainConfigurationRegistryService` - responsible for providing custom chain configurations such as altruists and timeouts. 21 | 22 | ## QoS Controls 23 | 24 | The gateway kit server determines if a set of nodes are healthy based off a simple weight formula with the following 25 | heuristics: 26 | 27 | 1. Latency (round-trip-tim) 28 | 2. Success Responses (uptime & availability) 29 | 3. Correctness in regard to other node operators (quorum checks) 30 | 4. Liveliness (syn checks) 31 | 32 | ## Node Selector 33 | 34 | After the sessions are primed, the nodes are fed to the `NodeSelectorService` which is responsible for: 35 | 36 | 1. Running various QoS checks (Height and Data Integrity Checks) 37 | 2. Exposing functions for the main process to select a healthy node `FindNode(chainId string) string` 38 | 39 | ### Checks Framework 40 | 41 | The gateway server provides a simple interface called a `CheckJob`. This interface consists of three simple functions 42 | 43 | ```go 44 | type CheckJob interface { 45 | Perform() 46 | Name() string 47 | ShouldRun() bool 48 | } 49 | ``` 50 | 51 | Under the hood, the NodeSelectorService is responsible for asynchronously executing all the initialized `CheckJobs`. 52 | 53 | ### Existing QoS checks 54 | 55 | #### Height Check (i.e. sync checks) 56 | 57 | **The general flow would be:** 58 | 59 | 1. Query all node operators height, 60 | 2. compares heights with other node operators within a specific threshold 61 | 3. filters out node operators that exceed the configurable block height tolerance. 62 | 63 | #### Data Integrity Check (quorum checks) 64 | 65 | The general flow would be: 66 | 67 | 1. Retrieve a unique block identifier (i.e block hash or total block tx count, etc) with a configurable block offset for randomness, 68 | 2. Query other node operators for the same block identifier 69 | 3. Filter out other node operators that return a different identifier. 70 | 71 | Some existing implementations of Checks can be found in: 72 | 73 | 1. [evm_data_integrity_check.go](../internal/node_selector_service/checks/evm_data_integrity_check/evm_data_integrity_check.go) 74 | 2. [evm_height_check.go](../internal/node_selector_service/checks/evm_height_check/evm_height_check.go) 75 | 3. [pokt_height_check.go](../internal/node_selector_service/checks/pokt_height_check/pokt_height_check.go) 76 | 4. [pokt_data_integrity_check.go](../internal/node_selector_service/checks/pokt_data_integrity_check/pokt_data_integrity_check.go) 77 | 5. [solana_height_check.go](../internal/node_selector_service/checks/solana_height_check/solana_height_check.go) 78 | 6. [solana_data_integrity_check.go](../internal/node_selector_service/checks/solana_data_integrity_check/solana_data_integrity_check.go) 79 | 80 | ### Adding custom QoS checks 81 | 82 | Every custom check must conform to the `CheckJob` interface. The gateway server provides a base check: 83 | 84 | ```go 85 | type Check struct { 86 | NodeList []*qos_models.QosNode 87 | PocketRelayer pokt_v0.PocketRelayer 88 | ChainConfiguration chain_configurations_registry.ChainConfigurationsService 89 | } 90 | ``` 91 | 92 | that developers should inherit. This base check provides a list of nodes to check and a `PocketRelayer` that allows the developer to send requests to the nodes in the network, and `ChainConfiguration` service that allows for per-chain specific check configurations. 93 | 94 | Checks are designed to be opinionated and there are numerous ways to implement whether a node is healthy or not by definition. Therefore, implementing custom QoS checks will be dependent on the chain or data source the developer is looking to support. For example, the developer may want to send a request to a custom blockchain node with a custom JSON-RPC method to see if the node is synced by using the provided `PocketRelayer` to send a request to the node through Pocket network. 95 | If the node is not synced, the developer can set a custom punishment through the various functions exposed in [qos_node.go](../internal/node_selector_service/models/qos_node.go), such as `SetTimeoutUntil` to punish the node. 96 | 97 | Once the developer is finished implementing the CheckJob, they can enable the QoS check by initializing the newly created check into the `enabledChecks` variable inside [node_selector_service.go](../internal/node_selector_service/node_selector_service.go) and are encouraged to open up a PR for inclusion in the official repository. 98 | 99 | ## Future Improvements 100 | 101 | - Long term persistent results 102 | - Pros: More data to work with on determining if a node is healthy 103 | - Cons: Expensive, more complex logic (due to geographic regions) and can be punishing to new node operators 104 | - Rolling up the results for long term storage & historical look back 105 | -------------------------------------------------------------------------------- /docs/overview.md: -------------------------------------------------------------------------------- 1 | # POKT Gateway Server Overview 2 | 3 | The Gateway server's goal is to reduce the complexity associated with directly interfacing with the protocol with an library that any developer can contribute to. The Gateway server kickstarts off as a light-weight process that enables developers of all kinds to be able to interact with the protocol and engage with 50+ blockchains without the need to store terabytes of data, require heavy computational power, or understand the POKT protocol specifications with a simple docker-compose file. The rhetorical question that we pose to future actors who want to maintain a blockchain node is: Why spin up an Ethereum node and maintain it yourself whenever you can just leverage POKT natively using the Gateway server? After all, using POKT would require a fraction of the required resources and technical staffing. 4 | 5 | ## Features 6 | 7 | - Simple docker-compose file with minimal dependencies to spin up 8 | - a single tenancy HTTP endpoint for each blockchain that POKT supports, abstracting POKT's Relay Specification. This endpoint's throughput should scale based on the number of app stakes the developer provides. 9 | - QoS checks to allow for optimized latency and success rates 10 | - Provides Prometheus metrics for success, error rates, and latency for sending a relay to the network 11 | - Custom Pocket client and web server that allows for efficient computational resources and memory footprint 12 | - FastHTTP for optimized webserver and client 13 | - FastJSON for efficient JSON Deserialization 14 | - Custom Integration leveraging the two for efficient resource management 15 | - Functionality improvement such as allowing for proper decoding of POKT Node error messages such as max evidence sealed errors. 16 | 17 | ## What's not included in the Gateway Server 18 | 19 | - Authentication 20 | - Rate Limiting & Multi-tenancy endpoints 21 | - SaaS-based UX 22 | - Reverse Proxy / Load Balancing Mechanisms 23 | - Any other Opinionated SaaS-like design decision. 24 | 25 | The decision to exclude certain features such as Authentication, Rate Limiting and multi-tenancy endpoints, SaaS-based UX, and Reverse Proxy/Load Balancing Mechanisms is rooted in the project's philosophy. These aspects are often regarded as opinionated web2 functionalities, and there are already numerous resources available on how to build SaaS products with various authentication mechanisms, rate-limiting strategies, and user experience design patterns. 26 | 27 | The Gateway server aims to simplify the POKT protocol, not reinventing the wheel. Each Gateway, being a distinct entity with its unique requirements and team dynamics, is better suited to decide on these aspects independently. For instance, the choice of authentication mechanisms can vary widely between teams, ranging from widely-used services like Auth0 and Amazon Cognito to in-house authentication solutions tailored to the specific language and skill set of the development team. 28 | 29 | By not including these opinionated web2 functionalities, the Gateway server acknowledges the diversity of preferences and needs among developers and businesses. This approach allows teams to integrate their preferred solutions seamlessly, fostering flexibility and ensuring that the Gateway server remains lightweight and adaptable to a wide range of use cases. 30 | 31 | As the project evolves, we anticipate that individual Gateways will incorporate their implementations of these features based on their unique requirements and preferences. This decentralized approach empowers developers to make decisions that align with their specific use cases, promoting a more customized and efficient integration with the Gateway server. 32 | 33 | ## Future 34 | 35 | We envision that the server will be used as a foundation for the entirety of the ecosystem to continue to build on top of such as: 36 | 37 | - Building their frontends and extending their backend to include POKT by using the gateway server for their own SaaS business 38 | - Create Demo SaaS gateways that use the gateway server as the underlying foundation. 39 | - Using POKT as a hyper scaler whenever they need more computational power or access to more blockchains (sticking the process into their LB rotation) 40 | - Using POKT as a backend as a failover whenever their centralized nodes go down (sticking the process into their LB rotation) 41 | 42 | Over time, as more gateways enter the network, there will be re-occurring patterns on what is needed on the foundational level and developers can create RFPs to have them included. For example, while rate limiting and multi-tenancy endpoints feel too opinionated right now, there is a future where we can create a service that distributes these endpoints natively in the GW server. The use cases are limitless and we expect that over time, community contributions into the gateway server will enable some of the aforementioned use cases natively. 43 | -------------------------------------------------------------------------------- /docs/performance-optimizations.md: -------------------------------------------------------------------------------- 1 | # Performance Optimizations 2 | 3 | 1. [FastHTTP](https://github.com/valyala/fasthttp) for both HTTP Client/Server 4 | 2. [FastJSON](https://github.com/pquerna/ffjson) for performant JSON Serialization and Deserialization 5 | 3. Lightweight Pocket Client 6 | 7 | ## Pocket Client Optimizations 8 | 9 | We have implemented our own lightweight Pocket client to enhance speed and efficiency. 10 | 11 | Leveraging the power of [FastHTTP](https://github.com/valyala/fasthttp) and [FastJSON](https://github.com/pquerna/ffjson), our custom client achieves remarkable performance gains. 12 | 13 | Additionally, it has the capability to properly parse node runner's POKT errors properly given that the network runs diverse POKT clients (geomesh, leanpokt, their own custom client). 14 | 15 | ### Why It's More Efficient/Faster 16 | 17 | 1. **FastHTTP:** This library is designed for high-performance scenarios, providing a faster alternative to standard HTTP clients. Its concurrency-focused design allows our Pocket client to handle multiple requests concurrently, improving overall responsiveness. 18 | 2. **FastJSON:** The use of FastJSON ensures swift and efficient JSON serialization and deserialization. This directly contributes to reduced processing times, making our Pocket client an excellent choice for high-scale web traffic. 19 | -------------------------------------------------------------------------------- /docs/pokt-primer.md: -------------------------------------------------------------------------------- 1 | # POKT Primer 2 | 3 | - [POKT Network: A Quick Overview](#pokt-network-a-quick-overview) 4 | - [The Challenge: Interacting with the Protocol](#the-challenge-interacting-with-the-protocol) 5 | - [The Solution: Gateway Operators](#the-solution-gateway-operators) 6 | - [Conclusion](#conclusion) 7 | - [Footnotes](#footnotes) 8 | 9 | ## POKT Network: A Quick Overview 10 | 11 | 1. **Apps (App developers)**: Individuals or entities that stake into the Pocket Network and obtain access to external blockchain nodes in return. 12 | 2. **Node Runners**: Individuals or entities that stake into the network and provide access to external blockchain nodes, such as Ethereum & Polygon in return for $POKT. 13 | 14 | ### The Challenge: Interacting with the Protocol 15 | 16 | For application developers, directly engaging with the POKT Network can be intimidating due to its inherent complexities. The technical barriers and protocol nuances can dissuade many from integrating and adopting the network. 17 | 18 | **Challenges faced by App Developers using the protocol:** 19 | 20 | 1. **Managing Throughput**: The network supports approximately `250 app stakes` and around `10B relays`. With each app stake being limited to roughly `20M requests per hour`, developers who surpass this need to stake multiple applications and balance the load among these stakes. 21 | 2. **Determining Quality of Service (QoS)**: The network doesn't currently enforce QoS standards. Apps are assigned a set of pseudo-randomly selected node runners, rotated over specified intervals, also known as sessions. It falls on the application developer to implement strategies, such as filtering and predictive analysis, to select node runners that align with their criteria for reliability, availability, and data integrity. 22 | 3. **Protocol Interaction**: Unlike the straightforward procedure of sending requests to an HTTP JSON-RPC server, interacting with the POKT Network requires far more complexities given its blockchain nature. (i.e. signing a request for a relay proof) 23 | 24 | ### The Solution: Gateway Operators 25 | 26 | Gateway Operators act as a conduit between app developers and the POKT Network, streamlining the process by abstracting the network's complexities. Their operations on a high level can be seen as: 27 | 28 | 1. **Managing Throughput**: By staking and load-balancing app stakes, Gateway Operators ensure the required throughput for smooth network interactions. 29 | 2. **Determining the Quality of Service**: Gateway Operators filter malicious, out-of-sync, offline, or under-performing nodes. 30 | 3. **Protocol Interaction**: Gateway Operators offer a seamless HTTP JSON-RPC Server interface, making it simpler for developers to send and receive requests, akin to interactions with conventional servers. Under the hood, the web server will contain the necessary business logic in order to interact with the protocol. 31 | 32 | ### Conclusion 33 | 34 | Engaging with the POKT Network's capabilities doesn't have to be an uphill task. Thanks to Gateway Operators, app developers can concentrate on their core competencies—developing remarkable applications using a familiar HTTP interface, like traditional RPC providers—all while reaping the benefits of a decentralized RPC platform. 35 | 36 | --- 37 | 38 | ## Footnotes 39 | 40 | 1. _As of 9/14/2023, the app stakes are permissioned and overseen by the Pocket Network Foundation for security considerations._ 41 | 2. _The amount of POKT staked into an app doesn't carry significant implications as all gateway operators are charged a fee for every request sent through an app stake._ 42 | 3. _Historically, Grove (formerly known as Pocket Network Inc.) has been the sole gateway operator. This will change by 2024 Q2 as more gateway operators join the network._ 43 | 4. _Our research aims to invite more gateway operators to join the network in a sustainable fashion by documenting the protocol specifications and limitations and leveraging and providing open-source software and noncloud vendor-lock-in services._ 44 | -------------------------------------------------------------------------------- /docs/resources/gateway-server-architecture.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pokt-network/gateway-server/9f5e8be1165ba6966e29e936a68674052ddf8cf5/docs/resources/gateway-server-architecture.png -------------------------------------------------------------------------------- /docs/resources/gateway-server-logo.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pokt-network/gateway-server/9f5e8be1165ba6966e29e936a68674052ddf8cf5/docs/resources/gateway-server-logo.jpg -------------------------------------------------------------------------------- /docs/resources/gateway-server-node-selection-system.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pokt-network/gateway-server/9f5e8be1165ba6966e29e936a68674052ddf8cf5/docs/resources/gateway-server-node-selection-system.png -------------------------------------------------------------------------------- /docs/resources/gateway-server-session-cache.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pokt-network/gateway-server/9f5e8be1165ba6966e29e936a68674052ddf8cf5/docs/resources/gateway-server-session-cache.png -------------------------------------------------------------------------------- /docs/system-architecture.md: -------------------------------------------------------------------------------- 1 | # POKT Gateway Server Architecture 2 | 3 | ![gateway-server-architecture.png](resources/gateway-server-architecture.png) 4 | 5 | - [Gateway Server Responsibilities](#gateway-server-responsibilities) 6 | - [Primary Features](#primary-features) 7 | - [Secondary Features](#secondary-features) 8 | - [Gateway Operator Responsibilities](#gateway-operator-responsibilities) 9 | 10 | ## Gateway Server Responsibilities 11 | 12 | ### Primary Features 13 | 14 | Under the hood, the gateway server handles everything in regard to protocol interaction to abstract away the complexity of: 15 | 16 | 1. Retrieving a session 17 | 2. Signing a relay 18 | 3. Sending a relay to a node operator & receiving a response 19 | 20 | ### Secondary Features 21 | 22 | 1. **Node Selection & Routing (QoS)** - determining which nodes are healthy based off responses 23 | 2. **Metrics** - Provides underlying Prometheus metrics endpoint for relay performance metadata 24 | 3. **HTTP Interface** - Providing an efficient HTTP endpoint to send requests to 25 | 26 | ## Gateway Operator Responsibilities 27 | 28 | 1. **Key management** - Keeping the encryption key and respectively the app stakes keys secure. 29 | 2. **App stake management** - Staking in the approriate chains 30 | 3. **SaaS business support** - Any features in regard to a SaaS business as mentioned in the [overview](overview.md). 31 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/pokt-network/gateway-server 2 | 3 | go 1.21.4 4 | 5 | require ( 6 | github.com/fasthttp/router v1.4.22 7 | github.com/flf2ko/fasthttp-prometheus v0.1.0 8 | github.com/golang-migrate/migrate/v4 v4.17.0 9 | github.com/jackc/pgconn v1.14.0 10 | github.com/jackc/pgtype v1.14.0 11 | github.com/jackc/pgx/v4 v4.18.1 12 | github.com/jellydator/ttlcache/v3 v3.1.0 13 | github.com/joho/godotenv v1.5.1 14 | github.com/pkg/errors v0.9.1 15 | github.com/pquerna/ffjson v0.0.0-20190930134022-aa0246cd15f7 16 | github.com/prometheus/client_golang v1.15.0 17 | github.com/stretchr/testify v1.8.4 18 | github.com/valyala/fasthttp v1.51.0 19 | go.uber.org/zap v1.26.0 20 | golang.org/x/crypto v0.17.0 21 | gonum.org/v1/gonum v0.15.0 22 | ) 23 | 24 | require ( 25 | github.com/andybalholm/brotli v1.0.6 // indirect 26 | github.com/beorn7/perks v1.0.1 // indirect 27 | github.com/cespare/xxhash/v2 v2.2.0 // indirect 28 | github.com/davecgh/go-spew v1.1.1 // indirect 29 | github.com/golang/protobuf v1.5.3 // indirect 30 | github.com/hashicorp/errwrap v1.1.0 // indirect 31 | github.com/hashicorp/go-multierror v1.1.1 // indirect 32 | github.com/influxdata/tdigest v0.0.1 // indirect 33 | github.com/jackc/chunkreader/v2 v2.0.1 // indirect 34 | github.com/jackc/pgio v1.0.0 // indirect 35 | github.com/jackc/pgpassfile v1.0.0 // indirect 36 | github.com/jackc/pgproto3/v2 v2.3.2 // indirect 37 | github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a // indirect 38 | github.com/jackc/puddle v1.3.0 // indirect 39 | github.com/klauspost/compress v1.17.3 // indirect 40 | github.com/lib/pq v1.10.9 // indirect 41 | github.com/matttproud/golang_protobuf_extensions v1.0.4 // indirect 42 | github.com/pmezard/go-difflib v1.0.0 // indirect 43 | github.com/prometheus/client_model v0.3.0 // indirect 44 | github.com/prometheus/common v0.42.0 // indirect 45 | github.com/prometheus/procfs v0.9.0 // indirect 46 | github.com/rogpeppe/go-internal v1.12.0 // indirect 47 | github.com/savsgio/gotils v0.0.0-20230208104028-c358bd845dee // indirect 48 | github.com/stretchr/objx v0.5.1 // indirect 49 | github.com/valyala/bytebufferpool v1.0.0 // indirect 50 | go.uber.org/atomic v1.7.0 // indirect 51 | go.uber.org/multierr v1.11.0 // indirect 52 | golang.org/x/sync v0.5.0 // indirect 53 | golang.org/x/sys v0.15.0 // indirect 54 | golang.org/x/text v0.14.0 // indirect 55 | google.golang.org/protobuf v1.31.0 // indirect 56 | gopkg.in/yaml.v3 v3.0.1 // indirect 57 | ) 58 | -------------------------------------------------------------------------------- /internal/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pokt-network/gateway-server/9f5e8be1165ba6966e29e936a68674052ddf8cf5/internal/.gitkeep -------------------------------------------------------------------------------- /internal/apps_registry/app_registry_service.go: -------------------------------------------------------------------------------- 1 | package apps_registry 2 | 3 | import "github.com/pokt-network/gateway-server/internal/apps_registry/models" 4 | 5 | type AppsRegistryService interface { 6 | GetApplications() []*models.PoktApplicationSigner 7 | GetApplicationsByChainId(chainId string) ([]*models.PoktApplicationSigner, bool) 8 | GetApplicationByPublicKey(publicKey string) (*models.PoktApplicationSigner, bool) 9 | } 10 | -------------------------------------------------------------------------------- /internal/apps_registry/cached_app_registry_test.go: -------------------------------------------------------------------------------- 1 | package apps_registry 2 | 3 | import ( 4 | "github.com/pokt-network/gateway-server/internal/apps_registry/models" 5 | pokt_models "github.com/pokt-network/gateway-server/pkg/pokt/pokt_v0/models" 6 | "testing" 7 | ) 8 | 9 | func Test_arePoktApplicationSignersEqual(t *testing.T) { 10 | type args struct { 11 | slice1 []*models.PoktApplicationSigner 12 | slice2 []*models.PoktApplicationSigner 13 | } 14 | tests := []struct { 15 | name string 16 | args args 17 | want bool 18 | }{ 19 | { 20 | name: "different length", 21 | args: args{ 22 | slice1: []*models.PoktApplicationSigner{}, 23 | slice2: []*models.PoktApplicationSigner{{NetworkApp: &pokt_models.PoktApplication{ 24 | Address: "123", 25 | Chains: []string{"123", "123"}, 26 | PublicKey: "", 27 | Status: 0, 28 | MaxRelays: 0, 29 | }}}, 30 | }, 31 | want: false, 32 | }, 33 | { 34 | name: "different address", 35 | args: args{ 36 | slice1: []*models.PoktApplicationSigner{{NetworkApp: &pokt_models.PoktApplication{ 37 | Address: "1234", 38 | Chains: []string{"123", "123"}, 39 | PublicKey: "", 40 | Status: 0, 41 | MaxRelays: 0, 42 | }}}, 43 | slice2: []*models.PoktApplicationSigner{{NetworkApp: &pokt_models.PoktApplication{ 44 | Address: "123", 45 | Chains: []string{"123", "123"}, 46 | PublicKey: "", 47 | Status: 0, 48 | MaxRelays: 0, 49 | }}}, 50 | }, 51 | want: false, 52 | }, 53 | { 54 | name: "different chains", 55 | args: args{ 56 | slice1: []*models.PoktApplicationSigner{{NetworkApp: &pokt_models.PoktApplication{ 57 | Address: "1234", 58 | Chains: []string{"123", "123"}, 59 | PublicKey: "", 60 | Status: 0, 61 | MaxRelays: 0, 62 | }}}, 63 | slice2: []*models.PoktApplicationSigner{{NetworkApp: &pokt_models.PoktApplication{ 64 | Address: "123", 65 | Chains: []string{"123"}, 66 | PublicKey: "", 67 | Status: 0, 68 | MaxRelays: 0, 69 | }}}, 70 | }, 71 | want: false, 72 | }, 73 | { 74 | name: "same apps with exact chains", 75 | args: args{ 76 | slice1: []*models.PoktApplicationSigner{{NetworkApp: &pokt_models.PoktApplication{ 77 | Address: "123", 78 | Chains: []string{"123", "123"}, 79 | PublicKey: "", 80 | Status: 0, 81 | MaxRelays: 0, 82 | }}}, 83 | slice2: []*models.PoktApplicationSigner{{NetworkApp: &pokt_models.PoktApplication{ 84 | Address: "123", 85 | Chains: []string{"123", "123"}, 86 | PublicKey: "", 87 | Status: 0, 88 | MaxRelays: 0, 89 | }}}, 90 | }, 91 | want: true, 92 | }, 93 | { 94 | name: "same apps with same chains different ordering", 95 | args: args{ 96 | slice1: []*models.PoktApplicationSigner{{NetworkApp: &pokt_models.PoktApplication{ 97 | Address: "123", 98 | Chains: []string{"123", "1234", "1235"}, 99 | PublicKey: "", 100 | Status: 0, 101 | MaxRelays: 0, 102 | }}}, 103 | slice2: []*models.PoktApplicationSigner{{NetworkApp: &pokt_models.PoktApplication{ 104 | Address: "123", 105 | Chains: []string{"123", "1235", "1234"}, 106 | PublicKey: "", 107 | Status: 0, 108 | MaxRelays: 0, 109 | }}}, 110 | }, 111 | want: true, 112 | }, 113 | } 114 | for _, tt := range tests { 115 | t.Run(tt.name, func(t *testing.T) { 116 | if got := arePoktApplicationSignersEqual(tt.args.slice1, tt.args.slice2); got != tt.want { 117 | t.Errorf("arePoktApplicationSignersEqual() = %v, want %v", got, tt.want) 118 | } 119 | }) 120 | } 121 | } 122 | -------------------------------------------------------------------------------- /internal/apps_registry/models/application.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | import ( 4 | "github.com/pokt-network/gateway-server/pkg/pokt/pokt_v0/models" 5 | ) 6 | 7 | type PoktApplicationSigner struct { 8 | Signer *models.Ed25519Account 9 | NetworkApp *models.PoktApplication 10 | ID string 11 | } 12 | 13 | func NewPoktApplicationSigner(id string, account *models.Ed25519Account) *PoktApplicationSigner { 14 | return &PoktApplicationSigner{Signer: account, ID: id} 15 | } 16 | -------------------------------------------------------------------------------- /internal/chain_configurations_registry/cached_chain_configurations_registry.go: -------------------------------------------------------------------------------- 1 | package chain_configurations_registry 2 | 3 | import ( 4 | "context" 5 | "github.com/pokt-network/gateway-server/internal/db_query" 6 | "go.uber.org/zap" 7 | "sync" 8 | "time" 9 | ) 10 | 11 | const ( 12 | chainConfigurationUpdateInterval = time.Minute * 1 13 | ) 14 | 15 | type CachedChainConfigurationRegistry struct { 16 | dbQuery db_query.Querier 17 | chainConfigurationCache map[string]db_query.GetChainConfigurationsRow // chain id > url 18 | cacheLock sync.RWMutex 19 | logger *zap.Logger 20 | } 21 | 22 | func NewCachedChainConfigurationRegistry(dbQuery db_query.Querier, logger *zap.Logger) *CachedChainConfigurationRegistry { 23 | chainConfigurationRegistry := &CachedChainConfigurationRegistry{dbQuery: dbQuery, chainConfigurationCache: map[string]db_query.GetChainConfigurationsRow{}, logger: logger} 24 | err := chainConfigurationRegistry.updateChainConfigurations() 25 | if err != nil { 26 | chainConfigurationRegistry.logger.Sugar().Warnw("Failed to retrieve chain global_config on startup", "err", err) 27 | } 28 | chainConfigurationRegistry.startCacheUpdater() 29 | return chainConfigurationRegistry 30 | } 31 | 32 | func (r *CachedChainConfigurationRegistry) GetChainConfiguration(chainId string) (db_query.GetChainConfigurationsRow, bool) { 33 | r.cacheLock.RLock() 34 | defer r.cacheLock.RUnlock() 35 | url, found := r.chainConfigurationCache[chainId] 36 | return url, found 37 | } 38 | 39 | func (r *CachedChainConfigurationRegistry) updateChainConfigurations() error { 40 | chainConfigurations, err := r.dbQuery.GetChainConfigurations(context.Background()) 41 | 42 | if err != nil { 43 | return err 44 | } 45 | 46 | chainConfigurationNew := map[string]db_query.GetChainConfigurationsRow{} 47 | for _, row := range chainConfigurations { 48 | chainConfigurationNew[row.ChainID.String] = row 49 | } 50 | 51 | // Update the cache 52 | r.cacheLock.Lock() 53 | defer r.cacheLock.Unlock() 54 | r.chainConfigurationCache = chainConfigurationNew 55 | return nil 56 | } 57 | 58 | // StartCacheUpdater starts a goroutine to periodically update the altruist cache. 59 | func (c *CachedChainConfigurationRegistry) startCacheUpdater() { 60 | ticker := time.Tick(chainConfigurationUpdateInterval) 61 | go func() { 62 | for { 63 | select { 64 | case <-ticker: 65 | // Call the updateChainConfigurations method 66 | err := c.updateChainConfigurations() 67 | if err != nil { 68 | c.logger.Sugar().Warnw("failed to update chain configuration registry", "err", err) 69 | } else { 70 | c.logger.Sugar().Infow("successfully updated chain configuration registry", "chainConfigurationLength", len(c.chainConfigurationCache)) 71 | } 72 | } 73 | } 74 | }() 75 | } 76 | -------------------------------------------------------------------------------- /internal/chain_configurations_registry/chain_configurations_registry_service.go: -------------------------------------------------------------------------------- 1 | package chain_configurations_registry 2 | 3 | import "github.com/pokt-network/gateway-server/internal/db_query" 4 | 5 | type ChainConfigurationsService interface { 6 | GetChainConfiguration(chainId string) (db_query.GetChainConfigurationsRow, bool) 7 | } 8 | -------------------------------------------------------------------------------- /internal/chain_network/chain_network.go: -------------------------------------------------------------------------------- 1 | package chain_network 2 | 3 | type ChainNetwork string 4 | 5 | const ( 6 | MorseMainnet ChainNetwork = "morse_mainnet" 7 | MorseTestnet ChainNetwork = "morse_testnet" 8 | ) 9 | -------------------------------------------------------------------------------- /internal/db_query/db.go: -------------------------------------------------------------------------------- 1 | package db_query 2 | 3 | import ( 4 | "context" 5 | "database/sql" 6 | "fmt" 7 | "github.com/golang-migrate/migrate/v4" 8 | "github.com/golang-migrate/migrate/v4/database/postgres" 9 | "github.com/golang-migrate/migrate/v4/source/iofs" 10 | "github.com/jackc/pgx/v4/pgxpool" 11 | "github.com/pkg/errors" 12 | root "github.com/pokt-network/gateway-server" 13 | "github.com/pokt-network/gateway-server/internal/global_config" 14 | "go.uber.org/zap" 15 | ) 16 | 17 | // InitDB - runs DB migrations and provides a code-generated query interface 18 | func InitDB(logger *zap.Logger, config global_config.DBCredentialsProvider, maxConnections uint) (Querier, *pgxpool.Pool, error) { 19 | 20 | // initialize database 21 | sqldb, err := sql.Open("postgres", config.GetDatabaseConnectionUrl()) 22 | if err != nil { 23 | return nil, nil, errors.WithMessage(err, "failed to init db") 24 | } 25 | 26 | postgresDriver, err := postgres.WithInstance(sqldb, &postgres.Config{}) 27 | if err != nil { 28 | return nil, nil, errors.WithMessage(err, "failed to init postgres driver") 29 | } 30 | 31 | source, err := iofs.New(root.Migrations, "db_migrations") 32 | 33 | if err != nil { 34 | return nil, nil, errors.WithMessage(err, "failed to create migration fs") 35 | } 36 | 37 | // Automatic Migrations 38 | m, err := migrate.NewWithInstance("iofs", source, "postgres", postgresDriver) 39 | 40 | if err != nil { 41 | return nil, nil, errors.WithMessage(err, "failed to migrate") 42 | } 43 | 44 | err = m.Up() 45 | if err != nil && err != migrate.ErrNoChange { 46 | logger.Sugar().Warn("Migration warning", "err", err) 47 | return nil, nil, err 48 | } 49 | 50 | // DB only needs to be open for migration 51 | err = postgresDriver.Close() 52 | if err != nil { 53 | return nil, nil, err 54 | } 55 | err = sqldb.Close() 56 | if err != nil { 57 | return nil, nil, err 58 | } 59 | 60 | // open up connection pool for actual sql queries 61 | pool, err := pgxpool.Connect(context.Background(), fmt.Sprintf("%s&pool_max_conns=%d", config.GetDatabaseConnectionUrl(), maxConnections)) 62 | if err != nil { 63 | return nil, pool, err 64 | } 65 | return NewQuerier(pool), nil, nil 66 | 67 | } 68 | -------------------------------------------------------------------------------- /internal/db_query/queries.sql: -------------------------------------------------------------------------------- 1 | -- name: GetPoktApplications :many 2 | SELECT id, pgp_sym_decrypt(encrypted_private_key, pggen.arg('encryption_key')) AS decrypted_private_key 3 | FROM pokt_applications; 4 | 5 | -- name: InsertPoktApplications :exec 6 | INSERT INTO pokt_applications (encrypted_private_key) 7 | VALUES (pgp_sym_encrypt(pggen.arg('private_key'), pggen.arg('encryption_key'))); 8 | 9 | -- name: DeletePoktApplication :exec 10 | DELETE FROM pokt_applications 11 | WHERE id = pggen.arg('application_id'); 12 | 13 | -- name: GetChainConfigurations :many 14 | SELECT * FROM chain_configurations; -------------------------------------------------------------------------------- /internal/global_config/config_provider.go: -------------------------------------------------------------------------------- 1 | package global_config 2 | 3 | import ( 4 | "github.com/pokt-network/gateway-server/internal/chain_network" 5 | "time" 6 | ) 7 | 8 | type EnvironmentStage string 9 | 10 | const ( 11 | StageProduction EnvironmentStage = "production" 12 | ) 13 | 14 | type GlobalConfigProvider interface { 15 | SecretProvider 16 | DBCredentialsProvider 17 | EnvironmentProvider 18 | PoktNodeConfigProvider 19 | AltruistConfigProvider 20 | PromMetricsProvider 21 | ChainNetworkProvider 22 | } 23 | 24 | type PromMetricsProvider interface { 25 | ShouldEmitServiceUrlPromMetrics() bool 26 | } 27 | 28 | type SecretProvider interface { 29 | GetPoktApplicationsEncryptionKey() string 30 | GetAPIKey() string 31 | } 32 | 33 | type DBCredentialsProvider interface { 34 | GetDatabaseConnectionUrl() string 35 | } 36 | 37 | type EnvironmentProvider interface { 38 | GetEnvironmentStage() EnvironmentStage 39 | } 40 | 41 | type PoktNodeConfigProvider interface { 42 | GetPoktRPCFullHost() string 43 | GetPoktRPCRequestTimeout() time.Duration 44 | } 45 | 46 | type AltruistConfigProvider interface { 47 | GetAltruistRequestTimeout() time.Duration 48 | } 49 | 50 | type ChainNetworkProvider interface { 51 | GetChainNetwork() chain_network.ChainNetwork 52 | } 53 | -------------------------------------------------------------------------------- /internal/logging/logger.go: -------------------------------------------------------------------------------- 1 | package logging 2 | 3 | import ( 4 | "github.com/pokt-network/gateway-server/internal/global_config" 5 | "go.uber.org/zap" 6 | ) 7 | 8 | func NewLogger(provider global_config.EnvironmentProvider) (*zap.Logger, error) { 9 | if provider.GetEnvironmentStage() == global_config.StageProduction { 10 | return zap.NewProduction() 11 | } 12 | return zap.NewDevelopment() 13 | } 14 | -------------------------------------------------------------------------------- /internal/node_selector_service/checks/async_relay_handler.go: -------------------------------------------------------------------------------- 1 | package checks 2 | 3 | import ( 4 | "github.com/pokt-network/gateway-server/internal/node_selector_service/models" 5 | "github.com/pokt-network/gateway-server/pkg/pokt/pokt_v0" 6 | relayer_models "github.com/pokt-network/gateway-server/pkg/pokt/pokt_v0/models" 7 | "sync" 8 | ) 9 | 10 | type nodeRelayResponse struct { 11 | Node *models.QosNode 12 | Relay *relayer_models.SendRelayResponse 13 | Error error 14 | } 15 | 16 | func SendRelaysAsync(relayer pokt_v0.PocketRelayer, nodes []*models.QosNode, payload string, method string, path string) chan *nodeRelayResponse { 17 | // Define a channel to receive relay responses 18 | relayResponses := make(chan *nodeRelayResponse, len(nodes)) 19 | var wg sync.WaitGroup 20 | 21 | // Define a function to handle sending relay requests concurrently 22 | sendRelayAsync := func(node *models.QosNode) { 23 | defer wg.Done() 24 | relay, err := relayer.SendRelay(&relayer_models.SendRelayRequest{ 25 | Signer: node.GetAppStakeSigner(), 26 | Payload: &relayer_models.Payload{Data: payload, Method: method, Path: path}, 27 | Chain: node.GetChain(), 28 | SelectedNodePubKey: node.GetPublicKey(), 29 | Session: node.MorseSession, 30 | }) 31 | relayResponses <- &nodeRelayResponse{ 32 | Node: node, 33 | Relay: relay, 34 | Error: err, 35 | } 36 | } 37 | 38 | // Start a goroutine for each node to send relay requests concurrently 39 | for _, node := range nodes { 40 | wg.Add(1) 41 | go sendRelayAsync(node) 42 | } 43 | 44 | wg.Wait() 45 | close(relayResponses) 46 | 47 | return relayResponses 48 | } 49 | -------------------------------------------------------------------------------- /internal/node_selector_service/checks/chain_config_handler.go: -------------------------------------------------------------------------------- 1 | package checks 2 | 3 | import "github.com/pokt-network/gateway-server/internal/chain_configurations_registry" 4 | 5 | // GetBlockHeightTolerance - helper function to retrieve block height tolerance across checks 6 | func GetBlockHeightTolerance(chainConfiguration chain_configurations_registry.ChainConfigurationsService, chainId string, defaultValue int) int { 7 | chainConfig, ok := chainConfiguration.GetChainConfiguration(chainId) 8 | if !ok { 9 | return defaultValue 10 | } 11 | return int(*chainConfig.HeightCheckBlockTolerance) 12 | } 13 | 14 | // GetDataIntegrityHeightLookback - helper function ro retrieve data integrity lookback across checks 15 | func GetDataIntegrityHeightLookback(chainConfiguration chain_configurations_registry.ChainConfigurationsService, chainId string, defaultValue int) int { 16 | chainConfig, ok := chainConfiguration.GetChainConfiguration(chainId) 17 | if !ok { 18 | return defaultValue 19 | } 20 | return int(*chainConfig.DataIntegrityCheckLookbackHeight) 21 | } 22 | -------------------------------------------------------------------------------- /internal/node_selector_service/checks/data_integrity_handler.go: -------------------------------------------------------------------------------- 1 | package checks 2 | 3 | import ( 4 | "fmt" 5 | "github.com/pokt-network/gateway-server/internal/node_selector_service/models" 6 | "github.com/pokt-network/gateway-server/pkg/common" 7 | "github.com/valyala/fasthttp" 8 | "go.uber.org/zap" 9 | "time" 10 | ) 11 | 12 | const ( 13 | // how often to check a node's data integrity 14 | dataIntegrityNodeCheckInterval = time.Minute * 10 15 | 16 | // penalty whenever a pocket node doesn't match other node providers responses 17 | dataIntegrityTimePenalty = time.Minute * 15 18 | 19 | // the look back we will use to determine which block number to do a data integrity against (latestBlockHeight - lookBack) 20 | dataIntegrityHeightLookbackDefault = 25 21 | ) 22 | 23 | type nodeHashRspPair struct { 24 | node *models.QosNode 25 | blockIdentifier string 26 | } 27 | 28 | type BlockHashParser func(response string) (string, error) 29 | 30 | type GetBlockByNumberPayloadFmter func(blockToFind uint64) string 31 | 32 | // PerformDataIntegrityCheck: is the default implementation of a data integrity check by: 33 | func PerformDataIntegrityCheck(check *Check, calculatePayload GetBlockByNumberPayloadFmter, path string, retrieveBlockIdentifier BlockHashParser, logger *zap.Logger) { 34 | // Find a node that has been reported as healthy to use as source of truth 35 | sourceOfTruth := findRandomHealthyNode(check.NodeList) 36 | 37 | // Node that is synced cannot be found, so we cannot run data integrity checks since we need a trusted source 38 | if sourceOfTruth == nil { 39 | logger.Sugar().Warnw("cannot find source of truth for data integrity check", "chain", check.NodeList[0].GetChain()) 40 | return 41 | } 42 | 43 | logger.Sugar().Infow("running default data integrity check", "chain", check.NodeList[0].GetChain()) 44 | 45 | // Map to count number of nodes that return blockHash -> counter 46 | nodeResponseCounts := make(map[string]int) 47 | 48 | var nodeResponsePairs []*nodeHashRspPair 49 | 50 | // find a random block to search that nodes should have access too 51 | blockNumberToSearch := sourceOfTruth.GetLastKnownHeight() - uint64(GetDataIntegrityHeightLookback(check.ChainConfiguration, sourceOfTruth.GetChain(), dataIntegrityHeightLookbackDefault)) 52 | 53 | attestationResponses := SendRelaysAsync(check.PocketRelayer, getEligibleDataIntegrityCheckNodes(check.NodeList), calculatePayload(blockNumberToSearch), "POST", path) 54 | for rsp := range attestationResponses { 55 | 56 | if rsp.Error != nil { 57 | DefaultPunishNode(rsp.Error, rsp.Node, logger) 58 | continue 59 | } 60 | 61 | blockIdentifier, err := retrieveBlockIdentifier(rsp.Relay.Response) 62 | if err != nil { 63 | logger.Sugar().Warnw("failed to unmarshal response", "err", err) 64 | DefaultPunishNode(fasthttp.ErrTimeout, rsp.Node, logger) 65 | continue 66 | } 67 | 68 | rsp.Node.SetLastDataIntegrityCheckTime(time.Now()) 69 | nodeResponsePairs = append(nodeResponsePairs, &nodeHashRspPair{ 70 | node: rsp.Node, 71 | blockIdentifier: blockIdentifier, 72 | }) 73 | nodeResponseCounts[blockIdentifier]++ 74 | } 75 | 76 | majorityBlockIdentifier := findMajorityBlockIdentifier(nodeResponseCounts) 77 | 78 | // Blcok blockIdentifier must not be empty 79 | if majorityBlockIdentifier == "" { 80 | return 81 | } 82 | 83 | // Penalize other node operators with a timeout if they don't attest with same block blockIdentifier. 84 | for _, nodeResp := range nodeResponsePairs { 85 | if nodeResp.blockIdentifier != majorityBlockIdentifier { 86 | logger.Sugar().Errorw("punishing node for failed data integrity check", "node", nodeResp.node.MorseNode.ServiceUrl, "nodeBlockHash", nodeResp.blockIdentifier, "trustedSourceBlockHash", majorityBlockIdentifier) 87 | nodeResp.node.SetTimeoutUntil(time.Now().Add(dataIntegrityTimePenalty), models.DataIntegrityTimeout, fmt.Errorf("nodeBlockHash %s, trustedSourceBlockHash %s", nodeResp.blockIdentifier, majorityBlockIdentifier)) 88 | } 89 | } 90 | 91 | } 92 | 93 | // findRandomHealthyNode - returns a healthy node that is synced so we can use it as a source of truth for data integrity checks 94 | func findRandomHealthyNode(nodes []*models.QosNode) *models.QosNode { 95 | var healthyNodes []*models.QosNode 96 | for _, node := range nodes { 97 | if node.IsHealthy() { 98 | healthyNodes = append(healthyNodes, node) 99 | } 100 | } 101 | healthyNode, ok := common.GetRandomElement(healthyNodes) 102 | if !ok { 103 | return nil 104 | } 105 | return healthyNode 106 | } 107 | 108 | func getEligibleDataIntegrityCheckNodes(nodes []*models.QosNode) []*models.QosNode { 109 | // Filter nodes based on last checked time 110 | var eligibleNodes []*models.QosNode 111 | for _, node := range nodes { 112 | if (node.GetLastDataIntegrityCheckTime().IsZero() || time.Since(node.GetLastDataIntegrityCheckTime()) >= dataIntegrityNodeCheckInterval) && node.IsHealthy() { 113 | eligibleNodes = append(eligibleNodes, node) 114 | } 115 | } 116 | return eligibleNodes 117 | } 118 | 119 | // findMajorityBlockIdentifier finds the blockIdentifier with the highest response count 120 | func findMajorityBlockIdentifier(responseCounts map[string]int) string { 121 | var highestResponseIdentifier string 122 | var highestResponseCount int 123 | for rsp, count := range responseCounts { 124 | if count > highestResponseCount { 125 | highestResponseIdentifier = rsp 126 | highestResponseCount = count 127 | } 128 | } 129 | return highestResponseIdentifier 130 | } 131 | -------------------------------------------------------------------------------- /internal/node_selector_service/checks/error_handler.go: -------------------------------------------------------------------------------- 1 | package checks 2 | 3 | import ( 4 | "github.com/pokt-network/gateway-server/internal/node_selector_service/models" 5 | relayer_models "github.com/pokt-network/gateway-server/pkg/pokt/pokt_v0/models" 6 | "github.com/valyala/fasthttp" 7 | "go.uber.org/zap" 8 | "strings" 9 | "time" 10 | ) 11 | 12 | // default timeout penalty whenever a node doesn't respond 13 | const timeoutErrorPenalty = time.Second * 15 14 | 15 | // 24 hours is analogous to indefinite 16 | const kickOutSessionPenalty = time.Hour * 24 17 | 18 | var ( 19 | errsKickSession = []string{"failed to find correct servicer PK", "the max number of relays serviced for this node is exceeded", "the evidence is sealed, either max relays reached or claim already submitted"} 20 | errsTimeout = []string{"connection refused", "the request block height is out of sync with the current block height", "no route to host", "unexpected EOF", "i/o timeout", "tls: failed to verify certificate", "no such host", "the block height passed is invalid", "request timeout", "error executing the http request"} 21 | ) 22 | 23 | func doesErrorContains(errsSubString []string, err error) bool { 24 | if err == nil { 25 | return false 26 | } 27 | errStr := err.Error() 28 | for _, errSubString := range errsSubString { 29 | if strings.Contains(errStr, errSubString) { 30 | return true 31 | } 32 | } 33 | return false 34 | } 35 | 36 | // isKickableSessionErr - determines if a node should be kicked from a session to send relays 37 | func isKickableSessionErr(err error) bool { 38 | // If evidence is sealed or the node has already overserviced, the node should no longer receive relays. 39 | if err == relayer_models.ErrPocketEvidenceSealed || err == relayer_models.ErrPocketCoreOverService { 40 | return true 41 | } 42 | // Fallback in the event the error is not parsed correctly due to node operator configurations / custom clients, resort to a simple string check 43 | // node runner cannot serve with expired ssl 44 | return doesErrorContains(errsKickSession, err) 45 | } 46 | 47 | func isTimeoutError(err error) bool { 48 | // If Invalid block height, pocket is not caught up to latest session 49 | if err == relayer_models.ErrPocketCoreInvalidBlockHeight { 50 | return true 51 | } 52 | 53 | // Check if pocket error returns 500 54 | pocketError, ok := err.(relayer_models.PocketRPCError) 55 | if ok && pocketError.HttpCode >= 500 { 56 | return true 57 | } 58 | // Fallback in the event the error is not parsed correctly due to node operator configurations / custom clients, resort to a simple string check 59 | return err == fasthttp.ErrConnectionClosed || err == fasthttp.ErrTimeout || err == fasthttp.ErrDialTimeout || err == fasthttp.ErrTLSHandshakeTimeout || doesErrorContains(errsTimeout, err) 60 | } 61 | 62 | // DefaultPunishNode generic punisher for whenever a node returns an error independent of a specific check 63 | func DefaultPunishNode(err error, node *models.QosNode, logger *zap.Logger) bool { 64 | if isKickableSessionErr(err) { 65 | node.SetTimeoutUntil(time.Now().Add(kickOutSessionPenalty), models.MaximumRelaysTimeout, err) 66 | return true 67 | } 68 | if isTimeoutError(err) { 69 | node.SetTimeoutUntil(time.Now().Add(timeoutErrorPenalty), models.NodeResponseTimeout, err) 70 | return true 71 | } 72 | logger.Sugar().Warnw("uncategorized error detected from pocket node", "node", node.MorseNode.ServiceUrl, "err", err) 73 | return false 74 | } 75 | -------------------------------------------------------------------------------- /internal/node_selector_service/checks/evm_data_integrity_check/evm_data_integrity_check.go: -------------------------------------------------------------------------------- 1 | package evm_data_integrity_check 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "github.com/pokt-network/gateway-server/internal/node_selector_service/checks" 7 | "github.com/pokt-network/gateway-server/internal/node_selector_service/models" 8 | "go.uber.org/zap" 9 | "strconv" 10 | "time" 11 | ) 12 | 13 | const ( 14 | // how often the job should run 15 | dataIntegrityCheckInterval = time.Second * 1 16 | 17 | //json rpc payload to send a data integrity check 18 | blockPayloadFmt = `{"jsonrpc":"2.0","method":"eth_getBlockByNumber","params":["%s", false],"id":1}` 19 | ) 20 | 21 | type blockByNumberResponse struct { 22 | Result struct { 23 | Hash string `json:"hash"` 24 | } `json:"result"` 25 | } 26 | 27 | type EvmDataIntegrityCheck struct { 28 | *checks.Check 29 | nextCheckTime time.Time 30 | logger *zap.Logger 31 | } 32 | 33 | func (c *EvmDataIntegrityCheck) getBlockHashFromNodeResponse(response string) (string, error) { 34 | var evmRsp blockByNumberResponse 35 | err := json.Unmarshal([]byte(response), &evmRsp) 36 | if err != nil { 37 | return "", err 38 | } 39 | return evmRsp.Result.Hash, nil 40 | } 41 | 42 | func NewEvmDataIntegrityCheck(check *checks.Check, logger *zap.Logger) *EvmDataIntegrityCheck { 43 | return &EvmDataIntegrityCheck{Check: check, nextCheckTime: time.Time{}, logger: logger} 44 | } 45 | 46 | func (c *EvmDataIntegrityCheck) Name() string { 47 | return "evm_data_integrity_check" 48 | } 49 | 50 | func (c *EvmDataIntegrityCheck) SetNodes(nodes []*models.QosNode) { 51 | c.NodeList = nodes 52 | } 53 | 54 | func (c *EvmDataIntegrityCheck) Perform() { 55 | 56 | // Session is not meant for EVM 57 | if len(c.NodeList) == 0 || !c.IsEvmChain(c.NodeList[0]) { 58 | return 59 | } 60 | checks.PerformDataIntegrityCheck(c.Check, getBlockByNumberPayload, "", c.getBlockHashFromNodeResponse, c.logger) 61 | c.nextCheckTime = time.Now().Add(dataIntegrityCheckInterval) 62 | } 63 | 64 | func (c *EvmDataIntegrityCheck) ShouldRun() bool { 65 | return c.nextCheckTime.IsZero() || time.Now().After(c.nextCheckTime) 66 | } 67 | 68 | func getBlockByNumberPayload(blockNumber uint64) string { 69 | return fmt.Sprintf(blockPayloadFmt, "0x"+strconv.FormatInt(int64(blockNumber), 16)) 70 | } 71 | -------------------------------------------------------------------------------- /internal/node_selector_service/checks/evm_height_check/evm_height_check.go: -------------------------------------------------------------------------------- 1 | package evm_height_check 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "github.com/pokt-network/gateway-server/internal/node_selector_service/checks" 7 | "github.com/pokt-network/gateway-server/internal/node_selector_service/models" 8 | "github.com/pquerna/ffjson/ffjson" 9 | "go.uber.org/zap" 10 | "strconv" 11 | "strings" 12 | "time" 13 | ) 14 | 15 | const ( 16 | 17 | // interval to run the evm height check 18 | evmHeightCheckInterval = time.Second * 1 19 | 20 | // jsonrpc payload to retrieve evm height 21 | heightJsonPayload = `{"jsonrpc":"2.0","method":"eth_blockNumber","params": [],"id":1}` 22 | ) 23 | 24 | type evmHeightResponse struct { 25 | Height uint64 `json:"blockByNumberResponse"` 26 | } 27 | 28 | func (r *evmHeightResponse) UnmarshalJSON(data []byte) error { 29 | 30 | type evmHeightResponseStr struct { 31 | Result string `json:"result"` 32 | } 33 | 34 | // Unmarshal the JSON into the custom type 35 | var hr evmHeightResponseStr 36 | if err := ffjson.Unmarshal(data, &hr); err != nil { 37 | return err 38 | } 39 | 40 | // Remove the "0x" prefix if present 41 | heightStr := strings.TrimPrefix(hr.Result, "0x") 42 | 43 | // Parse the hexadecimal string to uint64 44 | height, err := strconv.ParseUint(heightStr, 16, 64) 45 | if err != nil { 46 | return fmt.Errorf("failed to parse height: %v", err) 47 | } 48 | // Assign the parsed height to the struct field 49 | r.Height = height 50 | return nil 51 | } 52 | 53 | type EvmHeightCheck struct { 54 | *checks.Check 55 | nextCheckTime time.Time 56 | logger *zap.Logger 57 | } 58 | 59 | func NewEvmHeightCheck(check *checks.Check, logger *zap.Logger) *EvmHeightCheck { 60 | return &EvmHeightCheck{Check: check, nextCheckTime: time.Time{}, logger: logger} 61 | } 62 | 63 | func (c *EvmHeightCheck) Name() string { 64 | return "evm_height_check" 65 | } 66 | 67 | func (c *EvmHeightCheck) Perform() { 68 | 69 | // Session is not meant for EVM 70 | if len(c.NodeList) == 0 || !c.IsEvmChain(c.NodeList[0]) { 71 | return 72 | } 73 | checks.PerformDefaultHeightCheck(c.Check, heightJsonPayload, "", c.getHeightFromNodeResponse, c.logger) 74 | c.nextCheckTime = time.Now().Add(evmHeightCheckInterval) 75 | } 76 | 77 | func (c *EvmHeightCheck) SetNodes(nodes []*models.QosNode) { 78 | c.NodeList = nodes 79 | } 80 | 81 | func (c *EvmHeightCheck) ShouldRun() bool { 82 | return time.Now().After(c.nextCheckTime) 83 | } 84 | 85 | func (c *EvmHeightCheck) getHeightFromNodeResponse(response string) (uint64, error) { 86 | var evmRsp evmHeightResponse 87 | err := json.Unmarshal([]byte(response), &evmRsp) 88 | if err != nil { 89 | return 0, err 90 | } 91 | return evmRsp.Height, nil 92 | } 93 | -------------------------------------------------------------------------------- /internal/node_selector_service/checks/height_check_handler.go: -------------------------------------------------------------------------------- 1 | package checks 2 | 3 | import ( 4 | "fmt" 5 | "github.com/pokt-network/gateway-server/internal/node_selector_service/models" 6 | "github.com/valyala/fasthttp" 7 | "go.uber.org/zap" 8 | "gonum.org/v1/gonum/stat" 9 | "math" 10 | "time" 11 | ) 12 | 13 | const ( 14 | defaultNodeHeightCheckInterval = time.Minute * 5 15 | defaultZScoreHeightThreshold = 3 16 | defaultHeightTolerance int = 100 17 | defaultCheckPenalty = time.Minute * 5 18 | ) 19 | 20 | type HeightJsonParser func(response string) (uint64, error) 21 | 22 | // PerformDefaultHeightCheck is the default implementation of a height check by: 23 | // 0. Filtering out nodes that have not been checked since defaultNodeHeightCheckInterval 24 | // 1. Sending height request via payload to all the nodes 25 | // 2. Punishing all nodes that return an error 26 | // 3. Filtering out nodes that are returning a height out of the zScore threshold 27 | // 4. Punishing the nodes with defaultCheckPenalty that exceed the height tolerance. 28 | func PerformDefaultHeightCheck(check *Check, payload string, path string, parseHeight HeightJsonParser, logger *zap.Logger) { 29 | 30 | logger.Sugar().Infow("running default height check", "chain", check.NodeList[0].GetChain()) 31 | 32 | var nodesResponded []*models.QosNode 33 | // Send request to all nodes 34 | relayResponses := SendRelaysAsync(check.PocketRelayer, getEligibleHeightCheckNodes(check.NodeList), payload, "POST", path) 35 | 36 | // Process relay responses 37 | for resp := range relayResponses { 38 | err := resp.Error 39 | if err != nil { 40 | DefaultPunishNode(err, resp.Node, logger) 41 | continue 42 | } 43 | 44 | height, err := parseHeight(resp.Relay.Response) 45 | if err != nil { 46 | logger.Sugar().Warnw("failed to unmarshal response", "err", err) 47 | // Treat an invalid response as a timeout error 48 | DefaultPunishNode(fasthttp.ErrTimeout, resp.Node, logger) 49 | continue 50 | } 51 | 52 | resp.Node.SetLastHeightCheckTime(time.Now()) 53 | resp.Node.SetLastKnownHeight(height) 54 | nodesResponded = append(nodesResponded, resp.Node) 55 | } 56 | 57 | highestNodeHeight := getHighestNodeHeight(nodesResponded, defaultZScoreHeightThreshold) 58 | // Compare each node's reported height against the highest reported height 59 | for _, node := range nodesResponded { 60 | heightDifference := int(highestNodeHeight - node.GetLastKnownHeight()) 61 | // Penalize nodes whose reported height is significantly lower than the highest reported height 62 | if heightDifference > GetBlockHeightTolerance(check.ChainConfiguration, node.GetChain(), defaultHeightTolerance) { 63 | logger.Sugar().Infow("node is out of sync", "node", node.MorseNode.ServiceUrl, "heightDifference", heightDifference, "nodeSyncedHeight", node.GetLastKnownHeight(), "highestNodeHeight", highestNodeHeight, "chain", node.GetChain()) 64 | // Punish Node specifically due to timeout. 65 | node.SetSynced(false) 66 | node.SetTimeoutUntil(time.Now().Add(defaultCheckPenalty), models.OutOfSyncTimeout, fmt.Errorf("heightDifference: %d, nodeSyncedHeight: %d, highestNodeHeight: %d", heightDifference, node.GetLastKnownHeight(), highestNodeHeight)) 67 | } else { 68 | node.SetSynced(true) 69 | } 70 | } 71 | } 72 | 73 | // getHighestHeight returns the highest height reported from a slice of nodes 74 | // by using z-score threshhold to prevent any misconfigured or malicious node 75 | func getHighestNodeHeight(nodes []*models.QosNode, zScoreHeightThreshhold float64) uint64 { 76 | 77 | var nodeHeights []float64 78 | for _, node := range nodes { 79 | nodeHeights = append(nodeHeights, float64(node.GetLastKnownHeight())) 80 | } 81 | 82 | // Calculate mean and standard deviation 83 | meanValue := stat.Mean(nodeHeights, nil) 84 | stdDevValue := stat.StdDev(nodeHeights, nil) 85 | 86 | var highestNodeHeight float64 87 | for _, nodeHeight := range nodeHeights { 88 | 89 | zScore := stat.StdScore(nodeHeight, meanValue, stdDevValue) 90 | 91 | // height is an outlier according to zScore threshold 92 | if math.Abs(zScore) > zScoreHeightThreshhold { 93 | continue 94 | } 95 | // Height is higher than last recorded height 96 | if nodeHeight > highestNodeHeight { 97 | highestNodeHeight = nodeHeight 98 | } 99 | } 100 | return uint64(highestNodeHeight) 101 | } 102 | 103 | func getEligibleHeightCheckNodes(nodes []*models.QosNode) []*models.QosNode { 104 | // Filter nodes based on last checked time 105 | var eligibleNodes []*models.QosNode 106 | for _, node := range nodes { 107 | if node.GetLastHeightCheckTime().IsZero() || time.Since(node.GetLastHeightCheckTime()) >= defaultNodeHeightCheckInterval { 108 | eligibleNodes = append(eligibleNodes, node) 109 | } 110 | } 111 | return eligibleNodes 112 | } 113 | -------------------------------------------------------------------------------- /internal/node_selector_service/checks/pokt_data_integrity_check/pokt_data_integrity_check.go: -------------------------------------------------------------------------------- 1 | package pokt_data_integrity_check 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "github.com/pokt-network/gateway-server/internal/node_selector_service/checks" 7 | "github.com/pokt-network/gateway-server/internal/node_selector_service/models" 8 | "go.uber.org/zap" 9 | "time" 10 | ) 11 | 12 | const poktBlockTxEndpoint = "/v1/query/blocktxs" 13 | 14 | const ( 15 | // how often the job should run 16 | dataIntegrityCheckInterval = time.Second * 1 17 | 18 | //json rpc payload to send a data integrity check 19 | blockPayloadFmt = `{"height": %d}` 20 | ) 21 | 22 | type blockTxResponse struct { 23 | TotalTxs int `json:"total_txs"` 24 | } 25 | 26 | type PoktDataIntegrityCheck struct { 27 | *checks.Check 28 | nextCheckTime time.Time 29 | logger *zap.Logger 30 | } 31 | 32 | // getBlockIdentifierFromNodeResponse: We use total txs as the block identifier because retrieving block hash from POKT RPC can lead up to 33 | // 8MB+ payloads per node operator response, whereas blocktxs is only ~110kb 34 | func (c *PoktDataIntegrityCheck) getBlockIdentifierFromNodeResponse(response string) (string, error) { 35 | var blockTxRsp blockTxResponse 36 | err := json.Unmarshal([]byte(response), &blockTxRsp) 37 | if err != nil { 38 | return "", err 39 | } 40 | return fmt.Sprintf("%d", blockTxRsp.TotalTxs), nil 41 | } 42 | 43 | func NewPoktDataIntegrityCheck(check *checks.Check, logger *zap.Logger) *PoktDataIntegrityCheck { 44 | return &PoktDataIntegrityCheck{Check: check, nextCheckTime: time.Time{}, logger: logger} 45 | } 46 | 47 | func (c *PoktDataIntegrityCheck) Name() string { 48 | return "pokt_data_integrity_check" 49 | } 50 | 51 | func (c *PoktDataIntegrityCheck) SetNodes(nodes []*models.QosNode) { 52 | c.NodeList = nodes 53 | } 54 | 55 | func (c *PoktDataIntegrityCheck) Perform() { 56 | 57 | // Session is not meant for POKT 58 | if len(c.NodeList) == 0 || !c.IsPoktChain(c.NodeList[0]) { 59 | return 60 | } 61 | checks.PerformDataIntegrityCheck(c.Check, getBlockByNumberPayload, poktBlockTxEndpoint, c.getBlockIdentifierFromNodeResponse, c.logger) 62 | c.nextCheckTime = time.Now().Add(dataIntegrityCheckInterval) 63 | } 64 | 65 | func (c *PoktDataIntegrityCheck) ShouldRun() bool { 66 | return c.nextCheckTime.IsZero() || time.Now().After(c.nextCheckTime) 67 | } 68 | 69 | func getBlockByNumberPayload(blockNumber uint64) string { 70 | return fmt.Sprintf(blockPayloadFmt, blockNumber) 71 | } 72 | -------------------------------------------------------------------------------- /internal/node_selector_service/checks/pokt_height_check/pokt_height_check.go: -------------------------------------------------------------------------------- 1 | package pokt_height_check 2 | 3 | import ( 4 | "encoding/json" 5 | "github.com/pokt-network/gateway-server/internal/node_selector_service/checks" 6 | "github.com/pokt-network/gateway-server/internal/node_selector_service/models" 7 | "go.uber.org/zap" 8 | "time" 9 | ) 10 | 11 | const ( 12 | 13 | // interval to run the pokt height check job 14 | poktHeightCheckInterval = time.Second * 1 15 | 16 | // jsonrpc payload to pokt evm height 17 | heightJsonPayload = `` 18 | ) 19 | 20 | type poktHeightResponse struct { 21 | Height uint64 `json:"height"` 22 | } 23 | 24 | type PoktHeightCheck struct { 25 | *checks.Check 26 | nextCheckTime time.Time 27 | logger *zap.Logger 28 | } 29 | 30 | func NewPoktHeightCheck(check *checks.Check, logger *zap.Logger) *PoktHeightCheck { 31 | return &PoktHeightCheck{Check: check, nextCheckTime: time.Time{}, logger: logger} 32 | } 33 | 34 | func (c *PoktHeightCheck) Name() string { 35 | return "pokt_height_check" 36 | } 37 | 38 | func (c *PoktHeightCheck) Perform() { 39 | 40 | // Session is not meant for POKT 41 | if len(c.NodeList) == 0 || !c.IsPoktChain(c.NodeList[0]) { 42 | return 43 | } 44 | checks.PerformDefaultHeightCheck(c.Check, heightJsonPayload, "/v1/query/height", c.getHeightFromNodeResponse, c.logger) 45 | c.nextCheckTime = time.Now().Add(poktHeightCheckInterval) 46 | } 47 | 48 | func (c *PoktHeightCheck) SetNodes(nodes []*models.QosNode) { 49 | c.NodeList = nodes 50 | } 51 | 52 | func (c *PoktHeightCheck) ShouldRun() bool { 53 | return time.Now().After(c.nextCheckTime) 54 | } 55 | 56 | func (c *PoktHeightCheck) getHeightFromNodeResponse(response string) (uint64, error) { 57 | var poktRsp poktHeightResponse 58 | err := json.Unmarshal([]byte(response), &poktRsp) 59 | if err != nil { 60 | return 0, err 61 | } 62 | return poktRsp.Height, nil 63 | } 64 | -------------------------------------------------------------------------------- /internal/node_selector_service/checks/qos_check.go: -------------------------------------------------------------------------------- 1 | package checks 2 | 3 | import ( 4 | "github.com/pokt-network/gateway-server/internal/chain_configurations_registry" 5 | "github.com/pokt-network/gateway-server/internal/chain_network" 6 | config2 "github.com/pokt-network/gateway-server/internal/global_config" 7 | qos_models "github.com/pokt-network/gateway-server/internal/node_selector_service/models" 8 | "github.com/pokt-network/gateway-server/pkg/pokt/pokt_v0" 9 | ) 10 | 11 | const ( 12 | chainMorseMainnetSolanaCustom = "C006" 13 | chainMorseMainnetPokt = "0001" 14 | chainMorseMainnetSolana = "0006" 15 | ) 16 | 17 | const ( 18 | chainMorseTestnetPokt = "0013" 19 | chainMorseTestnetSolana = "0008" 20 | ) 21 | 22 | type CheckJob interface { 23 | Perform() 24 | Name() string 25 | ShouldRun() bool 26 | SetNodes(nodes []*qos_models.QosNode) 27 | } 28 | 29 | type Check struct { 30 | NodeList []*qos_models.QosNode 31 | PocketRelayer pokt_v0.PocketRelayer 32 | ChainConfiguration chain_configurations_registry.ChainConfigurationsService 33 | ChainNetworkProvider config2.ChainNetworkProvider 34 | } 35 | 36 | func NewCheck(pocketRelayer pokt_v0.PocketRelayer, chainConfiguration chain_configurations_registry.ChainConfigurationsService, chainNetworkProvider config2.ChainNetworkProvider) *Check { 37 | return &Check{PocketRelayer: pocketRelayer, ChainConfiguration: chainConfiguration, ChainNetworkProvider: chainNetworkProvider} 38 | } 39 | 40 | func (c *Check) IsSolanaChain(node *qos_models.QosNode) bool { 41 | chainId := node.GetChain() 42 | if c.ChainNetworkProvider.GetChainNetwork() == chain_network.MorseTestnet { 43 | return chainId == chainMorseTestnetSolana 44 | } 45 | return chainId == chainMorseMainnetSolana || chainId == chainMorseMainnetSolanaCustom 46 | } 47 | 48 | func (c *Check) IsPoktChain(node *qos_models.QosNode) bool { 49 | chainId := node.GetChain() 50 | if c.ChainNetworkProvider.GetChainNetwork() == chain_network.MorseTestnet { 51 | return chainId == chainMorseTestnetPokt 52 | } 53 | return chainId == chainMorseMainnetPokt 54 | } 55 | 56 | func (c *Check) IsEvmChain(node *qos_models.QosNode) bool { 57 | return !c.IsPoktChain(node) && !c.IsSolanaChain(node) 58 | } 59 | -------------------------------------------------------------------------------- /internal/node_selector_service/checks/solana_data_integrity_check/solana_data_integrity_check.go: -------------------------------------------------------------------------------- 1 | package solana_data_integrity_check 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "github.com/pokt-network/gateway-server/internal/node_selector_service/checks" 7 | "github.com/pokt-network/gateway-server/internal/node_selector_service/models" 8 | "go.uber.org/zap" 9 | "time" 10 | ) 11 | 12 | const ( 13 | // how often the job should run 14 | dataIntegrityCheckInterval = time.Second * 1 15 | 16 | //json rpc payload to send a data integrity check 17 | // we use signatures for transaction detail to prevent large payloads and we don't need anything but block hash 18 | blockPayloadFmt = `{"jsonrpc":"2.0","method":"getBlock","params":[%d, {"encoding": "jsonParsed", "maxSupportedTransactionVersion":0, "transactionDetails":"signatures"}],"id":1}` 19 | ) 20 | 21 | type blockByNumberResponse struct { 22 | Result struct { 23 | Hash string `json:"blockhash"` 24 | } `json:"result"` 25 | } 26 | 27 | type SolanaDataIntegrityCheck struct { 28 | *checks.Check 29 | nextCheckTime time.Time 30 | logger *zap.Logger 31 | } 32 | 33 | func (c *SolanaDataIntegrityCheck) getBlockIdentifierFromNodeResponse(response string) (string, error) { 34 | var blockRsp blockByNumberResponse 35 | err := json.Unmarshal([]byte(response), &blockRsp) 36 | if err != nil { 37 | return "", err 38 | } 39 | return blockRsp.Result.Hash, nil 40 | } 41 | 42 | func NewSolanaDataIntegrityCheck(check *checks.Check, logger *zap.Logger) *SolanaDataIntegrityCheck { 43 | return &SolanaDataIntegrityCheck{Check: check, nextCheckTime: time.Time{}, logger: logger} 44 | } 45 | 46 | func (c *SolanaDataIntegrityCheck) Name() string { 47 | return "solana_data_integrity_check" 48 | } 49 | 50 | func (c *SolanaDataIntegrityCheck) SetNodes(nodes []*models.QosNode) { 51 | c.NodeList = nodes 52 | } 53 | 54 | func (c *SolanaDataIntegrityCheck) Perform() { 55 | 56 | // Session is not meant for Solana 57 | if len(c.NodeList) == 0 || !c.IsSolanaChain(c.NodeList[0]) { 58 | return 59 | } 60 | checks.PerformDataIntegrityCheck(c.Check, getBlockByNumberPayload, "", c.getBlockIdentifierFromNodeResponse, c.logger) 61 | c.nextCheckTime = time.Now().Add(dataIntegrityCheckInterval) 62 | } 63 | 64 | func (c *SolanaDataIntegrityCheck) ShouldRun() bool { 65 | return c.nextCheckTime.IsZero() || time.Now().After(c.nextCheckTime) 66 | } 67 | 68 | func getBlockByNumberPayload(blockNumber uint64) string { 69 | return fmt.Sprintf(blockPayloadFmt, blockNumber) 70 | } 71 | -------------------------------------------------------------------------------- /internal/node_selector_service/checks/solana_height_check/solana_height_check.go: -------------------------------------------------------------------------------- 1 | package solana_height_check 2 | 3 | import ( 4 | "encoding/json" 5 | "github.com/pokt-network/gateway-server/internal/node_selector_service/checks" 6 | "github.com/pokt-network/gateway-server/internal/node_selector_service/models" 7 | "go.uber.org/zap" 8 | "time" 9 | ) 10 | 11 | const ( 12 | 13 | // interval to run the solana height check 14 | solanaHeightCheckInterval = time.Second * 1 15 | 16 | // jsonrpc payload to retrieve solana height 17 | heightJsonPayload = `{"jsonrpc":"2.0","method":"getSlot","params": [],"id":1}` 18 | ) 19 | 20 | type solanaHeightResponse struct { 21 | Result uint64 `json:"result"` 22 | } 23 | 24 | type SolanaHeightCheck struct { 25 | *checks.Check 26 | nextCheckTime time.Time 27 | logger *zap.Logger 28 | } 29 | 30 | func NewSolanaHeightCheck(check *checks.Check, logger *zap.Logger) *SolanaHeightCheck { 31 | return &SolanaHeightCheck{Check: check, nextCheckTime: time.Time{}, logger: logger} 32 | } 33 | 34 | func (c *SolanaHeightCheck) Name() string { 35 | return "solana_height_check" 36 | } 37 | 38 | func (c *SolanaHeightCheck) Perform() { 39 | 40 | // Session is not meant for Solana 41 | if len(c.NodeList) == 0 || !c.IsSolanaChain(c.NodeList[0]) { 42 | return 43 | } 44 | checks.PerformDefaultHeightCheck(c.Check, heightJsonPayload, "", c.getHeightFromNodeResponse, c.logger) 45 | c.nextCheckTime = time.Now().Add(solanaHeightCheckInterval) 46 | } 47 | 48 | func (c *SolanaHeightCheck) SetNodes(nodes []*models.QosNode) { 49 | c.NodeList = nodes 50 | } 51 | 52 | func (c *SolanaHeightCheck) ShouldRun() bool { 53 | return time.Now().After(c.nextCheckTime) 54 | } 55 | 56 | func (c *SolanaHeightCheck) getHeightFromNodeResponse(response string) (uint64, error) { 57 | var solanaRsp solanaHeightResponse 58 | err := json.Unmarshal([]byte(response), &solanaRsp) 59 | if err != nil { 60 | return 0, err 61 | } 62 | return solanaRsp.Result, nil 63 | } 64 | -------------------------------------------------------------------------------- /internal/node_selector_service/models/qos_node.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | import ( 4 | "github.com/influxdata/tdigest" 5 | "github.com/pokt-network/gateway-server/pkg/pokt/pokt_v0/models" 6 | "sync" 7 | "time" 8 | ) 9 | 10 | type TimeoutReason string 11 | 12 | const ( 13 | maxErrorStr int = 100 14 | // we use TDigest to quickly calculate percentile while conserving memory by using TDigest and its compression properties. 15 | // Higher compression is more accuracy 16 | latencyCompression = 1000 17 | ) 18 | 19 | const ( 20 | OutOfSyncTimeout TimeoutReason = "out_of_sync_timeout" 21 | DataIntegrityTimeout TimeoutReason = "invalid_data_timeout" 22 | MaximumRelaysTimeout TimeoutReason = "maximum_relays_timeout" 23 | NodeResponseTimeout TimeoutReason = "node_response_timeout" 24 | ) 25 | 26 | type LatencyTracker struct { 27 | tDigest *tdigest.TDigest 28 | lock sync.RWMutex 29 | } 30 | 31 | func (l *LatencyTracker) RecordMeasurement(time float64) { 32 | l.lock.Lock() 33 | defer l.lock.Unlock() 34 | l.tDigest.Add(time, 1) 35 | } 36 | 37 | func (l *LatencyTracker) GetMeasurementCount() float64 { 38 | l.lock.RLock() 39 | defer l.lock.RUnlock() 40 | return l.tDigest.Count() 41 | } 42 | 43 | func (l *LatencyTracker) GetP90Latency() float64 { 44 | return l.tDigest.Quantile(.90) 45 | } 46 | 47 | type SessionChainKey struct { 48 | SessionHeight uint `json:"session_height"` 49 | Chain string `json:"chain"` 50 | } 51 | 52 | // QosNode a FAT model to store the QoS information of a specific node in a session. 53 | type QosNode struct { 54 | MorseNode *models.Node 55 | MorseSession *models.Session 56 | MorseSigner *models.Ed25519Account 57 | LatencyTracker *LatencyTracker 58 | timeoutUntil time.Time 59 | timeoutReason TimeoutReason 60 | lastDataIntegrityCheckTime time.Time 61 | latestKnownHeight uint64 62 | synced bool 63 | lastKnownError error 64 | lastHeightCheckTime time.Time 65 | } 66 | 67 | func NewQosNode(morseNode *models.Node, pocketSession *models.Session, appSigner *models.Ed25519Account) *QosNode { 68 | return &QosNode{MorseNode: morseNode, MorseSession: pocketSession, MorseSigner: appSigner, LatencyTracker: &LatencyTracker{tDigest: tdigest.NewWithCompression(latencyCompression)}} 69 | } 70 | 71 | func (n *QosNode) IsHealthy() bool { 72 | return !n.IsInTimeout() && n.IsSynced() 73 | } 74 | 75 | func (n *QosNode) IsSynced() bool { 76 | return n.synced 77 | } 78 | 79 | func (n *QosNode) SetSynced(synced bool) { 80 | n.synced = synced 81 | } 82 | 83 | func (n *QosNode) IsInTimeout() bool { 84 | return !n.timeoutUntil.IsZero() && time.Now().Before(n.timeoutUntil) 85 | } 86 | 87 | func (n *QosNode) GetLastHeightCheckTime() time.Time { 88 | return n.lastHeightCheckTime 89 | } 90 | 91 | func (n *QosNode) SetTimeoutUntil(time time.Time, reason TimeoutReason, attachedErr error) { 92 | n.timeoutReason = reason 93 | n.timeoutUntil = time 94 | n.lastKnownError = attachedErr 95 | } 96 | 97 | func (n *QosNode) SetLastKnownHeight(lastKnownHeight uint64) { 98 | n.latestKnownHeight = lastKnownHeight 99 | } 100 | 101 | func (n *QosNode) SetLastHeightCheckTime(time time.Time) { 102 | n.lastHeightCheckTime = time 103 | } 104 | 105 | func (n *QosNode) GetLastKnownHeight() uint64 { 106 | return n.latestKnownHeight 107 | } 108 | 109 | func (n *QosNode) GetChain() string { 110 | return n.MorseSession.SessionHeader.Chain 111 | } 112 | 113 | func (n *QosNode) GetPublicKey() string { 114 | return n.MorseNode.PublicKey 115 | } 116 | 117 | func (n *QosNode) GetAppStakeSigner() *models.Ed25519Account { 118 | return n.MorseSigner 119 | } 120 | 121 | func (n *QosNode) GetLastDataIntegrityCheckTime() time.Time { 122 | return n.lastDataIntegrityCheckTime 123 | } 124 | func (n *QosNode) SetLastDataIntegrityCheckTime(lastDataIntegrityCheckTime time.Time) { 125 | n.lastDataIntegrityCheckTime = lastDataIntegrityCheckTime 126 | } 127 | 128 | func (n *QosNode) GetTimeoutReason() TimeoutReason { 129 | return n.timeoutReason 130 | } 131 | 132 | func (n *QosNode) GetLastKnownErrorStr() string { 133 | if n.lastKnownError == nil { 134 | return "" 135 | } 136 | errStr := n.lastKnownError.Error() 137 | if len(errStr) > maxErrorStr { 138 | return errStr[:maxErrorStr] 139 | } 140 | return errStr 141 | } 142 | 143 | func (n *QosNode) GetTimeoutUntil() time.Time { 144 | return n.timeoutUntil 145 | } 146 | 147 | func (n *QosNode) GetLatencyTracker() *LatencyTracker { 148 | return n.LatencyTracker 149 | } 150 | -------------------------------------------------------------------------------- /internal/node_selector_service/node_selector_service.go: -------------------------------------------------------------------------------- 1 | package node_selector_service 2 | 3 | import ( 4 | "github.com/pokt-network/gateway-server/internal/chain_configurations_registry" 5 | "github.com/pokt-network/gateway-server/internal/global_config" 6 | "github.com/pokt-network/gateway-server/internal/node_selector_service/checks" 7 | "github.com/pokt-network/gateway-server/internal/node_selector_service/checks/evm_data_integrity_check" 8 | "github.com/pokt-network/gateway-server/internal/node_selector_service/checks/evm_height_check" 9 | "github.com/pokt-network/gateway-server/internal/node_selector_service/checks/pokt_data_integrity_check" 10 | "github.com/pokt-network/gateway-server/internal/node_selector_service/checks/pokt_height_check" 11 | "github.com/pokt-network/gateway-server/internal/node_selector_service/checks/solana_data_integrity_check" 12 | "github.com/pokt-network/gateway-server/internal/node_selector_service/checks/solana_height_check" 13 | "github.com/pokt-network/gateway-server/internal/node_selector_service/models" 14 | "github.com/pokt-network/gateway-server/internal/session_registry" 15 | "github.com/pokt-network/gateway-server/pkg/common" 16 | "github.com/pokt-network/gateway-server/pkg/pokt/pokt_v0" 17 | "go.uber.org/zap" 18 | "sort" 19 | "time" 20 | ) 21 | 22 | const ( 23 | jobCheckInterval = time.Second 24 | ) 25 | 26 | type NodeSelectorService interface { 27 | FindNode(chainId string) (*models.QosNode, bool) 28 | } 29 | 30 | type NodeSelectorClient struct { 31 | sessionRegistry session_registry.SessionRegistryService 32 | pocketRelayer pokt_v0.PocketRelayer 33 | logger *zap.Logger 34 | checkJobs []checks.CheckJob 35 | } 36 | 37 | func NewNodeSelectorService(sessionRegistry session_registry.SessionRegistryService, pocketRelayer pokt_v0.PocketRelayer, chainConfiguration chain_configurations_registry.ChainConfigurationsService, networkProvider global_config.ChainNetworkProvider, logger *zap.Logger) *NodeSelectorClient { 38 | 39 | // base checks will share same node list and pocket relayer 40 | baseCheck := checks.NewCheck(pocketRelayer, chainConfiguration, networkProvider) 41 | 42 | // enabled checks 43 | enabledChecks := []checks.CheckJob{ 44 | evm_height_check.NewEvmHeightCheck(baseCheck, logger.Named("evm_height_checker")), 45 | evm_data_integrity_check.NewEvmDataIntegrityCheck(baseCheck, logger.Named("evm_data_integrity_checker")), 46 | solana_height_check.NewSolanaHeightCheck(baseCheck, logger.Named("solana_height_check")), 47 | solana_data_integrity_check.NewSolanaDataIntegrityCheck(baseCheck, logger.Named("solana_data_integrity_check")), 48 | pokt_height_check.NewPoktHeightCheck(baseCheck, logger.Named("pokt_height_check")), 49 | pokt_data_integrity_check.NewPoktDataIntegrityCheck(baseCheck, logger.Named("pokt_data_integrity_check")), 50 | } 51 | selectorService := &NodeSelectorClient{ 52 | sessionRegistry: sessionRegistry, 53 | logger: logger, 54 | checkJobs: enabledChecks, 55 | } 56 | selectorService.startJobChecker() 57 | return selectorService 58 | } 59 | 60 | func (q NodeSelectorClient) FindNode(chainId string) (*models.QosNode, bool) { 61 | 62 | nodes := q.sessionRegistry.GetNodesByChain(chainId) 63 | if len(nodes) == 0 { 64 | return nil, false 65 | } 66 | 67 | // Filter nodes by health 68 | healthyNodes := filterByHealthyNodes(nodes) 69 | 70 | // Find a node that's closer to session height 71 | sortedSessionHeights, nodeMap := filterBySessionHeightNodes(healthyNodes) 72 | for _, sessionHeight := range sortedSessionHeights { 73 | node, ok := common.GetRandomElement(nodeMap[sessionHeight]) 74 | if ok { 75 | return node, true 76 | } 77 | } 78 | return nil, false 79 | } 80 | 81 | // filterBySessionHeightNodes - filter by session height descending. This allows node selector to send relays with 82 | // latest session height which nodes are more likely to serve vs session rollover relays. 83 | func filterBySessionHeightNodes(nodes []*models.QosNode) ([]uint, map[uint][]*models.QosNode) { 84 | nodesBySessionHeight := map[uint][]*models.QosNode{} 85 | 86 | // Create map to retrieve nodes by session height 87 | for _, r := range nodes { 88 | sessionHeight := r.MorseSession.SessionHeader.SessionHeight 89 | nodesBySessionHeight[sessionHeight] = append(nodesBySessionHeight[sessionHeight], r) 90 | } 91 | 92 | // Create slice to hold sorted session heights 93 | var sortedSessionHeights []uint 94 | for sessionHeight := range nodesBySessionHeight { 95 | sortedSessionHeights = append(sortedSessionHeights, sessionHeight) 96 | } 97 | 98 | // Sort the slice of session heights by descending order 99 | sort.Slice(sortedSessionHeights, func(i, j int) bool { 100 | return sortedSessionHeights[i] > sortedSessionHeights[j] 101 | }) 102 | 103 | return sortedSessionHeights, nodesBySessionHeight 104 | } 105 | 106 | func filterByHealthyNodes(nodes []*models.QosNode) []*models.QosNode { 107 | var healthyNodes []*models.QosNode 108 | 109 | for _, r := range nodes { 110 | if r.IsHealthy() { 111 | healthyNodes = append(healthyNodes, r) 112 | } 113 | } 114 | return healthyNodes 115 | } 116 | 117 | func (q NodeSelectorClient) startJobChecker() { 118 | ticker := time.Tick(jobCheckInterval) 119 | go func() { 120 | for { 121 | select { 122 | case <-ticker: 123 | for _, job := range q.checkJobs { 124 | if job.ShouldRun() { 125 | for _, nodes := range q.sessionRegistry.GetNodesMap() { 126 | job.SetNodes(nodes.Value()) 127 | job.Perform() 128 | } 129 | } 130 | } 131 | } 132 | } 133 | }() 134 | } 135 | -------------------------------------------------------------------------------- /internal/relayer/http_requester.go: -------------------------------------------------------------------------------- 1 | package relayer 2 | 3 | import ( 4 | "github.com/valyala/fasthttp" 5 | "time" 6 | ) 7 | 8 | type httpRequester interface { 9 | DoTimeout(req *fasthttp.Request, resp *fasthttp.Response, timeout time.Duration) error 10 | } 11 | 12 | // fastHttpRequester: used to mock fast http for testing 13 | type fastHttpRequester struct{} 14 | 15 | func (receiver fastHttpRequester) DoTimeout(req *fasthttp.Request, resp *fasthttp.Response, timeout time.Duration) error { 16 | return fasthttp.DoTimeout(req, resp, timeout) 17 | } 18 | -------------------------------------------------------------------------------- /internal/relayer/relayer_test.go: -------------------------------------------------------------------------------- 1 | package relayer 2 | 3 | // Basic imports 4 | import ( 5 | "github.com/jackc/pgtype" 6 | "github.com/pokt-network/gateway-server/internal/db_query" 7 | qos_models "github.com/pokt-network/gateway-server/internal/node_selector_service/models" 8 | apps_registry_mock "github.com/pokt-network/gateway-server/mocks/apps_registry" 9 | chain_configurations_registry_mock "github.com/pokt-network/gateway-server/mocks/chain_configurations_registry" 10 | global_config_mock "github.com/pokt-network/gateway-server/mocks/global_config" 11 | node_selector_mock "github.com/pokt-network/gateway-server/mocks/node_selector" 12 | pocket_service_mock "github.com/pokt-network/gateway-server/mocks/pocket_service" 13 | session_registry_mock "github.com/pokt-network/gateway-server/mocks/session_registry" 14 | "github.com/stretchr/testify/suite" 15 | "go.uber.org/zap" 16 | "time" 17 | 18 | "github.com/pokt-network/gateway-server/pkg/pokt/pokt_v0/models" 19 | "testing" 20 | ) 21 | 22 | type RelayerTestSuite struct { 23 | suite.Suite 24 | mockNodeSelectorService *node_selector_mock.NodeSelectorService 25 | mockChainConfigurationsService *chain_configurations_registry_mock.ChainConfigurationsService 26 | mockSessionRegistryService *session_registry_mock.SessionRegistryService 27 | mockPocketService *pocket_service_mock.PocketService 28 | mockAppRegistry *apps_registry_mock.AppsRegistryService 29 | mockConfigProvider *global_config_mock.GlobalConfigProvider 30 | relayer *Relayer 31 | } 32 | 33 | func (suite *RelayerTestSuite) SetupTest() { 34 | suite.mockPocketService = new(pocket_service_mock.PocketService) 35 | suite.mockNodeSelectorService = new(node_selector_mock.NodeSelectorService) 36 | suite.mockSessionRegistryService = new(session_registry_mock.SessionRegistryService) 37 | suite.mockChainConfigurationsService = new(chain_configurations_registry_mock.ChainConfigurationsService) 38 | suite.mockAppRegistry = new(apps_registry_mock.AppsRegistryService) 39 | suite.mockConfigProvider = new(global_config_mock.GlobalConfigProvider) 40 | suite.relayer = NewRelayer(suite.mockPocketService, suite.mockSessionRegistryService, suite.mockAppRegistry, suite.mockNodeSelectorService, suite.mockChainConfigurationsService, "", suite.mockConfigProvider, zap.NewNop()) 41 | } 42 | 43 | func (suite *RelayerTestSuite) TestNodeSelectorRelay() { 44 | 45 | expectedResponse := &models.SendRelayResponse{Response: "response"} 46 | // create test cases 47 | testCases := []struct { 48 | name string 49 | request *models.SendRelayRequest 50 | setupMocks func(*models.SendRelayRequest) 51 | expectedResponse *models.SendRelayResponse 52 | expectedNodeHost string 53 | expectedError error 54 | }{ 55 | { 56 | name: "NodeSelectorFailed", 57 | request: &models.SendRelayRequest{ 58 | Payload: &models.Payload{}, 59 | Chain: "1234", 60 | }, 61 | setupMocks: func(request *models.SendRelayRequest) { 62 | suite.mockNodeSelectorService.EXPECT().FindNode("1234").Return(nil, false) 63 | }, 64 | expectedResponse: nil, 65 | expectedNodeHost: "", 66 | expectedError: errSelectNodeFail, 67 | }, 68 | { 69 | name: "Success", 70 | request: &models.SendRelayRequest{ 71 | Payload: &models.Payload{}, 72 | Chain: "1234", 73 | }, 74 | setupMocks: func(request *models.SendRelayRequest) { 75 | 76 | signer := &models.Ed25519Account{} 77 | node := &models.Node{PublicKey: "123", ServiceUrl: "http://complex.subdomain.root.com/test/123"} 78 | session := &models.Session{} 79 | suite.mockConfigProvider.EXPECT().ShouldEmitServiceUrlPromMetrics().Return(true) 80 | suite.mockNodeSelectorService.EXPECT().FindNode("1234").Return(qos_models.NewQosNode(node, session, signer), true) 81 | // expect sendRelay to have same parameters as find node, otherwise validation will fail 82 | suite.mockPocketService.EXPECT().SendRelay(&models.SendRelayRequest{ 83 | Payload: request.Payload, 84 | Signer: signer, 85 | Chain: request.Chain, 86 | SelectedNodePubKey: node.PublicKey, 87 | Session: session, 88 | }).Return(expectedResponse, nil) 89 | }, 90 | expectedNodeHost: "root.com", 91 | expectedResponse: expectedResponse, 92 | expectedError: nil, 93 | }, 94 | } 95 | 96 | // run test cases 97 | for _, tc := range testCases { 98 | suite.Run(tc.name, func() { 99 | 100 | suite.SetupTest() // reset mocks 101 | 102 | tc.setupMocks(tc.request) // setup mocks 103 | 104 | rsp, host, err := suite.relayer.sendNodeSelectorRelay(tc.request) 105 | 106 | // assert results 107 | suite.Equal(tc.expectedResponse, rsp) 108 | suite.Equal(tc.expectedNodeHost, host) 109 | suite.Equal(tc.expectedError, err) 110 | }) 111 | } 112 | 113 | } 114 | 115 | // test TestNodeSelectorRelay using table driven tests 116 | func (suite *RelayerTestSuite) TestAltruistRelay() { 117 | 118 | // create test cases 119 | testCases := []struct { 120 | name string 121 | request *models.SendRelayRequest 122 | setupMocks func(*models.SendRelayRequest) 123 | expectedResponse *models.SendRelayResponse 124 | expectedError error 125 | }{ 126 | { 127 | name: "Altruist Missing", 128 | request: &models.SendRelayRequest{ 129 | Payload: &models.Payload{}, 130 | Chain: "1234", 131 | }, 132 | setupMocks: func(request *models.SendRelayRequest) { 133 | suite.mockChainConfigurationsService.EXPECT().GetChainConfiguration(request.Chain).Return(db_query.GetChainConfigurationsRow{}, false) 134 | }, 135 | expectedResponse: nil, 136 | expectedError: errAltruistNotFound, 137 | }, 138 | { 139 | name: "Altruist Registry successfully called", 140 | request: &models.SendRelayRequest{ 141 | Payload: &models.Payload{}, 142 | Chain: "1234", 143 | }, 144 | setupMocks: func(request *models.SendRelayRequest) { 145 | pgTypeStr := pgtype.Varchar{} 146 | pgTypeStr.Set("https://example.com") 147 | // We can only check if altruist url and if proper config is called 148 | suite.mockConfigProvider.EXPECT().GetAltruistRequestTimeout().Return(time.Second * 15) 149 | suite.mockChainConfigurationsService.EXPECT().GetChainConfiguration(request.Chain).Return(db_query.GetChainConfigurationsRow{AltruistUrl: pgTypeStr}, true) 150 | }, 151 | expectedResponse: nil, 152 | expectedError: nil, 153 | }, 154 | } 155 | 156 | // run test cases 157 | for _, tc := range testCases { 158 | suite.Run(tc.name, func() { 159 | 160 | suite.SetupTest() // reset mocks 161 | 162 | tc.setupMocks(tc.request) // setup mocks 163 | 164 | _, err := suite.relayer.altruistRelay(tc.request) 165 | 166 | // Check if error matches expected 167 | suite.Equal(tc.expectedError, err) 168 | 169 | }) 170 | } 171 | 172 | } 173 | 174 | func TestRelayerTestSuite(t *testing.T) { 175 | suite.Run(t, new(RelayerTestSuite)) 176 | } 177 | -------------------------------------------------------------------------------- /internal/session_registry/session_registry_service.go: -------------------------------------------------------------------------------- 1 | package session_registry 2 | 3 | import ( 4 | "github.com/jellydator/ttlcache/v3" 5 | qos_models "github.com/pokt-network/gateway-server/internal/node_selector_service/models" 6 | "github.com/pokt-network/gateway-server/pkg/pokt/pokt_v0/models" 7 | ) 8 | 9 | type Session struct { 10 | IsValid bool 11 | PocketSession *models.Session 12 | Nodes []*qos_models.QosNode 13 | } 14 | 15 | type SessionRegistryService interface { 16 | GetSession(req *models.GetSessionRequest) (*Session, error) 17 | GetNodesMap() map[qos_models.SessionChainKey]*ttlcache.Item[qos_models.SessionChainKey, []*qos_models.QosNode] 18 | GetNodesByChain(chainId string) []*qos_models.QosNode 19 | } 20 | -------------------------------------------------------------------------------- /migrationfs.go: -------------------------------------------------------------------------------- 1 | package os_gateway 2 | 3 | import "embed" 4 | 5 | //go:embed db_migrations 6 | var Migrations embed.FS 7 | -------------------------------------------------------------------------------- /mocks/chain_configurations_registry/chain_configurations_registry_mock.go: -------------------------------------------------------------------------------- 1 | // Code generated by mockery v2.40.1. DO NOT EDIT. 2 | 3 | package chain_configurations_registry_mock 4 | 5 | import ( 6 | db_query "github.com/pokt-network/gateway-server/internal/db_query" 7 | mock "github.com/stretchr/testify/mock" 8 | ) 9 | 10 | // ChainConfigurationsService is an autogenerated mock type for the ChainConfigurationsService type 11 | type ChainConfigurationsService struct { 12 | mock.Mock 13 | } 14 | 15 | type ChainConfigurationsService_Expecter struct { 16 | mock *mock.Mock 17 | } 18 | 19 | func (_m *ChainConfigurationsService) EXPECT() *ChainConfigurationsService_Expecter { 20 | return &ChainConfigurationsService_Expecter{mock: &_m.Mock} 21 | } 22 | 23 | // GetChainConfiguration provides a mock function with given fields: chainId 24 | func (_m *ChainConfigurationsService) GetChainConfiguration(chainId string) (db_query.GetChainConfigurationsRow, bool) { 25 | ret := _m.Called(chainId) 26 | 27 | if len(ret) == 0 { 28 | panic("no return value specified for GetChainConfiguration") 29 | } 30 | 31 | var r0 db_query.GetChainConfigurationsRow 32 | var r1 bool 33 | if rf, ok := ret.Get(0).(func(string) (db_query.GetChainConfigurationsRow, bool)); ok { 34 | return rf(chainId) 35 | } 36 | if rf, ok := ret.Get(0).(func(string) db_query.GetChainConfigurationsRow); ok { 37 | r0 = rf(chainId) 38 | } else { 39 | r0 = ret.Get(0).(db_query.GetChainConfigurationsRow) 40 | } 41 | 42 | if rf, ok := ret.Get(1).(func(string) bool); ok { 43 | r1 = rf(chainId) 44 | } else { 45 | r1 = ret.Get(1).(bool) 46 | } 47 | 48 | return r0, r1 49 | } 50 | 51 | // ChainConfigurationsService_GetChainConfiguration_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'GetChainConfiguration' 52 | type ChainConfigurationsService_GetChainConfiguration_Call struct { 53 | *mock.Call 54 | } 55 | 56 | // GetChainConfiguration is a helper method to define mock.On call 57 | // - chainId string 58 | func (_e *ChainConfigurationsService_Expecter) GetChainConfiguration(chainId interface{}) *ChainConfigurationsService_GetChainConfiguration_Call { 59 | return &ChainConfigurationsService_GetChainConfiguration_Call{Call: _e.mock.On("GetChainConfiguration", chainId)} 60 | } 61 | 62 | func (_c *ChainConfigurationsService_GetChainConfiguration_Call) Run(run func(chainId string)) *ChainConfigurationsService_GetChainConfiguration_Call { 63 | _c.Call.Run(func(args mock.Arguments) { 64 | run(args[0].(string)) 65 | }) 66 | return _c 67 | } 68 | 69 | func (_c *ChainConfigurationsService_GetChainConfiguration_Call) Return(_a0 db_query.GetChainConfigurationsRow, _a1 bool) *ChainConfigurationsService_GetChainConfiguration_Call { 70 | _c.Call.Return(_a0, _a1) 71 | return _c 72 | } 73 | 74 | func (_c *ChainConfigurationsService_GetChainConfiguration_Call) RunAndReturn(run func(string) (db_query.GetChainConfigurationsRow, bool)) *ChainConfigurationsService_GetChainConfiguration_Call { 75 | _c.Call.Return(run) 76 | return _c 77 | } 78 | 79 | // NewChainConfigurationsService creates a new instance of ChainConfigurationsService. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. 80 | // The first argument is typically a *testing.T value. 81 | func NewChainConfigurationsService(t interface { 82 | mock.TestingT 83 | Cleanup(func()) 84 | }) *ChainConfigurationsService { 85 | mock := &ChainConfigurationsService{} 86 | mock.Mock.Test(t) 87 | 88 | t.Cleanup(func() { mock.AssertExpectations(t) }) 89 | 90 | return mock 91 | } 92 | -------------------------------------------------------------------------------- /mocks/node_selector/node_selector_mock.go: -------------------------------------------------------------------------------- 1 | // Code generated by mockery v2.40.1. DO NOT EDIT. 2 | 3 | package node_selector_mock 4 | 5 | import ( 6 | models "github.com/pokt-network/gateway-server/internal/node_selector_service/models" 7 | mock "github.com/stretchr/testify/mock" 8 | ) 9 | 10 | // NodeSelectorService is an autogenerated mock type for the NodeSelectorService type 11 | type NodeSelectorService struct { 12 | mock.Mock 13 | } 14 | 15 | type NodeSelectorService_Expecter struct { 16 | mock *mock.Mock 17 | } 18 | 19 | func (_m *NodeSelectorService) EXPECT() *NodeSelectorService_Expecter { 20 | return &NodeSelectorService_Expecter{mock: &_m.Mock} 21 | } 22 | 23 | // FindNode provides a mock function with given fields: chainId 24 | func (_m *NodeSelectorService) FindNode(chainId string) (*models.QosNode, bool) { 25 | ret := _m.Called(chainId) 26 | 27 | if len(ret) == 0 { 28 | panic("no return value specified for FindNode") 29 | } 30 | 31 | var r0 *models.QosNode 32 | var r1 bool 33 | if rf, ok := ret.Get(0).(func(string) (*models.QosNode, bool)); ok { 34 | return rf(chainId) 35 | } 36 | if rf, ok := ret.Get(0).(func(string) *models.QosNode); ok { 37 | r0 = rf(chainId) 38 | } else { 39 | if ret.Get(0) != nil { 40 | r0 = ret.Get(0).(*models.QosNode) 41 | } 42 | } 43 | 44 | if rf, ok := ret.Get(1).(func(string) bool); ok { 45 | r1 = rf(chainId) 46 | } else { 47 | r1 = ret.Get(1).(bool) 48 | } 49 | 50 | return r0, r1 51 | } 52 | 53 | // NodeSelectorService_FindNode_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'FindNode' 54 | type NodeSelectorService_FindNode_Call struct { 55 | *mock.Call 56 | } 57 | 58 | // FindNode is a helper method to define mock.On call 59 | // - chainId string 60 | func (_e *NodeSelectorService_Expecter) FindNode(chainId interface{}) *NodeSelectorService_FindNode_Call { 61 | return &NodeSelectorService_FindNode_Call{Call: _e.mock.On("FindNode", chainId)} 62 | } 63 | 64 | func (_c *NodeSelectorService_FindNode_Call) Run(run func(chainId string)) *NodeSelectorService_FindNode_Call { 65 | _c.Call.Run(func(args mock.Arguments) { 66 | run(args[0].(string)) 67 | }) 68 | return _c 69 | } 70 | 71 | func (_c *NodeSelectorService_FindNode_Call) Return(_a0 *models.QosNode, _a1 bool) *NodeSelectorService_FindNode_Call { 72 | _c.Call.Return(_a0, _a1) 73 | return _c 74 | } 75 | 76 | func (_c *NodeSelectorService_FindNode_Call) RunAndReturn(run func(string) (*models.QosNode, bool)) *NodeSelectorService_FindNode_Call { 77 | _c.Call.Return(run) 78 | return _c 79 | } 80 | 81 | // NewNodeSelectorService creates a new instance of NodeSelectorService. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. 82 | // The first argument is typically a *testing.T value. 83 | func NewNodeSelectorService(t interface { 84 | mock.TestingT 85 | Cleanup(func()) 86 | }) *NodeSelectorService { 87 | mock := &NodeSelectorService{} 88 | mock.Mock.Test(t) 89 | 90 | t.Cleanup(func() { mock.AssertExpectations(t) }) 91 | 92 | return mock 93 | } 94 | -------------------------------------------------------------------------------- /pkg/common/crypto.go: -------------------------------------------------------------------------------- 1 | package common 2 | 3 | import ( 4 | "crypto/sha256" 5 | "encoding/hex" 6 | "github.com/pquerna/ffjson/ffjson" 7 | "golang.org/x/crypto/sha3" 8 | ) 9 | 10 | func Sha3_256Hash(obj any) []byte { 11 | jsonStr, _ := ffjson.Marshal(obj) 12 | sha256Hash := sha3.New256() 13 | sha256Hash.Write(jsonStr) 14 | return sha256Hash.Sum(nil) 15 | } 16 | 17 | func Sha3_256HashHex(obj any) string { 18 | return hex.EncodeToString(Sha3_256Hash(obj)) 19 | } 20 | 21 | func GetAddressFromPublicKey(publicKey string) (string, error) { 22 | bytes, err := hex.DecodeString(publicKey) 23 | if err != nil { 24 | return "", err 25 | } 26 | hasher := sha256.New() 27 | hasher.Write(bytes) 28 | hashBytes := hasher.Sum(nil) 29 | address := hex.EncodeToString(hashBytes) 30 | return address[:40], nil 31 | } 32 | -------------------------------------------------------------------------------- /pkg/common/crypto_test.go: -------------------------------------------------------------------------------- 1 | package common 2 | 3 | import ( 4 | "encoding/hex" 5 | "testing" 6 | 7 | "github.com/stretchr/testify/assert" 8 | ) 9 | 10 | // test for Sha3_256Hash function in pkg/common/crypto.go file 11 | func TestSha3_256Hash(t *testing.T) { 12 | 13 | type args struct { 14 | obj any 15 | } 16 | 17 | tests := []struct { 18 | name string 19 | args args 20 | want []byte 21 | }{ 22 | { 23 | name: "Sha3_256_Payload1", 24 | args: args{ 25 | obj: "test1", 26 | }, 27 | want: []byte{161, 215, 105, 209, 47, 54, 86, 186, 167, 27, 175, 135, 81, 219, 180, 189, 25, 16, 73, 106, 74, 199, 100, 120, 143, 167, 64, 188, 208, 89, 118, 36}, 28 | }, 29 | { 30 | name: "Sha3_256_Payload2", 31 | args: args{ 32 | obj: "test2", 33 | }, 34 | want: []byte{239, 187, 198, 219, 95, 41, 245, 181, 210, 133, 45, 90, 15, 238, 124, 55, 162, 73, 233, 105, 12, 8, 178, 172, 35, 95, 179, 119, 38, 168, 182, 85}, 35 | }, 36 | { 37 | name: "Sha3_256_Payload3", 38 | args: args{ 39 | obj: "", // empty string 40 | }, 41 | want: []byte{40, 145, 227, 117, 2, 135, 48, 190, 87, 9, 94, 152, 9, 107, 184, 175, 66, 239, 2, 126, 70, 228, 39, 71, 139, 124, 167, 209, 51, 43, 216, 95}, 42 | }, 43 | } 44 | 45 | for _, tt := range tests { 46 | t.Run(tt.name, func(t *testing.T) { 47 | assert.Equal(t, tt.want, Sha3_256Hash(tt.args.obj)) 48 | }) 49 | } 50 | 51 | } 52 | 53 | // test for Sha3_256HashHex function in pkg/common/crypto.go file 54 | func TestSha3_256HashHex(t *testing.T) { 55 | 56 | type args struct { 57 | obj any 58 | } 59 | 60 | tests := []struct { 61 | name string 62 | args args 63 | want string 64 | }{ 65 | { 66 | name: "Sha3_256_Payload1", 67 | args: args{ 68 | obj: "test1", 69 | }, 70 | want: "a1d769d12f3656baa71baf8751dbb4bd1910496a4ac764788fa740bcd0597624", 71 | }, 72 | { 73 | name: "Sha3_256_Payload2", 74 | args: args{ 75 | obj: "test2", 76 | }, 77 | want: "efbbc6db5f29f5b5d2852d5a0fee7c37a249e9690c08b2ac235fb37726a8b655", 78 | }, 79 | { 80 | name: "Sha3_256_Payload3", 81 | args: args{ 82 | obj: "", // empty string 83 | }, 84 | want: "2891e375028730be57095e98096bb8af42ef027e46e427478b7ca7d1332bd85f", 85 | }, 86 | } 87 | 88 | for _, tt := range tests { 89 | t.Run(tt.name, func(t *testing.T) { 90 | assert.Equal(t, tt.want, Sha3_256HashHex(tt.args.obj)) 91 | }) 92 | } 93 | 94 | } 95 | 96 | // test for GetAddressFromPublicKey function in pkg/common/crypto.go file 97 | func TestGetAddressFromPublicKey(t *testing.T) { 98 | 99 | type args struct { 100 | publicKey string 101 | } 102 | 103 | tests := []struct { 104 | name string 105 | args args 106 | want string 107 | wantErr error 108 | }{ 109 | { 110 | name: "invalid_public_key", 111 | args: args{ 112 | publicKey: "--invalid-public-key--", // invalid public key 113 | }, 114 | want: "", 115 | wantErr: hex.InvalidByteError('-'), 116 | }, 117 | { 118 | name: "valid_public_key_1", 119 | args: args{ 120 | publicKey: "d992d8915443e85f620a67bce0928bba16cc349b6b6878698fd9518c6f49d5f2", // valid public key 121 | }, 122 | want: "bdf642b7d84840f034c9fde6147358faed2db3ff", // address 123 | wantErr: nil, 124 | }, 125 | { 126 | name: "valid_public_key_2", 127 | args: args{ 128 | publicKey: "a59516f6f339699f6cfbe70046bc5cb5f1053cfee4c74e66be0ab1695e04a979", // valid public key 129 | }, 130 | want: "265f106d0e6524f14d14e96d26a88262234e2bf9", // address 131 | wantErr: nil, 132 | }, 133 | } 134 | 135 | for _, tt := range tests { 136 | t.Run(tt.name, func(t *testing.T) { 137 | 138 | got, err := GetAddressFromPublicKey(tt.args.publicKey) 139 | 140 | assert.Equal(t, tt.wantErr, err) 141 | assert.Equal(t, tt.want, got) 142 | 143 | }) 144 | } 145 | 146 | } 147 | -------------------------------------------------------------------------------- /pkg/common/http.go: -------------------------------------------------------------------------------- 1 | package common 2 | 3 | func IsHttpOk(statusCode int) bool { 4 | return statusCode >= 200 && statusCode <= 299 5 | } 6 | -------------------------------------------------------------------------------- /pkg/common/http_test.go: -------------------------------------------------------------------------------- 1 | package common 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/assert" 7 | ) 8 | 9 | // test for IsHttpOk function in pkg/common/http_test.go file 10 | func TestIsHttpOk(t *testing.T) { 11 | 12 | tests := []struct { 13 | name string 14 | statusCode int 15 | want bool 16 | }{ 17 | { 18 | name: "Informational", 19 | statusCode: 100, 20 | want: false, 21 | }, 22 | { 23 | name: "Successful", 24 | statusCode: 200, 25 | want: true, 26 | }, 27 | { 28 | name: "Redirection", 29 | statusCode: 300, 30 | want: false, 31 | }, 32 | { 33 | name: "ClientError", 34 | statusCode: 400, 35 | want: false, 36 | }, 37 | { 38 | name: "ServerError", 39 | statusCode: 500, 40 | want: false, 41 | }, 42 | } 43 | 44 | for _, tt := range tests { 45 | t.Run(tt.name, func(t *testing.T) { 46 | assert.Equal(t, tt.want, IsHttpOk(tt.statusCode)) 47 | }) 48 | } 49 | 50 | } 51 | -------------------------------------------------------------------------------- /pkg/common/slices.go: -------------------------------------------------------------------------------- 1 | package common 2 | 3 | import ( 4 | "math/rand" 5 | ) 6 | 7 | func GetRandomElement[T any](elements []T) (T, bool) { 8 | if len(elements) == 0 { 9 | return *new(T), false 10 | } 11 | randomIndex := rand.Intn(len(elements)) 12 | return elements[randomIndex], true 13 | } 14 | -------------------------------------------------------------------------------- /pkg/pokt/common/json-rpc.go: -------------------------------------------------------------------------------- 1 | //go:generate ffjson $GOFILE 2 | package common 3 | 4 | type EvmJsonRpcPayload struct { 5 | Id string `json:"id"` 6 | Method string `json:"method"` 7 | } 8 | -------------------------------------------------------------------------------- /pkg/pokt/common/json-rpc_ffjson.go: -------------------------------------------------------------------------------- 1 | // Code generated by ffjson . DO NOT EDIT. 2 | // source: json-rpc.go 3 | 4 | package common 5 | 6 | import ( 7 | "bytes" 8 | "fmt" 9 | 10 | fflib "github.com/pquerna/ffjson/fflib/v1" 11 | ) 12 | 13 | // MarshalJSON marshal bytes to json - template 14 | func (j *EvmJsonRpcPayload) MarshalJSON() ([]byte, error) { 15 | var buf fflib.Buffer 16 | if j == nil { 17 | buf.WriteString("null") 18 | return buf.Bytes(), nil 19 | } 20 | err := j.MarshalJSONBuf(&buf) 21 | if err != nil { 22 | return nil, err 23 | } 24 | return buf.Bytes(), nil 25 | } 26 | 27 | // MarshalJSONBuf marshal buff to json - template 28 | func (j *EvmJsonRpcPayload) MarshalJSONBuf(buf fflib.EncodingBuffer) error { 29 | if j == nil { 30 | buf.WriteString("null") 31 | return nil 32 | } 33 | var err error 34 | var obj []byte 35 | _ = obj 36 | _ = err 37 | buf.WriteString(`{"id":`) 38 | fflib.WriteJsonString(buf, string(j.Id)) 39 | buf.WriteString(`,"method":`) 40 | fflib.WriteJsonString(buf, string(j.Method)) 41 | buf.WriteByte('}') 42 | return nil 43 | } 44 | 45 | const ( 46 | ffjtEvmJsonRpcPayloadbase = iota 47 | ffjtEvmJsonRpcPayloadnosuchkey 48 | 49 | ffjtEvmJsonRpcPayloadId 50 | 51 | ffjtEvmJsonRpcPayloadMethod 52 | ) 53 | 54 | var ffjKeyEvmJsonRpcPayloadId = []byte("id") 55 | 56 | var ffjKeyEvmJsonRpcPayloadMethod = []byte("method") 57 | 58 | // UnmarshalJSON umarshall json - template of ffjson 59 | func (j *EvmJsonRpcPayload) UnmarshalJSON(input []byte) error { 60 | fs := fflib.NewFFLexer(input) 61 | return j.UnmarshalJSONFFLexer(fs, fflib.FFParse_map_start) 62 | } 63 | 64 | // UnmarshalJSONFFLexer fast json unmarshall - template ffjson 65 | func (j *EvmJsonRpcPayload) UnmarshalJSONFFLexer(fs *fflib.FFLexer, state fflib.FFParseState) error { 66 | var err error 67 | currentKey := ffjtEvmJsonRpcPayloadbase 68 | _ = currentKey 69 | tok := fflib.FFTok_init 70 | wantedTok := fflib.FFTok_init 71 | 72 | mainparse: 73 | for { 74 | tok = fs.Scan() 75 | // println(fmt.Sprintf("debug: tok: %v state: %v", tok, state)) 76 | if tok == fflib.FFTok_error { 77 | goto tokerror 78 | } 79 | 80 | switch state { 81 | 82 | case fflib.FFParse_map_start: 83 | if tok != fflib.FFTok_left_bracket { 84 | wantedTok = fflib.FFTok_left_bracket 85 | goto wrongtokenerror 86 | } 87 | state = fflib.FFParse_want_key 88 | continue 89 | 90 | case fflib.FFParse_after_value: 91 | if tok == fflib.FFTok_comma { 92 | state = fflib.FFParse_want_key 93 | } else if tok == fflib.FFTok_right_bracket { 94 | goto done 95 | } else { 96 | wantedTok = fflib.FFTok_comma 97 | goto wrongtokenerror 98 | } 99 | 100 | case fflib.FFParse_want_key: 101 | // json {} ended. goto exit. woo. 102 | if tok == fflib.FFTok_right_bracket { 103 | goto done 104 | } 105 | if tok != fflib.FFTok_string { 106 | wantedTok = fflib.FFTok_string 107 | goto wrongtokenerror 108 | } 109 | 110 | kn := fs.Output.Bytes() 111 | if len(kn) <= 0 { 112 | // "" case. hrm. 113 | currentKey = ffjtEvmJsonRpcPayloadnosuchkey 114 | state = fflib.FFParse_want_colon 115 | goto mainparse 116 | } else { 117 | switch kn[0] { 118 | 119 | case 'i': 120 | 121 | if bytes.Equal(ffjKeyEvmJsonRpcPayloadId, kn) { 122 | currentKey = ffjtEvmJsonRpcPayloadId 123 | state = fflib.FFParse_want_colon 124 | goto mainparse 125 | } 126 | 127 | case 'm': 128 | 129 | if bytes.Equal(ffjKeyEvmJsonRpcPayloadMethod, kn) { 130 | currentKey = ffjtEvmJsonRpcPayloadMethod 131 | state = fflib.FFParse_want_colon 132 | goto mainparse 133 | } 134 | 135 | } 136 | 137 | if fflib.SimpleLetterEqualFold(ffjKeyEvmJsonRpcPayloadMethod, kn) { 138 | currentKey = ffjtEvmJsonRpcPayloadMethod 139 | state = fflib.FFParse_want_colon 140 | goto mainparse 141 | } 142 | 143 | if fflib.SimpleLetterEqualFold(ffjKeyEvmJsonRpcPayloadId, kn) { 144 | currentKey = ffjtEvmJsonRpcPayloadId 145 | state = fflib.FFParse_want_colon 146 | goto mainparse 147 | } 148 | 149 | currentKey = ffjtEvmJsonRpcPayloadnosuchkey 150 | state = fflib.FFParse_want_colon 151 | goto mainparse 152 | } 153 | 154 | case fflib.FFParse_want_colon: 155 | if tok != fflib.FFTok_colon { 156 | wantedTok = fflib.FFTok_colon 157 | goto wrongtokenerror 158 | } 159 | state = fflib.FFParse_want_value 160 | continue 161 | case fflib.FFParse_want_value: 162 | 163 | if tok == fflib.FFTok_left_brace || tok == fflib.FFTok_left_bracket || tok == fflib.FFTok_integer || tok == fflib.FFTok_double || tok == fflib.FFTok_string || tok == fflib.FFTok_bool || tok == fflib.FFTok_null { 164 | switch currentKey { 165 | 166 | case ffjtEvmJsonRpcPayloadId: 167 | goto handle_Id 168 | 169 | case ffjtEvmJsonRpcPayloadMethod: 170 | goto handle_Method 171 | 172 | case ffjtEvmJsonRpcPayloadnosuchkey: 173 | err = fs.SkipField(tok) 174 | if err != nil { 175 | return fs.WrapErr(err) 176 | } 177 | state = fflib.FFParse_after_value 178 | goto mainparse 179 | } 180 | } else { 181 | goto wantedvalue 182 | } 183 | } 184 | } 185 | 186 | handle_Id: 187 | 188 | /* handler: j.Id type=string kind=string quoted=false*/ 189 | 190 | { 191 | 192 | { 193 | if tok != fflib.FFTok_string && tok != fflib.FFTok_null { 194 | return fs.WrapErr(fmt.Errorf("cannot unmarshal %s into Go value for string", tok)) 195 | } 196 | } 197 | 198 | if tok == fflib.FFTok_null { 199 | 200 | } else { 201 | 202 | outBuf := fs.Output.Bytes() 203 | 204 | j.Id = string(string(outBuf)) 205 | 206 | } 207 | } 208 | 209 | state = fflib.FFParse_after_value 210 | goto mainparse 211 | 212 | handle_Method: 213 | 214 | /* handler: j.Method type=string kind=string quoted=false*/ 215 | 216 | { 217 | 218 | { 219 | if tok != fflib.FFTok_string && tok != fflib.FFTok_null { 220 | return fs.WrapErr(fmt.Errorf("cannot unmarshal %s into Go value for string", tok)) 221 | } 222 | } 223 | 224 | if tok == fflib.FFTok_null { 225 | 226 | } else { 227 | 228 | outBuf := fs.Output.Bytes() 229 | 230 | j.Method = string(string(outBuf)) 231 | 232 | } 233 | } 234 | 235 | state = fflib.FFParse_after_value 236 | goto mainparse 237 | 238 | wantedvalue: 239 | return fs.WrapErr(fmt.Errorf("wanted value token, but got token: %v", tok)) 240 | wrongtokenerror: 241 | return fs.WrapErr(fmt.Errorf("ffjson: wanted token: %v, but got token: %v output=%s", wantedTok, tok, fs.Output.String())) 242 | tokerror: 243 | if fs.BigError != nil { 244 | return fs.WrapErr(fs.BigError) 245 | } 246 | err = fs.Error.ToError() 247 | if err != nil { 248 | return fs.WrapErr(err) 249 | } 250 | panic("ffjson-generated: unreachable, please report bug.") 251 | done: 252 | 253 | return nil 254 | } 255 | -------------------------------------------------------------------------------- /pkg/pokt/pokt_v0/basic_client.go: -------------------------------------------------------------------------------- 1 | package pokt_v0 2 | 3 | import ( 4 | "errors" 5 | "github.com/pokt-network/gateway-server/pkg/common" 6 | "github.com/pokt-network/gateway-server/pkg/pokt/pokt_v0/models" 7 | "github.com/pquerna/ffjson/ffjson" 8 | "github.com/valyala/fasthttp" 9 | "math/rand" 10 | "time" 11 | ) 12 | 13 | const ( 14 | endpointClientPrefix = "/v1/client" 15 | endpointQueryPrefix = "/v1/query" 16 | endpointDispatch = endpointClientPrefix + "/dispatch" 17 | endpointSendRelay = endpointClientPrefix + "/relay" 18 | endpointGetHeight = endpointQueryPrefix + "/height" 19 | endpointGetApps = endpointQueryPrefix + "/apps" 20 | maxApplications = 5000 21 | ) 22 | 23 | // BasicClient represents a basic client with a logging, full node host, and a global request timeout. 24 | type BasicClient struct { 25 | fullNodeHost string 26 | globalRequestTimeout time.Duration 27 | userAgent string 28 | } 29 | 30 | // NewBasicClient creates a new BasicClient instance. 31 | // Parameters: 32 | // - fullNodeHost: Full node host address. 33 | // - logging: Logger instance. 34 | // - timeout: Global request timeout duration. 35 | // 36 | // Returns: 37 | // - (*BasicClient): New BasicClient instance. 38 | // - (error): Error, if any. 39 | func NewBasicClient(fullNodeHost string, userAgent string, timeout time.Duration) (*BasicClient, error) { 40 | if len(fullNodeHost) == 0 { 41 | return nil, models.ErrMissingFullNodes 42 | } 43 | return &BasicClient{ 44 | fullNodeHost: fullNodeHost, 45 | globalRequestTimeout: timeout, 46 | userAgent: userAgent, 47 | }, nil 48 | } 49 | 50 | // GetSession obtains a session from the full node. 51 | // Parameters: 52 | // - req: GetSessionRequest instance containing the request parameters. 53 | // 54 | // Returns: 55 | // - (*GetSessionResponse): Session response. 56 | // - (error): Error, if any. 57 | func (r BasicClient) GetSession(req *models.GetSessionRequest) (*models.GetSessionResponse, error) { 58 | var sessionResponse models.GetSessionResponse 59 | err := r.makeRequest(endpointDispatch, "POST", req, &sessionResponse, nil, nil) 60 | if err != nil { 61 | return nil, err 62 | } 63 | 64 | // The current POKT Node implementation returns the latest session height instead of what was requested. 65 | // This can result in undesired functionality without explicit error handling (such as caching sesions, as the wrong session could become cahed) 66 | if req.SessionHeight != 0 && sessionResponse.Session.SessionHeader.SessionHeight != req.SessionHeight { 67 | return nil, errors.New("GetSession: failed, dispatcher returned a different session than what was requested") 68 | } 69 | 70 | return &sessionResponse, nil 71 | } 72 | 73 | // GetLatestStakedApplications obtains all the applications from the latest block then filters for staked. 74 | // Returns: 75 | // - ([]*models.PoktApplication): list of staked applications 76 | // - (error): Error, if any. 77 | func (r BasicClient) GetLatestStakedApplications() ([]*models.PoktApplication, error) { 78 | reqParams := map[string]any{"opts": map[string]any{"per_page": maxApplications}} 79 | var resp models.GetApplicationResponse 80 | err := r.makeRequest(endpointGetApps, "POST", reqParams, &resp, nil, nil) 81 | if err != nil { 82 | return nil, err 83 | } 84 | stakedApplications := []*models.PoktApplication{} 85 | for _, app := range resp.Result { 86 | stakedApplications = append(stakedApplications, app) 87 | } 88 | if len(stakedApplications) == 0 { 89 | return nil, errors.New("zero applications found") 90 | } 91 | return stakedApplications, nil 92 | } 93 | 94 | // SendRelay sends a relay request to the full node. 95 | // Parameters: 96 | // - req: SendRelayRequest instance containing the relay request parameters. 97 | // 98 | // Returns: 99 | // - (*SendRelayResponse): Relay response. 100 | // - (error): Error, if any. 101 | func (r BasicClient) SendRelay(req *models.SendRelayRequest) (*models.SendRelayResponse, error) { 102 | 103 | // Get a session from the request or retrieve from full node 104 | session, err := GetSessionFromRequest(r, req) 105 | 106 | if err != nil { 107 | return nil, err 108 | } 109 | 110 | // Get the preferred selected node, or chose a random one. 111 | node, err := getNodeFromRequest(session, req.SelectedNodePubKey) 112 | 113 | if err != nil { 114 | return nil, err 115 | } 116 | 117 | currentSessionHeight := session.SessionHeader.SessionHeight 118 | 119 | relayMetadata := &models.RelayMeta{BlockHeight: currentSessionHeight} 120 | 121 | entropy := uint64(rand.Int63()) 122 | relayProof := generateRelayProof(entropy, req.Chain, currentSessionHeight, node.PublicKey, relayMetadata, req.Payload, req.Signer) 123 | 124 | // Relay created, generating a request to the servicer 125 | var sessionResponse models.SendRelayResponse 126 | err = r.makeRequest(endpointSendRelay, "POST", &models.Relay{ 127 | Payload: req.Payload, 128 | Metadata: relayMetadata, 129 | RelayProof: relayProof, 130 | }, &sessionResponse, &node.ServiceUrl, req.Timeout) 131 | 132 | if err != nil { 133 | return nil, err 134 | } 135 | 136 | return &sessionResponse, nil 137 | } 138 | 139 | // GetLatestBlockHeight gets the latest block height from the full node. 140 | // Returns: 141 | // - (*GetLatestBlockHeightResponse): Latest block height response. 142 | // - (error): Error, if any. 143 | func (r BasicClient) GetLatestBlockHeight() (*models.GetLatestBlockHeightResponse, error) { 144 | 145 | var height models.GetLatestBlockHeightResponse 146 | err := r.makeRequest(endpointGetHeight, "POST", nil, &height, nil, nil) 147 | 148 | if err != nil { 149 | return nil, err 150 | } 151 | 152 | return &height, nil 153 | } 154 | 155 | func (r BasicClient) makeRequest(endpoint string, method string, requestData any, responseModel any, hostOverride *string, providedReqTimeout *time.Duration) error { 156 | reqPayload, err := ffjson.Marshal(requestData) 157 | if err != nil { 158 | return err 159 | } 160 | 161 | request := fasthttp.AcquireRequest() 162 | response := fasthttp.AcquireResponse() 163 | 164 | defer func() { 165 | fasthttp.ReleaseRequest(request) 166 | fasthttp.ReleaseResponse(response) 167 | }() 168 | 169 | request.Header.SetUserAgent(r.userAgent) 170 | 171 | if hostOverride != nil { 172 | request.SetRequestURI(*hostOverride + endpoint) 173 | } else { 174 | request.SetRequestURI(r.fullNodeHost + endpoint) 175 | } 176 | request.Header.SetMethod(method) 177 | 178 | if method == "POST" { 179 | request.SetBody(reqPayload) 180 | } 181 | 182 | var requestTimeout *time.Duration 183 | if providedReqTimeout != nil { 184 | requestTimeout = providedReqTimeout 185 | } else { 186 | requestTimeout = &r.globalRequestTimeout 187 | } 188 | err = fasthttp.DoTimeout(request, response, *requestTimeout) 189 | if err != nil { 190 | return err 191 | } 192 | 193 | // Check for a successful HTTP status code 194 | if !common.IsHttpOk(response.StatusCode()) { 195 | var pocketError models.PocketRPCError 196 | err := ffjson.Unmarshal(response.Body(), pocketError) 197 | // failed to unmarshal, not sure what the response code is 198 | if err != nil { 199 | return models.PocketRPCError{HttpCode: response.StatusCode(), Message: string(response.Body())} 200 | } 201 | return pocketError 202 | } 203 | return ffjson.Unmarshal(response.Body(), responseModel) 204 | } 205 | -------------------------------------------------------------------------------- /pkg/pokt/pokt_v0/generate_proof_bytes.go: -------------------------------------------------------------------------------- 1 | package pokt_v0 2 | 3 | import ( 4 | "encoding/hex" 5 | "github.com/pokt-network/gateway-server/pkg/common" 6 | "github.com/pokt-network/gateway-server/pkg/pokt/pokt_v0/models" 7 | ) 8 | 9 | // generateRelayProof generates a relay proof. 10 | // Parameters: 11 | // - entropy - random generated number to signify unique proof 12 | // - chainId: Blockchain ID. 13 | // - sessionHeight: Session block height. 14 | // - servicerPubKey: Servicer public key. 15 | // - requestMetadata: Request metadata. 16 | // - account: Ed25519 account used for signing. 17 | // 18 | // Returns: 19 | // - models.RelayProof: Generated relay proof. 20 | func generateRelayProof(entropy uint64, chainId string, sessionHeight uint, servicerPubKey string, relayMetadata *models.RelayMeta, reqPayload *models.Payload, account *models.Ed25519Account) *models.RelayProof { 21 | aat := account.GetAAT() 22 | 23 | requestMetadata := models.RequestHashPayload{ 24 | Metadata: relayMetadata, 25 | Payload: reqPayload, 26 | } 27 | 28 | requestHash := requestMetadata.Hash() 29 | 30 | unsignedAAT := &models.AAT{ 31 | Version: aat.Version, 32 | AppPubKey: aat.AppPubKey, 33 | ClientPubKey: aat.ClientPubKey, 34 | Signature: "", 35 | } 36 | 37 | proofObj := &models.RelayProofHashPayload{ 38 | RequestHash: requestHash, 39 | Entropy: entropy, 40 | SessionBlockHeight: sessionHeight, 41 | ServicerPubKey: servicerPubKey, 42 | Blockchain: chainId, 43 | Signature: "", 44 | UnsignedAAT: unsignedAAT.Hash(), 45 | } 46 | 47 | hashedPayload := common.Sha3_256Hash(proofObj) 48 | hashSignature := hex.EncodeToString(account.Sign(hashedPayload)) 49 | return &models.RelayProof{ 50 | RequestHash: requestHash, 51 | Entropy: entropy, 52 | SessionBlockHeight: sessionHeight, 53 | ServicerPubKey: servicerPubKey, 54 | Blockchain: chainId, 55 | AAT: aat, 56 | Signature: hashSignature, 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /pkg/pokt/pokt_v0/generate_proof_bytes_test.go: -------------------------------------------------------------------------------- 1 | package pokt_v0 2 | 3 | import ( 4 | "github.com/pokt-network/gateway-server/pkg/pokt/pokt_v0/models" 5 | "github.com/stretchr/testify/assert" 6 | "testing" 7 | ) 8 | 9 | func Test_generateRelayProof(t *testing.T) { 10 | 11 | account, err := models.NewAccount("3fe64039816c44e8872e4ef981725b968422e3d49e95a1eb800707591df30fe374039dbe881dd2744e2e0c469cc2241e1e45f14af6975dd89079d22938377849") 12 | assert.Equal(t, err, nil) 13 | 14 | chainId := "0001" 15 | sessionHeight := uint(1) 16 | servicerPubKey := "0x" 17 | relayMetadata := &models.RelayMeta{BlockHeight: sessionHeight} 18 | requestPayload := &models.Payload{ 19 | Data: "randomJsonPayload", 20 | Method: "post", 21 | Path: "", 22 | Headers: nil, 23 | } 24 | entropy := uint64(1) 25 | assert.Equal(t, generateRelayProof(entropy, chainId, sessionHeight, servicerPubKey, relayMetadata, requestPayload, account), &models.RelayProof{ 26 | Entropy: 1, 27 | SessionBlockHeight: 1, 28 | ServicerPubKey: "0x", 29 | Blockchain: "0001", 30 | AAT: &models.AAT{ 31 | Version: "0.0.1", 32 | AppPubKey: "74039dbe881dd2744e2e0c469cc2241e1e45f14af6975dd89079d22938377849", 33 | ClientPubKey: "74039dbe881dd2744e2e0c469cc2241e1e45f14af6975dd89079d22938377849", 34 | Signature: "f233ca857b4ada2ca4996e0da8c1761cfbc855edf282fc5a753d4631785946d6c2b08c781c84abbca2dc929de50008729079124e5c5c16921a81139279020a05", 35 | }, 36 | Signature: "befcc42130fb9e46fb9874acfb5bd8a9f783db60f86d1b1eb61cdba23fdb7e9e17544cb99afb480c9e1308532e07cdf6f4e2da27790f47dae30133725191b309", 37 | RequestHash: "c5b64f9a7901ed8c3341f7440913a5ddd7b694dc7b4daeb234a47a9c42b653bb", 38 | }) 39 | 40 | } 41 | -------------------------------------------------------------------------------- /pkg/pokt/pokt_v0/get_node_from_request.go: -------------------------------------------------------------------------------- 1 | package pokt_v0 2 | 3 | import ( 4 | "github.com/pokt-network/gateway-server/pkg/common" 5 | "github.com/pokt-network/gateway-server/pkg/pokt/pokt_v0/models" 6 | "slices" 7 | ) 8 | 9 | // getNodeFromRequest obtains a node from a relay request. 10 | // Parameters: 11 | // - req: SendRelayRequest instance containing the relay request parameters. 12 | // 13 | // Returns: 14 | // - (*models.Node): Node instance. 15 | // - (error): Error, if any. 16 | func getNodeFromRequest(session *models.Session, selectedNodePubKey string) (*models.Node, error) { 17 | if selectedNodePubKey == "" { 18 | return getRandomNodeOrError(session.Nodes, models.ErrSessionHasZeroNodes) 19 | } 20 | return findNodeOrError(session.Nodes, selectedNodePubKey, models.ErrNodeNotFound) 21 | } 22 | 23 | // getRandomNodeOrError gets a random node or returns an error if the node list is empty. 24 | // Parameters: 25 | // - nodes: List of nodes. 26 | // - err: Error to be returned if the node list is empty. 27 | // 28 | // Returns: 29 | // - (*models.Node): Random node. 30 | // - (error): Error, if any. 31 | func getRandomNodeOrError(nodes []*models.Node, err error) (*models.Node, error) { 32 | node, ok := common.GetRandomElement(nodes) 33 | if !ok || node == nil { 34 | return nil, err 35 | } 36 | return node, nil 37 | } 38 | 39 | // findNodeOrError finds a node by public key or returns an error if the node is not found. 40 | // Parameters: 41 | // - nodes: List of nodes. 42 | // - pubKey: Public key of the node to find. 43 | // - err: Error to be returned if the node is not found. 44 | // 45 | // Returns: 46 | // - (*models.Node): Found node. 47 | // - (error): Error, if any. 48 | func findNodeOrError(nodes []*models.Node, pubKey string, err error) (*models.Node, error) { 49 | idx := slices.IndexFunc(nodes, func(node *models.Node) bool { 50 | return node.PublicKey == pubKey 51 | }) 52 | if idx == -1 { 53 | return nil, err 54 | } 55 | return nodes[idx], nil 56 | } 57 | -------------------------------------------------------------------------------- /pkg/pokt/pokt_v0/get_node_from_request_test.go: -------------------------------------------------------------------------------- 1 | package pokt_v0 2 | 3 | import ( 4 | "github.com/pokt-network/gateway-server/pkg/pokt/pokt_v0/models" 5 | "testing" 6 | 7 | "github.com/stretchr/testify/assert" 8 | ) 9 | 10 | func TestGetNodeFromRequest(t *testing.T) { 11 | // Prepare a mock session with nodes 12 | mockNodes := []*models.Node{ 13 | {PublicKey: "pubKey1"}, 14 | {PublicKey: "pubKey2"}, 15 | {PublicKey: "pubKey3"}, 16 | } 17 | mockSession := &models.Session{Nodes: mockNodes} 18 | 19 | testCases := []struct { 20 | Name string 21 | SelectedNodePubKey string 22 | ExpectedError error 23 | ExpectedNode *models.Node 24 | ExpectedRandom bool 25 | }{ 26 | { 27 | "Get random node if selectedNodePubKey is empty", 28 | "", 29 | nil, 30 | nil, // The expectation for the node can be adjusted based on the test case. 31 | true, 32 | }, 33 | { 34 | "Get specific node by public key", 35 | "pubKey2", 36 | nil, 37 | &models.Node{PublicKey: "pubKey2"}, 38 | false, 39 | }, 40 | { 41 | "Error if selectedNodePubKey is not found", 42 | "nonexistentKey", 43 | models.ErrNodeNotFound, 44 | nil, 45 | false, 46 | }, 47 | } 48 | 49 | for _, tc := range testCases { 50 | t.Run(tc.Name, func(t *testing.T) { 51 | node, err := getNodeFromRequest(mockSession, tc.SelectedNodePubKey) 52 | assert.Equal(t, tc.ExpectedError, err) 53 | if tc.ExpectedRandom { 54 | assert.Contains(t, mockNodes, node) 55 | } else { 56 | assert.Equal(t, tc.ExpectedNode, node) 57 | } 58 | }) 59 | } 60 | } 61 | 62 | func TestGetRandomNodeOrError(t *testing.T) { 63 | mockNodes := []*models.Node{ 64 | {PublicKey: "pubKey1"}, 65 | {PublicKey: "pubKey2"}, 66 | {PublicKey: "pubKey3"}, 67 | } 68 | 69 | testCases := []struct { 70 | Name string 71 | Nodes []*models.Node 72 | ExpectedError error 73 | ExpectedNode *models.Node 74 | }{ 75 | { 76 | "Get random node successfully", 77 | mockNodes, 78 | nil, 79 | nil, 80 | }, 81 | { 82 | "Error if node list is empty", 83 | []*models.Node{}, 84 | models.ErrSessionHasZeroNodes, 85 | nil, 86 | }, 87 | } 88 | 89 | for _, tc := range testCases { 90 | t.Run(tc.Name, func(t *testing.T) { 91 | node, err := getRandomNodeOrError(tc.Nodes, tc.ExpectedError) 92 | 93 | assert.Equal(t, tc.ExpectedError, err) 94 | if err == nil { 95 | // use contains since random node to prevent flakiness 96 | assert.Contains(t, tc.Nodes, node) 97 | } 98 | }) 99 | } 100 | } 101 | 102 | func TestFindNodeOrError(t *testing.T) { 103 | mockNodes := []*models.Node{ 104 | {PublicKey: "pubKey1"}, 105 | {PublicKey: "pubKey2"}, 106 | {PublicKey: "pubKey3"}, 107 | } 108 | 109 | testCases := []struct { 110 | Name string 111 | Nodes []*models.Node 112 | PubKeyToFind string 113 | ExpectedError error 114 | ExpectedNode *models.Node 115 | }{ 116 | { 117 | "Find node by public key successfully", 118 | mockNodes, 119 | "pubKey2", 120 | nil, 121 | &models.Node{PublicKey: "pubKey2"}, 122 | }, 123 | { 124 | "Error if node is not found", 125 | mockNodes, 126 | "nonexistentKey", 127 | models.ErrNodeNotFound, 128 | nil, 129 | }, 130 | } 131 | 132 | for _, tc := range testCases { 133 | t.Run(tc.Name, func(t *testing.T) { 134 | node, err := findNodeOrError(tc.Nodes, tc.PubKeyToFind, tc.ExpectedError) 135 | 136 | assert.Equal(t, tc.ExpectedError, err) 137 | 138 | assert.Equal(t, tc.ExpectedNode, node) 139 | }) 140 | } 141 | } 142 | -------------------------------------------------------------------------------- /pkg/pokt/pokt_v0/get_session_from_request.go: -------------------------------------------------------------------------------- 1 | package pokt_v0 2 | 3 | import "github.com/pokt-network/gateway-server/pkg/pokt/pokt_v0/models" 4 | 5 | // GetSessionFromRequest obtains a session from a relay request. 6 | // Parameters: 7 | // - req: SendRelayRequest instance containing the relay request parameters. 8 | // 9 | // Returns: 10 | // - (*GetSessionResponse): Session response. 11 | // - (error): Error, if any. 12 | func GetSessionFromRequest(pocketService PocketDispatcher, req *models.SendRelayRequest) (*models.Session, error) { 13 | if req.Session != nil { 14 | return req.Session, nil 15 | } 16 | sessionResp, err := pocketService.GetSession(&models.GetSessionRequest{ 17 | AppPubKey: req.Signer.PublicKey, 18 | Chain: req.Chain, 19 | }) 20 | if err != nil { 21 | return nil, err 22 | } 23 | return sessionResp.Session, nil 24 | } 25 | -------------------------------------------------------------------------------- /pkg/pokt/pokt_v0/get_session_from_request_test.go: -------------------------------------------------------------------------------- 1 | package pokt_v0 2 | 3 | import ( 4 | "errors" 5 | pocket_service_mock "github.com/pokt-network/gateway-server/mocks/pocket_service" 6 | "github.com/pokt-network/gateway-server/pkg/pokt/pokt_v0/models" 7 | "github.com/stretchr/testify/assert" 8 | "github.com/stretchr/testify/mock" 9 | "testing" 10 | ) 11 | 12 | func TestGetSessionFromRequest(t *testing.T) { 13 | 14 | mockSession := &models.Session{ 15 | Nodes: []*models.Node{ 16 | { 17 | ServiceUrl: "example-node-1", 18 | PublicKey: "example-pub-key-1", 19 | }, 20 | }, 21 | SessionHeader: &models.SessionHeader{ 22 | SessionHeight: uint(1), 23 | }, 24 | } 25 | 26 | mockErr := errors.New("failure") 27 | 28 | type args struct { 29 | pocketService PocketService 30 | req *models.SendRelayRequest 31 | } 32 | tests := []struct { 33 | name string 34 | generateArgs func() args 35 | expectedSession *models.Session 36 | expectedErr error 37 | }{ 38 | { 39 | name: "SessionFromInnerRequest", 40 | generateArgs: func() args { 41 | return args{ 42 | pocketService: nil, 43 | req: &models.SendRelayRequest{Session: mockSession}, 44 | } 45 | }, 46 | expectedErr: nil, 47 | expectedSession: mockSession, 48 | }, 49 | { 50 | name: "SessionFromPocketServiceError", 51 | generateArgs: func() args { 52 | mockPocketService := new(pocket_service_mock.PocketService) 53 | mockPocketService.EXPECT().GetSession(mock.Anything).Return(nil, mockErr).Times(1) 54 | return args{ 55 | pocketService: mockPocketService, 56 | req: &models.SendRelayRequest{Session: nil, Signer: &models.Ed25519Account{}}, 57 | } 58 | }, 59 | expectedErr: mockErr, 60 | expectedSession: nil, 61 | }, 62 | { 63 | name: "SessionFromPocketServiceSuccess", 64 | generateArgs: func() args { 65 | mockPocketService := new(pocket_service_mock.PocketService) 66 | mockPocketService.EXPECT().GetSession(mock.Anything).Return(&models.GetSessionResponse{Session: mockSession}, nil).Times(1) 67 | return args{ 68 | pocketService: mockPocketService, 69 | req: &models.SendRelayRequest{Session: nil, Signer: &models.Ed25519Account{}}, 70 | } 71 | }, 72 | expectedErr: nil, 73 | expectedSession: mockSession, 74 | }, 75 | } 76 | for _, tt := range tests { 77 | t.Run(tt.name, func(t *testing.T) { 78 | args := tt.generateArgs() 79 | session, err := GetSessionFromRequest(args.pocketService, args.req) 80 | assert.Equal(t, err, tt.expectedErr) 81 | assert.Equal(t, session, tt.expectedSession) 82 | }) 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /pkg/pokt/pokt_v0/models/aat.go: -------------------------------------------------------------------------------- 1 | //go:generate ffjson $GOFILE 2 | package models 3 | 4 | import ( 5 | "github.com/pokt-network/gateway-server/pkg/common" 6 | ) 7 | 8 | type AAT struct { 9 | Version string `json:"version"` 10 | AppPubKey string `json:"app_pub_key"` 11 | ClientPubKey string `json:"client_pub_key"` 12 | Signature string `json:"signature"` 13 | } 14 | 15 | func (a AAT) Hash() string { 16 | return common.Sha3_256HashHex(a) 17 | } 18 | -------------------------------------------------------------------------------- /pkg/pokt/pokt_v0/models/application.go: -------------------------------------------------------------------------------- 1 | //go:generate ffjson $GOFILE 2 | package models 3 | 4 | import ( 5 | "encoding/json" 6 | "strconv" 7 | ) 8 | 9 | type PoktApplicationStatus uint 10 | 11 | const ( 12 | StatusJailed PoktApplicationStatus = 0 13 | StatusUnstaking PoktApplicationStatus = 1 14 | StatusStaked PoktApplicationStatus = 2 15 | ) 16 | 17 | // MaxRelays is a custom type for handling the MaxRelays field. 18 | type MaxRelays int 19 | 20 | // UnmarshalJSON customizes the JSON unmarshalling for MaxRelays. 21 | func (mr *MaxRelays) UnmarshalJSON(data []byte) error { 22 | var maxRelaysString string 23 | if err := json.Unmarshal(data, &maxRelaysString); err != nil { 24 | return err 25 | } 26 | 27 | // Parse the string into an integer 28 | maxRelays, err := strconv.Atoi(maxRelaysString) 29 | if err != nil { 30 | return err 31 | } 32 | 33 | // Set the MaxRelays field with the parsed integer 34 | *mr = MaxRelays(maxRelays) 35 | 36 | return nil 37 | } 38 | 39 | type GetApplicationResponse struct { 40 | Result []*PoktApplication `json:"result"` 41 | } 42 | 43 | type PoktApplication struct { 44 | Address string `json:"address"` 45 | Chains []string `json:"chains"` 46 | PublicKey string `json:"public_key"` 47 | Status PoktApplicationStatus `json:"status"` 48 | MaxRelays MaxRelays `json:"max_relays"` 49 | } 50 | -------------------------------------------------------------------------------- /pkg/pokt/pokt_v0/models/block.go: -------------------------------------------------------------------------------- 1 | //go:generate ffjson $GOFILE 2 | package models 3 | 4 | type GetLatestBlockHeightResponse struct { 5 | Height uint `json:"height"` 6 | } 7 | -------------------------------------------------------------------------------- /pkg/pokt/pokt_v0/models/block_ffjson.go: -------------------------------------------------------------------------------- 1 | // Code generated by ffjson . DO NOT EDIT. 2 | // source: block.go 3 | 4 | package models 5 | 6 | import ( 7 | "bytes" 8 | "fmt" 9 | fflib "github.com/pquerna/ffjson/fflib/v1" 10 | ) 11 | 12 | // MarshalJSON marshal bytes to json - template 13 | func (j *GetLatestBlockHeightResponse) MarshalJSON() ([]byte, error) { 14 | var buf fflib.Buffer 15 | if j == nil { 16 | buf.WriteString("null") 17 | return buf.Bytes(), nil 18 | } 19 | err := j.MarshalJSONBuf(&buf) 20 | if err != nil { 21 | return nil, err 22 | } 23 | return buf.Bytes(), nil 24 | } 25 | 26 | // MarshalJSONBuf marshal buff to json - template 27 | func (j *GetLatestBlockHeightResponse) MarshalJSONBuf(buf fflib.EncodingBuffer) error { 28 | if j == nil { 29 | buf.WriteString("null") 30 | return nil 31 | } 32 | var err error 33 | var obj []byte 34 | _ = obj 35 | _ = err 36 | buf.WriteString(`{"height":`) 37 | fflib.FormatBits2(buf, uint64(j.Height), 10, false) 38 | buf.WriteByte('}') 39 | return nil 40 | } 41 | 42 | const ( 43 | ffjtGetLatestBlockHeightResponsebase = iota 44 | ffjtGetLatestBlockHeightResponsenosuchkey 45 | 46 | ffjtGetLatestBlockHeightResponseHeight 47 | ) 48 | 49 | var ffjKeyGetLatestBlockHeightResponseHeight = []byte("height") 50 | 51 | // UnmarshalJSON umarshall json - template of ffjson 52 | func (j *GetLatestBlockHeightResponse) UnmarshalJSON(input []byte) error { 53 | fs := fflib.NewFFLexer(input) 54 | return j.UnmarshalJSONFFLexer(fs, fflib.FFParse_map_start) 55 | } 56 | 57 | // UnmarshalJSONFFLexer fast json unmarshall - template ffjson 58 | func (j *GetLatestBlockHeightResponse) UnmarshalJSONFFLexer(fs *fflib.FFLexer, state fflib.FFParseState) error { 59 | var err error 60 | currentKey := ffjtGetLatestBlockHeightResponsebase 61 | _ = currentKey 62 | tok := fflib.FFTok_init 63 | wantedTok := fflib.FFTok_init 64 | 65 | mainparse: 66 | for { 67 | tok = fs.Scan() 68 | // println(fmt.Sprintf("debug: tok: %v state: %v", tok, state)) 69 | if tok == fflib.FFTok_error { 70 | goto tokerror 71 | } 72 | 73 | switch state { 74 | 75 | case fflib.FFParse_map_start: 76 | if tok != fflib.FFTok_left_bracket { 77 | wantedTok = fflib.FFTok_left_bracket 78 | goto wrongtokenerror 79 | } 80 | state = fflib.FFParse_want_key 81 | continue 82 | 83 | case fflib.FFParse_after_value: 84 | if tok == fflib.FFTok_comma { 85 | state = fflib.FFParse_want_key 86 | } else if tok == fflib.FFTok_right_bracket { 87 | goto done 88 | } else { 89 | wantedTok = fflib.FFTok_comma 90 | goto wrongtokenerror 91 | } 92 | 93 | case fflib.FFParse_want_key: 94 | // json {} ended. goto exit. woo. 95 | if tok == fflib.FFTok_right_bracket { 96 | goto done 97 | } 98 | if tok != fflib.FFTok_string { 99 | wantedTok = fflib.FFTok_string 100 | goto wrongtokenerror 101 | } 102 | 103 | kn := fs.Output.Bytes() 104 | if len(kn) <= 0 { 105 | // "" case. hrm. 106 | currentKey = ffjtGetLatestBlockHeightResponsenosuchkey 107 | state = fflib.FFParse_want_colon 108 | goto mainparse 109 | } else { 110 | switch kn[0] { 111 | 112 | case 'h': 113 | 114 | if bytes.Equal(ffjKeyGetLatestBlockHeightResponseHeight, kn) { 115 | currentKey = ffjtGetLatestBlockHeightResponseHeight 116 | state = fflib.FFParse_want_colon 117 | goto mainparse 118 | } 119 | 120 | } 121 | 122 | if fflib.SimpleLetterEqualFold(ffjKeyGetLatestBlockHeightResponseHeight, kn) { 123 | currentKey = ffjtGetLatestBlockHeightResponseHeight 124 | state = fflib.FFParse_want_colon 125 | goto mainparse 126 | } 127 | 128 | currentKey = ffjtGetLatestBlockHeightResponsenosuchkey 129 | state = fflib.FFParse_want_colon 130 | goto mainparse 131 | } 132 | 133 | case fflib.FFParse_want_colon: 134 | if tok != fflib.FFTok_colon { 135 | wantedTok = fflib.FFTok_colon 136 | goto wrongtokenerror 137 | } 138 | state = fflib.FFParse_want_value 139 | continue 140 | case fflib.FFParse_want_value: 141 | 142 | if tok == fflib.FFTok_left_brace || tok == fflib.FFTok_left_bracket || tok == fflib.FFTok_integer || tok == fflib.FFTok_double || tok == fflib.FFTok_string || tok == fflib.FFTok_bool || tok == fflib.FFTok_null { 143 | switch currentKey { 144 | 145 | case ffjtGetLatestBlockHeightResponseHeight: 146 | goto handle_Height 147 | 148 | case ffjtGetLatestBlockHeightResponsenosuchkey: 149 | err = fs.SkipField(tok) 150 | if err != nil { 151 | return fs.WrapErr(err) 152 | } 153 | state = fflib.FFParse_after_value 154 | goto mainparse 155 | } 156 | } else { 157 | goto wantedvalue 158 | } 159 | } 160 | } 161 | 162 | handle_Height: 163 | 164 | /* handler: j.Height type=uint kind=uint quoted=false*/ 165 | 166 | { 167 | if tok != fflib.FFTok_integer && tok != fflib.FFTok_null { 168 | return fs.WrapErr(fmt.Errorf("cannot unmarshal %s into Go value for uint", tok)) 169 | } 170 | } 171 | 172 | { 173 | 174 | if tok == fflib.FFTok_null { 175 | 176 | } else { 177 | 178 | tval, err := fflib.ParseUint(fs.Output.Bytes(), 10, 64) 179 | 180 | if err != nil { 181 | return fs.WrapErr(err) 182 | } 183 | 184 | j.Height = uint(tval) 185 | 186 | } 187 | } 188 | 189 | state = fflib.FFParse_after_value 190 | goto mainparse 191 | 192 | wantedvalue: 193 | return fs.WrapErr(fmt.Errorf("wanted value token, but got token: %v", tok)) 194 | wrongtokenerror: 195 | return fs.WrapErr(fmt.Errorf("ffjson: wanted token: %v, but got token: %v output=%s", wantedTok, tok, fs.Output.String())) 196 | tokerror: 197 | if fs.BigError != nil { 198 | return fs.WrapErr(fs.BigError) 199 | } 200 | err = fs.Error.ToError() 201 | if err != nil { 202 | return fs.WrapErr(err) 203 | } 204 | panic("ffjson-generated: unreachable, please report bug.") 205 | done: 206 | 207 | return nil 208 | } 209 | -------------------------------------------------------------------------------- /pkg/pokt/pokt_v0/models/client_errors.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | import "errors" 4 | 5 | var ( 6 | ErrMissingFullNodes = errors.New("require full node host") 7 | ErrSessionHasZeroNodes = errors.New("session missing valid nodes") 8 | ErrNodeNotFound = errors.New("node not found") 9 | ErrMalformedSendRelayRequest = errors.New("malformed send relay request") 10 | ) 11 | -------------------------------------------------------------------------------- /pkg/pokt/pokt_v0/models/ed25519-account.go: -------------------------------------------------------------------------------- 1 | //go:generate ffjson $GOFILE 2 | package models 3 | 4 | import ( 5 | "crypto/ed25519" 6 | "encoding/hex" 7 | "errors" 8 | "github.com/pokt-network/gateway-server/pkg/common" 9 | "sync" 10 | ) 11 | 12 | // Ed25519Account represents an account using the Ed25519 cryptographic algorithm. 13 | type Ed25519Account struct { 14 | privateKeyBytes []byte 15 | aat *AAT 16 | aatOnce sync.Once 17 | PrivateKey string `json:"privateKey"` 18 | PublicKey string `json:"publicKey"` 19 | Address string `json:"address"` 20 | } 21 | 22 | const ( 23 | // CurrentAATVersion represents the current version of the Application Authentication Token (AAT). 24 | CurrentAATVersion = "0.0.1" 25 | 26 | // privKeyMaxLength represents the required length of the private key. 27 | privKeyMaxLength = 128 28 | ) 29 | 30 | var ( 31 | // ErrInvalidPrivateKey is returned when the private key is invalid. 32 | ErrInvalidPrivateKey = errors.New("invalid private key, requires 128 chars") 33 | ) 34 | 35 | // NewAccount creates a new Ed25519Account instance. 36 | // 37 | // Parameters: 38 | // - privateKey: Private key as a string. 39 | // 40 | // Returns: 41 | // - (*Ed25519Account): New Ed25519Account instance. 42 | // - (error): Error, if any. 43 | func NewAccount(privateKey string) (*Ed25519Account, error) { 44 | if len(privateKey) != privKeyMaxLength { 45 | return nil, ErrInvalidPrivateKey 46 | } 47 | publicKey := privateKey[64:] 48 | addr, err := common.GetAddressFromPublicKey(publicKey) 49 | if err != nil { 50 | return nil, err 51 | } 52 | privateKeyBytes, err := hex.DecodeString(privateKey) 53 | if err != nil { 54 | return nil, err 55 | } 56 | return &Ed25519Account{ 57 | privateKeyBytes: privateKeyBytes, 58 | PrivateKey: privateKey, 59 | PublicKey: publicKey, 60 | Address: addr, 61 | }, nil 62 | } 63 | 64 | // Sign signs a given message using the account's private key. 65 | // 66 | // Parameters: 67 | // - message: Message to sign. 68 | // 69 | // Returns: 70 | // - []byte: Signature. 71 | func (a *Ed25519Account) Sign(message []byte) []byte { 72 | return ed25519.Sign(a.privateKeyBytes, message) 73 | } 74 | 75 | // GetAAT retrieves the Application Authentication Token (AAT) associated with the account. 76 | // 77 | // Returns: 78 | // - (*AAT): AAT for the account. 79 | func (a *Ed25519Account) GetAAT() *AAT { 80 | a.aatOnce.Do(func() { 81 | aat := AAT{ 82 | Version: CurrentAATVersion, 83 | AppPubKey: a.PublicKey, 84 | ClientPubKey: a.PublicKey, 85 | Signature: "", 86 | } 87 | bytes := common.Sha3_256Hash(aat) 88 | aat.Signature = hex.EncodeToString(a.Sign(bytes)) 89 | a.aat = &aat 90 | }) 91 | return a.aat 92 | } 93 | -------------------------------------------------------------------------------- /pkg/pokt/pokt_v0/models/ed25519-account_test.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | import ( 4 | "encoding/hex" 5 | "github.com/stretchr/testify/assert" 6 | "testing" 7 | ) 8 | 9 | func TestNewAccount(t *testing.T) { 10 | tests := []struct { 11 | name string 12 | privateKey string 13 | expectedPublicKey string 14 | expectedAddress string 15 | err error 16 | }{ 17 | { 18 | name: "BadPrivateKey", 19 | privateKey: "badKey", 20 | expectedPublicKey: "", 21 | expectedAddress: "", 22 | err: ErrInvalidPrivateKey, 23 | }, 24 | { 25 | name: "Success", 26 | privateKey: "3fe64039816c44e8872e4ef981725b968422e3d49e95a1eb800707591df30fe374039dbe881dd2744e2e0c469cc2241e1e45f14af6975dd89079d22938377849", 27 | expectedPublicKey: "74039dbe881dd2744e2e0c469cc2241e1e45f14af6975dd89079d22938377849", 28 | expectedAddress: "9d6ad1ee870d32d12cf0cff9fb0fbbfedb2ee71f", 29 | err: nil, 30 | }, 31 | } 32 | for _, tt := range tests { 33 | t.Run(tt.name, func(t *testing.T) { 34 | acc, err := NewAccount(tt.privateKey) 35 | assert.Equal(t, err, tt.err) 36 | if err == nil { 37 | assert.Equal(t, acc.PublicKey, tt.expectedPublicKey) 38 | assert.Equal(t, acc.Address, tt.expectedAddress) 39 | } 40 | }) 41 | } 42 | } 43 | 44 | func TestEd25519Account_GetAAT(t *testing.T) { 45 | a, err := NewAccount("3fe64039816c44e8872e4ef981725b968422e3d49e95a1eb800707591df30fe374039dbe881dd2744e2e0c469cc2241e1e45f14af6975dd89079d22938377849") 46 | assert.Equal(t, err, nil) 47 | assert.Equal(t, &AAT{ 48 | Version: "0.0.1", 49 | AppPubKey: "74039dbe881dd2744e2e0c469cc2241e1e45f14af6975dd89079d22938377849", 50 | ClientPubKey: "74039dbe881dd2744e2e0c469cc2241e1e45f14af6975dd89079d22938377849", 51 | Signature: "f233ca857b4ada2ca4996e0da8c1761cfbc855edf282fc5a753d4631785946d6c2b08c781c84abbca2dc929de50008729079124e5c5c16921a81139279020a05", 52 | }, a.GetAAT()) 53 | } 54 | 55 | func TestEd25519Account_Sign(t *testing.T) { 56 | a, err := NewAccount("3fe64039816c44e8872e4ef981725b968422e3d49e95a1eb800707591df30fe374039dbe881dd2744e2e0c469cc2241e1e45f14af6975dd89079d22938377849") 57 | assert.Equal(t, err, nil) 58 | assert.Equal(t, hex.EncodeToString(a.Sign([]byte("TestMessage"))), "6cf23f8aa00793ef6aec4d3c408f5be249f01ddc96778f3ea03ef8fcdd301e09ce175fbcb97778b222de57469857d99ef97ad978dc49992f70a108aafd3d3001") 59 | } 60 | -------------------------------------------------------------------------------- /pkg/pokt/pokt_v0/models/pokt_errors.go: -------------------------------------------------------------------------------- 1 | //go:generate ffjson $GOFILE 2 | package models 3 | 4 | import ( 5 | "fmt" 6 | "regexp" 7 | "strconv" 8 | "strings" 9 | ) 10 | 11 | const modulePocketCore = "pocketcore" 12 | const moduleRoot = "sdk" 13 | 14 | var ( 15 | ErrPocketCoreInvalidBlockHeight = PocketSdkError{Codespace: modulePocketCore, Code: 60} 16 | ErrPocketCoreOverService = PocketSdkError{Codespace: modulePocketCore, Code: 71} 17 | ErrPocketEvidenceSealed = PocketSdkError{Codespace: modulePocketCore, Code: 90} 18 | ErrorServicerNotFound = PocketSdkError{Codespace: moduleRoot, Message: "Failed to find correct servicer PK"} 19 | ) 20 | 21 | var ( 22 | sdkErrorRegexCodespace = regexp.MustCompile(`codespace: (\w+)`) 23 | sdkErrorRegexCode = regexp.MustCompile(`code: (\d+)`) 24 | sdkErrorRegexMessage = regexp.MustCompile(`message: \\"(.+?)\\"`) 25 | ) 26 | 27 | type PocketRPCError struct { 28 | HttpCode int `json:"code"` 29 | Message string `json:"message"` 30 | } 31 | 32 | func (r PocketRPCError) Error() string { 33 | return fmt.Sprintf(`ERROR: HttpCode: %d Message: %s`, r.HttpCode, r.Message) 34 | } 35 | 36 | type PocketSdkError struct { 37 | Codespace string 38 | Code uint64 39 | Message string // Fallback if code does not exist 40 | } 41 | 42 | func (r PocketSdkError) Error() string { 43 | return fmt.Sprintf(`ERROR: Codespace: %s Code: %d`, r.Codespace, r.Code) 44 | } 45 | 46 | func (r PocketRPCError) ToSdkError() *PocketSdkError { 47 | msgLower := strings.ToLower(r.Message) 48 | codespaceMatches := sdkErrorRegexCodespace.FindStringSubmatch(msgLower) 49 | if len(codespaceMatches) != 2 { 50 | return nil 51 | } 52 | 53 | sdkError := PocketSdkError{ 54 | Codespace: codespaceMatches[1], 55 | } 56 | 57 | codeMatches := sdkErrorRegexCode.FindStringSubmatch(msgLower) 58 | if len(codeMatches) == 2 { 59 | code, err := strconv.ParseUint(codeMatches[1], 10, 0) 60 | if err == nil { 61 | sdkError.Code = code 62 | } 63 | } 64 | 65 | // If the code parsed is zero, then pocket core did not return a code 66 | // Internal errors do not have a code attached to it, so we should parse message 67 | if sdkError.Code == 0 { 68 | matchesMessage := sdkErrorRegexMessage.FindStringSubmatch(msgLower) 69 | if len(matchesMessage) == 2 { 70 | sdkError.Message = matchesMessage[1] 71 | } 72 | } 73 | 74 | return &sdkError 75 | } 76 | -------------------------------------------------------------------------------- /pkg/pokt/pokt_v0/models/relay.go: -------------------------------------------------------------------------------- 1 | //go:generate ffjson $GOFILE 2 | package models 3 | 4 | import ( 5 | "github.com/pokt-network/gateway-server/pkg/common" 6 | "time" 7 | ) 8 | 9 | type SendRelayRequest struct { 10 | Payload *Payload 11 | Signer *Ed25519Account 12 | Chain string 13 | SelectedNodePubKey string 14 | Session *Session 15 | Timeout *time.Duration 16 | } 17 | 18 | func (req SendRelayRequest) Validate() error { 19 | if req.Payload == nil || req.Signer == nil { 20 | return ErrMalformedSendRelayRequest 21 | } 22 | return nil 23 | } 24 | 25 | type SendRelayResponse struct { 26 | Response string `json:"response"` 27 | } 28 | 29 | // ffjson: skip 30 | type Payload struct { 31 | Data string `json:"data"` 32 | Method string `json:"method"` 33 | Path string `json:"path"` 34 | Headers map[string]string `json:"headers"` 35 | } 36 | 37 | // ffjson: skip 38 | type Relay struct { 39 | Payload *Payload `json:"payload"` 40 | Metadata *RelayMeta `json:"meta"` 41 | RelayProof *RelayProof `json:"proof"` 42 | } 43 | 44 | type RelayMeta struct { 45 | BlockHeight uint `json:"block_height"` 46 | } 47 | 48 | // RelayProof represents proof of a relay 49 | // ffjson: skip 50 | type RelayProof struct { 51 | Entropy uint64 `json:"entropy"` 52 | SessionBlockHeight uint `json:"session_block_height"` 53 | ServicerPubKey string `json:"servicer_pub_key"` 54 | Blockchain string `json:"blockchain"` 55 | AAT *AAT `json:"aat"` 56 | Signature string `json:"signature"` 57 | RequestHash string `json:"request_hash"` 58 | } 59 | 60 | // RequestHashPayload struct holding data needed to create a request hash 61 | // ffjson: skip 62 | type RequestHashPayload struct { 63 | Payload *Payload `json:"payload"` 64 | Metadata *RelayMeta `json:"meta"` 65 | } 66 | 67 | func (a *RequestHashPayload) Hash() string { 68 | return common.Sha3_256HashHex(a) 69 | } 70 | 71 | // ffjson: skip 72 | type RelayProofHashPayload struct { 73 | Entropy uint64 `json:"entropy"` 74 | SessionBlockHeight uint `json:"session_block_height"` 75 | ServicerPubKey string `json:"servicer_pub_key"` 76 | Blockchain string `json:"blockchain"` 77 | Signature string `json:"signature"` 78 | UnsignedAAT string `json:"token"` 79 | RequestHash string `json:"request_hash"` 80 | } 81 | -------------------------------------------------------------------------------- /pkg/pokt/pokt_v0/models/session.go: -------------------------------------------------------------------------------- 1 | //go:generate ffjson $GOFILE 2 | package models 3 | 4 | type Node struct { 5 | ServiceUrl string `json:"service_url"` 6 | PublicKey string `json:"public_key"` 7 | } 8 | 9 | type SessionHeader struct { 10 | SessionHeight uint `json:"session_height"` 11 | Chain string `json:"chain"` 12 | } 13 | 14 | type GetSessionResponse struct { 15 | Session *Session `json:"session"` 16 | } 17 | 18 | type Session struct { 19 | Nodes []*Node `json:"nodes"` 20 | SessionHeader *SessionHeader `json:"header"` 21 | } 22 | 23 | type GetSessionRequest struct { 24 | AppPubKey string `json:"app_public_key"` 25 | Chain string `json:"chain"` 26 | SessionHeight uint `json:"session_height"` 27 | } 28 | -------------------------------------------------------------------------------- /pkg/pokt/pokt_v0/service.go: -------------------------------------------------------------------------------- 1 | package pokt_v0 2 | 3 | import ( 4 | "github.com/pokt-network/gateway-server/pkg/pokt/pokt_v0/models" 5 | ) 6 | 7 | type PocketRelayer interface { 8 | SendRelay(req *models.SendRelayRequest) (*models.SendRelayResponse, error) 9 | } 10 | 11 | type PocketDispatcher interface { 12 | GetSession(req *models.GetSessionRequest) (*models.GetSessionResponse, error) 13 | } 14 | type PocketService interface { 15 | PocketRelayer 16 | PocketDispatcher 17 | GetLatestBlockHeight() (*models.GetLatestBlockHeightResponse, error) 18 | GetLatestStakedApplications() ([]*models.PoktApplication, error) 19 | } 20 | -------------------------------------------------------------------------------- /pkg/ttl_cache/ttl_cache.go: -------------------------------------------------------------------------------- 1 | package ttl_cache 2 | 3 | import ( 4 | "time" 5 | 6 | "github.com/jellydator/ttlcache/v3" 7 | ) 8 | 9 | type TTLCacheService[K comparable, V any] interface { 10 | Has(key K) bool 11 | Get(key K, opts ...ttlcache.Option[K, V]) *ttlcache.Item[K, V] 12 | Set(key K, value V, ttl time.Duration) *ttlcache.Item[K, V] 13 | Start() 14 | Items() map[K]*ttlcache.Item[K, V] 15 | DeleteExpired() 16 | } 17 | -------------------------------------------------------------------------------- /scripts/migration.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Set working directory to one level behind 4 | cd "$(dirname "$0")/.." 5 | 6 | # Parse command line arguments 7 | if [[ $# -eq 0 ]]; then 8 | echo "Error: Missing required argument --name, --up, or --down" 9 | exit 1 10 | fi 11 | 12 | UP_MIGRATION_NUMBER="" # Default to applying all migrations for up 13 | DOWN_MIGRATION_NUMBER="" # No default for down, must be explicitly provided 14 | 15 | while [[ $# -gt 0 ]] 16 | do 17 | key="$1" 18 | 19 | case $key in 20 | -n|--name) 21 | NAME="$2" 22 | shift # past argument 23 | shift # past value 24 | ;; 25 | -u|--up) 26 | UP="true" 27 | if [[ -n "$2" ]] && ! [[ "$2" =~ ^- ]]; then 28 | UP_MIGRATION_NUMBER="$2" 29 | shift 30 | fi 31 | shift # past argument 32 | ;; 33 | -d|--down) 34 | DOWN="true" 35 | if [[ -n "$2" ]]; then 36 | DOWN_MIGRATION_NUMBER="$2" 37 | shift 38 | else 39 | echo "Error: Down migration requires a specific number of migrations or --all flag to revert all migrations." 40 | exit 1 41 | fi 42 | shift # past argument 43 | ;; 44 | *) # unknown option 45 | echo "Unknown option: $1" 46 | exit 1 47 | ;; 48 | esac 49 | done 50 | 51 | # Load environment variables from .env file located one level behind 52 | if [ -f .env ]; then 53 | export $(cat .env | sed 's/#.*//g' | xargs) 54 | else 55 | echo "Error: .env file not found in parent directory." 56 | exit 1 57 | fi 58 | 59 | # Check if migrating up, down, or creating a new migration 60 | if [ "$UP" = "true" ]; then 61 | # Migrate up to a number of steps or to the latest version 62 | migrate -database "$DB_CONNECTION_URL" -path "db_migrations" up ${UP_MIGRATION_NUMBER} 63 | elif [ "$DOWN" = "true" ]; then 64 | # Migrate down to a number of steps or to the initial version 65 | migrate -database "$DB_CONNECTION_URL" -path "db_migrations" down ${DOWN_MIGRATION_NUMBER} 66 | else 67 | # Create new migration 68 | if [ -z "$NAME" ]; then 69 | echo "Error: Missing required argument --name" 70 | exit 1 71 | fi 72 | migrate -database "$DB_CONNECTION_URL" create -ext sql -seq -dir "db_migrations" "$NAME" 73 | fi 74 | -------------------------------------------------------------------------------- /scripts/mockgen.sh: -------------------------------------------------------------------------------- 1 | mockery --dir=./pkg/pokt/pokt_v0 --name=PocketService --filename=pocket_service_mock.go --output=./mocks/pocket_service --outpkg=pocket_service_mock --with-expecter 2 | mockery --dir=./pkg/ttl_cache --name=TTLCacheService --filename=ttl_cache_service_mock.go --output=./mocks/ttl_cache_service --outpkg=ttl_cache_service_mock --with-expecter 3 | mockery --dir=./internal/pokt_apps_registry --name=AppsRegistryService --filename=pokt_apps_registry_mock.go --output=./mocks/apps_registry --outpkg=app_registry_mock --with-expecter 4 | mockery --dir=./internal/session_registry --name=SessionRegistryService --filename=session_registry_mock.go --output=./mocks/session_registry --outpkg=session_registry_mock --with-expecter 5 | mockery --dir=./internal/chain_configurations_registry --name=ChainConfigurationsService --filename=chain_configurations_registry_mock.go --output=./mocks/chain_configurations_registry --outpkg=chain_configurations_registry_mock --with-expecter 6 | mockery --dir=./internal/node_selector_service --name=NodeSelectorService --filename=node_selector_mock.go --output=./mocks/node_selector --outpkg=node_selector_mock --with-expecter 7 | mockery --dir=./internal/apps_registry --name=AppsRegistryService --filename=app_registry_mock.go --output=./mocks/apps_registry --outpkg=apps_registry_mock --with-expecter 8 | mockery --dir=./internal/global_config --name=GlobalConfigProvider --filename=config_provider.go --output=./mocks/global_config --outpkg=global_config_mock --with-expecter 9 | -------------------------------------------------------------------------------- /scripts/querygen.sh: -------------------------------------------------------------------------------- 1 | # Set working directory to one level behind 2 | cd "$(dirname "$0")/.." 3 | 4 | # Load environment variables from .env file located one level behind 5 | if [ -f .env ]; then 6 | export $(cat .env | sed 's/#.*//g' | xargs) 7 | else 8 | echo "Error: .env file not found in parent directory." 9 | exit 1 10 | fi 11 | 12 | pggen gen go \ 13 | --postgres-connection "$DB_CONNECTION_URL" \ 14 | --query-glob internal/db_query/*.sql \ 15 | --go-type 'int8=int' \ 16 | --go-type 'text=string' --------------------------------------------------------------------------------