├── .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 |

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 | 
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 | 
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 | 
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 | 
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 | 
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'
--------------------------------------------------------------------------------