Compare commits
45 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 1bdcf9c3cf | |||
| 4ebb9cd239 | |||
| 620d68ea2f | |||
| b747308f74 | |||
| afdd721cc3 | |||
| 9f5c4c5968 | |||
| 9583a5c6c8 | |||
| da60fc0ade | |||
| 96b54c455e | |||
| cc983963d4 | |||
| 40d9321aec | |||
| e5a29cc76e | |||
| 56c55f6b95 | |||
| 2f051fd943 | |||
| c03cf86000 | |||
| ab11508235 | |||
| e65bfaeb31 | |||
| 5a6982fd10 | |||
| 7abe1f505c | |||
| 0ec2514edf | |||
| d6435a8270 | |||
| 9efc50e067 | |||
| 1532547e2b | |||
| 0cb11632e6 | |||
| f71ea52d5d | |||
| 338835698c | |||
| e65e261cd3 | |||
| 2d78f6939e | |||
| 9a45de5874 | |||
| 2f894b9be3 | |||
| d36ea20366 | |||
| 7b1200f338 | |||
| d291582128 | |||
| 18c6fd3e3e | |||
| d75c7eaaaf | |||
| 99cf7d1eec | |||
| f786dbeaa7 | |||
| eae76cce10 | |||
| 9341db5d08 | |||
| 75a5192c6d | |||
| 25ad0920cf | |||
| a1e75e1dff | |||
| e59a9a59b6 | |||
| 4c51a8975c | |||
| a4c6f51fe0 |
@@ -57,6 +57,12 @@ jobs:
|
|||||||
command: fmt
|
command: fmt
|
||||||
args: --all -- --check
|
args: --all -- --check
|
||||||
|
|
||||||
|
- name: Clippy
|
||||||
|
uses: actions-rs/cargo@v1
|
||||||
|
with:
|
||||||
|
command: clippy
|
||||||
|
args: --workspace --all-targets -- -D warnings
|
||||||
|
|
||||||
- name: Build all binaries
|
- name: Build all binaries
|
||||||
uses: actions-rs/cargo@v1
|
uses: actions-rs/cargo@v1
|
||||||
with:
|
with:
|
||||||
@@ -82,9 +88,3 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
command: test
|
command: test
|
||||||
args: --workspace -- --ignored
|
args: --workspace -- --ignored
|
||||||
|
|
||||||
- name: Clippy
|
|
||||||
uses: actions-rs/cargo@v1
|
|
||||||
with:
|
|
||||||
command: clippy
|
|
||||||
args: --workspace --all-targets -- -D warnings
|
|
||||||
|
|||||||
@@ -0,0 +1,55 @@
|
|||||||
|
name: Build and upload Data observatory container to harbor.nymte.ch
|
||||||
|
on:
|
||||||
|
workflow_dispatch:
|
||||||
|
|
||||||
|
env:
|
||||||
|
WORKING_DIRECTORY: "nym-data-observatory"
|
||||||
|
CONTAINER_NAME: "data-observatory"
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build-container:
|
||||||
|
runs-on: arc-ubuntu-22.04-dind
|
||||||
|
steps:
|
||||||
|
- name: Login to Harbor
|
||||||
|
uses: docker/login-action@v3
|
||||||
|
with:
|
||||||
|
registry: harbor.nymte.ch
|
||||||
|
username: ${{ secrets.HARBOR_ROBOT_USERNAME }}
|
||||||
|
password: ${{ secrets.HARBOR_ROBOT_SECRET }}
|
||||||
|
|
||||||
|
- name: Checkout repo
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Configure git identity
|
||||||
|
run: |
|
||||||
|
git config --global user.email "lawrence@nymtech.net"
|
||||||
|
git config --global user.name "Lawrence Stalder"
|
||||||
|
|
||||||
|
- name: Get version from cargo.toml
|
||||||
|
uses: mikefarah/yq@v4.44.3
|
||||||
|
id: get_version
|
||||||
|
with:
|
||||||
|
cmd: yq -oy '.package.version' ${{ env.WORKING_DIRECTORY }}/Cargo.toml
|
||||||
|
|
||||||
|
- name: Check if tag exists
|
||||||
|
run: |
|
||||||
|
if git rev-parse ${{ steps.get_version.outputs.value }} >/dev/null 2>&1; then
|
||||||
|
echo "Tag ${{ steps.get_version.outputs.value }} already exists"
|
||||||
|
fi
|
||||||
|
|
||||||
|
- name: Remove existing tag if exists
|
||||||
|
run: |
|
||||||
|
if git rev-parse ${{ env.WORKING_DIRECTORY }}-${{ steps.get_version.outputs.result }} >/dev/null 2>&1; then
|
||||||
|
git push --delete origin ${{ env.WORKING_DIRECTORY }}-${{ steps.get_version.outputs.result }}
|
||||||
|
git tag -d ${{ env.WORKING_DIRECTORY }}-${{ steps.get_version.outputs.result }}
|
||||||
|
fi
|
||||||
|
|
||||||
|
- name: Create tag
|
||||||
|
run: |
|
||||||
|
git tag -a ${{ env.WORKING_DIRECTORY }}-${{ steps.get_version.outputs.result }} -m "Version ${{ steps.get_version.outputs.result }}"
|
||||||
|
git push origin ${{ env.WORKING_DIRECTORY }}-${{ steps.get_version.outputs.result }}
|
||||||
|
|
||||||
|
- name: BuildAndPushImageOnHarbor
|
||||||
|
run: |
|
||||||
|
docker build -f ${{ env.WORKING_DIRECTORY }}/Dockerfile . -t harbor.nymte.ch/nym/${{ env.CONTAINER_NAME }}:${{ steps.get_version.outputs.result }} -t harbor.nymte.ch/nym/${{ env.CONTAINER_NAME }}:latest
|
||||||
|
docker push harbor.nymte.ch/nym/${{ env.CONTAINER_NAME }} --all-tags
|
||||||
@@ -0,0 +1,56 @@
|
|||||||
|
name: Build and upload Node Status agent container to harbor.nymte.ch
|
||||||
|
|
||||||
|
on:
|
||||||
|
workflow_dispatch:
|
||||||
|
|
||||||
|
env:
|
||||||
|
WORKING_DIRECTORY: "nym-node-status-agent"
|
||||||
|
CONTAINER_NAME: "node-status-agent"
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build-container:
|
||||||
|
runs-on: arc-ubuntu-22.04-dind
|
||||||
|
steps:
|
||||||
|
- name: Login to Harbor
|
||||||
|
uses: docker/login-action@v3
|
||||||
|
with:
|
||||||
|
registry: harbor.nymte.ch
|
||||||
|
username: ${{ secrets.HARBOR_ROBOT_USERNAME }}
|
||||||
|
password: ${{ secrets.HARBOR_ROBOT_SECRET }}
|
||||||
|
|
||||||
|
- name: Checkout repo
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Configure git identity
|
||||||
|
run: |
|
||||||
|
git config --global user.email "lawrence@nymtech.net"
|
||||||
|
git config --global user.name "Lawrence Stalder"
|
||||||
|
|
||||||
|
- name: Get version from cargo.toml
|
||||||
|
uses: mikefarah/yq@v4.44.3
|
||||||
|
id: get_version
|
||||||
|
with:
|
||||||
|
cmd: yq -oy '.package.version' ${{ env.WORKING_DIRECTORY }}/Cargo.toml
|
||||||
|
|
||||||
|
- name: Check if tag exists
|
||||||
|
run: |
|
||||||
|
if git rev-parse ${{ steps.get_version.outputs.value }} >/dev/null 2>&1; then
|
||||||
|
echo "Tag ${{ steps.get_version.outputs.value }} already exists"
|
||||||
|
fi
|
||||||
|
|
||||||
|
- name: Remove existing tag if exists
|
||||||
|
run: |
|
||||||
|
if git rev-parse ${{ env.WORKING_DIRECTORY }}-${{ steps.get_version.outputs.result }} >/dev/null 2>&1; then
|
||||||
|
git push --delete origin ${{ env.WORKING_DIRECTORY }}-${{ steps.get_version.outputs.result }}
|
||||||
|
git tag -d ${{ env.WORKING_DIRECTORY }}-${{ steps.get_version.outputs.result }}
|
||||||
|
fi
|
||||||
|
|
||||||
|
- name: Create tag
|
||||||
|
run: |
|
||||||
|
git tag -a ${{ env.WORKING_DIRECTORY }}-${{ steps.get_version.outputs.result }} -m "Version ${{ steps.get_version.outputs.result }}"
|
||||||
|
git push origin ${{ env.WORKING_DIRECTORY }}-${{ steps.get_version.outputs.result }}
|
||||||
|
|
||||||
|
- name: BuildAndPushImageOnHarbor
|
||||||
|
run: |
|
||||||
|
docker build -f ${{ env.WORKING_DIRECTORY }}/Dockerfile . -t harbor.nymte.ch/nym/${{ env.CONTAINER_NAME }}:${{ steps.get_version.outputs.result }} -t harbor.nymte.ch/nym/${{ env.CONTAINER_NAME }}:latest
|
||||||
|
docker push harbor.nymte.ch/nym/${{ env.CONTAINER_NAME }} --all-tags
|
||||||
@@ -1,11 +1,55 @@
|
|||||||
name: Build and upload Node Status API container to harbor.nymte.ch
|
name: Build and upload Node Status API container to harbor.nymte.ch
|
||||||
|
|
||||||
on:
|
on:
|
||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
|
|
||||||
|
env:
|
||||||
|
WORKING_DIRECTORY: "nym-node-status-api"
|
||||||
|
CONTAINER_NAME: "node-status-api"
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
my-job:
|
build-container:
|
||||||
runs-on: arc-ubuntu-22.04
|
runs-on: arc-ubuntu-22.04-dind
|
||||||
steps:
|
steps:
|
||||||
- name: my-step
|
- name: Login to Harbor
|
||||||
run: echo "Hello World!"
|
uses: docker/login-action@v3
|
||||||
|
with:
|
||||||
|
registry: harbor.nymte.ch
|
||||||
|
username: ${{ secrets.HARBOR_ROBOT_USERNAME }}
|
||||||
|
password: ${{ secrets.HARBOR_ROBOT_SECRET }}
|
||||||
|
|
||||||
|
- name: Checkout repo
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Configure git identity
|
||||||
|
run: |
|
||||||
|
git config --global user.email "lawrence@nymtech.net"
|
||||||
|
git config --global user.name "Lawrence Stalder"
|
||||||
|
|
||||||
|
- name: Get version from cargo.toml
|
||||||
|
uses: mikefarah/yq@v4.44.3
|
||||||
|
id: get_version
|
||||||
|
with:
|
||||||
|
cmd: yq -oy '.package.version' ${{ env.WORKING_DIRECTORY }}/Cargo.toml
|
||||||
|
|
||||||
|
- name: Check if tag exists
|
||||||
|
run: |
|
||||||
|
if git rev-parse ${{ env.WORKING_DIRECTORY }}-${{ steps.get_version.outputs.result }} >/dev/null 2>&1; then
|
||||||
|
echo "Tag ${{ steps.get_version.outputs.result }} already exists"
|
||||||
|
fi
|
||||||
|
|
||||||
|
- name: Remove existing tag if exists
|
||||||
|
run: |
|
||||||
|
if git rev-parse ${{ env.WORKING_DIRECTORY }}-${{ steps.get_version.outputs.result }} >/dev/null 2>&1; then
|
||||||
|
git push --delete origin ${{ env.WORKING_DIRECTORY }}-${{ steps.get_version.outputs.result }}
|
||||||
|
git tag -d ${{ env.WORKING_DIRECTORY }}-${{ steps.get_version.outputs.result }}
|
||||||
|
fi
|
||||||
|
|
||||||
|
- name: Create tag
|
||||||
|
run: |
|
||||||
|
git tag -a ${{ env.WORKING_DIRECTORY }}-${{ steps.get_version.outputs.result }} -m "Version ${{ steps.get_version.outputs.result }}"
|
||||||
|
git push origin ${{ env.WORKING_DIRECTORY }}-${{ steps.get_version.outputs.result }}
|
||||||
|
|
||||||
|
- name: BuildAndPushImageOnHarbor
|
||||||
|
run: |
|
||||||
|
docker build -f ${{ env.WORKING_DIRECTORY }}/Dockerfile . -t harbor.nymte.ch/nym/${{ env.CONTAINER_NAME }}:${{ steps.get_version.outputs.result }} -t harbor.nymte.ch/nym/${{ env.CONTAINER_NAME }}:latest
|
||||||
|
docker push harbor.nymte.ch/nym/${{ env.CONTAINER_NAME }} --all-tags
|
||||||
|
|||||||
@@ -4,6 +4,80 @@ Post 1.0.0 release, the changelog format is based on [Keep a Changelog](https://
|
|||||||
|
|
||||||
## [Unreleased]
|
## [Unreleased]
|
||||||
|
|
||||||
|
## [2024.12-aero] (2024-10-17)
|
||||||
|
|
||||||
|
- nym-node: don't use bloomfilters for double spending checks ([#4960])
|
||||||
|
- bugfix: replace unreachable macro with an error return ([#4958])
|
||||||
|
- [DOCs:/operators]: Update FAQ sphinx size ([#4946])
|
||||||
|
- [DOCs/operators]: Release notes v2024.11-wedel ([#4939])
|
||||||
|
- Fix handle drop ([#4934])
|
||||||
|
- Assume offline mode ([#4926])
|
||||||
|
- Make ip-packet-request VERSION pub ([#4925])
|
||||||
|
- Expose error type ([#4924])
|
||||||
|
- Fix argument to cargo-deny action ([#4922])
|
||||||
|
- Fix nymvpn.com url in mainnet defaults ([#4920])
|
||||||
|
- Check both version and type in message header ([#4918])
|
||||||
|
- Bump http-api-client default timeout to 30 sec ([#4917])
|
||||||
|
- Max/proxy ffi ([#4906])
|
||||||
|
- Data Observatory stub ([#4905])
|
||||||
|
- Fix missing duplication of modified tables ([#4904])
|
||||||
|
- Update cargo deny ([#4901])
|
||||||
|
- docs: add hostname instructions for wss ([#4900])
|
||||||
|
- build(deps): bump the patch-updates group across 1 directory with 9 updates ([#4898])
|
||||||
|
- Fix clippy for beta toolchain ([#4897])
|
||||||
|
- Remove clippy github PR annotations ([#4896])
|
||||||
|
- Fix apt install in ci-build-upload-binaries.yml ([#4894])
|
||||||
|
- Update network monitor entrypoint ([#4893])
|
||||||
|
- Update nym-vpn metapackage and replace nymvpn-x with nym-vpn-app ([#4889])
|
||||||
|
- Entry wireguard tickets ([#4888])
|
||||||
|
- Build and Push CI ([#4887])
|
||||||
|
- Feature/updated gateway registration ([#4885])
|
||||||
|
- Few fixes to NNM pre deploy ([#4883])
|
||||||
|
- Fix sql serde with enum ([#4875])
|
||||||
|
- allow clients to send stateless gateway requests without prior registration ([#4873])
|
||||||
|
- chore: remove queued migration for adding explicit admin ([#4871])
|
||||||
|
- Gateway database modifications for different modes ([#4868])
|
||||||
|
- build(deps): bump strum from 0.25.0 to 0.26.3 ([#4848])
|
||||||
|
- Use serde from workspace ([#4833])
|
||||||
|
- build(deps): bump toml from 0.5.11 to 0.8.14 ([#4805])
|
||||||
|
- Max/rust sdk stream abstraction ([#4743])
|
||||||
|
|
||||||
|
[#4960]: https://github.com/nymtech/nym/pull/4960
|
||||||
|
[#4958]: https://github.com/nymtech/nym/pull/4958
|
||||||
|
[#4946]: https://github.com/nymtech/nym/pull/4946
|
||||||
|
[#4939]: https://github.com/nymtech/nym/pull/4939
|
||||||
|
[#4934]: https://github.com/nymtech/nym/pull/4934
|
||||||
|
[#4926]: https://github.com/nymtech/nym/pull/4926
|
||||||
|
[#4925]: https://github.com/nymtech/nym/pull/4925
|
||||||
|
[#4924]: https://github.com/nymtech/nym/pull/4924
|
||||||
|
[#4922]: https://github.com/nymtech/nym/pull/4922
|
||||||
|
[#4920]: https://github.com/nymtech/nym/pull/4920
|
||||||
|
[#4918]: https://github.com/nymtech/nym/pull/4918
|
||||||
|
[#4917]: https://github.com/nymtech/nym/pull/4917
|
||||||
|
[#4906]: https://github.com/nymtech/nym/pull/4906
|
||||||
|
[#4905]: https://github.com/nymtech/nym/pull/4905
|
||||||
|
[#4904]: https://github.com/nymtech/nym/pull/4904
|
||||||
|
[#4901]: https://github.com/nymtech/nym/pull/4901
|
||||||
|
[#4900]: https://github.com/nymtech/nym/pull/4900
|
||||||
|
[#4898]: https://github.com/nymtech/nym/pull/4898
|
||||||
|
[#4897]: https://github.com/nymtech/nym/pull/4897
|
||||||
|
[#4896]: https://github.com/nymtech/nym/pull/4896
|
||||||
|
[#4894]: https://github.com/nymtech/nym/pull/4894
|
||||||
|
[#4893]: https://github.com/nymtech/nym/pull/4893
|
||||||
|
[#4889]: https://github.com/nymtech/nym/pull/4889
|
||||||
|
[#4888]: https://github.com/nymtech/nym/pull/4888
|
||||||
|
[#4887]: https://github.com/nymtech/nym/pull/4887
|
||||||
|
[#4885]: https://github.com/nymtech/nym/pull/4885
|
||||||
|
[#4883]: https://github.com/nymtech/nym/pull/4883
|
||||||
|
[#4875]: https://github.com/nymtech/nym/pull/4875
|
||||||
|
[#4873]: https://github.com/nymtech/nym/pull/4873
|
||||||
|
[#4871]: https://github.com/nymtech/nym/pull/4871
|
||||||
|
[#4868]: https://github.com/nymtech/nym/pull/4868
|
||||||
|
[#4848]: https://github.com/nymtech/nym/pull/4848
|
||||||
|
[#4833]: https://github.com/nymtech/nym/pull/4833
|
||||||
|
[#4805]: https://github.com/nymtech/nym/pull/4805
|
||||||
|
[#4743]: https://github.com/nymtech/nym/pull/4743
|
||||||
|
|
||||||
## [2024.11-wedel] (2024-09-23)
|
## [2024.11-wedel] (2024-09-23)
|
||||||
|
|
||||||
- Backport #4894 to fix ci ([#4899])
|
- Backport #4894 to fix ci ([#4899])
|
||||||
|
|||||||
Generated
+1073
-443
File diff suppressed because it is too large
Load Diff
+21
-8
@@ -54,12 +54,14 @@ members = [
|
|||||||
"common/exit-policy",
|
"common/exit-policy",
|
||||||
"common/gateway-requests",
|
"common/gateway-requests",
|
||||||
"common/gateway-storage",
|
"common/gateway-storage",
|
||||||
|
"common/gateway-stats-storage",
|
||||||
"common/http-api-client",
|
"common/http-api-client",
|
||||||
"common/http-api-common",
|
"common/http-api-common",
|
||||||
"common/inclusion-probability",
|
"common/inclusion-probability",
|
||||||
"common/ip-packet-requests",
|
"common/ip-packet-requests",
|
||||||
"common/ledger",
|
"common/ledger",
|
||||||
"common/mixnode-common",
|
"common/mixnode-common",
|
||||||
|
"common/models",
|
||||||
"common/network-defaults",
|
"common/network-defaults",
|
||||||
"common/node-tester-utils",
|
"common/node-tester-utils",
|
||||||
"common/nonexhaustive-delayqueue",
|
"common/nonexhaustive-delayqueue",
|
||||||
@@ -119,6 +121,8 @@ members = [
|
|||||||
"nym-node",
|
"nym-node",
|
||||||
"nym-node/nym-node-http-api",
|
"nym-node/nym-node-http-api",
|
||||||
"nym-node/nym-node-requests",
|
"nym-node/nym-node-requests",
|
||||||
|
"nym-node-status-api",
|
||||||
|
"nym-node-status-agent",
|
||||||
"nym-outfox",
|
"nym-outfox",
|
||||||
"nym-validator-rewarder",
|
"nym-validator-rewarder",
|
||||||
"tools/echo-server",
|
"tools/echo-server",
|
||||||
@@ -146,13 +150,16 @@ members = [
|
|||||||
default-members = [
|
default-members = [
|
||||||
"clients/native",
|
"clients/native",
|
||||||
"clients/socks5",
|
"clients/socks5",
|
||||||
|
"common/models",
|
||||||
"explorer-api",
|
"explorer-api",
|
||||||
"gateway",
|
"gateway",
|
||||||
"mixnode",
|
"mixnode",
|
||||||
"nym-api",
|
"nym-api",
|
||||||
"nym-data-observatory",
|
"nym-data-observatory",
|
||||||
"nym-node",
|
"nym-node",
|
||||||
|
"nym-node-status-api",
|
||||||
"nym-validator-rewarder",
|
"nym-validator-rewarder",
|
||||||
|
"nym-node-status-api",
|
||||||
"service-providers/authenticator",
|
"service-providers/authenticator",
|
||||||
"service-providers/ip-packet-router",
|
"service-providers/ip-packet-router",
|
||||||
"service-providers/network-requester",
|
"service-providers/network-requester",
|
||||||
@@ -183,7 +190,7 @@ aes = "0.8.1"
|
|||||||
aes-gcm = "0.10.1"
|
aes-gcm = "0.10.1"
|
||||||
aes-gcm-siv = "0.11.1"
|
aes-gcm-siv = "0.11.1"
|
||||||
aead = "0.5.2"
|
aead = "0.5.2"
|
||||||
anyhow = "1.0.89"
|
anyhow = "1.0.90"
|
||||||
argon2 = "0.5.0"
|
argon2 = "0.5.0"
|
||||||
async-trait = "0.1.83"
|
async-trait = "0.1.83"
|
||||||
axum = "0.7.5"
|
axum = "0.7.5"
|
||||||
@@ -208,7 +215,7 @@ chacha20 = "0.9.0"
|
|||||||
chacha20poly1305 = "0.10.1"
|
chacha20poly1305 = "0.10.1"
|
||||||
chrono = "0.4.31"
|
chrono = "0.4.31"
|
||||||
cipher = "0.4.3"
|
cipher = "0.4.3"
|
||||||
clap = "4.5.18"
|
clap = "4.5.20"
|
||||||
clap_complete = "4.5"
|
clap_complete = "4.5"
|
||||||
clap_complete_fig = "4.5"
|
clap_complete_fig = "4.5"
|
||||||
colored = "2.0"
|
colored = "2.0"
|
||||||
@@ -233,10 +240,12 @@ dotenvy = "0.15.6"
|
|||||||
ecdsa = "0.16"
|
ecdsa = "0.16"
|
||||||
ed25519-dalek = "2.1"
|
ed25519-dalek = "2.1"
|
||||||
etherparse = "0.13.0"
|
etherparse = "0.13.0"
|
||||||
|
envy = "0.4"
|
||||||
eyre = "0.6.9"
|
eyre = "0.6.9"
|
||||||
fastrand = "2.1.1"
|
fastrand = "2.1.1"
|
||||||
flate2 = "1.0.34"
|
flate2 = "1.0.34"
|
||||||
futures = "0.3.28"
|
futures = "0.3.28"
|
||||||
|
futures-util = "0.3"
|
||||||
generic-array = "0.14.7"
|
generic-array = "0.14.7"
|
||||||
getrandom = "0.2.10"
|
getrandom = "0.2.10"
|
||||||
getset = "0.1.3"
|
getset = "0.1.3"
|
||||||
@@ -266,6 +275,7 @@ ledger-transport-hid = "0.10.0"
|
|||||||
log = "0.4"
|
log = "0.4"
|
||||||
maxminddb = "0.23.0"
|
maxminddb = "0.23.0"
|
||||||
mime = "0.3.17"
|
mime = "0.3.17"
|
||||||
|
moka = { version = "0.12", features = ["future"] }
|
||||||
nix = "0.27.1"
|
nix = "0.27.1"
|
||||||
notify = "5.1.0"
|
notify = "5.1.0"
|
||||||
okapi = "0.7.0"
|
okapi = "0.7.0"
|
||||||
@@ -275,7 +285,7 @@ opentelemetry-jaeger = "0.18.0"
|
|||||||
parking_lot = "0.12.3"
|
parking_lot = "0.12.3"
|
||||||
pem = "0.8"
|
pem = "0.8"
|
||||||
petgraph = "0.6.5"
|
petgraph = "0.6.5"
|
||||||
pin-project = "1.0"
|
pin-project = "1.1"
|
||||||
pin-project-lite = "0.2.14"
|
pin-project-lite = "0.2.14"
|
||||||
pretty_env_logger = "0.4.0"
|
pretty_env_logger = "0.4.0"
|
||||||
publicsuffix = "2.2.3"
|
publicsuffix = "2.2.3"
|
||||||
@@ -295,10 +305,11 @@ rocket_okapi = "0.8.0"
|
|||||||
safer-ffi = "0.1.13"
|
safer-ffi = "0.1.13"
|
||||||
schemars = "0.8.21"
|
schemars = "0.8.21"
|
||||||
semver = "1.0.23"
|
semver = "1.0.23"
|
||||||
serde = "1.0.210"
|
serde = "1.0.211"
|
||||||
serde_bytes = "0.11.15"
|
serde_bytes = "0.11.15"
|
||||||
serde_derive = "1.0"
|
serde_derive = "1.0"
|
||||||
serde_json = "1.0.128"
|
serde_json = "1.0.132"
|
||||||
|
serde_json_path = "0.6.7"
|
||||||
serde_repr = "0.1"
|
serde_repr = "0.1"
|
||||||
serde_with = "3.9.0"
|
serde_with = "3.9.0"
|
||||||
serde_yaml = "0.9.25"
|
serde_yaml = "0.9.25"
|
||||||
@@ -307,6 +318,7 @@ si-scale = "0.2.3"
|
|||||||
sphinx-packet = "0.1.1"
|
sphinx-packet = "0.1.1"
|
||||||
sqlx = "0.7.4"
|
sqlx = "0.7.4"
|
||||||
strum = "0.26"
|
strum = "0.26"
|
||||||
|
strum_macros = "0.26"
|
||||||
subtle-encoding = "0.5"
|
subtle-encoding = "0.5"
|
||||||
syn = "1"
|
syn = "1"
|
||||||
sysinfo = "0.30.13"
|
sysinfo = "0.30.13"
|
||||||
@@ -328,6 +340,7 @@ tracing = "0.1.37"
|
|||||||
tracing-opentelemetry = "0.19.0"
|
tracing-opentelemetry = "0.19.0"
|
||||||
tracing-subscriber = "0.3.16"
|
tracing-subscriber = "0.3.16"
|
||||||
tracing-tree = "0.2.2"
|
tracing-tree = "0.2.2"
|
||||||
|
tracing-log = "0.2"
|
||||||
ts-rs = "10.0.0"
|
ts-rs = "10.0.0"
|
||||||
tungstenite = { version = "0.20.1", default-features = false }
|
tungstenite = { version = "0.20.1", default-features = false }
|
||||||
url = "2.5"
|
url = "2.5"
|
||||||
@@ -389,10 +402,10 @@ indexed_db_futures = { git = "https://github.com/TiemenSch/rust-indexed-db", bra
|
|||||||
js-sys = "0.3.70"
|
js-sys = "0.3.70"
|
||||||
serde-wasm-bindgen = "0.6.5"
|
serde-wasm-bindgen = "0.6.5"
|
||||||
tsify = "0.4.5"
|
tsify = "0.4.5"
|
||||||
wasm-bindgen = "0.2.93"
|
wasm-bindgen = "0.2.95"
|
||||||
wasm-bindgen-futures = "0.4.43"
|
wasm-bindgen-futures = "0.4.45"
|
||||||
wasmtimer = "0.2.0"
|
wasmtimer = "0.2.0"
|
||||||
web-sys = "0.3.70"
|
web-sys = "0.3.72"
|
||||||
|
|
||||||
|
|
||||||
# Profile settings for individual crates
|
# Profile settings for individual crates
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "nym-client"
|
name = "nym-client"
|
||||||
version = "1.1.41"
|
version = "1.1.42"
|
||||||
authors = ["Dave Hrycyszyn <futurechimp@users.noreply.github.com>", "Jędrzej Stuczyński <andrew@nymtech.net>"]
|
authors = ["Dave Hrycyszyn <futurechimp@users.noreply.github.com>", "Jędrzej Stuczyński <andrew@nymtech.net>"]
|
||||||
description = "Implementation of the Nym Client"
|
description = "Implementation of the Nym Client"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "nym-socks5-client"
|
name = "nym-socks5-client"
|
||||||
version = "1.1.41"
|
version = "1.1.42"
|
||||||
authors = ["Dave Hrycyszyn <futurechimp@users.noreply.github.com>"]
|
authors = ["Dave Hrycyszyn <futurechimp@users.noreply.github.com>"]
|
||||||
description = "A SOCKS5 localhost proxy that converts incoming messages to Sphinx and sends them to a Nym address"
|
description = "A SOCKS5 localhost proxy that converts incoming messages to Sphinx and sends them to a Nym address"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
|
|||||||
@@ -45,3 +45,4 @@ tracing = [
|
|||||||
"opentelemetry",
|
"opentelemetry",
|
||||||
]
|
]
|
||||||
clap = [ "dep:clap", "dep:clap_complete", "dep:clap_complete_fig" ]
|
clap = [ "dep:clap", "dep:clap_complete", "dep:clap_complete_fig" ]
|
||||||
|
models = []
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ serde = { workspace = true, features = ["derive"] }
|
|||||||
serde_json = { workspace = true }
|
serde_json = { workspace = true }
|
||||||
nym-http-api-client = { path = "../../../common/http-api-client" }
|
nym-http-api-client = { path = "../../../common/http-api-client" }
|
||||||
thiserror = { workspace = true }
|
thiserror = { workspace = true }
|
||||||
log = { workspace = true }
|
tracing = { workspace = true }
|
||||||
url = { workspace = true, features = ["serde"] }
|
url = { workspace = true, features = ["serde"] }
|
||||||
tokio = { workspace = true, features = ["sync", "time"] }
|
tokio = { workspace = true, features = ["sync", "time"] }
|
||||||
time = { workspace = true, features = ["formatting"] }
|
time = { workspace = true, features = ["formatting"] }
|
||||||
|
|||||||
@@ -265,6 +265,13 @@ impl NymApiClient {
|
|||||||
NymApiClient { nym_api }
|
NymApiClient { nym_api }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(not(target_arch = "wasm32"))]
|
||||||
|
pub fn new_with_timeout(api_url: Url, timeout: std::time::Duration) -> Self {
|
||||||
|
let nym_api = nym_api::Client::new(api_url, Some(timeout));
|
||||||
|
|
||||||
|
NymApiClient { nym_api }
|
||||||
|
}
|
||||||
|
|
||||||
pub fn new_with_user_agent(api_url: Url, user_agent: UserAgent) -> Self {
|
pub fn new_with_user_agent(api_url: Url, user_agent: UserAgent) -> Self {
|
||||||
let nym_api = nym_api::Client::builder::<_, ValidatorClientError>(api_url)
|
let nym_api = nym_api::Client::builder::<_, ValidatorClientError>(api_url)
|
||||||
.expect("invalid api url")
|
.expect("invalid api url")
|
||||||
|
|||||||
@@ -121,36 +121,36 @@ async fn test_nyxd_connection(
|
|||||||
{
|
{
|
||||||
Ok(Err(NyxdError::TendermintErrorRpc(e))) => {
|
Ok(Err(NyxdError::TendermintErrorRpc(e))) => {
|
||||||
// If we get a tendermint-rpc error, we classify the node as not contactable
|
// If we get a tendermint-rpc error, we classify the node as not contactable
|
||||||
log::warn!("Checking: nyxd url: {url}: {}: {}", "failed".red(), e);
|
tracing::warn!("Checking: nyxd url: {url}: {}: {}", "failed".red(), e);
|
||||||
false
|
false
|
||||||
}
|
}
|
||||||
Ok(Err(NyxdError::AbciError { code, log, .. })) => {
|
Ok(Err(NyxdError::AbciError { code, log, .. })) => {
|
||||||
// We accept the mixnet contract not found as ok from a connection standpoint. This happens
|
// We accept the mixnet contract not found as ok from a connection standpoint. This happens
|
||||||
// for example on a pre-launch network.
|
// for example on a pre-launch network.
|
||||||
log::debug!(
|
tracing::debug!(
|
||||||
"Checking: nyxd url: {url}: {}, but with abci error: {code}: {log}",
|
"Checking: nyxd url: {url}: {}, but with abci error: {code}: {log}",
|
||||||
"success".green()
|
"success".green()
|
||||||
);
|
);
|
||||||
code == 18
|
code == 18
|
||||||
}
|
}
|
||||||
Ok(Err(error @ NyxdError::NoContractAddressAvailable(_))) => {
|
Ok(Err(error @ NyxdError::NoContractAddressAvailable(_))) => {
|
||||||
log::warn!("Checking: nyxd url: {url}: {}: {error}", "failed".red());
|
tracing::warn!("Checking: nyxd url: {url}: {}: {error}", "failed".red());
|
||||||
false
|
false
|
||||||
}
|
}
|
||||||
Ok(Err(e)) => {
|
Ok(Err(e)) => {
|
||||||
// For any other error, we're optimistic and just try anyway.
|
// For any other error, we're optimistic and just try anyway.
|
||||||
log::warn!(
|
tracing::warn!(
|
||||||
"Checking: nyxd_url: {url}: {}, but with error: {e}",
|
"Checking: nyxd_url: {url}: {}, but with error: {e}",
|
||||||
"success".green()
|
"success".green()
|
||||||
);
|
);
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
Ok(Ok(_)) => {
|
Ok(Ok(_)) => {
|
||||||
log::debug!("Checking: nyxd_url: {url}: {}", "success".green());
|
tracing::debug!("Checking: nyxd_url: {url}: {}", "success".green());
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
log::warn!("Checking: nyxd_url: {url}: {}: {e}", "failed".red());
|
tracing::warn!("Checking: nyxd_url: {url}: {}: {e}", "failed".red());
|
||||||
false
|
false
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -169,15 +169,15 @@ async fn test_nym_api_connection(
|
|||||||
.await
|
.await
|
||||||
{
|
{
|
||||||
Ok(Ok(_)) => {
|
Ok(Ok(_)) => {
|
||||||
log::debug!("Checking: api_url: {url}: {}", "success".green());
|
tracing::debug!("Checking: api_url: {url}: {}", "success".green());
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
Ok(Err(e)) => {
|
Ok(Err(e)) => {
|
||||||
log::debug!("Checking: api_url: {url}: {}: {e}", "failed".red());
|
tracing::debug!("Checking: api_url: {url}: {}: {e}", "failed".red());
|
||||||
false
|
false
|
||||||
}
|
}
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
log::debug!("Checking: api_url: {url}: {}: {e}", "failed".red());
|
tracing::debug!("Checking: api_url: {url}: {}: {e}", "failed".red());
|
||||||
false
|
false
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -41,6 +41,7 @@ use nym_mixnet_contract_common::mixnode::MixNodeDetails;
|
|||||||
use nym_mixnet_contract_common::{GatewayBond, IdentityKeyRef, NodeId};
|
use nym_mixnet_contract_common::{GatewayBond, IdentityKeyRef, NodeId};
|
||||||
use time::format_description::BorrowedFormatItem;
|
use time::format_description::BorrowedFormatItem;
|
||||||
use time::Date;
|
use time::Date;
|
||||||
|
use tracing::instrument;
|
||||||
|
|
||||||
pub mod error;
|
pub mod error;
|
||||||
pub mod routes;
|
pub mod routes;
|
||||||
@@ -52,11 +53,13 @@ pub fn rfc_3339_date() -> Vec<BorrowedFormatItem<'static>> {
|
|||||||
#[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
|
#[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
|
||||||
#[cfg_attr(not(target_arch = "wasm32"), async_trait)]
|
#[cfg_attr(not(target_arch = "wasm32"), async_trait)]
|
||||||
pub trait NymApiClientExt: ApiClient {
|
pub trait NymApiClientExt: ApiClient {
|
||||||
|
#[instrument(level = "debug", skip(self))]
|
||||||
async fn get_mixnodes(&self) -> Result<Vec<MixNodeDetails>, NymAPIError> {
|
async fn get_mixnodes(&self) -> Result<Vec<MixNodeDetails>, NymAPIError> {
|
||||||
self.get_json(&[routes::API_VERSION, routes::MIXNODES], NO_PARAMS)
|
self.get_json(&[routes::API_VERSION, routes::MIXNODES], NO_PARAMS)
|
||||||
.await
|
.await
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[instrument(level = "debug", skip(self))]
|
||||||
async fn get_mixnodes_detailed(&self) -> Result<Vec<MixNodeBondAnnotated>, NymAPIError> {
|
async fn get_mixnodes_detailed(&self) -> Result<Vec<MixNodeBondAnnotated>, NymAPIError> {
|
||||||
self.get_json(
|
self.get_json(
|
||||||
&[
|
&[
|
||||||
@@ -70,6 +73,7 @@ pub trait NymApiClientExt: ApiClient {
|
|||||||
.await
|
.await
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[instrument(level = "debug", skip(self))]
|
||||||
async fn get_gateways_detailed(&self) -> Result<Vec<GatewayBondAnnotated>, NymAPIError> {
|
async fn get_gateways_detailed(&self) -> Result<Vec<GatewayBondAnnotated>, NymAPIError> {
|
||||||
self.get_json(
|
self.get_json(
|
||||||
&[
|
&[
|
||||||
@@ -83,6 +87,7 @@ pub trait NymApiClientExt: ApiClient {
|
|||||||
.await
|
.await
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[instrument(level = "debug", skip(self))]
|
||||||
async fn get_mixnodes_detailed_unfiltered(
|
async fn get_mixnodes_detailed_unfiltered(
|
||||||
&self,
|
&self,
|
||||||
) -> Result<Vec<MixNodeBondAnnotated>, NymAPIError> {
|
) -> Result<Vec<MixNodeBondAnnotated>, NymAPIError> {
|
||||||
@@ -98,11 +103,13 @@ pub trait NymApiClientExt: ApiClient {
|
|||||||
.await
|
.await
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[instrument(level = "debug", skip(self))]
|
||||||
async fn get_gateways(&self) -> Result<Vec<GatewayBond>, NymAPIError> {
|
async fn get_gateways(&self) -> Result<Vec<GatewayBond>, NymAPIError> {
|
||||||
self.get_json(&[routes::API_VERSION, routes::GATEWAYS], NO_PARAMS)
|
self.get_json(&[routes::API_VERSION, routes::GATEWAYS], NO_PARAMS)
|
||||||
.await
|
.await
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[instrument(level = "debug", skip(self))]
|
||||||
async fn get_gateways_described(&self) -> Result<Vec<LegacyDescribedGateway>, NymAPIError> {
|
async fn get_gateways_described(&self) -> Result<Vec<LegacyDescribedGateway>, NymAPIError> {
|
||||||
self.get_json(
|
self.get_json(
|
||||||
&[routes::API_VERSION, routes::GATEWAYS, routes::DESCRIBED],
|
&[routes::API_VERSION, routes::GATEWAYS, routes::DESCRIBED],
|
||||||
@@ -111,6 +118,7 @@ pub trait NymApiClientExt: ApiClient {
|
|||||||
.await
|
.await
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[instrument(level = "debug", skip(self))]
|
||||||
async fn get_mixnodes_described(&self) -> Result<Vec<LegacyDescribedMixNode>, NymAPIError> {
|
async fn get_mixnodes_described(&self) -> Result<Vec<LegacyDescribedMixNode>, NymAPIError> {
|
||||||
self.get_json(
|
self.get_json(
|
||||||
&[routes::API_VERSION, routes::MIXNODES, routes::DESCRIBED],
|
&[routes::API_VERSION, routes::MIXNODES, routes::DESCRIBED],
|
||||||
@@ -119,6 +127,7 @@ pub trait NymApiClientExt: ApiClient {
|
|||||||
.await
|
.await
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[tracing::instrument(level = "debug", skip_all)]
|
||||||
async fn get_basic_mixnodes(
|
async fn get_basic_mixnodes(
|
||||||
&self,
|
&self,
|
||||||
semver_compatibility: Option<String>,
|
semver_compatibility: Option<String>,
|
||||||
@@ -142,6 +151,7 @@ pub trait NymApiClientExt: ApiClient {
|
|||||||
.await
|
.await
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[instrument(level = "debug", skip(self))]
|
||||||
async fn get_basic_gateways(
|
async fn get_basic_gateways(
|
||||||
&self,
|
&self,
|
||||||
semver_compatibility: Option<String>,
|
semver_compatibility: Option<String>,
|
||||||
@@ -167,6 +177,7 @@ pub trait NymApiClientExt: ApiClient {
|
|||||||
|
|
||||||
/// retrieve basic information for nodes are capable of operating as an entry gateway
|
/// retrieve basic information for nodes are capable of operating as an entry gateway
|
||||||
/// this includes legacy gateways and nym-nodes
|
/// this includes legacy gateways and nym-nodes
|
||||||
|
#[instrument(level = "debug", skip(self))]
|
||||||
async fn get_all_basic_entry_assigned_nodes(
|
async fn get_all_basic_entry_assigned_nodes(
|
||||||
&self,
|
&self,
|
||||||
semver_compatibility: Option<String>,
|
semver_compatibility: Option<String>,
|
||||||
@@ -208,6 +219,7 @@ pub trait NymApiClientExt: ApiClient {
|
|||||||
|
|
||||||
/// retrieve basic information for nodes that got assigned 'mixing' node in this epoch
|
/// retrieve basic information for nodes that got assigned 'mixing' node in this epoch
|
||||||
/// this includes legacy mixnodes and nym-nodes
|
/// this includes legacy mixnodes and nym-nodes
|
||||||
|
#[instrument(level = "debug", skip(self))]
|
||||||
async fn get_basic_active_mixing_assigned_nodes(
|
async fn get_basic_active_mixing_assigned_nodes(
|
||||||
&self,
|
&self,
|
||||||
semver_compatibility: Option<String>,
|
semver_compatibility: Option<String>,
|
||||||
@@ -247,6 +259,7 @@ pub trait NymApiClientExt: ApiClient {
|
|||||||
.await
|
.await
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[instrument(level = "debug", skip(self))]
|
||||||
async fn get_active_mixnodes(&self) -> Result<Vec<MixNodeDetails>, NymAPIError> {
|
async fn get_active_mixnodes(&self) -> Result<Vec<MixNodeDetails>, NymAPIError> {
|
||||||
self.get_json(
|
self.get_json(
|
||||||
&[routes::API_VERSION, routes::MIXNODES, routes::ACTIVE],
|
&[routes::API_VERSION, routes::MIXNODES, routes::ACTIVE],
|
||||||
@@ -255,6 +268,7 @@ pub trait NymApiClientExt: ApiClient {
|
|||||||
.await
|
.await
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[instrument(level = "debug", skip(self))]
|
||||||
async fn get_active_mixnodes_detailed(&self) -> Result<Vec<MixNodeBondAnnotated>, NymAPIError> {
|
async fn get_active_mixnodes_detailed(&self) -> Result<Vec<MixNodeBondAnnotated>, NymAPIError> {
|
||||||
self.get_json(
|
self.get_json(
|
||||||
&[
|
&[
|
||||||
@@ -269,6 +283,7 @@ pub trait NymApiClientExt: ApiClient {
|
|||||||
.await
|
.await
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[instrument(level = "debug", skip(self))]
|
||||||
async fn get_rewarded_mixnodes(&self) -> Result<Vec<MixNodeDetails>, NymAPIError> {
|
async fn get_rewarded_mixnodes(&self) -> Result<Vec<MixNodeDetails>, NymAPIError> {
|
||||||
self.get_json(
|
self.get_json(
|
||||||
&[routes::API_VERSION, routes::MIXNODES, routes::REWARDED],
|
&[routes::API_VERSION, routes::MIXNODES, routes::REWARDED],
|
||||||
@@ -277,6 +292,7 @@ pub trait NymApiClientExt: ApiClient {
|
|||||||
.await
|
.await
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[instrument(level = "debug", skip(self))]
|
||||||
async fn get_mixnode_report(
|
async fn get_mixnode_report(
|
||||||
&self,
|
&self,
|
||||||
mix_id: NodeId,
|
mix_id: NodeId,
|
||||||
@@ -294,6 +310,7 @@ pub trait NymApiClientExt: ApiClient {
|
|||||||
.await
|
.await
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[instrument(level = "debug", skip(self))]
|
||||||
async fn get_gateway_report(
|
async fn get_gateway_report(
|
||||||
&self,
|
&self,
|
||||||
identity: IdentityKeyRef<'_>,
|
identity: IdentityKeyRef<'_>,
|
||||||
@@ -311,6 +328,7 @@ pub trait NymApiClientExt: ApiClient {
|
|||||||
.await
|
.await
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[instrument(level = "debug", skip(self))]
|
||||||
async fn get_mixnode_history(
|
async fn get_mixnode_history(
|
||||||
&self,
|
&self,
|
||||||
mix_id: NodeId,
|
mix_id: NodeId,
|
||||||
@@ -328,6 +346,7 @@ pub trait NymApiClientExt: ApiClient {
|
|||||||
.await
|
.await
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[instrument(level = "debug", skip(self))]
|
||||||
async fn get_gateway_history(
|
async fn get_gateway_history(
|
||||||
&self,
|
&self,
|
||||||
identity: IdentityKeyRef<'_>,
|
identity: IdentityKeyRef<'_>,
|
||||||
@@ -345,6 +364,7 @@ pub trait NymApiClientExt: ApiClient {
|
|||||||
.await
|
.await
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[instrument(level = "debug", skip(self))]
|
||||||
async fn get_rewarded_mixnodes_detailed(
|
async fn get_rewarded_mixnodes_detailed(
|
||||||
&self,
|
&self,
|
||||||
) -> Result<Vec<MixNodeBondAnnotated>, NymAPIError> {
|
) -> Result<Vec<MixNodeBondAnnotated>, NymAPIError> {
|
||||||
@@ -361,6 +381,7 @@ pub trait NymApiClientExt: ApiClient {
|
|||||||
.await
|
.await
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[instrument(level = "debug", skip(self))]
|
||||||
async fn get_gateway_core_status_count(
|
async fn get_gateway_core_status_count(
|
||||||
&self,
|
&self,
|
||||||
identity: IdentityKeyRef<'_>,
|
identity: IdentityKeyRef<'_>,
|
||||||
@@ -392,6 +413,7 @@ pub trait NymApiClientExt: ApiClient {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[instrument(level = "debug", skip(self))]
|
||||||
async fn get_mixnode_core_status_count(
|
async fn get_mixnode_core_status_count(
|
||||||
&self,
|
&self,
|
||||||
mix_id: NodeId,
|
mix_id: NodeId,
|
||||||
@@ -424,6 +446,7 @@ pub trait NymApiClientExt: ApiClient {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[instrument(level = "debug", skip(self))]
|
||||||
async fn get_mixnode_status(
|
async fn get_mixnode_status(
|
||||||
&self,
|
&self,
|
||||||
mix_id: NodeId,
|
mix_id: NodeId,
|
||||||
@@ -441,6 +464,7 @@ pub trait NymApiClientExt: ApiClient {
|
|||||||
.await
|
.await
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[instrument(level = "debug", skip(self))]
|
||||||
async fn get_mixnode_reward_estimation(
|
async fn get_mixnode_reward_estimation(
|
||||||
&self,
|
&self,
|
||||||
mix_id: NodeId,
|
mix_id: NodeId,
|
||||||
@@ -458,6 +482,7 @@ pub trait NymApiClientExt: ApiClient {
|
|||||||
.await
|
.await
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[instrument(level = "debug", skip(self))]
|
||||||
async fn compute_mixnode_reward_estimation(
|
async fn compute_mixnode_reward_estimation(
|
||||||
&self,
|
&self,
|
||||||
mix_id: NodeId,
|
mix_id: NodeId,
|
||||||
@@ -477,6 +502,7 @@ pub trait NymApiClientExt: ApiClient {
|
|||||||
.await
|
.await
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[instrument(level = "debug", skip(self))]
|
||||||
async fn get_mixnode_stake_saturation(
|
async fn get_mixnode_stake_saturation(
|
||||||
&self,
|
&self,
|
||||||
mix_id: NodeId,
|
mix_id: NodeId,
|
||||||
@@ -494,6 +520,7 @@ pub trait NymApiClientExt: ApiClient {
|
|||||||
.await
|
.await
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[instrument(level = "debug", skip(self))]
|
||||||
async fn get_mixnode_inclusion_probability(
|
async fn get_mixnode_inclusion_probability(
|
||||||
&self,
|
&self,
|
||||||
mix_id: NodeId,
|
mix_id: NodeId,
|
||||||
@@ -511,6 +538,7 @@ pub trait NymApiClientExt: ApiClient {
|
|||||||
.await
|
.await
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[instrument(level = "debug", skip(self))]
|
||||||
async fn get_current_node_performance(
|
async fn get_current_node_performance(
|
||||||
&self,
|
&self,
|
||||||
node_id: NodeId,
|
node_id: NodeId,
|
||||||
@@ -541,6 +569,7 @@ pub trait NymApiClientExt: ApiClient {
|
|||||||
.await
|
.await
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[instrument(level = "debug", skip(self))]
|
||||||
async fn get_mixnodes_blacklisted(&self) -> Result<Vec<NodeId>, NymAPIError> {
|
async fn get_mixnodes_blacklisted(&self) -> Result<Vec<NodeId>, NymAPIError> {
|
||||||
self.get_json(
|
self.get_json(
|
||||||
&[routes::API_VERSION, routes::MIXNODES, routes::BLACKLISTED],
|
&[routes::API_VERSION, routes::MIXNODES, routes::BLACKLISTED],
|
||||||
@@ -549,6 +578,7 @@ pub trait NymApiClientExt: ApiClient {
|
|||||||
.await
|
.await
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[instrument(level = "debug", skip(self))]
|
||||||
async fn get_gateways_blacklisted(&self) -> Result<Vec<IdentityKey>, NymAPIError> {
|
async fn get_gateways_blacklisted(&self) -> Result<Vec<IdentityKey>, NymAPIError> {
|
||||||
self.get_json(
|
self.get_json(
|
||||||
&[routes::API_VERSION, routes::GATEWAYS, routes::BLACKLISTED],
|
&[routes::API_VERSION, routes::GATEWAYS, routes::BLACKLISTED],
|
||||||
@@ -557,6 +587,7 @@ pub trait NymApiClientExt: ApiClient {
|
|||||||
.await
|
.await
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[instrument(level = "debug", skip(self, request_body))]
|
||||||
async fn blind_sign(
|
async fn blind_sign(
|
||||||
&self,
|
&self,
|
||||||
request_body: &BlindSignRequestBody,
|
request_body: &BlindSignRequestBody,
|
||||||
@@ -573,6 +604,7 @@ pub trait NymApiClientExt: ApiClient {
|
|||||||
.await
|
.await
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[instrument(level = "debug", skip(self, request_body))]
|
||||||
async fn verify_ecash_ticket(
|
async fn verify_ecash_ticket(
|
||||||
&self,
|
&self,
|
||||||
request_body: &VerifyEcashTicketBody,
|
request_body: &VerifyEcashTicketBody,
|
||||||
@@ -589,6 +621,7 @@ pub trait NymApiClientExt: ApiClient {
|
|||||||
.await
|
.await
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[instrument(level = "debug", skip(self, request_body))]
|
||||||
async fn batch_redeem_ecash_tickets(
|
async fn batch_redeem_ecash_tickets(
|
||||||
&self,
|
&self,
|
||||||
request_body: &BatchRedeemTicketsBody,
|
request_body: &BatchRedeemTicketsBody,
|
||||||
@@ -605,6 +638,7 @@ pub trait NymApiClientExt: ApiClient {
|
|||||||
.await
|
.await
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[instrument(level = "debug", skip(self))]
|
||||||
async fn double_spending_filter_v1(&self) -> Result<SpentCredentialsResponse, NymAPIError> {
|
async fn double_spending_filter_v1(&self) -> Result<SpentCredentialsResponse, NymAPIError> {
|
||||||
self.get_json(
|
self.get_json(
|
||||||
&[
|
&[
|
||||||
@@ -617,6 +651,7 @@ pub trait NymApiClientExt: ApiClient {
|
|||||||
.await
|
.await
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[instrument(level = "debug", skip(self))]
|
||||||
async fn partial_expiration_date_signatures(
|
async fn partial_expiration_date_signatures(
|
||||||
&self,
|
&self,
|
||||||
expiration_date: Option<Date>,
|
expiration_date: Option<Date>,
|
||||||
@@ -640,6 +675,7 @@ pub trait NymApiClientExt: ApiClient {
|
|||||||
.await
|
.await
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[instrument(level = "debug", skip(self))]
|
||||||
async fn partial_coin_indices_signatures(
|
async fn partial_coin_indices_signatures(
|
||||||
&self,
|
&self,
|
||||||
epoch_id: Option<EpochId>,
|
epoch_id: Option<EpochId>,
|
||||||
@@ -660,6 +696,7 @@ pub trait NymApiClientExt: ApiClient {
|
|||||||
.await
|
.await
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[instrument(level = "debug", skip(self))]
|
||||||
async fn global_expiration_date_signatures(
|
async fn global_expiration_date_signatures(
|
||||||
&self,
|
&self,
|
||||||
expiration_date: Option<Date>,
|
expiration_date: Option<Date>,
|
||||||
@@ -683,6 +720,7 @@ pub trait NymApiClientExt: ApiClient {
|
|||||||
.await
|
.await
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[instrument(level = "debug", skip(self))]
|
||||||
async fn global_coin_indices_signatures(
|
async fn global_coin_indices_signatures(
|
||||||
&self,
|
&self,
|
||||||
epoch_id: Option<EpochId>,
|
epoch_id: Option<EpochId>,
|
||||||
@@ -703,6 +741,7 @@ pub trait NymApiClientExt: ApiClient {
|
|||||||
.await
|
.await
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[instrument(level = "debug", skip(self))]
|
||||||
async fn master_verification_key(
|
async fn master_verification_key(
|
||||||
&self,
|
&self,
|
||||||
epoch_id: Option<EpochId>,
|
epoch_id: Option<EpochId>,
|
||||||
@@ -722,6 +761,7 @@ pub trait NymApiClientExt: ApiClient {
|
|||||||
.await
|
.await
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[instrument(level = "debug", skip(self))]
|
||||||
async fn epoch_credentials(
|
async fn epoch_credentials(
|
||||||
&self,
|
&self,
|
||||||
dkg_epoch: EpochId,
|
dkg_epoch: EpochId,
|
||||||
@@ -738,6 +778,7 @@ pub trait NymApiClientExt: ApiClient {
|
|||||||
.await
|
.await
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[instrument(level = "debug", skip(self))]
|
||||||
async fn issued_credential(
|
async fn issued_credential(
|
||||||
&self,
|
&self,
|
||||||
credential_id: i64,
|
credential_id: i64,
|
||||||
@@ -754,6 +795,7 @@ pub trait NymApiClientExt: ApiClient {
|
|||||||
.await
|
.await
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[instrument(level = "debug", skip(self))]
|
||||||
async fn issued_credentials(
|
async fn issued_credentials(
|
||||||
&self,
|
&self,
|
||||||
credential_ids: Vec<i64>,
|
credential_ids: Vec<i64>,
|
||||||
|
|||||||
@@ -8,9 +8,9 @@ use crate::nyxd::CosmWasmClient;
|
|||||||
use async_trait::async_trait;
|
use async_trait::async_trait;
|
||||||
use cosmrs::AccountId;
|
use cosmrs::AccountId;
|
||||||
use cosmwasm_std::Addr;
|
use cosmwasm_std::Addr;
|
||||||
use log::trace;
|
|
||||||
use nym_coconut_dkg_common::types::{ChunkIndex, NodeIndex, StateAdvanceResponse};
|
use nym_coconut_dkg_common::types::{ChunkIndex, NodeIndex, StateAdvanceResponse};
|
||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
|
use tracing::trace;
|
||||||
|
|
||||||
use nym_coconut_dkg_common::dealer::RegisteredDealerDetails;
|
use nym_coconut_dkg_common::dealer::RegisteredDealerDetails;
|
||||||
pub use nym_coconut_dkg_common::{
|
pub use nym_coconut_dkg_common::{
|
||||||
|
|||||||
+3
-4
@@ -29,7 +29,6 @@ use cosmrs::proto::cosmwasm::wasm::v1::{
|
|||||||
};
|
};
|
||||||
use cosmrs::tendermint::{block, chain, Hash};
|
use cosmrs::tendermint::{block, chain, Hash};
|
||||||
use cosmrs::{AccountId, Coin as CosmosCoin, Tx};
|
use cosmrs::{AccountId, Coin as CosmosCoin, Tx};
|
||||||
use log::trace;
|
|
||||||
use prost::Message;
|
use prost::Message;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
@@ -68,7 +67,7 @@ pub trait CosmWasmClient: TendermintRpcClient {
|
|||||||
Res: Message + Default,
|
Res: Message + Default,
|
||||||
{
|
{
|
||||||
if let Some(ref abci_path) = path {
|
if let Some(ref abci_path) = path {
|
||||||
trace!("performing query on abci path {abci_path}")
|
tracing::trace!("performing query on abci path {abci_path}")
|
||||||
}
|
}
|
||||||
let mut buf = Vec::with_capacity(req.encoded_len());
|
let mut buf = Vec::with_capacity(req.encoded_len());
|
||||||
req.encode(&mut buf)?;
|
req.encode(&mut buf)?;
|
||||||
@@ -297,7 +296,7 @@ pub trait CosmWasmClient: TendermintRpcClient {
|
|||||||
|
|
||||||
let start = Instant::now();
|
let start = Instant::now();
|
||||||
loop {
|
loop {
|
||||||
log::debug!(
|
tracing::debug!(
|
||||||
"Polling for result of including {} in a block...",
|
"Polling for result of including {} in a block...",
|
||||||
broadcasted.hash
|
broadcasted.hash
|
||||||
);
|
);
|
||||||
@@ -522,7 +521,7 @@ pub trait CosmWasmClient: TendermintRpcClient {
|
|||||||
.make_abci_query::<_, QuerySmartContractStateResponse>(path, req)
|
.make_abci_query::<_, QuerySmartContractStateResponse>(path, req)
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
trace!("raw query response: {}", String::from_utf8_lossy(&res.data));
|
tracing::trace!("raw query response: {}", String::from_utf8_lossy(&res.data));
|
||||||
Ok(serde_json::from_slice(&res.data)?)
|
Ok(serde_json::from_slice(&res.data)?)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+1
-1
@@ -25,12 +25,12 @@ use cosmrs::proto::cosmos::tx::signing::v1beta1::SignMode;
|
|||||||
use cosmrs::staking::{MsgDelegate, MsgUndelegate};
|
use cosmrs::staking::{MsgDelegate, MsgUndelegate};
|
||||||
use cosmrs::tx::{self, Msg};
|
use cosmrs::tx::{self, Msg};
|
||||||
use cosmrs::{cosmwasm, AccountId, Any, Tx};
|
use cosmrs::{cosmwasm, AccountId, Any, Tx};
|
||||||
use log::debug;
|
|
||||||
use serde::Serialize;
|
use serde::Serialize;
|
||||||
use sha2::Digest;
|
use sha2::Digest;
|
||||||
use sha2::Sha256;
|
use sha2::Sha256;
|
||||||
use std::time::SystemTime;
|
use std::time::SystemTime;
|
||||||
use tendermint_rpc::endpoint::broadcast;
|
use tendermint_rpc::endpoint::broadcast;
|
||||||
|
use tracing::debug;
|
||||||
|
|
||||||
fn empty_fee() -> tx::Fee {
|
fn empty_fee() -> tx::Fee {
|
||||||
tx::Fee {
|
tx::Fee {
|
||||||
|
|||||||
@@ -7,9 +7,9 @@ use base64::Engine;
|
|||||||
use cosmrs::abci::TxMsgData;
|
use cosmrs::abci::TxMsgData;
|
||||||
use cosmrs::cosmwasm::MsgExecuteContractResponse;
|
use cosmrs::cosmwasm::MsgExecuteContractResponse;
|
||||||
use cosmrs::proto::cosmos::base::query::v1beta1::{PageRequest, PageResponse};
|
use cosmrs::proto::cosmos::base::query::v1beta1::{PageRequest, PageResponse};
|
||||||
use log::error;
|
|
||||||
use prost::bytes::Bytes;
|
use prost::bytes::Bytes;
|
||||||
use tendermint_rpc::endpoint::broadcast;
|
use tendermint_rpc::endpoint::broadcast;
|
||||||
|
use tracing::error;
|
||||||
|
|
||||||
pub use cosmrs::abci::MsgResponse;
|
pub use cosmrs::abci::MsgResponse;
|
||||||
|
|
||||||
|
|||||||
@@ -1,100 +0,0 @@
|
|||||||
// Copyright 2022-2024 - Nym Technologies SA <contact@nymtech.net>
|
|
||||||
// SPDX-License-Identifier: GPL-3.0-only
|
|
||||||
|
|
||||||
use crate::ecash::error::EcashTicketError;
|
|
||||||
use crate::ecash::state::SharedState;
|
|
||||||
use nym_ecash_double_spending::DoubleSpendingFilter;
|
|
||||||
use nym_gateway_storage::Storage;
|
|
||||||
use nym_task::TaskClient;
|
|
||||||
use nym_validator_client::client::NymApiClientExt;
|
|
||||||
use nym_validator_client::EcashApiClient;
|
|
||||||
use rand::prelude::SliceRandom;
|
|
||||||
use rand::thread_rng;
|
|
||||||
use std::sync::Arc;
|
|
||||||
use tokio::sync::{RwLock, RwLockReadGuard};
|
|
||||||
use tokio::time::{interval, Duration};
|
|
||||||
use tracing::{info, trace, warn};
|
|
||||||
|
|
||||||
#[derive(Clone)]
|
|
||||||
pub(crate) struct DoubleSpendingDetector<S> {
|
|
||||||
spent_serial_numbers: Arc<RwLock<DoubleSpendingFilter>>,
|
|
||||||
shared_state: SharedState<S>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<S> DoubleSpendingDetector<S>
|
|
||||||
where
|
|
||||||
S: Storage + Clone + Send + Sync + 'static,
|
|
||||||
{
|
|
||||||
pub(crate) fn new(shared_state: SharedState<S>) -> Self {
|
|
||||||
DoubleSpendingDetector {
|
|
||||||
spent_serial_numbers: Arc::new(RwLock::new(DoubleSpendingFilter::new_empty_ecash())),
|
|
||||||
shared_state,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(crate) async fn check(&self, serial_number: &Vec<u8>) -> bool {
|
|
||||||
self.spent_serial_numbers.read().await.check(serial_number)
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn latest_api_endpoints(
|
|
||||||
&self,
|
|
||||||
) -> Result<RwLockReadGuard<Vec<EcashApiClient>>, EcashTicketError> {
|
|
||||||
let epoch_id = self.shared_state.current_epoch_id().await?;
|
|
||||||
self.shared_state.api_clients(epoch_id).await
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn refresh_bloomfilter(&self) {
|
|
||||||
let mut filter_builder = self.spent_serial_numbers.read().await.rebuild();
|
|
||||||
|
|
||||||
let api_clients = match self.latest_api_endpoints().await {
|
|
||||||
Ok(clients) => clients,
|
|
||||||
Err(err) => {
|
|
||||||
warn!("failed to obtain current api clients: {err}");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
let mut clients = api_clients
|
|
||||||
.iter()
|
|
||||||
.map(|c| c.api_client.clone())
|
|
||||||
.collect::<Vec<_>>();
|
|
||||||
clients.shuffle(&mut thread_rng());
|
|
||||||
|
|
||||||
for client in clients {
|
|
||||||
match client.nym_api.double_spending_filter_v1().await {
|
|
||||||
Ok(response) => {
|
|
||||||
// due to relative big size of the filter, query only one api since all of them should contain
|
|
||||||
// roughly the same data anyway.
|
|
||||||
filter_builder.add_bytes(&response.bitmap);
|
|
||||||
*self.spent_serial_numbers.write().await = filter_builder.build();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
Err(err) => {
|
|
||||||
warn!("Validator @ {} could not be reached. There might be a problem with the ecash endpoint: {err}", client.api_url());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
warn!("none of the validators could be reached. the bloomfilter will remain unchanged.");
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn run(&self, mut shutdown: TaskClient) {
|
|
||||||
info!("Starting Ecash DoubleSpendingDetector");
|
|
||||||
let mut interval = interval(Duration::from_secs(600));
|
|
||||||
|
|
||||||
while !shutdown.is_shutdown() {
|
|
||||||
tokio::select! {
|
|
||||||
biased;
|
|
||||||
_ = shutdown.recv() => {
|
|
||||||
trace!("ecash_verifier::DoubleSpendingDetector : received shutdown");
|
|
||||||
},
|
|
||||||
_ = interval.tick() => self.refresh_bloomfilter().await,
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(crate) fn start(self, shutdown: nym_task::TaskClient) {
|
|
||||||
tokio::spawn(async move { self.run(shutdown).await });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -4,7 +4,6 @@
|
|||||||
use crate::Error;
|
use crate::Error;
|
||||||
use credential_sender::CredentialHandler;
|
use credential_sender::CredentialHandler;
|
||||||
use credential_sender::CredentialHandlerConfig;
|
use credential_sender::CredentialHandlerConfig;
|
||||||
use double_spending::DoubleSpendingDetector;
|
|
||||||
use error::EcashTicketError;
|
use error::EcashTicketError;
|
||||||
use futures::channel::mpsc::{self, UnboundedSender};
|
use futures::channel::mpsc::{self, UnboundedSender};
|
||||||
use nym_credentials::CredentialSpendingData;
|
use nym_credentials::CredentialSpendingData;
|
||||||
@@ -18,7 +17,6 @@ use tokio::sync::{Mutex, RwLockReadGuard};
|
|||||||
use tracing::error;
|
use tracing::error;
|
||||||
|
|
||||||
pub mod credential_sender;
|
pub mod credential_sender;
|
||||||
pub(crate) mod double_spending;
|
|
||||||
pub mod error;
|
pub mod error;
|
||||||
mod helpers;
|
mod helpers;
|
||||||
mod state;
|
mod state;
|
||||||
@@ -31,7 +29,6 @@ pub struct EcashManager<S> {
|
|||||||
pk_bytes: [u8; 32], // bytes representation of a pub key representing the verifier
|
pk_bytes: [u8; 32], // bytes representation of a pub key representing the verifier
|
||||||
pay_infos: Mutex<Vec<NymPayInfo>>,
|
pay_infos: Mutex<Vec<NymPayInfo>>,
|
||||||
cred_sender: UnboundedSender<ClientTicket>,
|
cred_sender: UnboundedSender<ClientTicket>,
|
||||||
double_spend_detector: DoubleSpendingDetector<S>,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<S> EcashManager<S>
|
impl<S> EcashManager<S>
|
||||||
@@ -47,9 +44,6 @@ where
|
|||||||
) -> Result<Self, Error> {
|
) -> Result<Self, Error> {
|
||||||
let shared_state = SharedState::new(nyxd_client, storage).await?;
|
let shared_state = SharedState::new(nyxd_client, storage).await?;
|
||||||
|
|
||||||
let double_spend_detector = DoubleSpendingDetector::new(shared_state.clone());
|
|
||||||
double_spend_detector.clone().start(shutdown.clone());
|
|
||||||
|
|
||||||
let (cred_sender, cred_receiver) = mpsc::unbounded();
|
let (cred_sender, cred_receiver) = mpsc::unbounded();
|
||||||
|
|
||||||
let cs =
|
let cs =
|
||||||
@@ -62,7 +56,6 @@ where
|
|||||||
pk_bytes,
|
pk_bytes,
|
||||||
pay_infos: Default::default(),
|
pay_infos: Default::default(),
|
||||||
cred_sender,
|
cred_sender,
|
||||||
double_spend_detector,
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -163,10 +156,6 @@ where
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn check_double_spend(&self, serial_number: &Vec<u8>) -> bool {
|
|
||||||
self.double_spend_detector.check(serial_number).await
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn async_verify(&self, ticket: ClientTicket) {
|
pub fn async_verify(&self, ticket: ClientTicket) {
|
||||||
// TODO: I guess do something for shutdowns
|
// TODO: I guess do something for shutdowns
|
||||||
let _ = self
|
let _ = self
|
||||||
|
|||||||
@@ -53,18 +53,6 @@ impl<S: Storage + Clone + 'static> CredentialVerifier<S> {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn check_bloomfilter(&self, serial_number: &Vec<u8>) -> Result<()> {
|
|
||||||
trace!("checking the bloomfilter...");
|
|
||||||
|
|
||||||
let spent = self.ecash_verifier.check_double_spend(serial_number).await;
|
|
||||||
|
|
||||||
if spent {
|
|
||||||
trace!("the credential has already been spent before at some gateway before (bloomfilter failure)");
|
|
||||||
return Err(Error::BandwidthCredentialAlreadySpent);
|
|
||||||
}
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn check_local_db_for_double_spending(&self, serial_number: &[u8]) -> Result<()> {
|
async fn check_local_db_for_double_spending(&self, serial_number: &[u8]) -> Result<()> {
|
||||||
trace!("checking local db for double spending...");
|
trace!("checking local db for double spending...");
|
||||||
|
|
||||||
@@ -128,7 +116,6 @@ impl<S: Storage + Clone + 'static> CredentialVerifier<S> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
self.check_credential_spending_date(spend_date.ecash_date())?;
|
self.check_credential_spending_date(spend_date.ecash_date())?;
|
||||||
self.check_bloomfilter(&serial_number).await?;
|
|
||||||
self.check_local_db_for_double_spending(&serial_number)
|
self.check_local_db_for_double_spending(&serial_number)
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,34 @@
|
|||||||
|
[package]
|
||||||
|
name = "nym-gateway-stats-storage"
|
||||||
|
version = "0.1.0"
|
||||||
|
authors.workspace = true
|
||||||
|
repository.workspace = true
|
||||||
|
homepage.workspace = true
|
||||||
|
documentation.workspace = true
|
||||||
|
edition.workspace = true
|
||||||
|
license.workspace = true
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
sqlx = { workspace = true, features = [
|
||||||
|
"runtime-tokio-rustls",
|
||||||
|
"sqlite",
|
||||||
|
"macros",
|
||||||
|
"migrate",
|
||||||
|
"time",
|
||||||
|
] }
|
||||||
|
time = { workspace = true }
|
||||||
|
thiserror = { workspace = true }
|
||||||
|
tracing = { workspace = true }
|
||||||
|
|
||||||
|
nym-sphinx = { path = "../nymsphinx" }
|
||||||
|
nym-credentials-interface = { path = "../credentials-interface" }
|
||||||
|
|
||||||
|
|
||||||
|
[build-dependencies]
|
||||||
|
tokio = { workspace = true, features = ["rt-multi-thread", "macros"] }
|
||||||
|
sqlx = { workspace = true, features = [
|
||||||
|
"runtime-tokio-rustls",
|
||||||
|
"sqlite",
|
||||||
|
"macros",
|
||||||
|
"migrate",
|
||||||
|
] }
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
// Copyright 2024 - Nym Technologies SA <contact@nymtech.net>
|
||||||
|
// SPDX-License-Identifier: GPL-3.0-only
|
||||||
|
|
||||||
|
use sqlx::{Connection, SqliteConnection};
|
||||||
|
use std::env;
|
||||||
|
|
||||||
|
#[tokio::main]
|
||||||
|
async fn main() {
|
||||||
|
let out_dir = env::var("OUT_DIR").unwrap();
|
||||||
|
let database_path = format!("{}/gateway-stats-example.sqlite", out_dir);
|
||||||
|
|
||||||
|
let mut conn = SqliteConnection::connect(&format!("sqlite://{}?mode=rwc", database_path))
|
||||||
|
.await
|
||||||
|
.expect("Failed to create SQLx database connection");
|
||||||
|
|
||||||
|
sqlx::migrate!("./migrations")
|
||||||
|
.run(&mut conn)
|
||||||
|
.await
|
||||||
|
.expect("Failed to perform SQLx migrations");
|
||||||
|
|
||||||
|
#[cfg(target_family = "unix")]
|
||||||
|
println!("cargo:rustc-env=DATABASE_URL=sqlite://{}", &database_path);
|
||||||
|
|
||||||
|
#[cfg(target_family = "windows")]
|
||||||
|
// for some strange reason we need to add a leading `/` to the windows path even though it's
|
||||||
|
// not a valid windows path... but hey, it works...
|
||||||
|
println!("cargo:rustc-env=DATABASE_URL=sqlite:///{}", &database_path);
|
||||||
|
}
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2024 - Nym Technologies SA <contact@nymtech.net>
|
||||||
|
* SPDX-License-Identifier: GPL-3.0-only
|
||||||
|
*/
|
||||||
|
|
||||||
|
CREATE TABLE sessions_active
|
||||||
|
(
|
||||||
|
client_address TEXT NOT NULL PRIMARY KEY UNIQUE,
|
||||||
|
start_time TIMESTAMP WITHOUT TIME ZONE NOT NULL,
|
||||||
|
typ TEXT NOT NULL
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE sessions_finished
|
||||||
|
(
|
||||||
|
id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
|
||||||
|
day DATE NOT NULL,
|
||||||
|
duration_ms INTEGER NOT NULL,
|
||||||
|
typ TEXT NOT NULL
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE sessions_unique_users
|
||||||
|
(
|
||||||
|
day DATE NOT NULL,
|
||||||
|
client_address TEXT NOT NULL,
|
||||||
|
PRIMARY KEY (day, client_address)
|
||||||
|
);
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
// Copyright 2024 - Nym Technologies SA <contact@nymtech.net>
|
||||||
|
// SPDX-License-Identifier: GPL-3.0-only
|
||||||
|
|
||||||
|
use thiserror::Error;
|
||||||
|
|
||||||
|
#[derive(Error, Debug)]
|
||||||
|
pub enum StatsStorageError {
|
||||||
|
#[error("Database experienced an internal error: {0}")]
|
||||||
|
InternalDatabaseError(#[from] sqlx::Error),
|
||||||
|
|
||||||
|
#[error("Failed to perform database migration: {0}")]
|
||||||
|
MigrationError(#[from] sqlx::migrate::MigrateError),
|
||||||
|
}
|
||||||
@@ -0,0 +1,195 @@
|
|||||||
|
// Copyright 2024 - Nym Technologies SA <contact@nymtech.net>
|
||||||
|
// SPDX-License-Identifier: GPL-3.0-only
|
||||||
|
|
||||||
|
use error::StatsStorageError;
|
||||||
|
use models::{ActiveSession, FinishedSession, SessionType, StoredFinishedSession};
|
||||||
|
use nym_sphinx::DestinationAddressBytes;
|
||||||
|
use sessions::SessionManager;
|
||||||
|
use sqlx::ConnectOptions;
|
||||||
|
use std::path::Path;
|
||||||
|
use time::Date;
|
||||||
|
use tracing::{debug, error};
|
||||||
|
|
||||||
|
pub mod error;
|
||||||
|
pub mod models;
|
||||||
|
mod sessions;
|
||||||
|
|
||||||
|
// note that clone here is fine as upon cloning the same underlying pool will be used
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub struct PersistentStatsStorage {
|
||||||
|
session_manager: SessionManager,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl PersistentStatsStorage {
|
||||||
|
/// Initialises `PersistentStatsStorage` using the provided path.
|
||||||
|
///
|
||||||
|
/// # Arguments
|
||||||
|
///
|
||||||
|
/// * `database_path`: path to the database.
|
||||||
|
pub async fn init<P: AsRef<Path> + Send>(database_path: P) -> Result<Self, StatsStorageError> {
|
||||||
|
debug!(
|
||||||
|
"Attempting to connect to database {:?}",
|
||||||
|
database_path.as_ref().as_os_str()
|
||||||
|
);
|
||||||
|
|
||||||
|
// TODO: we can inject here more stuff based on our gateway global config
|
||||||
|
// struct. Maybe different pool size or timeout intervals?
|
||||||
|
let opts = sqlx::sqlite::SqliteConnectOptions::new()
|
||||||
|
.filename(database_path)
|
||||||
|
.create_if_missing(true)
|
||||||
|
.disable_statement_logging();
|
||||||
|
|
||||||
|
// TODO: do we want auto_vacuum ?
|
||||||
|
|
||||||
|
let connection_pool = match sqlx::SqlitePool::connect_with(opts).await {
|
||||||
|
Ok(db) => db,
|
||||||
|
Err(err) => {
|
||||||
|
error!("Failed to connect to SQLx database: {err}");
|
||||||
|
return Err(err.into());
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if let Err(err) = sqlx::migrate!("./migrations").run(&connection_pool).await {
|
||||||
|
error!("Failed to perform migration on the SQLx database: {err}");
|
||||||
|
return Err(err.into());
|
||||||
|
}
|
||||||
|
|
||||||
|
// the cloning here are cheap as connection pool is stored behind an Arc
|
||||||
|
Ok(PersistentStatsStorage {
|
||||||
|
session_manager: sessions::SessionManager::new(connection_pool),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
//Sessions fn
|
||||||
|
pub async fn insert_finished_session(
|
||||||
|
&self,
|
||||||
|
date: Date,
|
||||||
|
session: FinishedSession,
|
||||||
|
) -> Result<(), StatsStorageError> {
|
||||||
|
Ok(self
|
||||||
|
.session_manager
|
||||||
|
.insert_finished_session(
|
||||||
|
date,
|
||||||
|
session.duration.whole_milliseconds() as i64,
|
||||||
|
session.typ.to_string().into(),
|
||||||
|
)
|
||||||
|
.await?)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn get_finished_sessions(
|
||||||
|
&self,
|
||||||
|
date: Date,
|
||||||
|
) -> Result<Vec<StoredFinishedSession>, StatsStorageError> {
|
||||||
|
Ok(self.session_manager.get_finished_sessions(date).await?)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn delete_finished_sessions(
|
||||||
|
&self,
|
||||||
|
before_date: Date,
|
||||||
|
) -> Result<(), StatsStorageError> {
|
||||||
|
Ok(self
|
||||||
|
.session_manager
|
||||||
|
.delete_finished_sessions(before_date)
|
||||||
|
.await?)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn insert_unique_user(
|
||||||
|
&self,
|
||||||
|
date: Date,
|
||||||
|
client_address_bs58: String,
|
||||||
|
) -> Result<(), StatsStorageError> {
|
||||||
|
Ok(self
|
||||||
|
.session_manager
|
||||||
|
.insert_unique_user(date, client_address_bs58)
|
||||||
|
.await?)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn get_unique_users_count(&self, date: Date) -> Result<i32, StatsStorageError> {
|
||||||
|
Ok(self.session_manager.get_unique_users_count(date).await?)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn delete_unique_users(&self, before_date: Date) -> Result<(), StatsStorageError> {
|
||||||
|
Ok(self
|
||||||
|
.session_manager
|
||||||
|
.delete_unique_users(before_date)
|
||||||
|
.await?)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn insert_active_session(
|
||||||
|
&self,
|
||||||
|
client_address: DestinationAddressBytes,
|
||||||
|
session: ActiveSession,
|
||||||
|
) -> Result<(), StatsStorageError> {
|
||||||
|
Ok(self
|
||||||
|
.session_manager
|
||||||
|
.insert_active_session(
|
||||||
|
client_address.as_base58_string(),
|
||||||
|
session.start,
|
||||||
|
session.typ.to_string().into(),
|
||||||
|
)
|
||||||
|
.await?)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn update_active_session_type(
|
||||||
|
&self,
|
||||||
|
client_address: DestinationAddressBytes,
|
||||||
|
session_type: SessionType,
|
||||||
|
) -> Result<(), StatsStorageError> {
|
||||||
|
Ok(self
|
||||||
|
.session_manager
|
||||||
|
.update_active_session_type(
|
||||||
|
client_address.as_base58_string(),
|
||||||
|
session_type.to_string().into(),
|
||||||
|
)
|
||||||
|
.await?)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn get_active_session(
|
||||||
|
&self,
|
||||||
|
client_address: DestinationAddressBytes,
|
||||||
|
) -> Result<Option<ActiveSession>, StatsStorageError> {
|
||||||
|
Ok(self
|
||||||
|
.session_manager
|
||||||
|
.get_active_session(client_address.as_base58_string())
|
||||||
|
.await?
|
||||||
|
.map(Into::into))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn get_all_active_sessions(&self) -> Result<Vec<ActiveSession>, StatsStorageError> {
|
||||||
|
Ok(self
|
||||||
|
.session_manager
|
||||||
|
.get_all_active_sessions()
|
||||||
|
.await?
|
||||||
|
.into_iter()
|
||||||
|
.map(Into::into)
|
||||||
|
.collect())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn get_started_sessions_count(
|
||||||
|
&self,
|
||||||
|
start_date: Date,
|
||||||
|
) -> Result<i32, StatsStorageError> {
|
||||||
|
Ok(self
|
||||||
|
.session_manager
|
||||||
|
.get_started_sessions_count(start_date)
|
||||||
|
.await?)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn get_active_users(&self) -> Result<Vec<String>, StatsStorageError> {
|
||||||
|
Ok(self.session_manager.get_active_users().await?)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn delete_active_session(
|
||||||
|
&self,
|
||||||
|
client_address: DestinationAddressBytes,
|
||||||
|
) -> Result<(), StatsStorageError> {
|
||||||
|
Ok(self
|
||||||
|
.session_manager
|
||||||
|
.delete_active_session(client_address.as_base58_string())
|
||||||
|
.await?)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn cleanup_active_sessions(&self) -> Result<(), StatsStorageError> {
|
||||||
|
Ok(self.session_manager.cleanup_active_sessions().await?)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,109 @@
|
|||||||
|
// Copyright 2024 - Nym Technologies SA <contact@nymtech.net>
|
||||||
|
// SPDX-License-Identifier: GPL-3.0-only
|
||||||
|
|
||||||
|
use nym_credentials_interface::TicketType;
|
||||||
|
use sqlx::prelude::FromRow;
|
||||||
|
use time::{Duration, OffsetDateTime};
|
||||||
|
|
||||||
|
#[derive(FromRow)]
|
||||||
|
pub struct StoredFinishedSession {
|
||||||
|
duration_ms: i64,
|
||||||
|
typ: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl StoredFinishedSession {
|
||||||
|
pub fn serialize(&self) -> (u64, String) {
|
||||||
|
(
|
||||||
|
self.duration_ms as u64, //we are sure that it fits in a u64, see `fn end_at`
|
||||||
|
self.typ.clone(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct FinishedSession {
|
||||||
|
pub duration: Duration,
|
||||||
|
pub typ: SessionType,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(PartialEq)]
|
||||||
|
pub enum SessionType {
|
||||||
|
Vpn,
|
||||||
|
Mixnet,
|
||||||
|
Unknown,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl SessionType {
|
||||||
|
pub fn to_string(&self) -> &str {
|
||||||
|
match self {
|
||||||
|
Self::Vpn => "vpn",
|
||||||
|
Self::Mixnet => "mixnet",
|
||||||
|
Self::Unknown => "unknown",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn from_string(s: &str) -> Self {
|
||||||
|
match s {
|
||||||
|
"vpn" => Self::Vpn,
|
||||||
|
"mixnet" => Self::Mixnet,
|
||||||
|
_ => Self::Unknown,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<TicketType> for SessionType {
|
||||||
|
fn from(value: TicketType) -> Self {
|
||||||
|
match value {
|
||||||
|
TicketType::V1MixnetEntry => Self::Mixnet,
|
||||||
|
TicketType::V1MixnetExit => Self::Mixnet,
|
||||||
|
TicketType::V1WireguardEntry => Self::Vpn,
|
||||||
|
TicketType::V1WireguardExit => Self::Vpn,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(FromRow)]
|
||||||
|
pub(crate) struct StoredActiveSession {
|
||||||
|
start_time: OffsetDateTime,
|
||||||
|
typ: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct ActiveSession {
|
||||||
|
pub start: OffsetDateTime,
|
||||||
|
pub typ: SessionType,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ActiveSession {
|
||||||
|
pub fn new(start_time: OffsetDateTime) -> Self {
|
||||||
|
ActiveSession {
|
||||||
|
start: start_time,
|
||||||
|
typ: SessionType::Unknown,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn set_type(&mut self, ticket_type: TicketType) {
|
||||||
|
self.typ = ticket_type.into();
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn end_at(self, stop_time: OffsetDateTime) -> Option<FinishedSession> {
|
||||||
|
let session_duration = stop_time - self.start;
|
||||||
|
//ensure duration is positive to fit in a u64
|
||||||
|
//u64::max milliseconds is 500k millenia so no overflow issue
|
||||||
|
if session_duration > Duration::ZERO {
|
||||||
|
Some(FinishedSession {
|
||||||
|
duration: session_duration,
|
||||||
|
typ: self.typ,
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<StoredActiveSession> for ActiveSession {
|
||||||
|
fn from(value: StoredActiveSession) -> Self {
|
||||||
|
ActiveSession {
|
||||||
|
start: value.start_time,
|
||||||
|
typ: SessionType::from_string(&value.typ),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,177 @@
|
|||||||
|
// Copyright 2024 - Nym Technologies SA <contact@nymtech.net>
|
||||||
|
// SPDX-License-Identifier: GPL-3.0-only
|
||||||
|
|
||||||
|
use time::{Date, OffsetDateTime};
|
||||||
|
|
||||||
|
use crate::models::{StoredActiveSession, StoredFinishedSession};
|
||||||
|
|
||||||
|
pub(crate) type Result<T> = std::result::Result<T, sqlx::Error>;
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub(crate) struct SessionManager {
|
||||||
|
connection_pool: sqlx::SqlitePool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl SessionManager {
|
||||||
|
/// Creates new instance of the `SessionsManager` with the provided sqlite connection pool.
|
||||||
|
///
|
||||||
|
/// # Arguments
|
||||||
|
///
|
||||||
|
/// * `connection_pool`: database connection pool to use.
|
||||||
|
pub(crate) fn new(connection_pool: sqlx::SqlitePool) -> Self {
|
||||||
|
SessionManager { connection_pool }
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) async fn insert_finished_session(
|
||||||
|
&self,
|
||||||
|
date: Date,
|
||||||
|
duration_ms: i64,
|
||||||
|
typ: String,
|
||||||
|
) -> Result<()> {
|
||||||
|
sqlx::query!(
|
||||||
|
"INSERT INTO sessions_finished (day, duration_ms, typ) VALUES (?, ?, ?)",
|
||||||
|
date,
|
||||||
|
duration_ms,
|
||||||
|
typ
|
||||||
|
)
|
||||||
|
.execute(&self.connection_pool)
|
||||||
|
.await?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) async fn get_finished_sessions(
|
||||||
|
&self,
|
||||||
|
date: Date,
|
||||||
|
) -> Result<Vec<StoredFinishedSession>> {
|
||||||
|
sqlx::query_as("SELECT duration_ms, typ FROM sessions_finished WHERE day = ?")
|
||||||
|
.bind(date)
|
||||||
|
.fetch_all(&self.connection_pool)
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) async fn delete_finished_sessions(&self, before_date: Date) -> Result<()> {
|
||||||
|
sqlx::query!("DELETE FROM sessions_finished WHERE day <= ? ", before_date)
|
||||||
|
.execute(&self.connection_pool)
|
||||||
|
.await?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) async fn insert_unique_user(
|
||||||
|
&self,
|
||||||
|
date: Date,
|
||||||
|
client_address_b58: String,
|
||||||
|
) -> Result<()> {
|
||||||
|
sqlx::query!(
|
||||||
|
"INSERT OR IGNORE INTO sessions_unique_users (day, client_address) VALUES (?, ?)",
|
||||||
|
date,
|
||||||
|
client_address_b58,
|
||||||
|
)
|
||||||
|
.execute(&self.connection_pool)
|
||||||
|
.await?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) async fn get_unique_users_count(&self, date: Date) -> Result<i32> {
|
||||||
|
Ok(sqlx::query!(
|
||||||
|
"SELECT COUNT(*) as count FROM sessions_unique_users WHERE day = ?",
|
||||||
|
date
|
||||||
|
)
|
||||||
|
.fetch_one(&self.connection_pool)
|
||||||
|
.await?
|
||||||
|
.count)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) async fn delete_unique_users(&self, before_date: Date) -> Result<()> {
|
||||||
|
sqlx::query!(
|
||||||
|
"DELETE FROM sessions_unique_users WHERE day <= ? ",
|
||||||
|
before_date
|
||||||
|
)
|
||||||
|
.execute(&self.connection_pool)
|
||||||
|
.await?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) async fn insert_active_session(
|
||||||
|
&self,
|
||||||
|
client_address_b58: String,
|
||||||
|
start_time: OffsetDateTime,
|
||||||
|
typ: String,
|
||||||
|
) -> Result<()> {
|
||||||
|
sqlx::query!(
|
||||||
|
"INSERT INTO sessions_active (client_address, start_time, typ) VALUES (?, ?, ?)",
|
||||||
|
client_address_b58,
|
||||||
|
start_time,
|
||||||
|
typ
|
||||||
|
)
|
||||||
|
.execute(&self.connection_pool)
|
||||||
|
.await?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) async fn update_active_session_type(
|
||||||
|
&self,
|
||||||
|
client_address_b58: String,
|
||||||
|
typ: String,
|
||||||
|
) -> Result<()> {
|
||||||
|
sqlx::query!(
|
||||||
|
"UPDATE sessions_active SET typ = ? WHERE client_address = ?",
|
||||||
|
typ,
|
||||||
|
client_address_b58,
|
||||||
|
)
|
||||||
|
.execute(&self.connection_pool)
|
||||||
|
.await?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) async fn get_active_session(
|
||||||
|
&self,
|
||||||
|
client_address_b58: String,
|
||||||
|
) -> Result<Option<StoredActiveSession>> {
|
||||||
|
sqlx::query_as("SELECT start_time, typ FROM sessions_active WHERE client_address = ?")
|
||||||
|
.bind(client_address_b58)
|
||||||
|
.fetch_optional(&self.connection_pool)
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) async fn get_all_active_sessions(&self) -> Result<Vec<StoredActiveSession>> {
|
||||||
|
sqlx::query_as("SELECT start_time, typ FROM sessions_active")
|
||||||
|
.fetch_all(&self.connection_pool)
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) async fn get_started_sessions_count(&self, start_date: Date) -> Result<i32> {
|
||||||
|
Ok(sqlx::query!(
|
||||||
|
"SELECT COUNT(*) as count FROM sessions_active WHERE date(start_time) = ?",
|
||||||
|
start_date
|
||||||
|
)
|
||||||
|
.fetch_one(&self.connection_pool)
|
||||||
|
.await?
|
||||||
|
.count)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) async fn get_active_users(&self) -> Result<Vec<String>> {
|
||||||
|
Ok(sqlx::query!("SELECT client_address from sessions_active")
|
||||||
|
.fetch_all(&self.connection_pool)
|
||||||
|
.await?
|
||||||
|
.into_iter()
|
||||||
|
.map(|record| record.client_address)
|
||||||
|
.collect())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) async fn delete_active_session(&self, client_address_b58: String) -> Result<()> {
|
||||||
|
sqlx::query!(
|
||||||
|
"DELETE FROM sessions_active WHERE client_address = ?",
|
||||||
|
client_address_b58
|
||||||
|
)
|
||||||
|
.execute(&self.connection_pool)
|
||||||
|
.await?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) async fn cleanup_active_sessions(&self) -> Result<()> {
|
||||||
|
sqlx::query!("DELETE FROM sessions_active")
|
||||||
|
.execute(&self.connection_pool)
|
||||||
|
.await?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -9,7 +9,7 @@ use serde::{Deserialize, Serialize};
|
|||||||
use std::fmt::Display;
|
use std::fmt::Display;
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
use thiserror::Error;
|
use thiserror::Error;
|
||||||
use tracing::warn;
|
use tracing::{instrument, warn};
|
||||||
use url::Url;
|
use url::Url;
|
||||||
|
|
||||||
pub use reqwest::IntoUrl;
|
pub use reqwest::IntoUrl;
|
||||||
@@ -202,6 +202,7 @@ impl Client {
|
|||||||
self.reqwest_client.get(url)
|
self.reqwest_client.get(url)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[instrument(level = "debug", skip_all, fields(path=?path))]
|
||||||
async fn send_get_request<K, V, E>(
|
async fn send_get_request<K, V, E>(
|
||||||
&self,
|
&self,
|
||||||
path: PathSegments<'_>,
|
path: PathSegments<'_>,
|
||||||
@@ -212,6 +213,7 @@ impl Client {
|
|||||||
V: AsRef<str>,
|
V: AsRef<str>,
|
||||||
E: Display,
|
E: Display,
|
||||||
{
|
{
|
||||||
|
tracing::trace!("Sending GET request");
|
||||||
let url = sanitize_url(&self.base_url, path, params);
|
let url = sanitize_url(&self.base_url, path, params);
|
||||||
|
|
||||||
#[cfg(target_arch = "wasm32")]
|
#[cfg(target_arch = "wasm32")]
|
||||||
@@ -277,6 +279,7 @@ impl Client {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[instrument(level = "debug", skip_all)]
|
||||||
pub async fn get_json<T, K, V, E>(
|
pub async fn get_json<T, K, V, E>(
|
||||||
&self,
|
&self,
|
||||||
path: PathSegments<'_>,
|
path: PathSegments<'_>,
|
||||||
@@ -512,12 +515,14 @@ pub fn sanitize_url<K: AsRef<str>, V: AsRef<str>>(
|
|||||||
url
|
url
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[tracing::instrument(level = "debug", skip_all)]
|
||||||
pub async fn parse_response<T, E>(res: Response, allow_empty: bool) -> Result<T, HttpClientError<E>>
|
pub async fn parse_response<T, E>(res: Response, allow_empty: bool) -> Result<T, HttpClientError<E>>
|
||||||
where
|
where
|
||||||
T: DeserializeOwned,
|
T: DeserializeOwned,
|
||||||
E: DeserializeOwned + Display,
|
E: DeserializeOwned + Display,
|
||||||
{
|
{
|
||||||
let status = res.status();
|
let status = res.status();
|
||||||
|
tracing::debug!("Status: {} (success: {})", &status, status.is_success());
|
||||||
|
|
||||||
if !allow_empty {
|
if !allow_empty {
|
||||||
if let Some(0) = res.content_length() {
|
if let Some(0) = res.content_length() {
|
||||||
@@ -526,6 +531,18 @@ where
|
|||||||
}
|
}
|
||||||
|
|
||||||
if res.status().is_success() {
|
if res.status().is_success() {
|
||||||
|
#[cfg(debug_assertions)]
|
||||||
|
{
|
||||||
|
let text = res.text().await.inspect_err(|err| {
|
||||||
|
tracing::error!("Couldn't even get response text: {err}");
|
||||||
|
})?;
|
||||||
|
tracing::trace!("Result:\n{:#?}", text);
|
||||||
|
|
||||||
|
serde_json::from_str(&text)
|
||||||
|
.map_err(|err| HttpClientError::GenericRequestFailure(err.to_string()))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(not(debug_assertions))]
|
||||||
Ok(res.json().await?)
|
Ok(res.json().await?)
|
||||||
} else if res.status() == StatusCode::NOT_FOUND {
|
} else if res.status() == StatusCode::NOT_FOUND {
|
||||||
Err(HttpClientError::NotFound)
|
Err(HttpClientError::NotFound)
|
||||||
|
|||||||
@@ -0,0 +1,14 @@
|
|||||||
|
[package]
|
||||||
|
name = "nym-common-models"
|
||||||
|
version = "0.1.0"
|
||||||
|
authors.workspace = true
|
||||||
|
repository.workspace = true
|
||||||
|
homepage.workspace = true
|
||||||
|
documentation.workspace = true
|
||||||
|
edition.workspace = true
|
||||||
|
license.workspace = true
|
||||||
|
rust-version.workspace = true
|
||||||
|
readme.workspace = true
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
serde = { workspace = true, features = ["derive"] }
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
pub mod ns_api;
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Deserialize, Serialize)]
|
||||||
|
pub struct TestrunAssignment {
|
||||||
|
/// has nothing to do with GW identity key. This is PK from `gateways` table
|
||||||
|
pub testrun_id: i64,
|
||||||
|
pub gateway_pk_id: i64,
|
||||||
|
}
|
||||||
@@ -19,10 +19,10 @@ pub enum TicketTypeRepr {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl TicketTypeRepr {
|
impl TicketTypeRepr {
|
||||||
pub const WIREGUARD_ENTRY_TICKET_SIZE: u64 = 512 * 1024 * 1024; // 512 MB
|
pub const WIREGUARD_ENTRY_TICKET_SIZE: u64 = 500 * 1000 * 1000; // 500 MB
|
||||||
pub const WIREGUARD_EXIT_TICKET_SIZE: u64 = 512 * 1024 * 1024; // 512 MB
|
pub const WIREGUARD_EXIT_TICKET_SIZE: u64 = 500 * 1000 * 1000; // 500 MB
|
||||||
pub const MIXNET_ENTRY_TICKET_SIZE: u64 = 128 * 1024 * 1024; // 128 MB
|
pub const MIXNET_ENTRY_TICKET_SIZE: u64 = 200 * 1000 * 1000; // 200 MB
|
||||||
pub const MIXNET_EXIT_TICKET_SIZE: u64 = 128 * 1024 * 1024; // 128 MB
|
pub const MIXNET_EXIT_TICKET_SIZE: u64 = 100 * 1000 * 1000; // 100 MB
|
||||||
|
|
||||||
/// How much bandwidth (in bytes) one ticket can grant
|
/// How much bandwidth (in bytes) one ticket can grant
|
||||||
pub const fn bandwidth_value(&self) -> u64 {
|
pub const fn bandwidth_value(&self) -> u64 {
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ nym-sphinx-types = { path = "../types" }
|
|||||||
nym-topology = { path = "../../topology" }
|
nym-topology = { path = "../../topology" }
|
||||||
|
|
||||||
[target."cfg(target_arch = \"wasm32\")".dependencies.wasm-bindgen]
|
[target."cfg(target_arch = \"wasm32\")".dependencies.wasm-bindgen]
|
||||||
version = "0.2.93"
|
version = "0.2.95"
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
rand_chacha = { workspace = true }
|
rand_chacha = { workspace = true }
|
||||||
|
|||||||
@@ -1,11 +1,8 @@
|
|||||||
// Copyright 2021 - Nym Technologies SA <contact@nymtech.net>
|
// Copyright 2021 - Nym Technologies SA <contact@nymtech.net>
|
||||||
// SPDX-License-Identifier: Apache-2.0
|
// SPDX-License-Identifier: Apache-2.0
|
||||||
|
|
||||||
use std::sync::LazyLock;
|
|
||||||
|
|
||||||
use crate::fragment::{linked_fragment_payload_max_len, unlinked_fragment_payload_max_len};
|
use crate::fragment::{linked_fragment_payload_max_len, unlinked_fragment_payload_max_len};
|
||||||
use dashmap::DashMap;
|
use fragment::FragmentHeader;
|
||||||
use fragment::{Fragment, FragmentHeader};
|
|
||||||
use nym_crypto::asymmetric::ed25519::PublicKey;
|
use nym_crypto::asymmetric::ed25519::PublicKey;
|
||||||
use serde::Serialize;
|
use serde::Serialize;
|
||||||
pub use set::split_into_sets;
|
pub use set::split_into_sets;
|
||||||
@@ -29,6 +26,59 @@ pub mod fragment;
|
|||||||
pub mod reconstruction;
|
pub mod reconstruction;
|
||||||
pub mod set;
|
pub mod set;
|
||||||
|
|
||||||
|
pub mod monitoring {
|
||||||
|
use crate::fragment::Fragment;
|
||||||
|
use crate::{ReceivedFragment, SentFragment};
|
||||||
|
use dashmap::DashMap;
|
||||||
|
use nym_crypto::asymmetric::ed25519::PublicKey;
|
||||||
|
use std::sync::atomic::{AtomicBool, Ordering};
|
||||||
|
use std::sync::LazyLock;
|
||||||
|
|
||||||
|
pub static ENABLED: AtomicBool = AtomicBool::new(false);
|
||||||
|
|
||||||
|
pub static FRAGMENTS_RECEIVED: LazyLock<DashMap<i32, Vec<ReceivedFragment>>> =
|
||||||
|
LazyLock::new(DashMap::new);
|
||||||
|
|
||||||
|
pub static FRAGMENTS_SENT: LazyLock<DashMap<i32, Vec<SentFragment>>> =
|
||||||
|
LazyLock::new(DashMap::new);
|
||||||
|
|
||||||
|
pub fn enable() {
|
||||||
|
ENABLED.store(true, Ordering::Relaxed)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn enabled() -> bool {
|
||||||
|
ENABLED.load(Ordering::Relaxed)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[macro_export]
|
||||||
|
macro_rules! now {
|
||||||
|
() => {
|
||||||
|
match std::time::SystemTime::now().duration_since(std::time::SystemTime::UNIX_EPOCH) {
|
||||||
|
Ok(n) => n.as_secs(),
|
||||||
|
Err(_) => 0,
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn fragment_received(fragment: &Fragment) {
|
||||||
|
if enabled() {
|
||||||
|
let id = fragment.fragment_identifier().set_id();
|
||||||
|
let mut entry = FRAGMENTS_RECEIVED.entry(id).or_default();
|
||||||
|
let r = ReceivedFragment::new(fragment.header(), now!());
|
||||||
|
entry.push(r);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn fragment_sent(fragment: &Fragment, client_nonce: i32, destination: PublicKey, hops: u8) {
|
||||||
|
if enabled() {
|
||||||
|
let id = fragment.fragment_identifier().set_id();
|
||||||
|
let mut entry = FRAGMENTS_SENT.entry(id).or_default();
|
||||||
|
let s = SentFragment::new(fragment.header(), now!(), client_nonce, destination, hops);
|
||||||
|
entry.push(s);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
pub struct FragmentMixParams {
|
pub struct FragmentMixParams {
|
||||||
destination: PublicKey,
|
destination: PublicKey,
|
||||||
@@ -112,35 +162,6 @@ impl ReceivedFragment {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub static FRAGMENTS_RECEIVED: LazyLock<DashMap<i32, Vec<ReceivedFragment>>> =
|
|
||||||
LazyLock::new(DashMap::new);
|
|
||||||
|
|
||||||
pub static FRAGMENTS_SENT: LazyLock<DashMap<i32, Vec<SentFragment>>> = LazyLock::new(DashMap::new);
|
|
||||||
|
|
||||||
#[macro_export]
|
|
||||||
macro_rules! now {
|
|
||||||
() => {
|
|
||||||
match std::time::SystemTime::now().duration_since(std::time::SystemTime::UNIX_EPOCH) {
|
|
||||||
Ok(n) => n.as_secs(),
|
|
||||||
Err(_) => 0,
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn fragment_received(fragment: &Fragment) {
|
|
||||||
let id = fragment.fragment_identifier().set_id();
|
|
||||||
let mut entry = FRAGMENTS_RECEIVED.entry(id).or_default();
|
|
||||||
let r = ReceivedFragment::new(fragment.header(), now!());
|
|
||||||
entry.push(r);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn fragment_sent(fragment: &Fragment, client_nonce: i32, destination: PublicKey, hops: u8) {
|
|
||||||
let id = fragment.fragment_identifier().set_id();
|
|
||||||
let mut entry = FRAGMENTS_SENT.entry(id).or_default();
|
|
||||||
let s = SentFragment::new(fragment.header(), now!(), client_nonce, destination, hops);
|
|
||||||
entry.push(s);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// The idea behind the process of chunking is to incur as little data overhead as possible due
|
/// The idea behind the process of chunking is to incur as little data overhead as possible due
|
||||||
/// to very computationally costly sphinx encapsulation procedure.
|
/// to very computationally costly sphinx encapsulation procedure.
|
||||||
///
|
///
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
// Copyright 2021 - Nym Technologies SA <contact@nymtech.net>
|
// Copyright 2021 - Nym Technologies SA <contact@nymtech.net>
|
||||||
// SPDX-License-Identifier: Apache-2.0
|
// SPDX-License-Identifier: Apache-2.0
|
||||||
use crate::fragment::Fragment;
|
use crate::fragment::Fragment;
|
||||||
use crate::{fragment_received, ChunkingError};
|
use crate::{monitoring, ChunkingError};
|
||||||
use log::*;
|
use log::*;
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
|
|
||||||
@@ -110,7 +110,7 @@ impl ReconstructionBuffer {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
fragment_received(&fragment);
|
monitoring::fragment_received(&fragment);
|
||||||
|
|
||||||
let fragment_index = fragment.current_fragment() as usize - 1;
|
let fragment_index = fragment.current_fragment() as usize - 1;
|
||||||
if self.fragments[fragment_index].is_some() {
|
if self.fragments[fragment_index].is_some() {
|
||||||
|
|||||||
@@ -12,7 +12,6 @@ use nym_sphinx_addressing::clients::Recipient;
|
|||||||
use nym_sphinx_addressing::nodes::NymNodeRoutingAddress;
|
use nym_sphinx_addressing::nodes::NymNodeRoutingAddress;
|
||||||
use nym_sphinx_anonymous_replies::reply_surb::ReplySurb;
|
use nym_sphinx_anonymous_replies::reply_surb::ReplySurb;
|
||||||
use nym_sphinx_chunking::fragment::{Fragment, FragmentIdentifier};
|
use nym_sphinx_chunking::fragment::{Fragment, FragmentIdentifier};
|
||||||
use nym_sphinx_chunking::fragment_sent;
|
|
||||||
use nym_sphinx_forwarding::packet::MixPacket;
|
use nym_sphinx_forwarding::packet::MixPacket;
|
||||||
use nym_sphinx_params::packet_sizes::PacketSize;
|
use nym_sphinx_params::packet_sizes::PacketSize;
|
||||||
use nym_sphinx_params::{PacketType, ReplySurbKeyDigestAlgorithm, DEFAULT_NUM_MIX_HOPS};
|
use nym_sphinx_params::{PacketType, ReplySurbKeyDigestAlgorithm, DEFAULT_NUM_MIX_HOPS};
|
||||||
@@ -21,6 +20,7 @@ use nym_topology::{NymTopology, NymTopologyError};
|
|||||||
use rand::{CryptoRng, Rng, SeedableRng};
|
use rand::{CryptoRng, Rng, SeedableRng};
|
||||||
use rand_chacha::ChaCha20Rng;
|
use rand_chacha::ChaCha20Rng;
|
||||||
|
|
||||||
|
use nym_sphinx_chunking::monitoring;
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
|
|
||||||
pub(crate) mod payload;
|
pub(crate) mod payload;
|
||||||
@@ -206,7 +206,7 @@ pub trait FragmentPreparer {
|
|||||||
|
|
||||||
let destination = packet_recipient.gateway();
|
let destination = packet_recipient.gateway();
|
||||||
let hops = mix_hops.unwrap_or(self.num_mix_hops());
|
let hops = mix_hops.unwrap_or(self.num_mix_hops());
|
||||||
fragment_sent(&fragment, self.nonce(), *destination, hops);
|
monitoring::fragment_sent(&fragment, self.nonce(), *destination, hops);
|
||||||
|
|
||||||
let non_reply_overhead = encryption::PUBLIC_KEY_SIZE;
|
let non_reply_overhead = encryption::PUBLIC_KEY_SIZE;
|
||||||
let expected_plaintext = match packet_type {
|
let expected_plaintext = match packet_type {
|
||||||
|
|||||||
@@ -42,8 +42,32 @@ impl PendingSync {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct BlockProcessorConfig {
|
||||||
|
pub pruning_options: PruningOptions,
|
||||||
|
pub store_precommits: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for BlockProcessorConfig {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self {
|
||||||
|
pruning_options: PruningOptions::nothing(),
|
||||||
|
store_precommits: true,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl BlockProcessorConfig {
|
||||||
|
pub fn new(pruning_options: PruningOptions, store_precommits: bool) -> Self {
|
||||||
|
Self {
|
||||||
|
pruning_options,
|
||||||
|
store_precommits,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub struct BlockProcessor {
|
pub struct BlockProcessor {
|
||||||
pruning_options: PruningOptions,
|
config: BlockProcessorConfig,
|
||||||
cancel: CancellationToken,
|
cancel: CancellationToken,
|
||||||
synced: Arc<Notify>,
|
synced: Arc<Notify>,
|
||||||
last_processed_height: u32,
|
last_processed_height: u32,
|
||||||
@@ -65,9 +89,10 @@ pub struct BlockProcessor {
|
|||||||
msg_modules: Vec<Box<dyn MsgModule + Send>>,
|
msg_modules: Vec<Box<dyn MsgModule + Send>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[allow(clippy::too_many_arguments)]
|
||||||
impl BlockProcessor {
|
impl BlockProcessor {
|
||||||
pub async fn new(
|
pub async fn new(
|
||||||
pruning_options: PruningOptions,
|
config: BlockProcessorConfig,
|
||||||
cancel: CancellationToken,
|
cancel: CancellationToken,
|
||||||
synced: Arc<Notify>,
|
synced: Arc<Notify>,
|
||||||
incoming: UnboundedReceiver<BlockToProcess>,
|
incoming: UnboundedReceiver<BlockToProcess>,
|
||||||
@@ -82,7 +107,7 @@ impl BlockProcessor {
|
|||||||
let last_pruned_height = last_pruned.try_into().unwrap_or_default();
|
let last_pruned_height = last_pruned.try_into().unwrap_or_default();
|
||||||
|
|
||||||
Ok(BlockProcessor {
|
Ok(BlockProcessor {
|
||||||
pruning_options,
|
config,
|
||||||
cancel,
|
cancel,
|
||||||
synced,
|
synced,
|
||||||
last_processed_height,
|
last_processed_height,
|
||||||
@@ -101,7 +126,7 @@ impl BlockProcessor {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn with_pruning(mut self, pruning_options: PruningOptions) -> Self {
|
pub fn with_pruning(mut self, pruning_options: PruningOptions) -> Self {
|
||||||
self.pruning_options = pruning_options;
|
self.config.pruning_options = pruning_options;
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -128,7 +153,7 @@ impl BlockProcessor {
|
|||||||
// we won't end up with a corrupted storage.
|
// we won't end up with a corrupted storage.
|
||||||
let mut tx = self.storage.begin_processing_tx().await?;
|
let mut tx = self.storage.begin_processing_tx().await?;
|
||||||
|
|
||||||
persist_block(&full_info, &mut tx).await?;
|
persist_block(&full_info, &mut tx, self.config.store_precommits).await?;
|
||||||
|
|
||||||
// let the modules do whatever they want
|
// let the modules do whatever they want
|
||||||
// the ones wanting the full block:
|
// the ones wanting the full block:
|
||||||
@@ -241,7 +266,7 @@ impl BlockProcessor {
|
|||||||
|
|
||||||
#[instrument(skip(self))]
|
#[instrument(skip(self))]
|
||||||
async fn prune_storage(&mut self) -> Result<(), ScraperError> {
|
async fn prune_storage(&mut self) -> Result<(), ScraperError> {
|
||||||
let keep_recent = self.pruning_options.strategy_keep_recent();
|
let keep_recent = self.config.pruning_options.strategy_keep_recent();
|
||||||
let last_to_keep = self.last_processed_height - keep_recent;
|
let last_to_keep = self.last_processed_height - keep_recent;
|
||||||
|
|
||||||
info!(
|
info!(
|
||||||
@@ -282,12 +307,12 @@ impl BlockProcessor {
|
|||||||
async fn maybe_prune_storage(&mut self) -> Result<(), ScraperError> {
|
async fn maybe_prune_storage(&mut self) -> Result<(), ScraperError> {
|
||||||
debug!("checking for storage pruning");
|
debug!("checking for storage pruning");
|
||||||
|
|
||||||
if self.pruning_options.strategy.is_nothing() {
|
if self.config.pruning_options.strategy.is_nothing() {
|
||||||
trace!("the current pruning strategy is 'nothing'");
|
trace!("the current pruning strategy is 'nothing'");
|
||||||
return Ok(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
|
|
||||||
let interval = self.pruning_options.strategy_interval();
|
let interval = self.config.pruning_options.strategy_interval();
|
||||||
if self.last_pruned_height + interval <= self.last_processed_height {
|
if self.last_pruned_height + interval <= self.last_processed_height {
|
||||||
self.prune_storage().await?;
|
self.prune_storage().await?;
|
||||||
}
|
}
|
||||||
@@ -371,7 +396,7 @@ impl BlockProcessor {
|
|||||||
if latest_block > self.last_processed_height && self.last_processed_height != 0 {
|
if latest_block > self.last_processed_height && self.last_processed_height != 0 {
|
||||||
// in case we were offline for a while,
|
// in case we were offline for a while,
|
||||||
// make sure we don't request blocks we'd have to prune anyway
|
// make sure we don't request blocks we'd have to prune anyway
|
||||||
let keep_recent = self.pruning_options.strategy_keep_recent();
|
let keep_recent = self.config.pruning_options.strategy_keep_recent();
|
||||||
let last_to_keep = latest_block - keep_recent;
|
let last_to_keep = latest_block - keep_recent;
|
||||||
self.last_processed_height = max(self.last_processed_height, last_to_keep);
|
self.last_processed_height = max(self.last_processed_height, last_to_keep);
|
||||||
|
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
// SPDX-License-Identifier: Apache-2.0
|
// SPDX-License-Identifier: Apache-2.0
|
||||||
|
|
||||||
use crate::block_processor::types::BlockToProcess;
|
use crate::block_processor::types::BlockToProcess;
|
||||||
use crate::block_processor::BlockProcessor;
|
use crate::block_processor::{BlockProcessor, BlockProcessorConfig};
|
||||||
use crate::block_requester::{BlockRequest, BlockRequester};
|
use crate::block_requester::{BlockRequest, BlockRequester};
|
||||||
use crate::error::ScraperError;
|
use crate::error::ScraperError;
|
||||||
use crate::modules::{BlockModule, MsgModule, TxModule};
|
use crate::modules::{BlockModule, MsgModule, TxModule};
|
||||||
@@ -34,6 +34,8 @@ pub struct Config {
|
|||||||
pub database_path: PathBuf,
|
pub database_path: PathBuf,
|
||||||
|
|
||||||
pub pruning_options: PruningOptions,
|
pub pruning_options: PruningOptions,
|
||||||
|
|
||||||
|
pub store_precommits: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct NyxdScraperBuilder {
|
pub struct NyxdScraperBuilder {
|
||||||
@@ -60,8 +62,14 @@ impl NyxdScraperBuilder {
|
|||||||
req_rx,
|
req_rx,
|
||||||
processing_tx.clone(),
|
processing_tx.clone(),
|
||||||
);
|
);
|
||||||
let mut block_processor = BlockProcessor::new(
|
|
||||||
|
let block_processor_config = BlockProcessorConfig::new(
|
||||||
scraper.config.pruning_options,
|
scraper.config.pruning_options,
|
||||||
|
scraper.config.store_precommits,
|
||||||
|
);
|
||||||
|
|
||||||
|
let mut block_processor = BlockProcessor::new(
|
||||||
|
block_processor_config,
|
||||||
scraper.cancel_token.clone(),
|
scraper.cancel_token.clone(),
|
||||||
scraper.startup_sync.clone(),
|
scraper.startup_sync.clone(),
|
||||||
processing_rx,
|
processing_rx,
|
||||||
@@ -275,8 +283,11 @@ impl NyxdScraper {
|
|||||||
req_tx: Sender<BlockRequest>,
|
req_tx: Sender<BlockRequest>,
|
||||||
processing_rx: UnboundedReceiver<BlockToProcess>,
|
processing_rx: UnboundedReceiver<BlockToProcess>,
|
||||||
) -> Result<BlockProcessor, ScraperError> {
|
) -> Result<BlockProcessor, ScraperError> {
|
||||||
|
let block_processor_config =
|
||||||
|
BlockProcessorConfig::new(self.config.pruning_options, self.config.store_precommits);
|
||||||
|
|
||||||
BlockProcessor::new(
|
BlockProcessor::new(
|
||||||
self.config.pruning_options,
|
block_processor_config,
|
||||||
self.cancel_token.clone(),
|
self.cancel_token.clone(),
|
||||||
self.startup_sync.clone(),
|
self.startup_sync.clone(),
|
||||||
processing_rx,
|
processing_rx,
|
||||||
|
|||||||
@@ -212,6 +212,7 @@ impl ScraperStorage {
|
|||||||
pub async fn persist_block(
|
pub async fn persist_block(
|
||||||
block: &FullBlockInformation,
|
block: &FullBlockInformation,
|
||||||
tx: &mut StorageTransaction,
|
tx: &mut StorageTransaction,
|
||||||
|
store_precommits: bool,
|
||||||
) -> Result<(), ScraperError> {
|
) -> Result<(), ScraperError> {
|
||||||
let total_gas = crate::helpers::tx_gas_sum(&block.transactions);
|
let total_gas = crate::helpers::tx_gas_sum(&block.transactions);
|
||||||
|
|
||||||
@@ -224,11 +225,12 @@ pub async fn persist_block(
|
|||||||
// persist block data
|
// persist block data
|
||||||
persist_block_data(&block.block, total_gas, tx).await?;
|
persist_block_data(&block.block, total_gas, tx).await?;
|
||||||
|
|
||||||
// persist commits
|
if store_precommits {
|
||||||
if let Some(commit) = &block.block.last_commit {
|
if let Some(commit) = &block.block.last_commit {
|
||||||
persist_commits(commit, &block.validators, tx).await?;
|
persist_commits(commit, &block.validators, tx).await?;
|
||||||
} else {
|
} else {
|
||||||
warn!("no commits for block {}", block.block.header.height)
|
warn!("no commits for block {}", block.block.header.height)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// persist txs
|
// persist txs
|
||||||
|
|||||||
@@ -3,9 +3,6 @@
|
|||||||
|
|
||||||
#[derive(Debug, thiserror::Error)]
|
#[derive(Debug, thiserror::Error)]
|
||||||
pub enum Error {
|
pub enum Error {
|
||||||
#[error("peers in wireguard don't match with in-memory ")]
|
|
||||||
PeerMismatch,
|
|
||||||
|
|
||||||
#[error("traffic byte data needs to be increasing")]
|
#[error("traffic byte data needs to be increasing")]
|
||||||
InconsistentConsumedBytes,
|
InconsistentConsumedBytes,
|
||||||
|
|
||||||
@@ -20,4 +17,7 @@ pub enum Error {
|
|||||||
|
|
||||||
#[error("{0}")]
|
#[error("{0}")]
|
||||||
GatewayStorage(#[from] nym_gateway_storage::error::StorageError),
|
GatewayStorage(#[from] nym_gateway_storage::error::StorageError),
|
||||||
|
|
||||||
|
#[error("{0}")]
|
||||||
|
SystemTime(#[from] std::time::SystemTimeError),
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ use crate::{error::Error, peer_handle::SharedBandwidthStorageManager};
|
|||||||
pub enum PeerControlRequest {
|
pub enum PeerControlRequest {
|
||||||
AddPeer {
|
AddPeer {
|
||||||
peer: Peer,
|
peer: Peer,
|
||||||
ticket_validation: bool,
|
client_id: Option<i64>,
|
||||||
response_tx: oneshot::Sender<AddPeerControlResponse>,
|
response_tx: oneshot::Sender<AddPeerControlResponse>,
|
||||||
},
|
},
|
||||||
RemovePeer {
|
RemovePeer {
|
||||||
@@ -46,7 +46,6 @@ pub enum PeerControlRequest {
|
|||||||
|
|
||||||
pub struct AddPeerControlResponse {
|
pub struct AddPeerControlResponse {
|
||||||
pub success: bool,
|
pub success: bool,
|
||||||
pub client_id: Option<i64>,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct RemovePeerControlResponse {
|
pub struct RemovePeerControlResponse {
|
||||||
@@ -118,13 +117,13 @@ impl<St: Storage + Clone + 'static> PeerController<St> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Function that should be used for peer insertion, to handle both storage and kernel interaction
|
// Function that should be used for peer insertion, to handle both storage and kernel interaction
|
||||||
pub async fn add_peer(&self, peer: &Peer, with_client_id: bool) -> Result<Option<i64>, Error> {
|
pub async fn add_peer(&self, peer: &Peer, client_id: Option<i64>) -> Result<(), Error> {
|
||||||
let client_id = self
|
if client_id.is_none() {
|
||||||
.storage
|
self.storage.insert_wireguard_peer(peer, false).await?;
|
||||||
.insert_wireguard_peer(peer, with_client_id)
|
}
|
||||||
.await?;
|
let ret: Result<(), defguard_wireguard_rs::error::WireguardInterfaceError> =
|
||||||
let ret = self.wg_api.inner.configure_peer(peer);
|
self.wg_api.inner.configure_peer(peer);
|
||||||
if ret.is_err() {
|
if client_id.is_none() && ret.is_err() {
|
||||||
// Try to revert the insertion in storage
|
// Try to revert the insertion in storage
|
||||||
if self
|
if self
|
||||||
.storage
|
.storage
|
||||||
@@ -135,8 +134,7 @@ impl<St: Storage + Clone + 'static> PeerController<St> {
|
|||||||
log::error!("The storage has been corrupted. Wireguard peer {} will persist in storage indefinitely.", peer.public_key);
|
log::error!("The storage has been corrupted. Wireguard peer {} will persist in storage indefinitely.", peer.public_key);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
ret?;
|
Ok(ret?)
|
||||||
Ok(client_id)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Function that should be used for peer removal, to handle both storage and kernel interaction
|
// Function that should be used for peer removal, to handle both storage and kernel interaction
|
||||||
@@ -160,13 +158,10 @@ impl<St: Storage + Clone + 'static> PeerController<St> {
|
|||||||
.ok_or(Error::MissingClientBandwidthEntry)?
|
.ok_or(Error::MissingClientBandwidthEntry)?
|
||||||
.client_id
|
.client_id
|
||||||
{
|
{
|
||||||
let bandwidth = storage
|
storage.create_bandwidth_entry(client_id).await?;
|
||||||
.get_available_bandwidth(client_id)
|
|
||||||
.await?
|
|
||||||
.ok_or(Error::MissingClientBandwidthEntry)?;
|
|
||||||
Ok(Some(BandwidthStorageManager::new(
|
Ok(Some(BandwidthStorageManager::new(
|
||||||
storage,
|
storage,
|
||||||
ClientBandwidth::new(bandwidth.into()),
|
ClientBandwidth::new(Default::default()),
|
||||||
client_id,
|
client_id,
|
||||||
BandwidthFlushingBehaviourConfig::default(),
|
BandwidthFlushingBehaviourConfig::default(),
|
||||||
true,
|
true,
|
||||||
@@ -179,9 +174,9 @@ impl<St: Storage + Clone + 'static> PeerController<St> {
|
|||||||
async fn handle_add_request(
|
async fn handle_add_request(
|
||||||
&mut self,
|
&mut self,
|
||||||
peer: &Peer,
|
peer: &Peer,
|
||||||
with_client_id: bool,
|
client_id: Option<i64>,
|
||||||
) -> Result<Option<i64>, Error> {
|
) -> Result<(), Error> {
|
||||||
let client_id = self.add_peer(peer, with_client_id).await?;
|
self.add_peer(peer, client_id).await?;
|
||||||
let bandwidth_storage_manager =
|
let bandwidth_storage_manager =
|
||||||
Self::generate_bandwidth_manager(self.storage.clone(), &peer.public_key)
|
Self::generate_bandwidth_manager(self.storage.clone(), &peer.public_key)
|
||||||
.await?
|
.await?
|
||||||
@@ -201,7 +196,7 @@ impl<St: Storage + Clone + 'static> PeerController<St> {
|
|||||||
log::error!("Peer handle shut down ungracefully - {e}");
|
log::error!("Peer handle shut down ungracefully - {e}");
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
Ok(client_id)
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn handle_query_peer(&self, key: &Key) -> Result<Option<Peer>, Error> {
|
async fn handle_query_peer(&self, key: &Key) -> Result<Option<Peer>, Error> {
|
||||||
@@ -228,14 +223,10 @@ impl<St: Storage + Clone + 'static> PeerController<St> {
|
|||||||
.available_bandwidth()
|
.available_bandwidth()
|
||||||
.await
|
.await
|
||||||
} else {
|
} else {
|
||||||
let peer = self
|
let Some(peer) = self.host_information.read().await.peers.get(key).cloned() else {
|
||||||
.host_information
|
// host information not updated yet
|
||||||
.read()
|
return Ok(None);
|
||||||
.await
|
};
|
||||||
.peers
|
|
||||||
.get(key)
|
|
||||||
.ok_or(Error::PeerMismatch)?
|
|
||||||
.clone();
|
|
||||||
BANDWIDTH_CAP_PER_DAY.saturating_sub((peer.rx_bytes + peer.tx_bytes) as i64)
|
BANDWIDTH_CAP_PER_DAY.saturating_sub((peer.rx_bytes + peer.tx_bytes) as i64)
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -260,12 +251,12 @@ impl<St: Storage + Clone + 'static> PeerController<St> {
|
|||||||
}
|
}
|
||||||
msg = self.request_rx.recv() => {
|
msg = self.request_rx.recv() => {
|
||||||
match msg {
|
match msg {
|
||||||
Some(PeerControlRequest::AddPeer { peer, ticket_validation, response_tx }) => {
|
Some(PeerControlRequest::AddPeer { peer, client_id, response_tx }) => {
|
||||||
let ret = self.handle_add_request(&peer, ticket_validation).await;
|
let ret = self.handle_add_request(&peer, client_id).await;
|
||||||
if let Ok(client_id) = ret {
|
if ret.is_ok() {
|
||||||
response_tx.send(AddPeerControlResponse { success: true, client_id }).ok();
|
response_tx.send(AddPeerControlResponse { success: true }).ok();
|
||||||
} else {
|
} else {
|
||||||
response_tx.send(AddPeerControlResponse { success: false, client_id: None }).ok();
|
response_tx.send(AddPeerControlResponse { success: false }).ok();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Some(PeerControlRequest::RemovePeer { key, response_tx }) => {
|
Some(PeerControlRequest::RemovePeer { key, response_tx }) => {
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
|
|
||||||
use crate::error::Error;
|
use crate::error::Error;
|
||||||
use crate::peer_controller::PeerControlRequest;
|
use crate::peer_controller::PeerControlRequest;
|
||||||
|
use defguard_wireguard_rs::host::Peer;
|
||||||
use defguard_wireguard_rs::{host::Host, key::Key};
|
use defguard_wireguard_rs::{host::Host, key::Key};
|
||||||
use futures::channel::oneshot;
|
use futures::channel::oneshot;
|
||||||
use nym_authenticator_requests::v2::registration::BANDWIDTH_CAP_PER_DAY;
|
use nym_authenticator_requests::v2::registration::BANDWIDTH_CAP_PER_DAY;
|
||||||
@@ -12,10 +13,12 @@ use nym_gateway_storage::Storage;
|
|||||||
use nym_task::TaskClient;
|
use nym_task::TaskClient;
|
||||||
use nym_wireguard_types::DEFAULT_PEER_TIMEOUT_CHECK;
|
use nym_wireguard_types::DEFAULT_PEER_TIMEOUT_CHECK;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
use std::time::{Duration, SystemTime};
|
||||||
use tokio::sync::{mpsc, RwLock};
|
use tokio::sync::{mpsc, RwLock};
|
||||||
use tokio_stream::{wrappers::IntervalStream, StreamExt};
|
use tokio_stream::{wrappers::IntervalStream, StreamExt};
|
||||||
|
|
||||||
pub(crate) type SharedBandwidthStorageManager<St> = Arc<RwLock<BandwidthStorageManager<St>>>;
|
pub(crate) type SharedBandwidthStorageManager<St> = Arc<RwLock<BandwidthStorageManager<St>>>;
|
||||||
|
const AUTO_REMOVE_AFTER: Duration = Duration::from_secs(60 * 60 * 24); // 24 hours
|
||||||
|
|
||||||
pub struct PeerHandle<St> {
|
pub struct PeerHandle<St> {
|
||||||
storage: St,
|
storage: St,
|
||||||
@@ -25,6 +28,7 @@ pub struct PeerHandle<St> {
|
|||||||
request_tx: mpsc::Sender<PeerControlRequest>,
|
request_tx: mpsc::Sender<PeerControlRequest>,
|
||||||
timeout_check_interval: IntervalStream,
|
timeout_check_interval: IntervalStream,
|
||||||
task_client: TaskClient,
|
task_client: TaskClient,
|
||||||
|
startup_timestamp: SystemTime,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<St: Storage + Clone + 'static> PeerHandle<St> {
|
impl<St: Storage + Clone + 'static> PeerHandle<St> {
|
||||||
@@ -39,7 +43,8 @@ impl<St: Storage + Clone + 'static> PeerHandle<St> {
|
|||||||
let timeout_check_interval = tokio_stream::wrappers::IntervalStream::new(
|
let timeout_check_interval = tokio_stream::wrappers::IntervalStream::new(
|
||||||
tokio::time::interval(DEFAULT_PEER_TIMEOUT_CHECK),
|
tokio::time::interval(DEFAULT_PEER_TIMEOUT_CHECK),
|
||||||
);
|
);
|
||||||
let task_client = task_client.fork(format!("peer{public_key}"));
|
let mut task_client = task_client.fork(format!("peer-{public_key}"));
|
||||||
|
task_client.disarm();
|
||||||
PeerHandle {
|
PeerHandle {
|
||||||
storage,
|
storage,
|
||||||
public_key,
|
public_key,
|
||||||
@@ -48,14 +53,11 @@ impl<St: Storage + Clone + 'static> PeerHandle<St> {
|
|||||||
request_tx,
|
request_tx,
|
||||||
timeout_check_interval,
|
timeout_check_interval,
|
||||||
task_client,
|
task_client,
|
||||||
|
startup_timestamp: SystemTime::now(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn remove_depleted_peer(&self) -> Result<bool, Error> {
|
async fn remove_peer(&self) -> Result<bool, Error> {
|
||||||
log::debug!(
|
|
||||||
"Peer {} doesn't have bandwidth anymore, removing it",
|
|
||||||
self.public_key.to_string()
|
|
||||||
);
|
|
||||||
let (response_tx, response_rx) = oneshot::channel();
|
let (response_tx, response_rx) = oneshot::channel();
|
||||||
self.request_tx
|
self.request_tx
|
||||||
.send(PeerControlRequest::RemovePeer {
|
.send(PeerControlRequest::RemovePeer {
|
||||||
@@ -71,15 +73,11 @@ impl<St: Storage + Clone + 'static> PeerHandle<St> {
|
|||||||
Ok(success)
|
Ok(success)
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn active_peer(&mut self, storage_peer: WireguardPeer) -> Result<bool, Error> {
|
async fn active_peer(
|
||||||
let kernel_peer = self
|
&mut self,
|
||||||
.host_information
|
storage_peer: WireguardPeer,
|
||||||
.read()
|
kernel_peer: Peer,
|
||||||
.await
|
) -> Result<bool, Error> {
|
||||||
.peers
|
|
||||||
.get(&self.public_key)
|
|
||||||
.ok_or(Error::PeerMismatch)?
|
|
||||||
.clone();
|
|
||||||
if let Some(bandwidth_manager) = &self.bandwidth_storage_manager {
|
if let Some(bandwidth_manager) = &self.bandwidth_storage_manager {
|
||||||
let spent_bandwidth = (kernel_peer.rx_bytes + kernel_peer.tx_bytes)
|
let spent_bandwidth = (kernel_peer.rx_bytes + kernel_peer.tx_bytes)
|
||||||
.checked_sub(storage_peer.rx_bytes as u64 + storage_peer.tx_bytes as u64)
|
.checked_sub(storage_peer.rx_bytes as u64 + storage_peer.tx_bytes as u64)
|
||||||
@@ -93,13 +91,25 @@ impl<St: Storage + Clone + 'static> PeerHandle<St> {
|
|||||||
.await
|
.await
|
||||||
.is_err()
|
.is_err()
|
||||||
{
|
{
|
||||||
let success = self.remove_depleted_peer().await?;
|
let success = self.remove_peer().await?;
|
||||||
return Ok(!success);
|
return Ok(!success);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
if SystemTime::now().duration_since(self.startup_timestamp)? >= AUTO_REMOVE_AFTER {
|
||||||
|
log::debug!(
|
||||||
|
"Peer {} has been present for 24 hours, removing it",
|
||||||
|
self.public_key.to_string()
|
||||||
|
);
|
||||||
|
let success = self.remove_peer().await?;
|
||||||
|
return Ok(!success);
|
||||||
|
}
|
||||||
let spent_bandwidth = kernel_peer.rx_bytes + kernel_peer.tx_bytes;
|
let spent_bandwidth = kernel_peer.rx_bytes + kernel_peer.tx_bytes;
|
||||||
if spent_bandwidth >= BANDWIDTH_CAP_PER_DAY {
|
if spent_bandwidth >= BANDWIDTH_CAP_PER_DAY {
|
||||||
let success = self.remove_depleted_peer().await?;
|
log::debug!(
|
||||||
|
"Peer {} doesn't have bandwidth anymore, removing it",
|
||||||
|
self.public_key.to_string()
|
||||||
|
);
|
||||||
|
let success = self.remove_peer().await?;
|
||||||
return Ok(!success);
|
return Ok(!success);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -111,11 +121,21 @@ impl<St: Storage + Clone + 'static> PeerHandle<St> {
|
|||||||
while !self.task_client.is_shutdown() {
|
while !self.task_client.is_shutdown() {
|
||||||
tokio::select! {
|
tokio::select! {
|
||||||
_ = self.timeout_check_interval.next() => {
|
_ = self.timeout_check_interval.next() => {
|
||||||
let Some(peer) = self.storage.get_wireguard_peer(&self.public_key.to_string()).await? else {
|
let Some(kernel_peer) = self
|
||||||
|
.host_information
|
||||||
|
.read()
|
||||||
|
.await
|
||||||
|
.peers
|
||||||
|
.get(&self.public_key)
|
||||||
|
.cloned() else {
|
||||||
|
// the host information hasn't beed updated yet
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
let Some(storage_peer) = self.storage.get_wireguard_peer(&self.public_key.to_string()).await? else {
|
||||||
log::debug!("Peer {:?} not in storage anymore, shutting down handle", self.public_key);
|
log::debug!("Peer {:?} not in storage anymore, shutting down handle", self.public_key);
|
||||||
return Ok(());
|
return Ok(());
|
||||||
};
|
};
|
||||||
if !self.active_peer(peer).await? {
|
if !self.active_peer(storage_peer, kernel_peer).await? {
|
||||||
log::debug!("Peer {:?} doesn't have bandwidth anymore, shutting down handle", self.public_key);
|
log::debug!("Peer {:?} doesn't have bandwidth anymore, shutting down handle", self.public_key);
|
||||||
return Ok(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
|
|||||||
+2
-2
@@ -19,5 +19,5 @@ MULTISIG_CONTRACT_ADDRESS=n1zwv6feuzhy6a9wekh96cd57lsarmqlwxdypdsplw6zhfncqw6ftq
|
|||||||
COCONUT_DKG_CONTRACT_ADDRESS=n1aakfpghcanxtc45gpqlx8j3rq0zcpyf49qmhm9mdjrfx036h4z5sy2vfh9
|
COCONUT_DKG_CONTRACT_ADDRESS=n1aakfpghcanxtc45gpqlx8j3rq0zcpyf49qmhm9mdjrfx036h4z5sy2vfh9
|
||||||
|
|
||||||
EXPLORER_API=https://canary-explorer.performance.nymte.ch/api
|
EXPLORER_API=https://canary-explorer.performance.nymte.ch/api
|
||||||
NYXD="https://canary-validator.performance.nymte.ch"
|
NYXD=https://canary-validator.performance.nymte.ch
|
||||||
NYM_API="https://canary-api.performance.nymte.ch/api"
|
NYM_API=https://canary-api.performance.nymte.ch/api
|
||||||
|
|||||||
+3
-3
@@ -21,8 +21,8 @@ COCONUT_DKG_CONTRACT_ADDRESS=n19604yflqggs9mk2z26mqygq43q2kr3n932egxx630svywd5mp
|
|||||||
|
|
||||||
REWARDING_VALIDATOR_ADDRESS=n10yyd98e2tuwu0f7ypz9dy3hhjw7v772q6287gy
|
REWARDING_VALIDATOR_ADDRESS=n10yyd98e2tuwu0f7ypz9dy3hhjw7v772q6287gy
|
||||||
STATISTICS_SERVICE_DOMAIN_ADDRESS="https://mainnet-stats.nymte.ch:8090"
|
STATISTICS_SERVICE_DOMAIN_ADDRESS="https://mainnet-stats.nymte.ch:8090"
|
||||||
NYXD="https://rpc.nymtech.net"
|
NYXD=https://rpc.nymtech.net
|
||||||
NYM_API="https://validator.nymtech.net/api/"
|
NYM_API=https://validator.nymtech.net/api/
|
||||||
NYXD_WS="wss://rpc.nymtech.net/websocket"
|
NYXD_WS="wss://rpc.nymtech.net/websocket"
|
||||||
EXPLORER_API="https://explorer.nymtech.net/api/"
|
EXPLORER_API=https://explorer.nymtech.net/api/
|
||||||
NYM_VPN_API="https://nymvpn.com/api"
|
NYM_VPN_API="https://nymvpn.com/api"
|
||||||
|
|||||||
+2
-2
@@ -19,5 +19,5 @@ VESTING_CONTRACT_ADDRESS=n1jlzdxnyces4hrhqz68dqk28mrw5jgwtcfq0c2funcwrmw0dx9l9s8
|
|||||||
REWARDING_VALIDATOR_ADDRESS=n1rfvpsynktze6wvn6ldskj8xgwfzzk5v6pnff39
|
REWARDING_VALIDATOR_ADDRESS=n1rfvpsynktze6wvn6ldskj8xgwfzzk5v6pnff39
|
||||||
|
|
||||||
EXPLORER_API=https://qa-network-explorer.qa.nymte.ch/api
|
EXPLORER_API=https://qa-network-explorer.qa.nymte.ch/api
|
||||||
NYXD="https://qa-validator.qa.nymte.ch"
|
NYXD=https://qa-validator.qa.nymte.ch
|
||||||
NYM_API="https://qa-nym-api.qa.nymte.ch/api"
|
NYM_API=https://qa-nym-api.qa.nymte.ch/api
|
||||||
|
|||||||
+3
-3
@@ -20,6 +20,6 @@ ECASH_CONTRACT_ADDRESS=n1v3vydvs2ued84yv3khqwtgldmgwn0elljsdh08dr5s2j9x4rc5fs9jl
|
|||||||
|
|
||||||
STATISTICS_SERVICE_DOMAIN_ADDRESS="http://0.0.0.0"
|
STATISTICS_SERVICE_DOMAIN_ADDRESS="http://0.0.0.0"
|
||||||
EXPLORER_API=https://sandbox-explorer.nymtech.net/api
|
EXPLORER_API=https://sandbox-explorer.nymtech.net/api
|
||||||
NYXD="https://rpc.sandbox.nymtech.net"
|
NYXD=https://rpc.sandbox.nymtech.net
|
||||||
NYXD_WS="wss://rpc.sandbox.nymtech.net/websocket"
|
NYXD_WS=wss://rpc.sandbox.nymtech.net/websocket
|
||||||
NYM_API="https://sandbox-nym-api1.nymtech.net/api"
|
NYM_API=https://sandbox-nym-api1.nymtech.net/api
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "explorer-api"
|
name = "explorer-api"
|
||||||
version = "1.1.40"
|
version = "1.1.41"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
license.workspace = true
|
license.workspace = true
|
||||||
|
|
||||||
|
|||||||
@@ -7,12 +7,12 @@ license.workspace = true
|
|||||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
log.workspace = true
|
|
||||||
nym-explorer-api-requests = { path = "../explorer-api-requests" }
|
nym-explorer-api-requests = { path = "../explorer-api-requests" }
|
||||||
reqwest = { workspace = true, features = ["json"] }
|
reqwest = { workspace = true, features = ["json"] }
|
||||||
serde.workspace = true
|
serde.workspace = true
|
||||||
thiserror.workspace = true
|
thiserror.workspace = true
|
||||||
url.workspace = true
|
url.workspace = true
|
||||||
|
tracing = {workspace = true, features = ["attributes"]}
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
tokio = { workspace = true, features = ["full"] }
|
tokio = { workspace = true, features = ["full"] }
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ use std::time::Duration;
|
|||||||
|
|
||||||
use reqwest::StatusCode;
|
use reqwest::StatusCode;
|
||||||
use thiserror::Error;
|
use thiserror::Error;
|
||||||
|
use tracing::instrument;
|
||||||
use url::Url;
|
use url::Url;
|
||||||
|
|
||||||
// Re-export request types
|
// Re-export request types
|
||||||
@@ -47,6 +48,12 @@ impl ExplorerClient {
|
|||||||
Ok(Self { client, url })
|
Ok(Self { client, url })
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(not(target_arch = "wasm32"))]
|
||||||
|
pub fn new_with_timeout(url: url::Url, timeout: Duration) -> Result<Self, ExplorerApiError> {
|
||||||
|
let client = reqwest::Client::builder().timeout(timeout).build()?;
|
||||||
|
Ok(Self { client, url })
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg(target_arch = "wasm32")]
|
#[cfg(target_arch = "wasm32")]
|
||||||
pub fn new(url: url::Url) -> Result<Self, ExplorerApiError> {
|
pub fn new(url: url::Url) -> Result<Self, ExplorerApiError> {
|
||||||
let client = reqwest::Client::builder().build()?;
|
let client = reqwest::Client::builder().build()?;
|
||||||
@@ -58,10 +65,11 @@ impl ExplorerClient {
|
|||||||
paths: &[&str],
|
paths: &[&str],
|
||||||
) -> Result<reqwest::Response, ExplorerApiError> {
|
) -> Result<reqwest::Response, ExplorerApiError> {
|
||||||
let url = combine_url(self.url.clone(), paths)?;
|
let url = combine_url(self.url.clone(), paths)?;
|
||||||
log::trace!("Sending GET request {url:?}");
|
tracing::debug!("Sending GET request");
|
||||||
Ok(self.client.get(url).send().await?)
|
Ok(self.client.get(url).send().await?)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[instrument(level = "trace", skip_all, fields(paths=?paths))]
|
||||||
async fn query_explorer_api<T>(&self, paths: &[&str]) -> Result<T, ExplorerApiError>
|
async fn query_explorer_api<T>(&self, paths: &[&str]) -> Result<T, ExplorerApiError>
|
||||||
where
|
where
|
||||||
T: std::fmt::Debug,
|
T: std::fmt::Debug,
|
||||||
@@ -70,12 +78,14 @@ impl ExplorerClient {
|
|||||||
let response = self.send_get_request(paths).await?;
|
let response = self.send_get_request(paths).await?;
|
||||||
if response.status().is_success() {
|
if response.status().is_success() {
|
||||||
let res = response.json::<T>().await?;
|
let res = response.json::<T>().await?;
|
||||||
log::trace!("Got response: {res:?}");
|
tracing::trace!("Got response: {res:?}");
|
||||||
Ok(res)
|
Ok(res)
|
||||||
} else if response.status() == StatusCode::NOT_FOUND {
|
} else if response.status() == StatusCode::NOT_FOUND {
|
||||||
Err(ExplorerApiError::NotFound)
|
Err(ExplorerApiError::NotFound)
|
||||||
} else {
|
} else {
|
||||||
Err(ExplorerApiError::RequestFailure(response.text().await?))
|
let status = response.status();
|
||||||
|
let err_msg = format!("{}: {}", response.text().await?, status);
|
||||||
|
Err(ExplorerApiError::RequestFailure(err_msg))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -69,6 +69,7 @@ nym-credentials-interface = { path = "../common/credentials-interface" }
|
|||||||
nym-credential-verification = { path = "../common/credential-verification" }
|
nym-credential-verification = { path = "../common/credential-verification" }
|
||||||
nym-crypto = { path = "../common/crypto" }
|
nym-crypto = { path = "../common/crypto" }
|
||||||
nym-gateway-storage = { path = "../common/gateway-storage" }
|
nym-gateway-storage = { path = "../common/gateway-storage" }
|
||||||
|
nym-gateway-stats-storage = { path = "../common/gateway-stats-storage" }
|
||||||
nym-gateway-requests = { path = "../common/gateway-requests" }
|
nym-gateway-requests = { path = "../common/gateway-requests" }
|
||||||
nym-mixnet-client = { path = "../common/client-libs/mixnet-client" }
|
nym-mixnet-client = { path = "../common/client-libs/mixnet-client" }
|
||||||
nym-mixnode-common = { path = "../common/mixnode-common" }
|
nym-mixnode-common = { path = "../common/mixnode-common" }
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ pub const DEFAULT_PRIVATE_SPHINX_KEY_FILENAME: &str = "private_sphinx.pem";
|
|||||||
pub const DEFAULT_PUBLIC_SPHINX_KEY_FILENAME: &str = "public_sphinx.pem";
|
pub const DEFAULT_PUBLIC_SPHINX_KEY_FILENAME: &str = "public_sphinx.pem";
|
||||||
|
|
||||||
pub const DEFAULT_CLIENTS_STORAGE_FILENAME: &str = "db.sqlite";
|
pub const DEFAULT_CLIENTS_STORAGE_FILENAME: &str = "db.sqlite";
|
||||||
|
pub const DEFAULT_STATS_STORAGE_FILENAME: &str = "stats.sqlite";
|
||||||
|
|
||||||
pub const DEFAULT_NETWORK_REQUESTER_CONFIG_FILENAME: &str = "network_requester_config.toml";
|
pub const DEFAULT_NETWORK_REQUESTER_CONFIG_FILENAME: &str = "network_requester_config.toml";
|
||||||
pub const DEFAULT_NETWORK_REQUESTER_DATA_DIR: &str = "network-requester-data";
|
pub const DEFAULT_NETWORK_REQUESTER_DATA_DIR: &str = "network-requester-data";
|
||||||
@@ -39,6 +40,9 @@ pub struct GatewayPaths {
|
|||||||
#[serde(alias = "persistent_storage")]
|
#[serde(alias = "persistent_storage")]
|
||||||
pub clients_storage: PathBuf,
|
pub clients_storage: PathBuf,
|
||||||
|
|
||||||
|
/// Path to sqlite database containing all persistent stats data.
|
||||||
|
pub stats_storage: PathBuf,
|
||||||
|
|
||||||
/// Path to the configuration of the embedded network requester.
|
/// Path to the configuration of the embedded network requester.
|
||||||
#[serde(deserialize_with = "de_maybe_stringified")]
|
#[serde(deserialize_with = "de_maybe_stringified")]
|
||||||
pub network_requester_config: Option<PathBuf>,
|
pub network_requester_config: Option<PathBuf>,
|
||||||
@@ -54,7 +58,9 @@ impl GatewayPaths {
|
|||||||
pub fn new_default<P: AsRef<Path>>(id: P) -> Self {
|
pub fn new_default<P: AsRef<Path>>(id: P) -> Self {
|
||||||
GatewayPaths {
|
GatewayPaths {
|
||||||
keys: KeysPaths::new_default(id.as_ref()),
|
keys: KeysPaths::new_default(id.as_ref()),
|
||||||
clients_storage: default_data_directory(id).join(DEFAULT_CLIENTS_STORAGE_FILENAME),
|
clients_storage: default_data_directory(id.as_ref())
|
||||||
|
.join(DEFAULT_CLIENTS_STORAGE_FILENAME),
|
||||||
|
stats_storage: default_data_directory(id).join(DEFAULT_STATS_STORAGE_FILENAME),
|
||||||
// node_description: default_config_filepath(id).join(DEFAULT_DESCRIPTION_FILENAME),
|
// node_description: default_config_filepath(id).join(DEFAULT_DESCRIPTION_FILENAME),
|
||||||
network_requester_config: None,
|
network_requester_config: None,
|
||||||
ip_packet_router_config: None,
|
ip_packet_router_config: None,
|
||||||
@@ -70,6 +76,7 @@ impl GatewayPaths {
|
|||||||
public_sphinx_key_file: Default::default(),
|
public_sphinx_key_file: Default::default(),
|
||||||
},
|
},
|
||||||
clients_storage: Default::default(),
|
clients_storage: Default::default(),
|
||||||
|
stats_storage: Default::default(),
|
||||||
network_requester_config: None,
|
network_requester_config: None,
|
||||||
ip_packet_router_config: None,
|
ip_packet_router_config: None,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
// SPDX-License-Identifier: GPL-3.0-only
|
// SPDX-License-Identifier: GPL-3.0-only
|
||||||
|
|
||||||
use nym_authenticator::error::AuthenticatorError;
|
use nym_authenticator::error::AuthenticatorError;
|
||||||
|
use nym_gateway_stats_storage::error::StatsStorageError;
|
||||||
use nym_gateway_storage::error::StorageError;
|
use nym_gateway_storage::error::StorageError;
|
||||||
use nym_ip_packet_router::error::IpPacketRouterError;
|
use nym_ip_packet_router::error::IpPacketRouterError;
|
||||||
use nym_network_requester::error::{ClientCoreError, NetworkRequesterError};
|
use nym_network_requester::error::{ClientCoreError, NetworkRequesterError};
|
||||||
@@ -115,6 +116,12 @@ pub enum GatewayError {
|
|||||||
source: StorageError,
|
source: StorageError,
|
||||||
},
|
},
|
||||||
|
|
||||||
|
#[error("stats storage failure: {source}")]
|
||||||
|
StatsStorageError {
|
||||||
|
#[from]
|
||||||
|
source: StatsStorageError,
|
||||||
|
},
|
||||||
|
|
||||||
#[error("Path to network requester configuration file hasn't been specified. Perhaps try to run `setup-network-requester`?")]
|
#[error("Path to network requester configuration file hasn't been specified. Perhaps try to run `setup-network-requester`?")]
|
||||||
UnspecifiedNetworkRequesterConfig,
|
UnspecifiedNetworkRequesterConfig,
|
||||||
|
|
||||||
|
|||||||
@@ -73,6 +73,9 @@ pub(crate) enum InitialAuthenticationError {
|
|||||||
#[error("Only 'Register' or 'Authenticate' requests are allowed")]
|
#[error("Only 'Register' or 'Authenticate' requests are allowed")]
|
||||||
InvalidRequest,
|
InvalidRequest,
|
||||||
|
|
||||||
|
#[error("received a Message of type {typ} which was not expected in this context")]
|
||||||
|
UnexpectedMessageType { typ: String },
|
||||||
|
|
||||||
#[error("Experienced connection error: {0}")]
|
#[error("Experienced connection error: {0}")]
|
||||||
ConnectionError(#[from] WsError),
|
ConnectionError(#[from] WsError),
|
||||||
|
|
||||||
@@ -861,9 +864,27 @@ where
|
|||||||
Message::Binary(_) => {
|
Message::Binary(_) => {
|
||||||
return Err(InitialAuthenticationError::BinaryRequestWithoutAuthentication);
|
return Err(InitialAuthenticationError::BinaryRequestWithoutAuthentication);
|
||||||
}
|
}
|
||||||
_ => unreachable!(
|
other => {
|
||||||
"the underlying tunsgenite stream should be handling other message types"
|
if other.is_ping() {
|
||||||
),
|
debug!("unexpected ping message!");
|
||||||
|
return Err(InitialAuthenticationError::UnexpectedMessageType {
|
||||||
|
typ: "ping".to_string(),
|
||||||
|
});
|
||||||
|
} else if other.is_pong() {
|
||||||
|
debug!("unexpected pong message!");
|
||||||
|
return Err(InitialAuthenticationError::UnexpectedMessageType {
|
||||||
|
typ: "pong".to_string(),
|
||||||
|
});
|
||||||
|
} else if other.is_close() {
|
||||||
|
debug!("unexpected close message!");
|
||||||
|
return Err(InitialAuthenticationError::UnexpectedMessageType {
|
||||||
|
typ: "close".to_string(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// at this point this is definitely unreachable, but just in case, let's not panic...
|
||||||
|
return Err(InitialAuthenticationError::InvalidRequest);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
text.parse()
|
text.parse()
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ use crate::config::Config;
|
|||||||
use crate::error::GatewayError;
|
use crate::error::GatewayError;
|
||||||
|
|
||||||
use nym_crypto::asymmetric::encryption;
|
use nym_crypto::asymmetric::encryption;
|
||||||
|
use nym_gateway_stats_storage::PersistentStatsStorage;
|
||||||
use nym_gateway_storage::PersistentStorage;
|
use nym_gateway_storage::PersistentStorage;
|
||||||
use nym_pemstore::traits::PemStorableKeyPair;
|
use nym_pemstore::traits::PemStorableKeyPair;
|
||||||
use nym_pemstore::KeyPairPath;
|
use nym_pemstore::KeyPairPath;
|
||||||
@@ -74,6 +75,14 @@ pub(crate) async fn initialise_main_storage(
|
|||||||
Ok(PersistentStorage::init(path, retrieval_limit).await?)
|
Ok(PersistentStorage::init(path, retrieval_limit).await?)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub(crate) async fn initialise_stats_storage(
|
||||||
|
config: &Config,
|
||||||
|
) -> Result<PersistentStatsStorage, GatewayError> {
|
||||||
|
let path = &config.storage_paths.stats_storage;
|
||||||
|
|
||||||
|
Ok(PersistentStatsStorage::init(path).await?)
|
||||||
|
}
|
||||||
|
|
||||||
pub fn load_keypair<T: PemStorableKeyPair>(
|
pub fn load_keypair<T: PemStorableKeyPair>(
|
||||||
paths: KeyPairPath,
|
paths: KeyPairPath,
|
||||||
name: impl Into<String>,
|
name: impl Into<String>,
|
||||||
|
|||||||
+25
-14
@@ -12,7 +12,9 @@ use crate::http::HttpApiBuilder;
|
|||||||
use crate::node::client_handling::active_clients::ActiveClientsStore;
|
use crate::node::client_handling::active_clients::ActiveClientsStore;
|
||||||
use crate::node::client_handling::embedded_clients::{LocalEmbeddedClientHandle, MessageRouter};
|
use crate::node::client_handling::embedded_clients::{LocalEmbeddedClientHandle, MessageRouter};
|
||||||
use crate::node::client_handling::websocket;
|
use crate::node::client_handling::websocket;
|
||||||
use crate::node::helpers::{initialise_main_storage, load_network_requester_config};
|
use crate::node::helpers::{
|
||||||
|
initialise_main_storage, initialise_stats_storage, load_network_requester_config,
|
||||||
|
};
|
||||||
use crate::node::mixnet_handling::receiver::connection_handler::ConnectionHandler;
|
use crate::node::mixnet_handling::receiver::connection_handler::ConnectionHandler;
|
||||||
use futures::channel::{mpsc, oneshot};
|
use futures::channel::{mpsc, oneshot};
|
||||||
use nym_credential_verification::ecash::{
|
use nym_credential_verification::ecash::{
|
||||||
@@ -41,6 +43,7 @@ pub(crate) mod helpers;
|
|||||||
pub(crate) mod mixnet_handling;
|
pub(crate) mod mixnet_handling;
|
||||||
pub(crate) mod statistics;
|
pub(crate) mod statistics;
|
||||||
|
|
||||||
|
pub use nym_gateway_stats_storage::PersistentStatsStorage;
|
||||||
pub use nym_gateway_storage::{PersistentStorage, Storage};
|
pub use nym_gateway_storage::{PersistentStorage, Storage};
|
||||||
|
|
||||||
// TODO: should this struct live here?
|
// TODO: should this struct live here?
|
||||||
@@ -96,6 +99,8 @@ pub async fn create_gateway(
|
|||||||
|
|
||||||
let storage = initialise_main_storage(&config).await?;
|
let storage = initialise_main_storage(&config).await?;
|
||||||
|
|
||||||
|
let stats_storage = initialise_stats_storage(&config).await?;
|
||||||
|
|
||||||
let nr_opts = network_requester_config.map(|config| LocalNetworkRequesterOpts {
|
let nr_opts = network_requester_config.map(|config| LocalNetworkRequesterOpts {
|
||||||
config: config.clone(),
|
config: config.clone(),
|
||||||
custom_mixnet_path: custom_mixnet.clone(),
|
custom_mixnet_path: custom_mixnet.clone(),
|
||||||
@@ -106,7 +111,7 @@ pub async fn create_gateway(
|
|||||||
custom_mixnet_path: custom_mixnet.clone(),
|
custom_mixnet_path: custom_mixnet.clone(),
|
||||||
});
|
});
|
||||||
|
|
||||||
Gateway::new(config, nr_opts, ip_opts, storage)
|
Gateway::new(config, nr_opts, ip_opts, storage, stats_storage)
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
@@ -147,7 +152,9 @@ pub struct Gateway<St = PersistentStorage> {
|
|||||||
/// x25519 keypair used for Diffie-Hellman. Currently only used for sphinx key derivation.
|
/// x25519 keypair used for Diffie-Hellman. Currently only used for sphinx key derivation.
|
||||||
sphinx_keypair: Arc<encryption::KeyPair>,
|
sphinx_keypair: Arc<encryption::KeyPair>,
|
||||||
|
|
||||||
storage: St,
|
client_storage: St,
|
||||||
|
|
||||||
|
stats_storage: PersistentStatsStorage,
|
||||||
|
|
||||||
wireguard_data: Option<nym_wireguard::WireguardData>,
|
wireguard_data: Option<nym_wireguard::WireguardData>,
|
||||||
|
|
||||||
@@ -163,10 +170,12 @@ impl<St> Gateway<St> {
|
|||||||
config: Config,
|
config: Config,
|
||||||
network_requester_opts: Option<LocalNetworkRequesterOpts>,
|
network_requester_opts: Option<LocalNetworkRequesterOpts>,
|
||||||
ip_packet_router_opts: Option<LocalIpPacketRouterOpts>,
|
ip_packet_router_opts: Option<LocalIpPacketRouterOpts>,
|
||||||
storage: St,
|
client_storage: St,
|
||||||
|
stats_storage: PersistentStatsStorage,
|
||||||
) -> Result<Self, GatewayError> {
|
) -> Result<Self, GatewayError> {
|
||||||
Ok(Gateway {
|
Ok(Gateway {
|
||||||
storage,
|
client_storage,
|
||||||
|
stats_storage,
|
||||||
identity_keypair: Arc::new(load_identity_keys(&config)?),
|
identity_keypair: Arc::new(load_identity_keys(&config)?),
|
||||||
sphinx_keypair: Arc::new(helpers::load_sphinx_keys(&config)?),
|
sphinx_keypair: Arc::new(helpers::load_sphinx_keys(&config)?),
|
||||||
config,
|
config,
|
||||||
@@ -179,7 +188,7 @@ impl<St> Gateway<St> {
|
|||||||
task_client: None,
|
task_client: None,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
#[allow(clippy::too_many_arguments)]
|
||||||
pub fn new_loaded(
|
pub fn new_loaded(
|
||||||
config: Config,
|
config: Config,
|
||||||
network_requester_opts: Option<LocalNetworkRequesterOpts>,
|
network_requester_opts: Option<LocalNetworkRequesterOpts>,
|
||||||
@@ -187,7 +196,8 @@ impl<St> Gateway<St> {
|
|||||||
authenticator_opts: Option<LocalAuthenticatorOpts>,
|
authenticator_opts: Option<LocalAuthenticatorOpts>,
|
||||||
identity_keypair: Arc<identity::KeyPair>,
|
identity_keypair: Arc<identity::KeyPair>,
|
||||||
sphinx_keypair: Arc<encryption::KeyPair>,
|
sphinx_keypair: Arc<encryption::KeyPair>,
|
||||||
storage: St,
|
client_storage: St,
|
||||||
|
stats_storage: PersistentStatsStorage,
|
||||||
) -> Self {
|
) -> Self {
|
||||||
Gateway {
|
Gateway {
|
||||||
config,
|
config,
|
||||||
@@ -196,7 +206,8 @@ impl<St> Gateway<St> {
|
|||||||
authenticator_opts,
|
authenticator_opts,
|
||||||
identity_keypair,
|
identity_keypair,
|
||||||
sphinx_keypair,
|
sphinx_keypair,
|
||||||
storage,
|
client_storage,
|
||||||
|
stats_storage,
|
||||||
wireguard_data: None,
|
wireguard_data: None,
|
||||||
session_stats: None,
|
session_stats: None,
|
||||||
run_http_server: true,
|
run_http_server: true,
|
||||||
@@ -240,7 +251,7 @@ impl<St> Gateway<St> {
|
|||||||
|
|
||||||
let connection_handler = ConnectionHandler::new(
|
let connection_handler = ConnectionHandler::new(
|
||||||
packet_processor,
|
packet_processor,
|
||||||
self.storage.clone(),
|
self.client_storage.clone(),
|
||||||
ack_sender,
|
ack_sender,
|
||||||
active_clients_store,
|
active_clients_store,
|
||||||
);
|
);
|
||||||
@@ -275,7 +286,7 @@ impl<St> Gateway<St> {
|
|||||||
forwarding_channel,
|
forwarding_channel,
|
||||||
router_tx,
|
router_tx,
|
||||||
);
|
);
|
||||||
let all_peers = self.storage.get_all_wireguard_peers().await?;
|
let all_peers = self.client_storage.get_all_wireguard_peers().await?;
|
||||||
let used_private_network_ips = all_peers
|
let used_private_network_ips = all_peers
|
||||||
.iter()
|
.iter()
|
||||||
.cloned()
|
.cloned()
|
||||||
@@ -330,7 +341,7 @@ impl<St> Gateway<St> {
|
|||||||
.start_with_shutdown(router_shutdown);
|
.start_with_shutdown(router_shutdown);
|
||||||
|
|
||||||
let wg_api = nym_wireguard::start_wireguard(
|
let wg_api = nym_wireguard::start_wireguard(
|
||||||
self.storage.clone(),
|
self.client_storage.clone(),
|
||||||
all_peers,
|
all_peers,
|
||||||
shutdown,
|
shutdown,
|
||||||
wireguard_data,
|
wireguard_data,
|
||||||
@@ -377,7 +388,7 @@ impl<St> Gateway<St> {
|
|||||||
|
|
||||||
let shared_state = websocket::CommonHandlerState {
|
let shared_state = websocket::CommonHandlerState {
|
||||||
ecash_verifier,
|
ecash_verifier,
|
||||||
storage: self.storage.clone(),
|
storage: self.client_storage.clone(),
|
||||||
local_identity: Arc::clone(&self.identity_keypair),
|
local_identity: Arc::clone(&self.identity_keypair),
|
||||||
only_coconut_credentials: self.config.gateway.only_coconut_credentials,
|
only_coconut_credentials: self.config.gateway.only_coconut_credentials,
|
||||||
bandwidth_cfg: (&self.config).into(),
|
bandwidth_cfg: (&self.config).into(),
|
||||||
@@ -415,7 +426,7 @@ impl<St> Gateway<St> {
|
|||||||
info!("Starting gateway stats collector...");
|
info!("Starting gateway stats collector...");
|
||||||
|
|
||||||
let (mut stats_collector, stats_event_sender) =
|
let (mut stats_collector, stats_event_sender) =
|
||||||
GatewayStatisticsCollector::new(shared_session_stats);
|
GatewayStatisticsCollector::new(shared_session_stats, self.stats_storage.clone());
|
||||||
tokio::spawn(async move { stats_collector.run(shutdown).await });
|
tokio::spawn(async move { stats_collector.run(shutdown).await });
|
||||||
stats_event_sender
|
stats_event_sender
|
||||||
}
|
}
|
||||||
@@ -654,7 +665,7 @@ impl<St> Gateway<St> {
|
|||||||
nyxd_client,
|
nyxd_client,
|
||||||
self.identity_keypair.public_key().to_bytes(),
|
self.identity_keypair.public_key().to_bytes(),
|
||||||
shutdown.fork("EcashVerifier"),
|
shutdown.fork("EcashVerifier"),
|
||||||
self.storage.clone(),
|
self.client_storage.clone(),
|
||||||
)
|
)
|
||||||
.await?,
|
.await?,
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -2,13 +2,14 @@
|
|||||||
// SPDX-License-Identifier: GPL-3.0-only
|
// SPDX-License-Identifier: GPL-3.0-only
|
||||||
|
|
||||||
use futures::{channel::mpsc, StreamExt};
|
use futures::{channel::mpsc, StreamExt};
|
||||||
|
use nym_gateway_stats_storage::PersistentStatsStorage;
|
||||||
use nym_node_http_api::state::metrics::SharedSessionStats;
|
use nym_node_http_api::state::metrics::SharedSessionStats;
|
||||||
use nym_statistics_common::events::{StatsEvent, StatsEventReceiver, StatsEventSender};
|
use nym_statistics_common::events::{StatsEvent, StatsEventReceiver, StatsEventSender};
|
||||||
use nym_task::TaskClient;
|
use nym_task::TaskClient;
|
||||||
use sessions::SessionStatsHandler;
|
use sessions::SessionStatsHandler;
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
use time::OffsetDateTime;
|
use time::OffsetDateTime;
|
||||||
use tracing::trace;
|
use tracing::{error, trace, warn};
|
||||||
|
|
||||||
pub mod sessions;
|
pub mod sessions;
|
||||||
|
|
||||||
@@ -23,21 +24,38 @@ pub(crate) struct GatewayStatisticsCollector {
|
|||||||
impl GatewayStatisticsCollector {
|
impl GatewayStatisticsCollector {
|
||||||
pub fn new(
|
pub fn new(
|
||||||
shared_session_stats: SharedSessionStats,
|
shared_session_stats: SharedSessionStats,
|
||||||
|
stats_storage: PersistentStatsStorage,
|
||||||
) -> (GatewayStatisticsCollector, StatsEventSender) {
|
) -> (GatewayStatisticsCollector, StatsEventSender) {
|
||||||
let (stats_event_tx, stats_event_rx) = mpsc::unbounded();
|
let (stats_event_tx, stats_event_rx) = mpsc::unbounded();
|
||||||
|
|
||||||
|
let session_stats = SessionStatsHandler::new(shared_session_stats, stats_storage);
|
||||||
let collector = GatewayStatisticsCollector {
|
let collector = GatewayStatisticsCollector {
|
||||||
stats_event_rx,
|
stats_event_rx,
|
||||||
session_stats: SessionStatsHandler::new(shared_session_stats),
|
session_stats,
|
||||||
};
|
};
|
||||||
(collector, stats_event_tx)
|
(collector, stats_event_tx)
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn update_shared_state(&mut self, update_time: OffsetDateTime) {
|
async fn update_shared_state(&mut self, update_time: OffsetDateTime) {
|
||||||
self.session_stats.update_shared_state(update_time).await;
|
if let Err(e) = self
|
||||||
|
.session_stats
|
||||||
|
.maybe_update_shared_state(update_time)
|
||||||
|
.await
|
||||||
|
{
|
||||||
|
error!("Failed to update session stats - {e}");
|
||||||
|
}
|
||||||
//here goes additionnal stats handler update
|
//here goes additionnal stats handler update
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async fn on_start(&mut self) {
|
||||||
|
if let Err(e) = self.session_stats.on_start().await {
|
||||||
|
error!("Failed to cleanup session stats handler - {e}");
|
||||||
|
}
|
||||||
|
//here goes additionnal stats handler start cleanup
|
||||||
|
}
|
||||||
|
|
||||||
pub async fn run(&mut self, mut shutdown: TaskClient) {
|
pub async fn run(&mut self, mut shutdown: TaskClient) {
|
||||||
|
self.on_start().await;
|
||||||
let mut update_interval = tokio::time::interval(STATISTICS_UPDATE_TIMER_INTERVAL);
|
let mut update_interval = tokio::time::interval(STATISTICS_UPDATE_TIMER_INTERVAL);
|
||||||
while !shutdown.is_shutdown() {
|
while !shutdown.is_shutdown() {
|
||||||
tokio::select! {
|
tokio::select! {
|
||||||
@@ -53,7 +71,10 @@ impl GatewayStatisticsCollector {
|
|||||||
Some(stat_event) = self.stats_event_rx.next() => {
|
Some(stat_event) = self.stats_event_rx.next() => {
|
||||||
//dispatching event to proper handler
|
//dispatching event to proper handler
|
||||||
match stat_event {
|
match stat_event {
|
||||||
StatsEvent::SessionStatsEvent(event) => self.session_stats.handle_event(event),
|
StatsEvent::SessionStatsEvent(event) => {
|
||||||
|
if let Err(e) = self.session_stats.handle_event(event).await{
|
||||||
|
warn!("Session event handling error - {e}");
|
||||||
|
}},
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|||||||
@@ -2,176 +2,158 @@
|
|||||||
// SPDX-License-Identifier: GPL-3.0-only
|
// SPDX-License-Identifier: GPL-3.0-only
|
||||||
|
|
||||||
use nym_credentials_interface::TicketType;
|
use nym_credentials_interface::TicketType;
|
||||||
|
use nym_gateway_stats_storage::models::FinishedSession;
|
||||||
|
use nym_gateway_stats_storage::PersistentStatsStorage;
|
||||||
|
use nym_gateway_stats_storage::{error::StatsStorageError, models::ActiveSession};
|
||||||
use nym_node_http_api::state::metrics::SharedSessionStats;
|
use nym_node_http_api::state::metrics::SharedSessionStats;
|
||||||
use nym_sphinx::DestinationAddressBytes;
|
use nym_sphinx::DestinationAddressBytes;
|
||||||
use std::collections::{HashMap, HashSet};
|
|
||||||
use time::{Date, Duration, OffsetDateTime};
|
use time::{Date, Duration, OffsetDateTime};
|
||||||
|
|
||||||
use nym_statistics_common::events::SessionEvent;
|
use nym_statistics_common::events::SessionEvent;
|
||||||
|
|
||||||
const FINISHED_SESSIONS_CAP: usize = 1_000_000; //to be on the safe side of memory blowups until persistent storage
|
|
||||||
|
|
||||||
#[derive(PartialEq)]
|
|
||||||
enum SessionType {
|
|
||||||
Vpn,
|
|
||||||
Mixnet,
|
|
||||||
Unknown,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl SessionType {
|
|
||||||
fn to_string(&self) -> &str {
|
|
||||||
match self {
|
|
||||||
Self::Vpn => "vpn",
|
|
||||||
Self::Mixnet => "mixnet",
|
|
||||||
Self::Unknown => "unknown",
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<TicketType> for SessionType {
|
|
||||||
fn from(value: TicketType) -> Self {
|
|
||||||
match value {
|
|
||||||
TicketType::V1MixnetEntry => Self::Mixnet,
|
|
||||||
TicketType::V1MixnetExit => Self::Mixnet,
|
|
||||||
TicketType::V1WireguardEntry => Self::Vpn,
|
|
||||||
TicketType::V1WireguardExit => Self::Vpn,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
struct FinishedSession {
|
|
||||||
duration: Duration,
|
|
||||||
typ: SessionType,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl FinishedSession {
|
|
||||||
fn serialize(&self) -> (u64, String) {
|
|
||||||
(
|
|
||||||
self.duration.whole_milliseconds() as u64, //we are sure that it fits in a u64, see `fn end_at`
|
|
||||||
self.typ.to_string().into(),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
struct ActiveSession {
|
|
||||||
start: OffsetDateTime,
|
|
||||||
typ: SessionType,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl ActiveSession {
|
|
||||||
fn new(start_time: OffsetDateTime) -> Self {
|
|
||||||
ActiveSession {
|
|
||||||
start: start_time,
|
|
||||||
typ: SessionType::Unknown,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn set_type(&mut self, ticket_type: TicketType) {
|
|
||||||
self.typ = ticket_type.into();
|
|
||||||
}
|
|
||||||
|
|
||||||
fn end_at(self, stop_time: OffsetDateTime) -> Option<FinishedSession> {
|
|
||||||
let session_duration = stop_time - self.start;
|
|
||||||
//ensure duration is positive to fit in a u64
|
|
||||||
//u64::max milliseconds is 500k millenia so no overflow issue
|
|
||||||
if session_duration > Duration::ZERO {
|
|
||||||
Some(FinishedSession {
|
|
||||||
duration: session_duration,
|
|
||||||
typ: self.typ,
|
|
||||||
})
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(crate) struct SessionStatsHandler {
|
pub(crate) struct SessionStatsHandler {
|
||||||
last_update_day: Date,
|
storage: PersistentStatsStorage,
|
||||||
|
current_day: Date,
|
||||||
|
|
||||||
shared_session_stats: SharedSessionStats,
|
shared_session_stats: SharedSessionStats,
|
||||||
active_sessions: HashMap<DestinationAddressBytes, ActiveSession>,
|
|
||||||
unique_users: HashSet<DestinationAddressBytes>,
|
|
||||||
sessions_started: u32,
|
|
||||||
finished_sessions: Vec<FinishedSession>,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl SessionStatsHandler {
|
impl SessionStatsHandler {
|
||||||
pub fn new(shared_session_stats: SharedSessionStats) -> Self {
|
pub fn new(shared_session_stats: SharedSessionStats, storage: PersistentStatsStorage) -> Self {
|
||||||
SessionStatsHandler {
|
SessionStatsHandler {
|
||||||
last_update_day: OffsetDateTime::now_utc().date(),
|
storage,
|
||||||
|
current_day: OffsetDateTime::now_utc().date(),
|
||||||
shared_session_stats,
|
shared_session_stats,
|
||||||
active_sessions: Default::default(),
|
|
||||||
unique_users: Default::default(),
|
|
||||||
sessions_started: 0,
|
|
||||||
finished_sessions: Default::default(),
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn handle_event(&mut self, event: SessionEvent) {
|
pub(crate) async fn handle_event(
|
||||||
|
&mut self,
|
||||||
|
event: SessionEvent,
|
||||||
|
) -> Result<(), StatsStorageError> {
|
||||||
match event {
|
match event {
|
||||||
SessionEvent::SessionStart { start_time, client } => {
|
SessionEvent::SessionStart { start_time, client } => {
|
||||||
self.handle_session_start(start_time, client);
|
self.handle_session_start(start_time, client).await
|
||||||
}
|
}
|
||||||
|
|
||||||
SessionEvent::SessionStop { stop_time, client } => {
|
SessionEvent::SessionStop { stop_time, client } => {
|
||||||
self.handle_session_stop(stop_time, client);
|
self.handle_session_stop(stop_time, client).await
|
||||||
}
|
}
|
||||||
|
|
||||||
SessionEvent::EcashTicket {
|
SessionEvent::EcashTicket {
|
||||||
ticket_type,
|
ticket_type,
|
||||||
client,
|
client,
|
||||||
} => self.handle_ecash_ticket(ticket_type, client),
|
} => self.handle_ecash_ticket(ticket_type, client).await,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
fn handle_session_start(
|
async fn handle_session_start(
|
||||||
&mut self,
|
&mut self,
|
||||||
start_time: OffsetDateTime,
|
start_time: OffsetDateTime,
|
||||||
client: DestinationAddressBytes,
|
client: DestinationAddressBytes,
|
||||||
) {
|
) -> Result<(), StatsStorageError> {
|
||||||
self.sessions_started += 1;
|
self.storage
|
||||||
self.unique_users.insert(client);
|
.insert_unique_user(self.current_day, client.as_base58_string())
|
||||||
self.active_sessions
|
.await?;
|
||||||
.insert(client, ActiveSession::new(start_time));
|
self.storage
|
||||||
}
|
.insert_active_session(client, ActiveSession::new(start_time))
|
||||||
fn handle_session_stop(&mut self, stop_time: OffsetDateTime, client: DestinationAddressBytes) {
|
.await?;
|
||||||
if let Some(session) = self.active_sessions.remove(&client) {
|
Ok(())
|
||||||
if let Some(finished_session) = session.end_at(stop_time) {
|
|
||||||
if self.finished_sessions.len() < FINISHED_SESSIONS_CAP {
|
|
||||||
self.finished_sessions.push(finished_session);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn handle_ecash_ticket(&mut self, ticket_type: TicketType, client: DestinationAddressBytes) {
|
async fn handle_session_stop(
|
||||||
if let Some(active_session) = self.active_sessions.get_mut(&client) {
|
&mut self,
|
||||||
if active_session.typ == SessionType::Unknown {
|
stop_time: OffsetDateTime,
|
||||||
active_session.set_type(ticket_type);
|
client: DestinationAddressBytes,
|
||||||
|
) -> Result<(), StatsStorageError> {
|
||||||
|
if let Some(session) = self.storage.get_active_session(client).await? {
|
||||||
|
if let Some(finished_session) = session.end_at(stop_time) {
|
||||||
|
self.storage
|
||||||
|
.insert_finished_session(self.current_day, finished_session)
|
||||||
|
.await?;
|
||||||
|
self.storage.delete_active_session(client).await?;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn handle_ecash_ticket(
|
||||||
|
&mut self,
|
||||||
|
ticket_type: TicketType,
|
||||||
|
client: DestinationAddressBytes,
|
||||||
|
) -> Result<(), StatsStorageError> {
|
||||||
|
self.storage
|
||||||
|
.update_active_session_type(client, ticket_type.into())
|
||||||
|
.await?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) async fn on_start(&mut self) -> Result<(), StatsStorageError> {
|
||||||
|
let yesterday = OffsetDateTime::now_utc().date() - Duration::DAY;
|
||||||
|
//publish yesterday's data if any
|
||||||
|
self.publish_stats(yesterday).await?;
|
||||||
|
//store "active" sessions as duration 0
|
||||||
|
for active_session in self.storage.get_all_active_sessions().await? {
|
||||||
|
self.storage
|
||||||
|
.insert_finished_session(
|
||||||
|
self.current_day,
|
||||||
|
FinishedSession {
|
||||||
|
duration: Duration::ZERO,
|
||||||
|
typ: active_session.typ,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.await?
|
||||||
|
}
|
||||||
|
//cleanup active sessions
|
||||||
|
self.storage.cleanup_active_sessions().await?;
|
||||||
|
|
||||||
|
//delete old entries
|
||||||
|
self.delete_old_stats(yesterday - Duration::DAY).await?;
|
||||||
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
//update shared state once a day has passed, with data from the previous day
|
//update shared state once a day has passed, with data from the previous day
|
||||||
pub(crate) async fn update_shared_state(&mut self, update_time: OffsetDateTime) {
|
async fn publish_stats(&mut self, stats_date: Date) -> Result<(), StatsStorageError> {
|
||||||
let update_date = update_time.date();
|
let finished_sessions = self.storage.get_finished_sessions(stats_date).await?;
|
||||||
if update_date != self.last_update_day {
|
let user_count = self.storage.get_unique_users_count(stats_date).await?;
|
||||||
{
|
let session_started = self.storage.get_started_sessions_count(stats_date).await? as u32;
|
||||||
let mut shared_state = self.shared_session_stats.write().await;
|
{
|
||||||
shared_state.update_time = self.last_update_day;
|
let mut shared_state = self.shared_session_stats.write().await;
|
||||||
shared_state.unique_active_users = self.unique_users.len() as u32;
|
shared_state.update_time = stats_date;
|
||||||
shared_state.session_started = self.sessions_started;
|
shared_state.unique_active_users = user_count as u32;
|
||||||
shared_state.sessions = self
|
shared_state.session_started = session_started;
|
||||||
.finished_sessions
|
shared_state.sessions = finished_sessions.iter().map(|s| s.serialize()).collect();
|
||||||
.iter()
|
|
||||||
.map(|s| s.serialize())
|
|
||||||
.collect();
|
|
||||||
}
|
|
||||||
self.reset_stats(update_date);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
pub(crate) async fn maybe_update_shared_state(
|
||||||
|
&mut self,
|
||||||
|
update_time: OffsetDateTime,
|
||||||
|
) -> Result<(), StatsStorageError> {
|
||||||
|
let update_date = update_time.date();
|
||||||
|
if update_date != self.current_day {
|
||||||
|
self.publish_stats(self.current_day).await?;
|
||||||
|
self.delete_old_stats(self.current_day - Duration::DAY)
|
||||||
|
.await?;
|
||||||
|
self.reset_stats(update_date).await?;
|
||||||
|
self.current_day = update_date;
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn reset_stats(&mut self, reset_day: Date) {
|
async fn reset_stats(&mut self, reset_day: Date) -> Result<(), StatsStorageError> {
|
||||||
self.last_update_day = reset_day;
|
//active users reset
|
||||||
self.unique_users = self.active_sessions.keys().copied().collect();
|
let new_active_users = self.storage.get_active_users().await?;
|
||||||
self.finished_sessions = Default::default();
|
for user in new_active_users {
|
||||||
self.sessions_started = 0;
|
self.storage.insert_unique_user(reset_day, user).await?;
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn delete_old_stats(&mut self, delete_before: Date) -> Result<(), StatsStorageError> {
|
||||||
|
self.storage.delete_finished_sessions(delete_before).await?;
|
||||||
|
self.storage.delete_unique_users(delete_before).await?;
|
||||||
|
Ok(())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,7 @@
|
|||||||
|
FROM rust:latest AS builder
|
||||||
|
|
||||||
|
COPY ./ /usr/src/nym
|
||||||
|
WORKDIR /usr/src/nym/nym-api
|
||||||
|
RUN cargo build --release
|
||||||
|
|
||||||
|
ENTRYPOINT ["/usr/src/nym/nym-api/entrypoint.sh"]
|
||||||
+2
-2
@@ -4,7 +4,7 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "nym-api"
|
name = "nym-api"
|
||||||
license = "GPL-3.0"
|
license = "GPL-3.0"
|
||||||
version = "1.1.44"
|
version = "1.1.45"
|
||||||
authors.workspace = true
|
authors.workspace = true
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
rust-version.workspace = true
|
rust-version.workspace = true
|
||||||
@@ -18,7 +18,7 @@ bip39 = { workspace = true }
|
|||||||
bincode.workspace = true
|
bincode.workspace = true
|
||||||
bloomfilter = { workspace = true }
|
bloomfilter = { workspace = true }
|
||||||
cfg-if = { workspace = true }
|
cfg-if = { workspace = true }
|
||||||
clap = { workspace = true, features = ["cargo", "derive"] }
|
clap = { workspace = true, features = ["cargo", "derive", "env"] }
|
||||||
console-subscriber = { workspace = true, optional = true } # validator-api needs to be built with RUSTFLAGS="--cfg tokio_unstable"
|
console-subscriber = { workspace = true, optional = true } # validator-api needs to be built with RUSTFLAGS="--cfg tokio_unstable"
|
||||||
dirs = { workspace = true }
|
dirs = { workspace = true }
|
||||||
futures = { workspace = true }
|
futures = { workspace = true }
|
||||||
|
|||||||
Executable
+5
@@ -0,0 +1,5 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
|
||||||
|
set -e
|
||||||
|
|
||||||
|
/usr/src/nym/target/release/nym-api init && /usr/src/nym/target/release/nym-api run
|
||||||
@@ -24,6 +24,9 @@ pub type Result<T, E = EcashError> = std::result::Result<T, E>;
|
|||||||
|
|
||||||
#[derive(Debug, Error)]
|
#[derive(Debug, Error)]
|
||||||
pub enum EcashError {
|
pub enum EcashError {
|
||||||
|
#[error("permanently restricted")]
|
||||||
|
Restricted,
|
||||||
|
|
||||||
#[error(transparent)]
|
#[error(transparent)]
|
||||||
IOError(#[from] std::io::Error),
|
IOError(#[from] std::io::Error),
|
||||||
|
|
||||||
|
|||||||
@@ -10,27 +10,33 @@ use std::net::SocketAddr;
|
|||||||
pub(crate) struct Args {
|
pub(crate) struct Args {
|
||||||
/// Id of the nym-api we want to initialise. if unspecified, a default value will be used.
|
/// Id of the nym-api we want to initialise. if unspecified, a default value will be used.
|
||||||
/// default: "default"
|
/// default: "default"
|
||||||
#[clap(long, default_value = "default")]
|
#[clap(long, default_value = "default", env = "NYMAPI_ID_ARG")]
|
||||||
pub(crate) id: String,
|
pub(crate) id: String,
|
||||||
|
|
||||||
/// Specifies whether network monitoring is enabled on this API
|
/// Specifies whether network monitoring is enabled on this API
|
||||||
/// default: false
|
/// default: false
|
||||||
#[clap(short = 'm', long)]
|
#[clap(short = 'm', long, env = "NYMAPI_ENABLE_MONITOR_ARG")]
|
||||||
pub(crate) enable_monitor: bool,
|
pub(crate) enable_monitor: bool,
|
||||||
|
|
||||||
/// Specifies whether network rewarding is enabled on this API
|
/// Specifies whether network rewarding is enabled on this API
|
||||||
/// default: false
|
/// default: false
|
||||||
#[clap(short = 'r', long, requires = "enable_monitor", requires = "mnemonic")]
|
#[clap(
|
||||||
|
short = 'r',
|
||||||
|
long,
|
||||||
|
requires = "enable_monitor",
|
||||||
|
requires = "mnemonic",
|
||||||
|
env = "NYMAPI_ENABLE_REWARDING_ARG"
|
||||||
|
)]
|
||||||
pub(crate) enable_rewarding: bool,
|
pub(crate) enable_rewarding: bool,
|
||||||
|
|
||||||
/// Endpoint to nyxd instance used for contract information.
|
/// Endpoint to nyxd instance used for contract information.
|
||||||
/// default: http://localhost:26657
|
/// default: http://localhost:26657
|
||||||
#[clap(long)]
|
#[clap(long, env = "NYMAPI_NYXD_VALIDATOR_ARG")]
|
||||||
pub(crate) nyxd_validator: Option<url::Url>,
|
pub(crate) nyxd_validator: Option<url::Url>,
|
||||||
|
|
||||||
/// Mnemonic of the network monitor used for sending rewarding and zk-nyms transactions
|
/// Mnemonic of the network monitor used for sending rewarding and zk-nyms transactions
|
||||||
/// default: None
|
/// default: None
|
||||||
#[clap(long)]
|
#[clap(long, env = "NYMAPI_MNEMONIC_ARG")]
|
||||||
pub(crate) mnemonic: Option<bip39::Mnemonic>,
|
pub(crate) mnemonic: Option<bip39::Mnemonic>,
|
||||||
|
|
||||||
/// Flag to indicate whether credential signer authority is enabled on this API
|
/// Flag to indicate whether credential signer authority is enabled on this API
|
||||||
@@ -39,18 +45,23 @@ pub(crate) struct Args {
|
|||||||
long,
|
long,
|
||||||
requires = "mnemonic",
|
requires = "mnemonic",
|
||||||
requires = "announce_address",
|
requires = "announce_address",
|
||||||
alias = "enable_coconut"
|
alias = "enable_coconut",
|
||||||
|
env = "NYMAPI_ENABLE_ZK_NYM_ARG"
|
||||||
)]
|
)]
|
||||||
pub(crate) enable_zk_nym: bool,
|
pub(crate) enable_zk_nym: bool,
|
||||||
|
|
||||||
/// Announced address that is going to be put in the DKG contract where zk-nym clients will connect
|
/// Announced address that is going to be put in the DKG contract where zk-nym clients will connect
|
||||||
/// to obtain their credentials
|
/// to obtain their credentials
|
||||||
/// default: None
|
/// default: None
|
||||||
#[clap(long)]
|
#[clap(long, env = "NYMAPI_ANNOUNCE_ADDRESS_NYM_ARG")]
|
||||||
pub(crate) announce_address: Option<url::Url>,
|
pub(crate) announce_address: Option<url::Url>,
|
||||||
|
|
||||||
/// Set this nym api to work in a enabled credentials that would attempt to use gateway with the bandwidth credential requirement
|
/// Set this nym api to work in a enabled credentials that would attempt to use gateway with the bandwidth credential requirement
|
||||||
#[clap(long, requires = "enable_monitor")]
|
#[clap(
|
||||||
|
long,
|
||||||
|
requires = "enable_monitor",
|
||||||
|
env = "NYMAPI_MONITOR_CREDENTIALS_MODE_ARG"
|
||||||
|
)]
|
||||||
pub(crate) monitor_credentials_mode: bool,
|
pub(crate) monitor_credentials_mode: bool,
|
||||||
|
|
||||||
/// Socket address this api will use for binding its http API.
|
/// Socket address this api will use for binding its http API.
|
||||||
|
|||||||
@@ -20,11 +20,11 @@ fn pretty_build_info_static() -> &'static str {
|
|||||||
#[clap(author = "Nymtech", version, long_version = pretty_build_info_static(), about)]
|
#[clap(author = "Nymtech", version, long_version = pretty_build_info_static(), about)]
|
||||||
pub(crate) struct Cli {
|
pub(crate) struct Cli {
|
||||||
/// Path pointing to an env file that configures the Nym API.
|
/// Path pointing to an env file that configures the Nym API.
|
||||||
#[clap(short, long)]
|
#[clap(short, long, env = "NYMAPI_CONFIG_ENV_FILE_ARG")]
|
||||||
pub(crate) config_env_file: Option<std::path::PathBuf>,
|
pub(crate) config_env_file: Option<std::path::PathBuf>,
|
||||||
|
|
||||||
/// A no-op flag included for consistency with other binaries (and compatibility with nymvisor, oops)
|
/// A no-op flag included for consistency with other binaries (and compatibility with nymvisor, oops)
|
||||||
#[clap(long)]
|
#[clap(long, env = "NYMAPI_NO_BANNER_ARG")]
|
||||||
pub(crate) no_banner: bool,
|
pub(crate) no_banner: bool,
|
||||||
|
|
||||||
#[clap(subcommand)]
|
#[clap(subcommand)]
|
||||||
|
|||||||
@@ -44,27 +44,33 @@ use tracing::{error, info};
|
|||||||
pub(crate) struct Args {
|
pub(crate) struct Args {
|
||||||
/// Id of the nym-api we want to run.if unspecified, a default value will be used.
|
/// Id of the nym-api we want to run.if unspecified, a default value will be used.
|
||||||
/// default: "default"
|
/// default: "default"
|
||||||
#[clap(long, default_value = "default")]
|
#[clap(long, default_value = "default", env = "NYMAPI_ID_ARG")]
|
||||||
pub(crate) id: String,
|
pub(crate) id: String,
|
||||||
|
|
||||||
/// Specifies whether network monitoring is enabled on this API
|
/// Specifies whether network monitoring is enabled on this API
|
||||||
/// default: None - config value will be used instead
|
/// default: None - config value will be used instead
|
||||||
#[clap(short = 'm', long)]
|
#[clap(short = 'm', long, env = "NYMAPI_ENABLE_MONITOR_ARG")]
|
||||||
pub(crate) enable_monitor: Option<bool>,
|
pub(crate) enable_monitor: Option<bool>,
|
||||||
|
|
||||||
/// Specifies whether network rewarding is enabled on this API
|
/// Specifies whether network rewarding is enabled on this API
|
||||||
/// default: None - config value will be used instead
|
/// default: None - config value will be used instead
|
||||||
#[clap(short = 'r', long, requires = "enable_monitor", requires = "mnemonic")]
|
#[clap(
|
||||||
|
short = 'r',
|
||||||
|
long,
|
||||||
|
requires = "enable_monitor",
|
||||||
|
requires = "mnemonic",
|
||||||
|
env = "NYMAPI_ENABLE_REWARDING_ARG"
|
||||||
|
)]
|
||||||
pub(crate) enable_rewarding: Option<bool>,
|
pub(crate) enable_rewarding: Option<bool>,
|
||||||
|
|
||||||
/// Endpoint to nyxd instance used for contract information.
|
/// Endpoint to nyxd instance used for contract information.
|
||||||
/// default: None - config value will be used instead
|
/// default: None - config value will be used instead
|
||||||
#[clap(long)]
|
#[clap(long, env = "NYMAPI_NYXD_VALIDATOR_ARG")]
|
||||||
pub(crate) nyxd_validator: Option<url::Url>,
|
pub(crate) nyxd_validator: Option<url::Url>,
|
||||||
|
|
||||||
/// Mnemonic of the network monitor used for sending rewarding and zk-nyms transactions
|
/// Mnemonic of the network monitor used for sending rewarding and zk-nyms transactions
|
||||||
/// default: None - config value will be used instead
|
/// default: None - config value will be used instead
|
||||||
#[clap(long)]
|
#[clap(long, env = "NYMAPI_MNEMONIC_ARG")]
|
||||||
pub(crate) mnemonic: Option<bip39::Mnemonic>,
|
pub(crate) mnemonic: Option<bip39::Mnemonic>,
|
||||||
|
|
||||||
/// Flag to indicate whether coconut signer authority is enabled on this API
|
/// Flag to indicate whether coconut signer authority is enabled on this API
|
||||||
@@ -73,19 +79,20 @@ pub(crate) struct Args {
|
|||||||
long,
|
long,
|
||||||
requires = "mnemonic",
|
requires = "mnemonic",
|
||||||
requires = "announce_address",
|
requires = "announce_address",
|
||||||
alias = "enable_coconut"
|
alias = "enable_coconut",
|
||||||
|
env = "NYMAPI_ENABLE_ZK_NYM_ARG"
|
||||||
)]
|
)]
|
||||||
pub(crate) enable_zk_nym: Option<bool>,
|
pub(crate) enable_zk_nym: Option<bool>,
|
||||||
|
|
||||||
/// Announced address that is going to be put in the DKG contract where zk-nym clients will connect
|
/// Announced address that is going to be put in the DKG contract where zk-nym clients will connect
|
||||||
/// to obtain their credentials
|
/// to obtain their credentials
|
||||||
/// default: None - config value will be used instead
|
/// default: None - config value will be used instead
|
||||||
#[clap(long)]
|
#[clap(long, env = "NYMAPI_ANNOUNCE_ADDRESS_ARG")]
|
||||||
pub(crate) announce_address: Option<url::Url>,
|
pub(crate) announce_address: Option<url::Url>,
|
||||||
|
|
||||||
/// Set this nym api to work in a enabled credentials that would attempt to use gateway with the bandwidth credential requirement
|
/// Set this nym api to work in a enabled credentials that would attempt to use gateway with the bandwidth credential requirement
|
||||||
/// default: None - config value will be used instead
|
/// default: None - config value will be used instead
|
||||||
#[clap(long)]
|
#[clap(long, env = "NYMAPI_MONITOR_CREDENTIALS_MODE_ARG")]
|
||||||
pub(crate) monitor_credentials_mode: Option<bool>,
|
pub(crate) monitor_credentials_mode: Option<bool>,
|
||||||
|
|
||||||
/// Socket address this api will use for binding its http API.
|
/// Socket address this api will use for binding its http API.
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "nym-credential-proxy"
|
name = "nym-credential-proxy"
|
||||||
version = "0.1.0"
|
version = "0.1.1"
|
||||||
authors.workspace = true
|
authors.workspace = true
|
||||||
repository.workspace = true
|
repository.workspace = true
|
||||||
homepage.workspace = true
|
homepage.workspace = true
|
||||||
|
|||||||
@@ -26,6 +26,8 @@ RUN cargo build --release
|
|||||||
|
|
||||||
FROM ubuntu:24.04
|
FROM ubuntu:24.04
|
||||||
|
|
||||||
|
RUN apt update && apt install -yy curl ca-certificates
|
||||||
|
|
||||||
WORKDIR /nym
|
WORKDIR /nym
|
||||||
|
|
||||||
COPY --from=builder /usr/src/nym/nym-credential-proxy/target/release/nym-credential-proxy ./
|
COPY --from=builder /usr/src/nym/nym-credential-proxy/target/release/nym-credential-proxy ./
|
||||||
|
|||||||
@@ -5,13 +5,21 @@ WORKDIR /usr/src/nym/nym-data-observatory
|
|||||||
|
|
||||||
RUN cargo build --release
|
RUN cargo build --release
|
||||||
|
|
||||||
|
#-------------------------------------------------------------------
|
||||||
|
# The following environment variables are required at runtime:
|
||||||
|
#
|
||||||
|
# NYM_DATA_OBSERVATORY_CONNECTION_URL
|
||||||
|
#
|
||||||
|
# And optionally:
|
||||||
|
#
|
||||||
|
# NYM_DATA_OBSERVATORY_HTTP_PORT
|
||||||
|
#
|
||||||
|
# see https://github.com/nymtech/nym/blob/develop/nym-data-observatory/src/main.rs for details
|
||||||
|
#-------------------------------------------------------------------
|
||||||
|
|
||||||
FROM ubuntu:24.04
|
FROM ubuntu:24.04
|
||||||
|
|
||||||
RUN echo "Acquire::http::Pipeline-Depth 0;" > /etc/apt/apt.conf.d/99custom && \
|
RUN apt update && apt install -yy curl ca-certificates
|
||||||
echo "Acquire::http::No-Cache true;" >> /etc/apt/apt.conf.d/99custom && \
|
|
||||||
echo "Acquire::BrokenProxy true;" >> /etc/apt/apt.conf.d/99custom
|
|
||||||
|
|
||||||
RUN apt update && apt install -yy curl
|
|
||||||
|
|
||||||
WORKDIR /nym
|
WORKDIR /nym
|
||||||
|
|
||||||
|
|||||||
@@ -68,6 +68,7 @@ warning: no queries found; do you have the `offline` feature enabled
|
|||||||
### Possible solutions
|
### Possible solutions
|
||||||
|
|
||||||
- does your `sqlx-cli` version match `sqlx` version from `Cargo.toml`?
|
- does your `sqlx-cli` version match `sqlx` version from `Cargo.toml`?
|
||||||
|
+ `cargo install -f sqlx-cli --version <specific version>`
|
||||||
```
|
```
|
||||||
cargo install sqlx-cli --version <exact semver version as sqlx> --force
|
cargo install sqlx-cli --version <exact semver version as sqlx> --force
|
||||||
```
|
```
|
||||||
|
|||||||
@@ -18,8 +18,14 @@ services:
|
|||||||
dockerfile: nym-data-observatory/Dockerfile
|
dockerfile: nym-data-observatory/Dockerfile
|
||||||
container_name: nym-data-observatory
|
container_name: nym-data-observatory
|
||||||
environment:
|
environment:
|
||||||
NYM_DATA_OBSERVATORY_CONNECTION_URL: "postgres://postgres:password@postgres:5432"
|
NYM_DATA_OBSERVATORY_CONNECTION_USERNAME: "postgres"
|
||||||
|
NYM_DATA_OBSERVATORY_CONNECTION_PASSWORD: "password"
|
||||||
|
NYM_DATA_OBSERVATORY_CONNECTION_HOST: "postgres"
|
||||||
|
NYM_DATA_OBSERVATORY_CONNECTION_PORT: "5432"
|
||||||
|
NYM_DATA_OBSERVATORY_CONNECTION_DB: ""
|
||||||
NYM_DATA_OBSERVATORY_HTTP_PORT: 8000
|
NYM_DATA_OBSERVATORY_HTTP_PORT: 8000
|
||||||
|
env_file:
|
||||||
|
- ../envs/qa.env
|
||||||
|
|
||||||
volumes:
|
volumes:
|
||||||
pgdata:
|
pgdata:
|
||||||
|
|||||||
@@ -14,9 +14,7 @@ pub(crate) struct Storage {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl Storage {
|
impl Storage {
|
||||||
pub async fn init(connection_url: Option<String>) -> Result<Self> {
|
pub async fn init(connection_url: String) -> Result<Self> {
|
||||||
let connection_url =
|
|
||||||
connection_url.ok_or_else(|| anyhow!("Missing the connection url for database!"))?;
|
|
||||||
let connect_options =
|
let connect_options =
|
||||||
PgConnectOptions::from_str(&connection_url)?.disable_statement_logging();
|
PgConnectOptions::from_str(&connection_url)?.disable_statement_logging();
|
||||||
|
|
||||||
|
|||||||
@@ -18,9 +18,25 @@ struct Args {
|
|||||||
#[arg(short, long, default_value = None, env = "NYM_DATA_OBSERVATORY_ENV_FILE")]
|
#[arg(short, long, default_value = None, env = "NYM_DATA_OBSERVATORY_ENV_FILE")]
|
||||||
env_file: Option<String>,
|
env_file: Option<String>,
|
||||||
|
|
||||||
/// DB connection url
|
/// DB connection username
|
||||||
#[arg(short, long, default_value = None, env = "NYM_DATA_OBSERVATORY_CONNECTION_URL")]
|
#[arg(long, default_value = None, env = "NYM_DATA_OBSERVATORY_CONNECTION_USERNAME")]
|
||||||
connection_url: Option<String>,
|
connection_username: String,
|
||||||
|
|
||||||
|
/// DB connection password
|
||||||
|
#[arg(long, default_value = None, env = "NYM_DATA_OBSERVATORY_CONNECTION_PASSWORD")]
|
||||||
|
connection_password: String,
|
||||||
|
|
||||||
|
/// DB connection host
|
||||||
|
#[arg(long, default_value = None, env = "NYM_DATA_OBSERVATORY_CONNECTION_HOST")]
|
||||||
|
connection_host: String,
|
||||||
|
|
||||||
|
/// DB connection port
|
||||||
|
#[arg(long, default_value = None, env = "NYM_DATA_OBSERVATORY_CONNECTION_PORT")]
|
||||||
|
connection_port: String,
|
||||||
|
|
||||||
|
/// DB connection database name
|
||||||
|
#[arg(long, default_value = None, env = "NYM_DATA_OBSERVATORY_CONNECTION_DB")]
|
||||||
|
connection_db: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tokio::main]
|
#[tokio::main]
|
||||||
@@ -31,7 +47,16 @@ async fn main() -> anyhow::Result<()> {
|
|||||||
|
|
||||||
setup_env(args.env_file); // Defaults to mainnet if empty
|
setup_env(args.env_file); // Defaults to mainnet if empty
|
||||||
|
|
||||||
let storage = db::Storage::init(args.connection_url).await?;
|
let connection_url = format!(
|
||||||
|
"postgres://{}:{}@{}:{}/{}",
|
||||||
|
args.connection_username,
|
||||||
|
args.connection_password,
|
||||||
|
args.connection_host,
|
||||||
|
args.connection_port,
|
||||||
|
args.connection_db
|
||||||
|
);
|
||||||
|
|
||||||
|
let storage = db::Storage::init(connection_url).await?;
|
||||||
let db_pool = storage.pool_owned().await;
|
let db_pool = storage.pool_owned().await;
|
||||||
tokio::spawn(async move {
|
tokio::spawn(async move {
|
||||||
background_task::spawn_in_background(db_pool).await;
|
background_task::spawn_in_background(db_pool).await;
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ use std::collections::{HashMap, HashSet};
|
|||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
use futures::{stream::FuturesUnordered, StreamExt};
|
use futures::{stream::FuturesUnordered, StreamExt};
|
||||||
use log::{debug, info};
|
use log::{debug, info};
|
||||||
use nym_sphinx::chunking::{SentFragment, FRAGMENTS_RECEIVED, FRAGMENTS_SENT};
|
use nym_sphinx::chunking::{monitoring, SentFragment};
|
||||||
use nym_topology::{gateway, mix, NymTopology};
|
use nym_topology::{gateway, mix, NymTopology};
|
||||||
use nym_types::monitoring::{MonitorMessage, NodeResult};
|
use nym_types::monitoring::{MonitorMessage, NodeResult};
|
||||||
use nym_validator_client::nym_api::routes::{API_VERSION, STATUS, SUBMIT_GATEWAY, SUBMIT_NODE};
|
use nym_validator_client::nym_api::routes::{API_VERSION, STATUS, SUBMIT_GATEWAY, SUBMIT_NODE};
|
||||||
@@ -115,8 +115,8 @@ impl NetworkAccount {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn empty_buffers() {
|
pub fn empty_buffers() {
|
||||||
FRAGMENTS_SENT.clear();
|
monitoring::FRAGMENTS_SENT.clear();
|
||||||
FRAGMENTS_RECEIVED.clear();
|
monitoring::FRAGMENTS_RECEIVED.clear();
|
||||||
}
|
}
|
||||||
|
|
||||||
fn new() -> Self {
|
fn new() -> Self {
|
||||||
@@ -125,7 +125,7 @@ impl NetworkAccount {
|
|||||||
topology,
|
topology,
|
||||||
..Default::default()
|
..Default::default()
|
||||||
};
|
};
|
||||||
for fragment_set in FRAGMENTS_SENT.iter() {
|
for fragment_set in monitoring::FRAGMENTS_SENT.iter() {
|
||||||
let sent_fragments = fragment_set
|
let sent_fragments = fragment_set
|
||||||
.value()
|
.value()
|
||||||
.first()
|
.first()
|
||||||
@@ -138,7 +138,7 @@ impl NetworkAccount {
|
|||||||
sent_fragments
|
sent_fragments
|
||||||
);
|
);
|
||||||
|
|
||||||
let recv = FRAGMENTS_RECEIVED.get(fragment_set.key());
|
let recv = monitoring::FRAGMENTS_RECEIVED.get(fragment_set.key());
|
||||||
let recv_fragments = recv.as_ref().map(|r| r.value().len()).unwrap_or(0);
|
let recv_fragments = recv.as_ref().map(|r| r.value().len()).unwrap_or(0);
|
||||||
debug!(
|
debug!(
|
||||||
"RECV Fragment set {} has {} fragments",
|
"RECV Fragment set {} has {} fragments",
|
||||||
@@ -170,7 +170,7 @@ impl NetworkAccount {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn hydrate_all_fragments(&mut self) -> Result<()> {
|
fn hydrate_all_fragments(&mut self) -> Result<()> {
|
||||||
for fragment_set in FRAGMENTS_SENT.iter() {
|
for fragment_set in monitoring::FRAGMENTS_SENT.iter() {
|
||||||
let fragment_set_id = fragment_set.key();
|
let fragment_set_id = fragment_set.key();
|
||||||
for fragment in fragment_set.value() {
|
for fragment in fragment_set.value() {
|
||||||
let route = self.hydrate_route(fragment.clone())?;
|
let route = self.hydrate_route(fragment.clone())?;
|
||||||
@@ -205,7 +205,7 @@ impl NetworkAccount {
|
|||||||
fn find_missing_fragments(&mut self) {
|
fn find_missing_fragments(&mut self) {
|
||||||
let mut missing_fragments_map = HashMap::new();
|
let mut missing_fragments_map = HashMap::new();
|
||||||
for fragment_set_id in &self.incomplete_fragment_sets {
|
for fragment_set_id in &self.incomplete_fragment_sets {
|
||||||
if let Some(fragment_ref) = FRAGMENTS_RECEIVED.get(fragment_set_id) {
|
if let Some(fragment_ref) = monitoring::FRAGMENTS_RECEIVED.get(fragment_set_id) {
|
||||||
if let Some(ref_fragment) = fragment_ref.value().first() {
|
if let Some(ref_fragment) = fragment_ref.value().first() {
|
||||||
let ref_header = ref_fragment.header();
|
let ref_header = ref_fragment.header();
|
||||||
let ref_id_set = (0..ref_header.total_fragments()).collect::<HashSet<u8>>();
|
let ref_id_set = (0..ref_header.total_fragments()).collect::<HashSet<u8>>();
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ use axum::{
|
|||||||
use futures::StreamExt;
|
use futures::StreamExt;
|
||||||
use log::{debug, error, warn};
|
use log::{debug, error, warn};
|
||||||
use nym_sdk::mixnet::MixnetMessageSender;
|
use nym_sdk::mixnet::MixnetMessageSender;
|
||||||
use nym_sphinx::chunking::{ReceivedFragment, SentFragment, FRAGMENTS_RECEIVED, FRAGMENTS_SENT};
|
use nym_sphinx::chunking::{monitoring, ReceivedFragment, SentFragment};
|
||||||
use petgraph::{dot::Dot, Graph};
|
use petgraph::{dot::Dot, Graph};
|
||||||
use rand::{distributions::Alphanumeric, seq::SliceRandom, Rng};
|
use rand::{distributions::Alphanumeric, seq::SliceRandom, Rng};
|
||||||
use serde::Serialize;
|
use serde::Serialize;
|
||||||
@@ -113,7 +113,7 @@ pub async fn graph_handler() -> Result<String, StatusCode> {
|
|||||||
)]
|
)]
|
||||||
pub async fn sent_handler() -> Json<FragmentsSent> {
|
pub async fn sent_handler() -> Json<FragmentsSent> {
|
||||||
Json(FragmentsSent(
|
Json(FragmentsSent(
|
||||||
(*FRAGMENTS_SENT)
|
(*monitoring::FRAGMENTS_SENT)
|
||||||
.clone()
|
.clone()
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.collect::<HashMap<_, _>>(),
|
.collect::<HashMap<_, _>>(),
|
||||||
@@ -129,7 +129,7 @@ pub async fn sent_handler() -> Json<FragmentsSent> {
|
|||||||
)]
|
)]
|
||||||
pub async fn recv_handler() -> Json<FragmentsReceived> {
|
pub async fn recv_handler() -> Json<FragmentsReceived> {
|
||||||
Json(FragmentsReceived(
|
Json(FragmentsReceived(
|
||||||
(*FRAGMENTS_RECEIVED)
|
(*monitoring::FRAGMENTS_RECEIVED)
|
||||||
.clone()
|
.clone()
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.collect::<HashMap<_, _>>(),
|
.collect::<HashMap<_, _>>(),
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ use nym_crypto::asymmetric::ed25519::PrivateKey;
|
|||||||
use nym_network_defaults::setup_env;
|
use nym_network_defaults::setup_env;
|
||||||
use nym_network_defaults::var_names::NYM_API;
|
use nym_network_defaults::var_names::NYM_API;
|
||||||
use nym_sdk::mixnet::{self, MixnetClient};
|
use nym_sdk::mixnet::{self, MixnetClient};
|
||||||
|
use nym_sphinx::chunking::monitoring;
|
||||||
use nym_topology::{HardcodedTopologyProvider, NymTopology};
|
use nym_topology::{HardcodedTopologyProvider, NymTopology};
|
||||||
use std::fs::File;
|
use std::fs::File;
|
||||||
use std::io::Write;
|
use std::io::Write;
|
||||||
@@ -154,6 +155,9 @@ async fn main() -> Result<()> {
|
|||||||
|
|
||||||
setup_env(args.env); // Defaults to mainnet if empty
|
setup_env(args.env); // Defaults to mainnet if empty
|
||||||
|
|
||||||
|
// enable monitoring client-side
|
||||||
|
monitoring::enable();
|
||||||
|
|
||||||
let cancel_token = CancellationToken::new();
|
let cancel_token = CancellationToken::new();
|
||||||
let server_cancel_token = cancel_token.clone();
|
let server_cancel_token = cancel_token.clone();
|
||||||
let clients = Arc::new(RwLock::new(VecDeque::with_capacity(args.n_clients)));
|
let clients = Arc::new(RwLock::new(VecDeque::with_capacity(args.n_clients)));
|
||||||
|
|||||||
@@ -0,0 +1 @@
|
|||||||
|
nym-gateway-probe
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
# Copyright 2024 - Nym Technologies SA <contact@nymtech.net>
|
||||||
|
# SPDX-License-Identifier: Apache-2.0
|
||||||
|
|
||||||
|
|
||||||
|
[package]
|
||||||
|
name = "nym-node-status-agent"
|
||||||
|
version = "0.1.0"
|
||||||
|
authors.workspace = true
|
||||||
|
repository.workspace = true
|
||||||
|
homepage.workspace = true
|
||||||
|
documentation.workspace = true
|
||||||
|
edition.workspace = true
|
||||||
|
license.workspace = true
|
||||||
|
rust-version.workspace = true
|
||||||
|
readme.workspace = true
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
anyhow = { workspace = true}
|
||||||
|
clap = { workspace = true, features = ["derive", "env"] }
|
||||||
|
nym-bin-common = { path = "../common/bin-common", features = ["models"]}
|
||||||
|
nym-common-models = { path = "../common/models" }
|
||||||
|
tokio = { workspace = true, features = ["macros", "rt-multi-thread", "process"] }
|
||||||
|
tokio-util = { workspace = true }
|
||||||
|
tracing = { workspace = true }
|
||||||
|
tracing-subscriber = { workspace = true, features = ["env-filter"] }
|
||||||
|
reqwest = { workspace = true, features = ["json"] }
|
||||||
|
serde_json = { workspace = true }
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
FROM rust:latest AS builder
|
||||||
|
|
||||||
|
RUN apt update && apt install -yy libdbus-1-dev pkg-config libclang-dev
|
||||||
|
|
||||||
|
# Install go
|
||||||
|
RUN wget https://go.dev/dl/go1.22.5.linux-amd64.tar.gz -O go.tar.gz
|
||||||
|
RUN tar -xzvf go.tar.gz -C /usr/local
|
||||||
|
|
||||||
|
RUN git clone https://github.com/nymtech/nym-vpn-client /usr/src/nym-vpn-client
|
||||||
|
ENV PATH=/go/bin:/usr/local/go/bin:$PATH
|
||||||
|
WORKDIR /usr/src/nym-vpn-client/nym-vpn-core
|
||||||
|
RUN cargo build --release --package nym-gateway-probe
|
||||||
|
|
||||||
|
COPY ./ /usr/src/nym
|
||||||
|
WORKDIR /usr/src/nym/nym-node-status-agent
|
||||||
|
RUN cargo build --release
|
||||||
|
|
||||||
|
FROM ubuntu:24.04
|
||||||
|
|
||||||
|
RUN apt-get update && apt-get install -y ca-certificates
|
||||||
|
|
||||||
|
WORKDIR /nym
|
||||||
|
|
||||||
|
COPY --from=builder /usr/src/nym/target/release/nym-node-status-agent ./
|
||||||
|
COPY --from=builder /usr/src/nym-vpn-client/nym-vpn-core/target/release/nym-gateway-probe ./
|
||||||
|
|
||||||
|
ENV NODE_STATUS_AGENT_PROBE_PATH=/nym/nym-gateway-probe
|
||||||
|
ENTRYPOINT [ "/nym/nym-node-status-agent", "run-probe" ]
|
||||||
Executable
+49
@@ -0,0 +1,49 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
set -eu
|
||||||
|
|
||||||
|
export RUST_LOG=${RUST_LOG:-debug}
|
||||||
|
|
||||||
|
crate_root=$(dirname $(realpath "$0"))
|
||||||
|
gateway_probe_src=$(dirname $(dirname "$crate_root"))/nym-vpn-client/nym-vpn-core
|
||||||
|
echo "gateway_probe_src=$gateway_probe_src"
|
||||||
|
echo "crate_root=$crate_root"
|
||||||
|
|
||||||
|
export NODE_STATUS_AGENT_PROBE_PATH="$crate_root/nym-gateway-probe"
|
||||||
|
|
||||||
|
# build & copy over GW probe
|
||||||
|
function copy_gw_probe() {
|
||||||
|
pushd $gateway_probe_src
|
||||||
|
cargo build --release --package nym-gateway-probe
|
||||||
|
cp target/release/nym-gateway-probe "$crate_root"
|
||||||
|
$crate_root/nym-gateway-probe --version
|
||||||
|
popd
|
||||||
|
}
|
||||||
|
|
||||||
|
function build_agent() {
|
||||||
|
cargo build --package nym-node-status-agent --release
|
||||||
|
}
|
||||||
|
|
||||||
|
function swarm() {
|
||||||
|
local workers=$1
|
||||||
|
echo "Running $workers in parallel"
|
||||||
|
|
||||||
|
build_agent
|
||||||
|
|
||||||
|
for ((i=1; i<=$workers; i++)); do
|
||||||
|
../target/release/nym-node-status-agent run-probe &
|
||||||
|
done
|
||||||
|
|
||||||
|
wait
|
||||||
|
|
||||||
|
echo "All agents completed"
|
||||||
|
}
|
||||||
|
|
||||||
|
export NODE_STATUS_AGENT_SERVER_ADDRESS="http://127.0.0.1"
|
||||||
|
export NODE_STATUS_AGENT_SERVER_PORT="8000"
|
||||||
|
|
||||||
|
copy_gw_probe
|
||||||
|
|
||||||
|
swarm 30
|
||||||
|
|
||||||
|
# cargo run -- run-probe
|
||||||
@@ -0,0 +1,109 @@
|
|||||||
|
use clap::{Parser, Subcommand};
|
||||||
|
use nym_bin_common::bin_info;
|
||||||
|
use nym_common_models::ns_api::TestrunAssignment;
|
||||||
|
use std::sync::OnceLock;
|
||||||
|
use tracing::instrument;
|
||||||
|
|
||||||
|
use crate::probe::GwProbe;
|
||||||
|
|
||||||
|
// Helper for passing LONG_VERSION to clap
|
||||||
|
fn pretty_build_info_static() -> &'static str {
|
||||||
|
static PRETTY_BUILD_INFORMATION: OnceLock<String> = OnceLock::new();
|
||||||
|
PRETTY_BUILD_INFORMATION.get_or_init(|| bin_info!().pretty_print())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Parser, Debug)]
|
||||||
|
#[clap(author = "Nymtech", version, long_version = pretty_build_info_static(), about)]
|
||||||
|
pub(crate) struct Args {
|
||||||
|
#[command(subcommand)]
|
||||||
|
pub(crate) command: Command,
|
||||||
|
#[arg(short, long, env = "NODE_STATUS_AGENT_SERVER_ADDRESS")]
|
||||||
|
pub(crate) server_address: String,
|
||||||
|
|
||||||
|
#[arg(short = 'p', long, env = "NODE_STATUS_AGENT_SERVER_PORT")]
|
||||||
|
pub(crate) server_port: u16,
|
||||||
|
// TODO dz accept keypair for identification / auth
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Subcommand, Debug)]
|
||||||
|
pub(crate) enum Command {
|
||||||
|
RunProbe {
|
||||||
|
/// path of binary to run
|
||||||
|
#[arg(long, env = "NODE_STATUS_AGENT_PROBE_PATH")]
|
||||||
|
probe_path: String,
|
||||||
|
#[arg(short, long, env = "NODE_STATUS_AGENT_GATEWAY_ID")]
|
||||||
|
gateway_id: Option<String>,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Args {
|
||||||
|
pub(crate) async fn execute(&self) -> anyhow::Result<()> {
|
||||||
|
match &self.command {
|
||||||
|
Command::RunProbe {
|
||||||
|
probe_path,
|
||||||
|
gateway_id,
|
||||||
|
} => self.run_probe(probe_path, gateway_id).await?,
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn run_probe(&self, probe_path: &str, gateway_id: &Option<String>) -> anyhow::Result<()> {
|
||||||
|
let server_address = format!("{}:{}", &self.server_address, self.server_port);
|
||||||
|
|
||||||
|
let probe = GwProbe::new(probe_path.to_string());
|
||||||
|
|
||||||
|
let version = probe.version().await;
|
||||||
|
tracing::info!("Probe version:\n{}", version);
|
||||||
|
|
||||||
|
let testrun = request_testrun(&server_address).await?;
|
||||||
|
|
||||||
|
let log = probe.run_and_get_log(gateway_id);
|
||||||
|
|
||||||
|
submit_results(&server_address, testrun.testrun_id, log).await?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const URL_BASE: &str = "internal/testruns";
|
||||||
|
|
||||||
|
#[instrument(level = "debug", skip_all)]
|
||||||
|
async fn request_testrun(server_addr: &str) -> anyhow::Result<TestrunAssignment> {
|
||||||
|
let target_url = format!("{}/{}", server_addr, URL_BASE);
|
||||||
|
let client = reqwest::Client::new();
|
||||||
|
let res = client
|
||||||
|
.get(target_url)
|
||||||
|
.send()
|
||||||
|
.await
|
||||||
|
.and_then(|response| response.error_for_status())?;
|
||||||
|
res.json()
|
||||||
|
.await
|
||||||
|
.map(|testrun| {
|
||||||
|
tracing::info!("Received testrun assignment: {:?}", testrun);
|
||||||
|
testrun
|
||||||
|
})
|
||||||
|
.map_err(|err| {
|
||||||
|
tracing::error!("err");
|
||||||
|
err.into()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
#[instrument(level = "debug", skip(probe_outcome))]
|
||||||
|
async fn submit_results(
|
||||||
|
server_addr: &str,
|
||||||
|
testrun_id: i64,
|
||||||
|
probe_outcome: String,
|
||||||
|
) -> anyhow::Result<()> {
|
||||||
|
let target_url = format!("{}/{}/{}", server_addr, URL_BASE, testrun_id);
|
||||||
|
let client = reqwest::Client::new();
|
||||||
|
let res = client
|
||||||
|
.post(target_url)
|
||||||
|
.body(probe_outcome)
|
||||||
|
.send()
|
||||||
|
.await
|
||||||
|
.and_then(|response| response.error_for_status())?;
|
||||||
|
|
||||||
|
tracing::debug!("Submitted results: {})", res.status());
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
@@ -0,0 +1,78 @@
|
|||||||
|
use crate::cli::Args;
|
||||||
|
use clap::Parser;
|
||||||
|
use tracing::level_filters::LevelFilter;
|
||||||
|
use tracing_subscriber::{filter::Directive, EnvFilter};
|
||||||
|
|
||||||
|
mod cli;
|
||||||
|
mod probe;
|
||||||
|
|
||||||
|
#[tokio::main]
|
||||||
|
async fn main() -> anyhow::Result<()> {
|
||||||
|
setup_tracing();
|
||||||
|
let args = Args::parse();
|
||||||
|
|
||||||
|
let server_addr = format!("{}:{}", args.server_address, args.server_port);
|
||||||
|
test_ns_api_conn(&server_addr).await?;
|
||||||
|
|
||||||
|
args.execute().await?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn test_ns_api_conn(server_addr: &str) -> anyhow::Result<()> {
|
||||||
|
reqwest::get(server_addr)
|
||||||
|
.await
|
||||||
|
.map(|res| {
|
||||||
|
tracing::info!(
|
||||||
|
"Testing connection to NS API at {server_addr}: {}",
|
||||||
|
res.status()
|
||||||
|
);
|
||||||
|
})
|
||||||
|
.map_err(|err| anyhow::anyhow!("Couldn't connect to server on {}: {}", server_addr, err))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn setup_tracing() {
|
||||||
|
fn directive_checked(directive: impl Into<String>) -> Directive {
|
||||||
|
directive
|
||||||
|
.into()
|
||||||
|
.parse()
|
||||||
|
.expect("Failed to parse log directive")
|
||||||
|
}
|
||||||
|
|
||||||
|
let log_builder = tracing_subscriber::fmt()
|
||||||
|
// Use a more compact, abbreviated log format
|
||||||
|
.compact()
|
||||||
|
// Display source code file paths
|
||||||
|
.with_file(true)
|
||||||
|
// Display source code line numbers
|
||||||
|
.with_line_number(true)
|
||||||
|
.with_thread_ids(true)
|
||||||
|
// Don't display the event's target (module path)
|
||||||
|
.with_target(false);
|
||||||
|
|
||||||
|
let mut filter = EnvFilter::builder()
|
||||||
|
// if RUST_LOG isn't set, set default level
|
||||||
|
.with_default_directive(LevelFilter::INFO.into())
|
||||||
|
.from_env_lossy();
|
||||||
|
// these crates are more granularly filtered
|
||||||
|
let filter_crates = [
|
||||||
|
"reqwest",
|
||||||
|
"rustls",
|
||||||
|
"hyper",
|
||||||
|
"sqlx",
|
||||||
|
"h2",
|
||||||
|
"tendermint_rpc",
|
||||||
|
"tower_http",
|
||||||
|
"axum",
|
||||||
|
];
|
||||||
|
for crate_name in filter_crates {
|
||||||
|
filter = filter.add_directive(directive_checked(format!("{}=warn", crate_name)));
|
||||||
|
}
|
||||||
|
|
||||||
|
filter = filter.add_directive(directive_checked("nym_bin_common=debug"));
|
||||||
|
filter = filter.add_directive(directive_checked("nym_explorer_client=debug"));
|
||||||
|
filter = filter.add_directive(directive_checked("nym_network_defaults=debug"));
|
||||||
|
filter = filter.add_directive(directive_checked("nym_validator_client=debug"));
|
||||||
|
|
||||||
|
log_builder.with_env_filter(filter).init();
|
||||||
|
}
|
||||||
@@ -0,0 +1,54 @@
|
|||||||
|
use tracing::error;
|
||||||
|
|
||||||
|
pub(crate) struct GwProbe {
|
||||||
|
path: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl GwProbe {
|
||||||
|
pub(crate) fn new(probe_path: String) -> Self {
|
||||||
|
Self { path: probe_path }
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) async fn version(&self) -> String {
|
||||||
|
let mut command = tokio::process::Command::new(&self.path);
|
||||||
|
command.stdout(std::process::Stdio::piped());
|
||||||
|
command.arg("--version");
|
||||||
|
|
||||||
|
match command.spawn() {
|
||||||
|
Ok(child) => {
|
||||||
|
if let Ok(output) = child.wait_with_output().await {
|
||||||
|
return String::from_utf8(output.stdout)
|
||||||
|
.unwrap_or("Unable to get log from test run".to_string());
|
||||||
|
}
|
||||||
|
"Unable to get probe version".to_string()
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
error!("Failed to get probe version: {}", e);
|
||||||
|
"Failed to get probe version".to_string()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn run_and_get_log(&self, gateway_key: &Option<String>) -> String {
|
||||||
|
let mut command = std::process::Command::new(&self.path);
|
||||||
|
command.stdout(std::process::Stdio::piped());
|
||||||
|
|
||||||
|
if let Some(gateway_id) = gateway_key {
|
||||||
|
command.arg("--gateway").arg(gateway_id);
|
||||||
|
}
|
||||||
|
|
||||||
|
match command.spawn() {
|
||||||
|
Ok(child) => {
|
||||||
|
if let Ok(output) = child.wait_with_output() {
|
||||||
|
return String::from_utf8(output.stdout)
|
||||||
|
.unwrap_or("Unable to get log from test run".to_string());
|
||||||
|
}
|
||||||
|
"Unable to get log from test run".to_string()
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
error!("Failed to spawn test: {}", e);
|
||||||
|
"Failed to spawn test run task".to_string()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
data/
|
||||||
|
enter_db.sh
|
||||||
|
nym-gateway-probe
|
||||||
|
nym-node-status-api
|
||||||
|
*.sqlite
|
||||||
|
*.sqlite-journal
|
||||||
@@ -0,0 +1,66 @@
|
|||||||
|
# Copyright 2024 - Nym Technologies SA <contact@nymtech.net>
|
||||||
|
# SPDX-License-Identifier: Apache-2.0
|
||||||
|
|
||||||
|
[package]
|
||||||
|
name = "nym-node-status-api"
|
||||||
|
version = "0.1.0"
|
||||||
|
authors.workspace = true
|
||||||
|
repository.workspace = true
|
||||||
|
homepage.workspace = true
|
||||||
|
documentation.workspace = true
|
||||||
|
edition.workspace = true
|
||||||
|
license.workspace = true
|
||||||
|
rust-version.workspace = true
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
anyhow = { workspace = true }
|
||||||
|
axum = { workspace = true, features = ["tokio", "macros"] }
|
||||||
|
chrono = { workspace = true }
|
||||||
|
clap = { workspace = true, features = ["cargo", "derive", "env", "string"] }
|
||||||
|
cosmwasm-std = { workspace = true }
|
||||||
|
envy = { workspace = true }
|
||||||
|
futures-util = { workspace = true }
|
||||||
|
moka = { workspace = true, features = ["future"] }
|
||||||
|
nym-bin-common = { path = "../common/bin-common", features = ["models"]}
|
||||||
|
nym-common-models = { path = "../common/models" }
|
||||||
|
nym-explorer-client = { path = "../explorer-api/explorer-client" }
|
||||||
|
# TODO dz: before Nym API client breaking changes. Update to latest develop once new Nym API is live
|
||||||
|
nym-network-defaults = { git = "https://github.com/nymtech/nym", branch = "pre-dir-v2-fork" }
|
||||||
|
nym-validator-client = { git = "https://github.com/nymtech/nym", branch = "pre-dir-v2-fork" }
|
||||||
|
# nym-network-defaults = { path = "../common/network-defaults" }
|
||||||
|
# nym-validator-client = { path = "../common/client-libs/validator-client" }
|
||||||
|
nym-task = { path = "../common/task" }
|
||||||
|
nym-node-requests = { git = "https://github.com/nymtech/nym", branch = "pre-dir-v2-fork" }
|
||||||
|
# nym-node-requests = { path = "../nym-node/nym-node-requests", features = ["openapi"] }
|
||||||
|
regex = { workspace = true }
|
||||||
|
reqwest = { workspace = true }
|
||||||
|
serde = { workspace = true, features = ["derive"] }
|
||||||
|
serde_json = { workspace = true }
|
||||||
|
serde_json_path = { workspace = true }
|
||||||
|
strum = { workspace = true }
|
||||||
|
strum_macros = { workspace = true }
|
||||||
|
sqlx = { workspace = true, features = ["runtime-tokio-rustls", "sqlite"] }
|
||||||
|
thiserror = { workspace = true }
|
||||||
|
tokio = { workspace = true, features = ["rt-multi-thread"] }
|
||||||
|
tokio-util = { workspace = true }
|
||||||
|
tracing = { workspace = true }
|
||||||
|
tracing-subscriber = { workspace = true, features = ["env-filter"] }
|
||||||
|
tracing-log = { workspace = true }
|
||||||
|
tower-http = { workspace = true, features = ["cors", "trace"] }
|
||||||
|
utoipa = { workspace = true, features = ["axum_extras", "time"] }
|
||||||
|
utoipa-swagger-ui = { workspace = true, features = ["axum"] }
|
||||||
|
# TODO dz `cargo update async-trait`
|
||||||
|
# for automatic schema detection, which was merged, but not released yet
|
||||||
|
# https://github.com/ProbablyClem/utoipauto/pull/38
|
||||||
|
# utoipauto = { git = "https://github.com/ProbablyClem/utoipauto", rev = "eb04cba" }
|
||||||
|
utoipauto = { workspace = true }
|
||||||
|
|
||||||
|
[build-dependencies]
|
||||||
|
anyhow = { workspace = true }
|
||||||
|
tokio = { workspace = true, features = ["macros"] }
|
||||||
|
sqlx = { workspace = true, features = [
|
||||||
|
"runtime-tokio-rustls",
|
||||||
|
"sqlite",
|
||||||
|
"macros",
|
||||||
|
"migrate",
|
||||||
|
] }
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
FROM rust:latest AS builder
|
||||||
|
|
||||||
|
COPY ./ /usr/src/nym
|
||||||
|
WORKDIR /usr/src/nym/nym-node-status-api
|
||||||
|
|
||||||
|
RUN cargo build --release
|
||||||
|
|
||||||
|
FROM ubuntu:24.04
|
||||||
|
|
||||||
|
RUN apt-get update && apt-get install -y ca-certificates
|
||||||
|
|
||||||
|
WORKDIR /nym
|
||||||
|
|
||||||
|
COPY --from=builder /usr/src/nym/target/release/nym-node-status-api ./
|
||||||
|
ENTRYPOINT [ "/nym/nym-node-status-api" ]
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
FROM ubuntu:22.04
|
||||||
|
|
||||||
|
RUN apt-get update && apt-get install -y ca-certificates
|
||||||
|
|
||||||
|
WORKDIR /nym
|
||||||
|
|
||||||
|
COPY nym-node-status-api/nym-node-status-api ./
|
||||||
|
ENTRYPOINT [ "/nym/nym-node-status-api" ]
|
||||||
@@ -0,0 +1,51 @@
|
|||||||
|
use anyhow::{anyhow, Result};
|
||||||
|
use sqlx::{Connection, SqliteConnection};
|
||||||
|
use std::fs::Permissions;
|
||||||
|
use std::os::unix::fs::PermissionsExt;
|
||||||
|
use tokio::{fs::File, io::AsyncWriteExt};
|
||||||
|
|
||||||
|
const SQLITE_DB_FILENAME: &str = "nym-node-status-api.sqlite";
|
||||||
|
|
||||||
|
/// If you need to re-run migrations or reset the db, just run
|
||||||
|
/// cargo clean -p nym-node-status-api
|
||||||
|
#[tokio::main(flavor = "current_thread")]
|
||||||
|
async fn main() -> Result<()> {
|
||||||
|
let out_dir = read_env_var("OUT_DIR")?;
|
||||||
|
let database_path = format!("sqlite://{}/{}?mode=rwc", out_dir, SQLITE_DB_FILENAME);
|
||||||
|
|
||||||
|
write_db_path_to_file(&out_dir, SQLITE_DB_FILENAME).await?;
|
||||||
|
let mut conn = SqliteConnection::connect(&database_path).await?;
|
||||||
|
sqlx::migrate!("./migrations").run(&mut conn).await?;
|
||||||
|
|
||||||
|
#[cfg(target_family = "unix")]
|
||||||
|
println!("cargo::rustc-env=DATABASE_URL=sqlite://{}", &database_path);
|
||||||
|
|
||||||
|
#[cfg(target_family = "windows")]
|
||||||
|
// for some strange reason we need to add a leading `/` to the windows path even though it's
|
||||||
|
// not a valid windows path... but hey, it works...
|
||||||
|
println!("cargo::rustc-env=DATABASE_URL=sqlite:///{}", &database_path);
|
||||||
|
|
||||||
|
rerun_if_changed();
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn read_env_var(var: &str) -> Result<String> {
|
||||||
|
std::env::var(var).map_err(|_| anyhow!("You need to set {} env var", var))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn rerun_if_changed() {
|
||||||
|
println!("cargo::rerun-if-changed=migrations");
|
||||||
|
println!("cargo::rerun-if-changed=src/db/queries");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// use `./enter_db.sh` to inspect DB
|
||||||
|
async fn write_db_path_to_file(out_dir: &str, db_filename: &str) -> anyhow::Result<()> {
|
||||||
|
let mut file = File::create("enter_db.sh").await?;
|
||||||
|
let _ = file.write(b"#!/bin/bash\n").await?;
|
||||||
|
file.write_all(format!("sqlite3 {}/{}", out_dir, db_filename).as_bytes())
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
file.set_permissions(Permissions::from_mode(0o755))
|
||||||
|
.await
|
||||||
|
.map_err(From::from)
|
||||||
|
}
|
||||||
+39
@@ -0,0 +1,39 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
set -e
|
||||||
|
|
||||||
|
export RUST_LOG=${RUST_LOG:-debug}
|
||||||
|
|
||||||
|
export NYM_API_CLIENT_TIMEOUT=60
|
||||||
|
export EXPLORER_CLIENT_TIMEOUT=60
|
||||||
|
|
||||||
|
export ENVIRONMENT="mainnet.env"
|
||||||
|
|
||||||
|
function run_bare() {
|
||||||
|
# export necessary env vars
|
||||||
|
set -a
|
||||||
|
source ../envs/$ENVIRONMENT
|
||||||
|
set +a
|
||||||
|
export RUST_LOG=debug
|
||||||
|
|
||||||
|
# --conection-url is provided in build.rs
|
||||||
|
cargo run --package nym-node-status-api
|
||||||
|
}
|
||||||
|
|
||||||
|
function run_docker() {
|
||||||
|
cargo build --package nym-node-status-api --release
|
||||||
|
cp ../target/release/nym-node-status-api .
|
||||||
|
|
||||||
|
cd ..
|
||||||
|
docker build -t node-status-api -f nym-node-status-api/Dockerfile.dev .
|
||||||
|
docker run --env-file envs/${ENVIRONMENT} \
|
||||||
|
-e EXPLORER_CLIENT_TIMEOUT=$EXPLORER_CLIENT_TIMEOUT \
|
||||||
|
-e NYM_API_CLIENT_TIMEOUT=$NYM_API_CLIENT_TIMEOUT \
|
||||||
|
-e DATABASE_URL="sqlite://node-status-api.sqlite?mode=rwc" \
|
||||||
|
-e RUST_LOG=${RUST_LOG} node-status-api
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
run_bare
|
||||||
|
|
||||||
|
# run_docker
|
||||||
@@ -0,0 +1,112 @@
|
|||||||
|
CREATE TABLE gateways
|
||||||
|
(
|
||||||
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
gateway_identity_key VARCHAR NOT NULL UNIQUE,
|
||||||
|
self_described VARCHAR,
|
||||||
|
explorer_pretty_bond VARCHAR,
|
||||||
|
last_probe_result VARCHAR,
|
||||||
|
last_probe_log VARCHAR,
|
||||||
|
config_score INTEGER NOT NULL DEFAULT (0),
|
||||||
|
config_score_successes REAL NOT NULL DEFAULT (0),
|
||||||
|
config_score_samples REAL NOT NULL DEFAULT (0),
|
||||||
|
routing_score INTEGER NOT NULL DEFAULT (0),
|
||||||
|
routing_score_successes REAL NOT NULL DEFAULT (0),
|
||||||
|
routing_score_samples REAL NOT NULL DEFAULT (0),
|
||||||
|
test_run_samples REAL NOT NULL DEFAULT (0),
|
||||||
|
last_testrun_utc INTEGER,
|
||||||
|
last_updated_utc INTEGER NOT NULL,
|
||||||
|
bonded INTEGER CHECK (bonded in (0, 1)) NOT NULL DEFAULT 0,
|
||||||
|
blacklisted INTEGER CHECK (bonded in (0, 1)) NOT NULL DEFAULT 0,
|
||||||
|
performance INTEGER NOT NULL DEFAULT 0
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE INDEX idx_gateway_description_gateway_identity_key ON gateways (gateway_identity_key);
|
||||||
|
|
||||||
|
|
||||||
|
CREATE TABLE mixnodes (
|
||||||
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
identity_key VARCHAR NOT NULL UNIQUE,
|
||||||
|
mix_id INTEGER NOT NULL UNIQUE,
|
||||||
|
bonded INTEGER CHECK (bonded in (0, 1)) NOT NULL DEFAULT 0,
|
||||||
|
total_stake INTEGER NOT NULL,
|
||||||
|
host VARCHAR NOT NULL,
|
||||||
|
http_api_port INTEGER NOT NULL,
|
||||||
|
blacklisted INTEGER CHECK (blacklisted in (0, 1)) NOT NULL DEFAULT 0,
|
||||||
|
full_details VARCHAR,
|
||||||
|
self_described VARCHAR,
|
||||||
|
last_updated_utc INTEGER NOT NULL
|
||||||
|
, is_dp_delegatee INTEGER CHECK (is_dp_delegatee IN (0, 1)) NOT NULL DEFAULT 0);
|
||||||
|
CREATE INDEX idx_mixnodes_mix_id ON mixnodes (mix_id);
|
||||||
|
CREATE INDEX idx_mixnodes_identity_key ON mixnodes (identity_key);
|
||||||
|
|
||||||
|
CREATE TABLE
|
||||||
|
mixnode_description (
|
||||||
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
mix_id INTEGER UNIQUE NOT NULL,
|
||||||
|
moniker VARCHAR,
|
||||||
|
website VARCHAR,
|
||||||
|
security_contact VARCHAR,
|
||||||
|
details VARCHAR,
|
||||||
|
last_updated_utc INTEGER NOT NULL,
|
||||||
|
FOREIGN KEY (mix_id) REFERENCES mixnodes (mix_id)
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Indexes for description table
|
||||||
|
CREATE INDEX idx_mixnode_description_mix_id ON mixnode_description (mix_id);
|
||||||
|
|
||||||
|
|
||||||
|
CREATE TABLE summary
|
||||||
|
(
|
||||||
|
key VARCHAR PRIMARY KEY,
|
||||||
|
value_json VARCHAR,
|
||||||
|
last_updated_utc INTEGER NOT NULL
|
||||||
|
);
|
||||||
|
|
||||||
|
|
||||||
|
CREATE TABLE summary_history
|
||||||
|
(
|
||||||
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
date VARCHAR UNIQUE NOT NULL,
|
||||||
|
timestamp_utc INTEGER NOT NULL,
|
||||||
|
value_json VARCHAR
|
||||||
|
);
|
||||||
|
CREATE INDEX idx_summary_history_timestamp_utc ON summary_history (timestamp_utc);
|
||||||
|
CREATE INDEX idx_summary_history_date ON summary_history (date);
|
||||||
|
|
||||||
|
|
||||||
|
CREATE TABLE gateway_description (
|
||||||
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
gateway_identity_key VARCHAR UNIQUE NOT NULL,
|
||||||
|
moniker VARCHAR,
|
||||||
|
website VARCHAR,
|
||||||
|
security_contact VARCHAR,
|
||||||
|
details VARCHAR,
|
||||||
|
last_updated_utc INTEGER NOT NULL,
|
||||||
|
FOREIGN KEY (gateway_identity_key) REFERENCES gateways (gateway_identity_key)
|
||||||
|
);
|
||||||
|
|
||||||
|
|
||||||
|
CREATE TABLE
|
||||||
|
mixnode_daily_stats (
|
||||||
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
mix_id INTEGER NOT NULL,
|
||||||
|
total_stake BIGINT NOT NULL,
|
||||||
|
date_utc VARCHAR NOT NULL,
|
||||||
|
packets_received INTEGER DEFAULT 0,
|
||||||
|
packets_sent INTEGER DEFAULT 0,
|
||||||
|
packets_dropped INTEGER DEFAULT 0,
|
||||||
|
FOREIGN KEY (mix_id) REFERENCES mixnodes (mix_id),
|
||||||
|
UNIQUE (mix_id, date_utc) -- This constraint automatically creates an index
|
||||||
|
);
|
||||||
|
|
||||||
|
|
||||||
|
CREATE TABLE testruns
|
||||||
|
(
|
||||||
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
gateway_id INTEGER,
|
||||||
|
status INTEGER NOT NULL, -- 0=pending, 1=in-progress, 2=complete
|
||||||
|
timestamp_utc INTEGER NOT NULL,
|
||||||
|
ip_address VARCHAR NOT NULL,
|
||||||
|
log VARCHAR NOT NULL,
|
||||||
|
FOREIGN KEY (gateway_id) REFERENCES gateways (id)
|
||||||
|
);
|
||||||
@@ -0,0 +1,77 @@
|
|||||||
|
use clap::Parser;
|
||||||
|
use nym_bin_common::bin_info;
|
||||||
|
use reqwest::Url;
|
||||||
|
use std::{sync::OnceLock, time::Duration};
|
||||||
|
|
||||||
|
// Helper for passing LONG_VERSION to clap
|
||||||
|
fn pretty_build_info_static() -> &'static str {
|
||||||
|
static PRETTY_BUILD_INFORMATION: OnceLock<String> = OnceLock::new();
|
||||||
|
PRETTY_BUILD_INFORMATION.get_or_init(|| bin_info!().pretty_print())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Parser)]
|
||||||
|
#[clap(author = "Nymtech", version, long_version = pretty_build_info_static(), about)]
|
||||||
|
pub(crate) struct Cli {
|
||||||
|
/// Network name for the network to which we're connecting.
|
||||||
|
#[clap(long, env = "NETWORK_NAME")]
|
||||||
|
pub(crate) network_name: String,
|
||||||
|
|
||||||
|
/// Explorer api url.
|
||||||
|
#[clap(short, long, env = "EXPLORER_API")]
|
||||||
|
pub(crate) explorer_api: String,
|
||||||
|
|
||||||
|
/// Nym api url.
|
||||||
|
#[clap(short, long, env = "NYM_API")]
|
||||||
|
pub(crate) nym_api: String,
|
||||||
|
|
||||||
|
/// TTL for the http cache.
|
||||||
|
#[clap(
|
||||||
|
long,
|
||||||
|
default_value_t = 30,
|
||||||
|
env = "NYM_NODE_STATUS_API_NYM_HTTP_CACHE_TTL"
|
||||||
|
)]
|
||||||
|
pub(crate) nym_http_cache_ttl: u64,
|
||||||
|
|
||||||
|
/// HTTP port on which to run node status api.
|
||||||
|
#[clap(long, default_value_t = 8000, env = "NYM_NODE_STATUS_API_HTTP_PORT")]
|
||||||
|
pub(crate) http_port: u16,
|
||||||
|
|
||||||
|
/// Nyxd address.
|
||||||
|
#[clap(long, env = "NYXD")]
|
||||||
|
pub(crate) nyxd_addr: Url,
|
||||||
|
|
||||||
|
/// Nym api client timeout.
|
||||||
|
#[clap(long, default_value = "15", env = "NYM_API_CLIENT_TIMEOUT")]
|
||||||
|
#[arg(value_parser = parse_duration)]
|
||||||
|
pub(crate) nym_api_client_timeout: Duration,
|
||||||
|
|
||||||
|
/// Explorer api client timeout.
|
||||||
|
#[clap(long, default_value = "15", env = "EXPLORER_CLIENT_TIMEOUT")]
|
||||||
|
#[arg(value_parser = parse_duration)]
|
||||||
|
pub(crate) explorer_client_timeout: Duration,
|
||||||
|
|
||||||
|
/// Connection url for the database.
|
||||||
|
#[clap(long, env = "DATABASE_URL")]
|
||||||
|
pub(crate) database_url: String,
|
||||||
|
|
||||||
|
#[clap(
|
||||||
|
long,
|
||||||
|
default_value = "600",
|
||||||
|
env = "NODE_STATUS_API_MONITOR_REFRESH_INTERVAL"
|
||||||
|
)]
|
||||||
|
#[arg(value_parser = parse_duration)]
|
||||||
|
pub(crate) monitor_refresh_interval: Duration,
|
||||||
|
|
||||||
|
#[clap(
|
||||||
|
long,
|
||||||
|
default_value = "600",
|
||||||
|
env = "NODE_STATUS_API_TESTRUN_REFRESH_INTERVAL"
|
||||||
|
)]
|
||||||
|
#[arg(value_parser = parse_duration)]
|
||||||
|
pub(crate) testruns_refresh_interval: Duration,
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parse_duration(arg: &str) -> Result<std::time::Duration, std::num::ParseIntError> {
|
||||||
|
let seconds = arg.parse()?;
|
||||||
|
Ok(std::time::Duration::from_secs(seconds))
|
||||||
|
}
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
use anyhow::{anyhow, Result};
|
||||||
|
use sqlx::{migrate::Migrator, sqlite::SqliteConnectOptions, ConnectOptions, SqlitePool};
|
||||||
|
use std::str::FromStr;
|
||||||
|
|
||||||
|
pub(crate) mod models;
|
||||||
|
pub(crate) mod queries;
|
||||||
|
|
||||||
|
static MIGRATOR: Migrator = sqlx::migrate!("./migrations");
|
||||||
|
|
||||||
|
pub(crate) type DbPool = SqlitePool;
|
||||||
|
|
||||||
|
pub(crate) struct Storage {
|
||||||
|
pool: DbPool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Storage {
|
||||||
|
pub async fn init(connection_url: String) -> Result<Self> {
|
||||||
|
let connect_options = SqliteConnectOptions::from_str(&connection_url)?
|
||||||
|
.create_if_missing(true)
|
||||||
|
.disable_statement_logging();
|
||||||
|
|
||||||
|
let pool = sqlx::SqlitePool::connect_with(connect_options)
|
||||||
|
.await
|
||||||
|
.map_err(|err| anyhow!("Failed to connect to {}: {}", &connection_url, err))?;
|
||||||
|
|
||||||
|
MIGRATOR.run(&pool).await?;
|
||||||
|
|
||||||
|
Ok(Storage { pool })
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Cloning pool is cheap, it's the same underlying set of connections
|
||||||
|
pub fn pool_owned(&self) -> DbPool {
|
||||||
|
self.pool.clone()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,334 @@
|
|||||||
|
use crate::{
|
||||||
|
http::{self, models::SummaryHistory},
|
||||||
|
monitor::NumericalCheckedCast,
|
||||||
|
};
|
||||||
|
use nym_node_requests::api::v1::node::models::NodeDescription;
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use strum_macros::{EnumString, FromRepr};
|
||||||
|
use utoipa::ToSchema;
|
||||||
|
|
||||||
|
pub(crate) struct GatewayRecord {
|
||||||
|
pub(crate) identity_key: String,
|
||||||
|
pub(crate) bonded: bool,
|
||||||
|
pub(crate) blacklisted: bool,
|
||||||
|
pub(crate) self_described: Option<String>,
|
||||||
|
pub(crate) explorer_pretty_bond: Option<String>,
|
||||||
|
pub(crate) last_updated_utc: i64,
|
||||||
|
pub(crate) performance: u8,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub(crate) struct GatewayDto {
|
||||||
|
pub(crate) gateway_identity_key: String,
|
||||||
|
pub(crate) bonded: bool,
|
||||||
|
pub(crate) blacklisted: bool,
|
||||||
|
pub(crate) performance: i64,
|
||||||
|
pub(crate) self_described: Option<String>,
|
||||||
|
pub(crate) explorer_pretty_bond: Option<String>,
|
||||||
|
pub(crate) last_probe_result: Option<String>,
|
||||||
|
pub(crate) last_probe_log: Option<String>,
|
||||||
|
pub(crate) last_testrun_utc: Option<i64>,
|
||||||
|
pub(crate) last_updated_utc: i64,
|
||||||
|
pub(crate) moniker: String,
|
||||||
|
pub(crate) security_contact: String,
|
||||||
|
pub(crate) details: String,
|
||||||
|
pub(crate) website: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TryFrom<GatewayDto> for http::models::Gateway {
|
||||||
|
type Error = anyhow::Error;
|
||||||
|
|
||||||
|
fn try_from(value: GatewayDto) -> Result<Self, Self::Error> {
|
||||||
|
// Instead of using routing_score_successes / routing_score_samples, we use the
|
||||||
|
// number of successful testruns in the last 24h.
|
||||||
|
let routing_score = 0f32;
|
||||||
|
let config_score = 0u32;
|
||||||
|
let last_updated_utc =
|
||||||
|
timestamp_as_utc(value.last_updated_utc.cast_checked()?).to_rfc3339();
|
||||||
|
let last_testrun_utc = value
|
||||||
|
.last_testrun_utc
|
||||||
|
.and_then(|i| i.cast_checked().ok())
|
||||||
|
.map(|t| timestamp_as_utc(t).to_rfc3339());
|
||||||
|
|
||||||
|
let self_described = value.self_described.clone().unwrap_or("null".to_string());
|
||||||
|
let explorer_pretty_bond = value
|
||||||
|
.explorer_pretty_bond
|
||||||
|
.clone()
|
||||||
|
.unwrap_or("null".to_string());
|
||||||
|
let last_probe_result = value
|
||||||
|
.last_probe_result
|
||||||
|
.clone()
|
||||||
|
.unwrap_or("null".to_string());
|
||||||
|
let last_probe_log = value.last_probe_log.clone();
|
||||||
|
|
||||||
|
let self_described = serde_json::from_str(&self_described).unwrap_or(None);
|
||||||
|
let explorer_pretty_bond = serde_json::from_str(&explorer_pretty_bond).unwrap_or(None);
|
||||||
|
let last_probe_result = serde_json::from_str(&last_probe_result).unwrap_or(None);
|
||||||
|
|
||||||
|
let bonded = value.bonded;
|
||||||
|
let blacklisted = value.blacklisted;
|
||||||
|
let performance = value.performance as u8;
|
||||||
|
|
||||||
|
let description = NodeDescription {
|
||||||
|
moniker: value.moniker.clone(),
|
||||||
|
website: value.website.clone(),
|
||||||
|
security_contact: value.security_contact.clone(),
|
||||||
|
details: value.details.clone(),
|
||||||
|
};
|
||||||
|
|
||||||
|
Ok(http::models::Gateway {
|
||||||
|
gateway_identity_key: value.gateway_identity_key.clone(),
|
||||||
|
bonded,
|
||||||
|
blacklisted,
|
||||||
|
performance,
|
||||||
|
self_described,
|
||||||
|
explorer_pretty_bond,
|
||||||
|
description,
|
||||||
|
last_probe_result,
|
||||||
|
last_probe_log,
|
||||||
|
routing_score,
|
||||||
|
config_score,
|
||||||
|
last_testrun_utc,
|
||||||
|
last_updated_utc,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn timestamp_as_utc(unix_timestamp: u64) -> chrono::DateTime<chrono::Utc> {
|
||||||
|
let d = std::time::UNIX_EPOCH + std::time::Duration::from_secs(unix_timestamp);
|
||||||
|
d.into()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) struct MixnodeRecord {
|
||||||
|
pub(crate) mix_id: u32,
|
||||||
|
pub(crate) identity_key: String,
|
||||||
|
pub(crate) bonded: bool,
|
||||||
|
pub(crate) total_stake: i64,
|
||||||
|
pub(crate) host: String,
|
||||||
|
pub(crate) http_port: u16,
|
||||||
|
pub(crate) blacklisted: bool,
|
||||||
|
pub(crate) full_details: String,
|
||||||
|
pub(crate) self_described: Option<String>,
|
||||||
|
pub(crate) last_updated_utc: i64,
|
||||||
|
pub(crate) is_dp_delegatee: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub(crate) struct MixnodeDto {
|
||||||
|
pub(crate) mix_id: i64,
|
||||||
|
pub(crate) bonded: bool,
|
||||||
|
pub(crate) blacklisted: bool,
|
||||||
|
pub(crate) is_dp_delegatee: bool,
|
||||||
|
pub(crate) total_stake: i64,
|
||||||
|
pub(crate) full_details: String,
|
||||||
|
pub(crate) self_described: Option<String>,
|
||||||
|
pub(crate) last_updated_utc: i64,
|
||||||
|
pub(crate) moniker: String,
|
||||||
|
pub(crate) website: String,
|
||||||
|
pub(crate) security_contact: String,
|
||||||
|
pub(crate) details: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TryFrom<MixnodeDto> for http::models::Mixnode {
|
||||||
|
type Error = anyhow::Error;
|
||||||
|
|
||||||
|
fn try_from(value: MixnodeDto) -> Result<Self, Self::Error> {
|
||||||
|
let mix_id = value.mix_id.cast_checked()?;
|
||||||
|
let full_details = value.full_details.clone();
|
||||||
|
let full_details = serde_json::from_str(&full_details).unwrap_or(None);
|
||||||
|
|
||||||
|
let self_described = value
|
||||||
|
.self_described
|
||||||
|
.clone()
|
||||||
|
.map(|v| serde_json::from_str(&v).unwrap_or(serde_json::Value::Null));
|
||||||
|
|
||||||
|
let last_updated_utc =
|
||||||
|
timestamp_as_utc(value.last_updated_utc.cast_checked()?).to_rfc3339();
|
||||||
|
let blacklisted = value.blacklisted;
|
||||||
|
let is_dp_delegatee = value.is_dp_delegatee;
|
||||||
|
let moniker = value.moniker.clone();
|
||||||
|
let website = value.website.clone();
|
||||||
|
let security_contact = value.security_contact.clone();
|
||||||
|
let details = value.details.clone();
|
||||||
|
|
||||||
|
Ok(http::models::Mixnode {
|
||||||
|
mix_id,
|
||||||
|
bonded: value.bonded,
|
||||||
|
blacklisted,
|
||||||
|
is_dp_delegatee,
|
||||||
|
total_stake: value.total_stake,
|
||||||
|
full_details,
|
||||||
|
description: NodeDescription {
|
||||||
|
moniker,
|
||||||
|
website,
|
||||||
|
security_contact,
|
||||||
|
details,
|
||||||
|
},
|
||||||
|
self_described,
|
||||||
|
last_updated_utc,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[allow(unused)]
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub(crate) struct BondedStatusDto {
|
||||||
|
pub(crate) id: i64,
|
||||||
|
pub(crate) identity_key: String,
|
||||||
|
pub(crate) bonded: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[allow(unused)]
|
||||||
|
#[derive(Debug, Clone, Default)]
|
||||||
|
pub(crate) struct SummaryDto {
|
||||||
|
pub(crate) key: String,
|
||||||
|
pub(crate) value_json: String,
|
||||||
|
pub(crate) last_updated_utc: i64,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Default)]
|
||||||
|
pub(crate) struct SummaryHistoryDto {
|
||||||
|
#[allow(dead_code)]
|
||||||
|
pub id: i64,
|
||||||
|
pub date: String,
|
||||||
|
pub value_json: String,
|
||||||
|
pub timestamp_utc: i64,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TryFrom<SummaryHistoryDto> for SummaryHistory {
|
||||||
|
type Error = anyhow::Error;
|
||||||
|
|
||||||
|
fn try_from(value: SummaryHistoryDto) -> Result<Self, Self::Error> {
|
||||||
|
let value_json = serde_json::from_str(&value.value_json).unwrap_or_default();
|
||||||
|
Ok(SummaryHistory {
|
||||||
|
value_json,
|
||||||
|
date: value.date.clone(),
|
||||||
|
timestamp_utc: timestamp_as_utc(value.timestamp_utc.cast_checked()?).to_rfc3339(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) const MIXNODES_BONDED_COUNT: &str = "mixnodes.bonded.count";
|
||||||
|
pub(crate) const MIXNODES_BONDED_ACTIVE: &str = "mixnodes.bonded.active";
|
||||||
|
pub(crate) const MIXNODES_BONDED_INACTIVE: &str = "mixnodes.bonded.inactive";
|
||||||
|
pub(crate) const MIXNODES_BONDED_RESERVE: &str = "mixnodes.bonded.reserve";
|
||||||
|
pub(crate) const MIXNODES_BLACKLISTED_COUNT: &str = "mixnodes.blacklisted.count";
|
||||||
|
|
||||||
|
pub(crate) const GATEWAYS_BONDED_COUNT: &str = "gateways.bonded.count";
|
||||||
|
pub(crate) const GATEWAYS_EXPLORER_COUNT: &str = "gateways.explorer.count";
|
||||||
|
pub(crate) const GATEWAYS_BLACKLISTED_COUNT: &str = "gateways.blacklisted.count";
|
||||||
|
|
||||||
|
pub(crate) const MIXNODES_HISTORICAL_COUNT: &str = "mixnodes.historical.count";
|
||||||
|
pub(crate) const GATEWAYS_HISTORICAL_COUNT: &str = "gateways.historical.count";
|
||||||
|
|
||||||
|
// `utoipa`` goes crazy if you use module-qualified prefix as field type so we
|
||||||
|
// have to import it
|
||||||
|
use gateway::GatewaySummary;
|
||||||
|
use mixnode::MixnodeSummary;
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Deserialize, Serialize, ToSchema)]
|
||||||
|
pub(crate) struct NetworkSummary {
|
||||||
|
pub(crate) mixnodes: MixnodeSummary,
|
||||||
|
pub(crate) gateways: GatewaySummary,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) mod mixnode {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Deserialize, Serialize, ToSchema)]
|
||||||
|
pub(crate) struct MixnodeSummary {
|
||||||
|
pub(crate) bonded: MixnodeSummaryBonded,
|
||||||
|
pub(crate) blacklisted: MixnodeSummaryBlacklisted,
|
||||||
|
pub(crate) historical: MixnodeSummaryHistorical,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Deserialize, Serialize, ToSchema)]
|
||||||
|
pub(crate) struct MixnodeSummaryBonded {
|
||||||
|
pub(crate) count: i32,
|
||||||
|
pub(crate) active: i32,
|
||||||
|
pub(crate) inactive: i32,
|
||||||
|
pub(crate) reserve: i32,
|
||||||
|
pub(crate) last_updated_utc: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Deserialize, Serialize, ToSchema)]
|
||||||
|
pub(crate) struct MixnodeSummaryBlacklisted {
|
||||||
|
pub(crate) count: i32,
|
||||||
|
pub(crate) last_updated_utc: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Deserialize, Serialize, ToSchema)]
|
||||||
|
pub(crate) struct MixnodeSummaryHistorical {
|
||||||
|
pub(crate) count: i32,
|
||||||
|
pub(crate) last_updated_utc: String,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) mod gateway {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Deserialize, Serialize, ToSchema)]
|
||||||
|
pub(crate) struct GatewaySummary {
|
||||||
|
pub(crate) bonded: GatewaySummaryBonded,
|
||||||
|
pub(crate) blacklisted: GatewaySummaryBlacklisted,
|
||||||
|
pub(crate) historical: GatewaySummaryHistorical,
|
||||||
|
pub(crate) explorer: GatewaySummaryExplorer,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Deserialize, Serialize, ToSchema)]
|
||||||
|
pub(crate) struct GatewaySummaryExplorer {
|
||||||
|
pub(crate) count: i32,
|
||||||
|
pub(crate) last_updated_utc: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Deserialize, Serialize, ToSchema)]
|
||||||
|
pub(crate) struct GatewaySummaryBonded {
|
||||||
|
pub(crate) count: i32,
|
||||||
|
pub(crate) last_updated_utc: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Deserialize, Serialize, ToSchema)]
|
||||||
|
pub(crate) struct GatewaySummaryHistorical {
|
||||||
|
pub(crate) count: i32,
|
||||||
|
pub(crate) last_updated_utc: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Deserialize, Serialize, ToSchema)]
|
||||||
|
pub(crate) struct GatewaySummaryBlacklisted {
|
||||||
|
pub(crate) count: i32,
|
||||||
|
pub(crate) last_updated_utc: String,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct TestRunDto {
|
||||||
|
pub id: i64,
|
||||||
|
pub gateway_id: i64,
|
||||||
|
pub status: i64,
|
||||||
|
pub timestamp_utc: i64,
|
||||||
|
pub ip_address: String,
|
||||||
|
pub log: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, strum_macros::Display, EnumString, FromRepr, PartialEq)]
|
||||||
|
#[repr(u8)]
|
||||||
|
pub(crate) enum TestRunStatus {
|
||||||
|
Complete = 2,
|
||||||
|
InProgress = 1,
|
||||||
|
Pending = 0,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct GatewayIdentityDto {
|
||||||
|
pub gateway_identity_key: String,
|
||||||
|
pub bonded: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[allow(dead_code)] // it's not dead code but clippy doesn't detect usage in sqlx macros
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct GatewayInfoDto {
|
||||||
|
pub id: i64,
|
||||||
|
pub gateway_identity_key: String,
|
||||||
|
pub self_described: Option<String>,
|
||||||
|
pub explorer_pretty_bond: Option<String>,
|
||||||
|
}
|
||||||
@@ -0,0 +1,160 @@
|
|||||||
|
use crate::{
|
||||||
|
db::{
|
||||||
|
models::{BondedStatusDto, GatewayDto, GatewayRecord},
|
||||||
|
DbPool,
|
||||||
|
},
|
||||||
|
http::models::Gateway,
|
||||||
|
};
|
||||||
|
use futures_util::TryStreamExt;
|
||||||
|
use nym_validator_client::models::DescribedGateway;
|
||||||
|
use tracing::error;
|
||||||
|
|
||||||
|
pub(crate) async fn insert_gateways(
|
||||||
|
pool: &DbPool,
|
||||||
|
gateways: Vec<GatewayRecord>,
|
||||||
|
) -> anyhow::Result<()> {
|
||||||
|
let mut db = pool.acquire().await?;
|
||||||
|
for record in gateways {
|
||||||
|
sqlx::query!(
|
||||||
|
"INSERT INTO gateways
|
||||||
|
(gateway_identity_key, bonded, blacklisted,
|
||||||
|
self_described, explorer_pretty_bond,
|
||||||
|
last_updated_utc, performance)
|
||||||
|
VALUES (?, ?, ?, ?, ?, ?, ?)
|
||||||
|
ON CONFLICT(gateway_identity_key) DO UPDATE SET
|
||||||
|
bonded=excluded.bonded,
|
||||||
|
blacklisted=excluded.blacklisted,
|
||||||
|
self_described=excluded.self_described,
|
||||||
|
explorer_pretty_bond=excluded.explorer_pretty_bond,
|
||||||
|
last_updated_utc=excluded.last_updated_utc,
|
||||||
|
performance = excluded.performance;",
|
||||||
|
record.identity_key,
|
||||||
|
record.bonded,
|
||||||
|
record.blacklisted,
|
||||||
|
record.self_described,
|
||||||
|
record.explorer_pretty_bond,
|
||||||
|
record.last_updated_utc,
|
||||||
|
record.performance
|
||||||
|
)
|
||||||
|
.execute(&mut *db)
|
||||||
|
.await?;
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) async fn write_blacklisted_gateways_to_db<'a, I>(
|
||||||
|
pool: &DbPool,
|
||||||
|
gateways: I,
|
||||||
|
) -> anyhow::Result<()>
|
||||||
|
where
|
||||||
|
I: Iterator<Item = &'a String>,
|
||||||
|
{
|
||||||
|
let mut conn = pool.acquire().await?;
|
||||||
|
for gateway_identity_key in gateways {
|
||||||
|
sqlx::query!(
|
||||||
|
"UPDATE gateways
|
||||||
|
SET blacklisted = true
|
||||||
|
WHERE gateway_identity_key = ?;",
|
||||||
|
gateway_identity_key,
|
||||||
|
)
|
||||||
|
.execute(&mut *conn)
|
||||||
|
.await?;
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Ensure all gateways that are set as bonded, are still bonded
|
||||||
|
pub(crate) async fn ensure_gateways_still_bonded(
|
||||||
|
pool: &DbPool,
|
||||||
|
gateways: &[DescribedGateway],
|
||||||
|
) -> anyhow::Result<usize> {
|
||||||
|
let bonded_gateways_rows = get_all_bonded_gateways_row_ids_by_status(pool, true).await?;
|
||||||
|
let unbonded_gateways_rows = bonded_gateways_rows.iter().filter(|v| {
|
||||||
|
!gateways
|
||||||
|
.iter()
|
||||||
|
.any(|bonded| *bonded.bond.identity() == v.identity_key)
|
||||||
|
});
|
||||||
|
|
||||||
|
let recently_unbonded_gateways = unbonded_gateways_rows.to_owned().count();
|
||||||
|
let last_updated_utc = chrono::offset::Utc::now().timestamp();
|
||||||
|
let mut transaction = pool.begin().await?;
|
||||||
|
for row in unbonded_gateways_rows {
|
||||||
|
sqlx::query!(
|
||||||
|
"UPDATE gateways
|
||||||
|
SET bonded = ?, last_updated_utc = ?
|
||||||
|
WHERE id = ?;",
|
||||||
|
false,
|
||||||
|
last_updated_utc,
|
||||||
|
row.id,
|
||||||
|
)
|
||||||
|
.execute(&mut *transaction)
|
||||||
|
.await?;
|
||||||
|
}
|
||||||
|
transaction.commit().await?;
|
||||||
|
|
||||||
|
Ok(recently_unbonded_gateways)
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn get_all_bonded_gateways_row_ids_by_status(
|
||||||
|
pool: &DbPool,
|
||||||
|
status: bool,
|
||||||
|
) -> anyhow::Result<Vec<BondedStatusDto>> {
|
||||||
|
let mut conn = pool.acquire().await?;
|
||||||
|
let items = sqlx::query_as!(
|
||||||
|
BondedStatusDto,
|
||||||
|
r#"SELECT
|
||||||
|
id as "id!",
|
||||||
|
gateway_identity_key as "identity_key!",
|
||||||
|
bonded as "bonded: bool"
|
||||||
|
FROM gateways
|
||||||
|
WHERE bonded = ?"#,
|
||||||
|
status,
|
||||||
|
)
|
||||||
|
.fetch(&mut *conn)
|
||||||
|
.try_collect::<Vec<_>>()
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
Ok(items)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) async fn get_all_gateways(pool: &DbPool) -> anyhow::Result<Vec<Gateway>> {
|
||||||
|
let mut conn = pool.acquire().await?;
|
||||||
|
let items = sqlx::query_as!(
|
||||||
|
GatewayDto,
|
||||||
|
r#"SELECT
|
||||||
|
gw.gateway_identity_key as "gateway_identity_key!",
|
||||||
|
gw.bonded as "bonded: bool",
|
||||||
|
gw.blacklisted as "blacklisted: bool",
|
||||||
|
gw.performance as "performance!",
|
||||||
|
gw.self_described as "self_described?",
|
||||||
|
gw.explorer_pretty_bond as "explorer_pretty_bond?",
|
||||||
|
gw.last_probe_result as "last_probe_result?",
|
||||||
|
gw.last_probe_log as "last_probe_log?",
|
||||||
|
gw.last_testrun_utc as "last_testrun_utc?",
|
||||||
|
gw.last_updated_utc as "last_updated_utc!",
|
||||||
|
COALESCE(gd.moniker, "NA") as "moniker!",
|
||||||
|
COALESCE(gd.website, "NA") as "website!",
|
||||||
|
COALESCE(gd.security_contact, "NA") as "security_contact!",
|
||||||
|
COALESCE(gd.details, "NA") as "details!"
|
||||||
|
FROM gateways gw
|
||||||
|
LEFT JOIN gateway_description gd
|
||||||
|
ON gw.gateway_identity_key = gd.gateway_identity_key
|
||||||
|
ORDER BY gw.gateway_identity_key"#,
|
||||||
|
)
|
||||||
|
.fetch(&mut *conn)
|
||||||
|
.try_collect::<Vec<_>>()
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
let items: Vec<Gateway> = items
|
||||||
|
.into_iter()
|
||||||
|
.map(|item| item.try_into())
|
||||||
|
.collect::<anyhow::Result<Vec<_>>>()
|
||||||
|
.map_err(|e| {
|
||||||
|
error!("Conversion from DTO failed: {e}. Invalidly stored data?");
|
||||||
|
e
|
||||||
|
})?;
|
||||||
|
tracing::trace!("Fetched {} gateways from DB", items.len());
|
||||||
|
Ok(items)
|
||||||
|
}
|
||||||
@@ -0,0 +1,88 @@
|
|||||||
|
use crate::db::{models::NetworkSummary, DbPool};
|
||||||
|
use chrono::{DateTime, Utc};
|
||||||
|
|
||||||
|
/// take `last_updated` instead of calculating it so that `summary` matches
|
||||||
|
/// `daily_summary`
|
||||||
|
pub(crate) async fn insert_summaries(
|
||||||
|
pool: &DbPool,
|
||||||
|
summaries: &[(&str, &usize)],
|
||||||
|
summary: &NetworkSummary,
|
||||||
|
last_updated: DateTime<Utc>,
|
||||||
|
) -> anyhow::Result<()> {
|
||||||
|
insert_summary(pool, summaries, last_updated).await?;
|
||||||
|
|
||||||
|
insert_summary_history(pool, summary, last_updated).await?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn insert_summary(
|
||||||
|
pool: &DbPool,
|
||||||
|
summaries: &[(&str, &usize)],
|
||||||
|
last_updated: DateTime<Utc>,
|
||||||
|
) -> anyhow::Result<()> {
|
||||||
|
let timestamp = last_updated.timestamp();
|
||||||
|
let mut tx = pool.begin().await?;
|
||||||
|
|
||||||
|
for (kind, value) in summaries {
|
||||||
|
let value = value.to_string();
|
||||||
|
sqlx::query!(
|
||||||
|
"INSERT INTO summary
|
||||||
|
(key, value_json, last_updated_utc)
|
||||||
|
VALUES (?, ?, ?)
|
||||||
|
ON CONFLICT(key) DO UPDATE SET
|
||||||
|
value_json=excluded.value_json,
|
||||||
|
last_updated_utc=excluded.last_updated_utc;",
|
||||||
|
kind,
|
||||||
|
value,
|
||||||
|
timestamp
|
||||||
|
)
|
||||||
|
.execute(&mut *tx)
|
||||||
|
.await
|
||||||
|
.map_err(|err| {
|
||||||
|
tracing::error!("Failed to insert data for {kind}: {err}, aborting transaction",);
|
||||||
|
err
|
||||||
|
})?;
|
||||||
|
}
|
||||||
|
|
||||||
|
tx.commit().await?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// For `<date_N>`, `summary_history` is updated with fresh data on every
|
||||||
|
/// iteration.
|
||||||
|
///
|
||||||
|
/// After UTC midnight, summary is inserted for `<date_N+1>` and last entry for
|
||||||
|
/// `<date_N>` stays there forever.
|
||||||
|
///
|
||||||
|
/// This is not aggregate data, it's a set of latest data points
|
||||||
|
async fn insert_summary_history(
|
||||||
|
pool: &DbPool,
|
||||||
|
summary: &NetworkSummary,
|
||||||
|
last_updated: DateTime<Utc>,
|
||||||
|
) -> anyhow::Result<()> {
|
||||||
|
let mut conn = pool.acquire().await?;
|
||||||
|
|
||||||
|
let value_json = serde_json::to_string(&summary)?;
|
||||||
|
let timestamp = last_updated.timestamp();
|
||||||
|
let now_rfc3339 = last_updated.to_rfc3339();
|
||||||
|
// YYYY-MM-DD, without time
|
||||||
|
let date = &now_rfc3339[..10];
|
||||||
|
|
||||||
|
sqlx::query!(
|
||||||
|
"INSERT INTO summary_history
|
||||||
|
(date, timestamp_utc, value_json)
|
||||||
|
VALUES (?, ?, ?)
|
||||||
|
ON CONFLICT(date) DO UPDATE SET
|
||||||
|
timestamp_utc=excluded.timestamp_utc,
|
||||||
|
value_json=excluded.value_json;",
|
||||||
|
date,
|
||||||
|
timestamp,
|
||||||
|
value_json
|
||||||
|
)
|
||||||
|
.execute(&mut *conn)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
@@ -0,0 +1,177 @@
|
|||||||
|
use futures_util::TryStreamExt;
|
||||||
|
use nym_validator_client::models::MixNodeBondAnnotated;
|
||||||
|
use tracing::error;
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
db::{
|
||||||
|
models::{BondedStatusDto, MixnodeDto, MixnodeRecord},
|
||||||
|
DbPool,
|
||||||
|
},
|
||||||
|
http::models::{DailyStats, Mixnode},
|
||||||
|
};
|
||||||
|
|
||||||
|
pub(crate) async fn insert_mixnodes(
|
||||||
|
pool: &DbPool,
|
||||||
|
mixnodes: Vec<MixnodeRecord>,
|
||||||
|
) -> anyhow::Result<()> {
|
||||||
|
let mut conn = pool.acquire().await?;
|
||||||
|
|
||||||
|
for record in mixnodes.iter() {
|
||||||
|
// https://www.sqlite.org/lang_upsert.html
|
||||||
|
sqlx::query!(
|
||||||
|
"INSERT INTO mixnodes
|
||||||
|
(mix_id, identity_key, bonded, total_stake,
|
||||||
|
host, http_api_port, blacklisted, full_details,
|
||||||
|
self_described, last_updated_utc, is_dp_delegatee)
|
||||||
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||||
|
ON CONFLICT(mix_id) DO UPDATE SET
|
||||||
|
bonded=excluded.bonded,
|
||||||
|
total_stake=excluded.total_stake, host=excluded.host,
|
||||||
|
http_api_port=excluded.http_api_port,blacklisted=excluded.blacklisted,
|
||||||
|
full_details=excluded.full_details,self_described=excluded.self_described,
|
||||||
|
last_updated_utc=excluded.last_updated_utc,
|
||||||
|
is_dp_delegatee = excluded.is_dp_delegatee;",
|
||||||
|
record.mix_id,
|
||||||
|
record.identity_key,
|
||||||
|
record.bonded,
|
||||||
|
record.total_stake,
|
||||||
|
record.host,
|
||||||
|
record.http_port,
|
||||||
|
record.blacklisted,
|
||||||
|
record.full_details,
|
||||||
|
record.self_described,
|
||||||
|
record.last_updated_utc,
|
||||||
|
record.is_dp_delegatee
|
||||||
|
)
|
||||||
|
.execute(&mut *conn)
|
||||||
|
.await?;
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) async fn get_all_mixnodes(pool: &DbPool) -> anyhow::Result<Vec<Mixnode>> {
|
||||||
|
let mut conn = pool.acquire().await?;
|
||||||
|
let items = sqlx::query_as!(
|
||||||
|
MixnodeDto,
|
||||||
|
r#"SELECT
|
||||||
|
mn.mix_id as "mix_id!",
|
||||||
|
mn.bonded as "bonded: bool",
|
||||||
|
mn.blacklisted as "blacklisted: bool",
|
||||||
|
mn.is_dp_delegatee as "is_dp_delegatee: bool",
|
||||||
|
mn.total_stake as "total_stake!",
|
||||||
|
mn.full_details as "full_details!",
|
||||||
|
mn.self_described as "self_described",
|
||||||
|
mn.last_updated_utc as "last_updated_utc!",
|
||||||
|
COALESCE(md.moniker, "NA") as "moniker!",
|
||||||
|
COALESCE(md.website, "NA") as "website!",
|
||||||
|
COALESCE(md.security_contact, "NA") as "security_contact!",
|
||||||
|
COALESCE(md.details, "NA") as "details!"
|
||||||
|
FROM mixnodes mn
|
||||||
|
LEFT JOIN mixnode_description md ON mn.mix_id = md.mix_id
|
||||||
|
ORDER BY mn.mix_id"#
|
||||||
|
)
|
||||||
|
.fetch(&mut *conn)
|
||||||
|
.try_collect::<Vec<_>>()
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
let items = items
|
||||||
|
.into_iter()
|
||||||
|
.map(|item| item.try_into())
|
||||||
|
.collect::<anyhow::Result<Vec<_>>>()
|
||||||
|
.map_err(|e| {
|
||||||
|
error!("Conversion from DTO failed: {e}. Invalidly stored data?");
|
||||||
|
e
|
||||||
|
})?;
|
||||||
|
Ok(items)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// We fetch the latest 30 days of data as a subquery and then
|
||||||
|
/// return it in ascending order, so we don't break existing UI
|
||||||
|
pub(crate) async fn get_daily_stats(pool: &DbPool) -> anyhow::Result<Vec<DailyStats>> {
|
||||||
|
let mut conn = pool.acquire().await?;
|
||||||
|
let items = sqlx::query_as!(
|
||||||
|
DailyStats,
|
||||||
|
r#"
|
||||||
|
SELECT
|
||||||
|
date_utc as "date_utc!",
|
||||||
|
packets_received as "total_packets_received!: i64",
|
||||||
|
packets_sent as "total_packets_sent!: i64",
|
||||||
|
packets_dropped as "total_packets_dropped!: i64",
|
||||||
|
total_stake as "total_stake!: i64"
|
||||||
|
FROM (
|
||||||
|
SELECT
|
||||||
|
date_utc,
|
||||||
|
SUM(packets_received) as packets_received,
|
||||||
|
SUM(packets_sent) as packets_sent,
|
||||||
|
SUM(packets_dropped) as packets_dropped,
|
||||||
|
SUM(total_stake) as total_stake
|
||||||
|
FROM mixnode_daily_stats
|
||||||
|
GROUP BY date_utc
|
||||||
|
ORDER BY date_utc DESC
|
||||||
|
LIMIT 30
|
||||||
|
)
|
||||||
|
GROUP BY date_utc
|
||||||
|
ORDER BY date_utc
|
||||||
|
"#
|
||||||
|
)
|
||||||
|
.fetch(&mut *conn)
|
||||||
|
.try_collect::<Vec<DailyStats>>()
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
Ok(items)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Ensure all mixnodes that are set as bonded, are still bonded
|
||||||
|
pub(crate) async fn ensure_mixnodes_still_bonded(
|
||||||
|
pool: &DbPool,
|
||||||
|
mixnodes: &[MixNodeBondAnnotated],
|
||||||
|
) -> anyhow::Result<usize> {
|
||||||
|
let bonded_mixnodes_rows = get_all_bonded_mixnodes_row_ids_by_status(pool, true).await?;
|
||||||
|
let unbonded_mixnodes_rows = bonded_mixnodes_rows.iter().filter(|v| {
|
||||||
|
!mixnodes
|
||||||
|
.iter()
|
||||||
|
.any(|bonded| *bonded.mixnode_details.bond_information.identity() == v.identity_key)
|
||||||
|
});
|
||||||
|
|
||||||
|
let recently_unbonded_mixnodes = unbonded_mixnodes_rows.to_owned().count();
|
||||||
|
let last_updated_utc = chrono::offset::Utc::now().timestamp();
|
||||||
|
let mut transaction = pool.begin().await?;
|
||||||
|
for row in unbonded_mixnodes_rows {
|
||||||
|
sqlx::query!(
|
||||||
|
"UPDATE mixnodes
|
||||||
|
SET bonded = ?, last_updated_utc = ?
|
||||||
|
WHERE id = ?;",
|
||||||
|
false,
|
||||||
|
last_updated_utc,
|
||||||
|
row.id,
|
||||||
|
)
|
||||||
|
.execute(&mut *transaction)
|
||||||
|
.await?;
|
||||||
|
}
|
||||||
|
transaction.commit().await?;
|
||||||
|
|
||||||
|
Ok(recently_unbonded_mixnodes)
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn get_all_bonded_mixnodes_row_ids_by_status(
|
||||||
|
pool: &DbPool,
|
||||||
|
status: bool,
|
||||||
|
) -> anyhow::Result<Vec<BondedStatusDto>> {
|
||||||
|
let mut conn = pool.acquire().await?;
|
||||||
|
let items = sqlx::query_as!(
|
||||||
|
BondedStatusDto,
|
||||||
|
r#"SELECT
|
||||||
|
id as "id!",
|
||||||
|
identity_key as "identity_key!",
|
||||||
|
bonded as "bonded: bool"
|
||||||
|
FROM mixnodes
|
||||||
|
WHERE bonded = ?"#,
|
||||||
|
status,
|
||||||
|
)
|
||||||
|
.fetch(&mut *conn)
|
||||||
|
.try_collect::<Vec<_>>()
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
Ok(items)
|
||||||
|
}
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
mod gateways;
|
||||||
|
mod misc;
|
||||||
|
mod mixnodes;
|
||||||
|
mod summary;
|
||||||
|
pub(crate) mod testruns;
|
||||||
|
|
||||||
|
pub(crate) use gateways::{
|
||||||
|
ensure_gateways_still_bonded, get_all_gateways, insert_gateways,
|
||||||
|
write_blacklisted_gateways_to_db,
|
||||||
|
};
|
||||||
|
pub(crate) use misc::insert_summaries;
|
||||||
|
pub(crate) use mixnodes::{
|
||||||
|
ensure_mixnodes_still_bonded, get_all_mixnodes, get_daily_stats, insert_mixnodes,
|
||||||
|
};
|
||||||
|
pub(crate) use summary::{get_summary, get_summary_history};
|
||||||
@@ -0,0 +1,209 @@
|
|||||||
|
use chrono::{DateTime, Utc};
|
||||||
|
use futures_util::TryStreamExt;
|
||||||
|
use std::collections::HashMap;
|
||||||
|
use tracing::error;
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
db::{
|
||||||
|
models::{
|
||||||
|
gateway::{
|
||||||
|
GatewaySummary, GatewaySummaryBlacklisted, GatewaySummaryBonded,
|
||||||
|
GatewaySummaryExplorer, GatewaySummaryHistorical,
|
||||||
|
},
|
||||||
|
mixnode::{
|
||||||
|
MixnodeSummary, MixnodeSummaryBlacklisted, MixnodeSummaryBonded,
|
||||||
|
MixnodeSummaryHistorical,
|
||||||
|
},
|
||||||
|
NetworkSummary, SummaryDto, SummaryHistoryDto,
|
||||||
|
},
|
||||||
|
DbPool,
|
||||||
|
},
|
||||||
|
http::{
|
||||||
|
error::{HttpError, HttpResult},
|
||||||
|
models::SummaryHistory,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
pub(crate) async fn get_summary_history(pool: &DbPool) -> anyhow::Result<Vec<SummaryHistory>> {
|
||||||
|
let mut conn = pool.acquire().await?;
|
||||||
|
let items = sqlx::query_as!(
|
||||||
|
SummaryHistoryDto,
|
||||||
|
r#"SELECT
|
||||||
|
id as "id!",
|
||||||
|
date as "date!",
|
||||||
|
timestamp_utc as "timestamp_utc!",
|
||||||
|
value_json as "value_json!"
|
||||||
|
FROM summary_history
|
||||||
|
ORDER BY date DESC
|
||||||
|
LIMIT 30"#,
|
||||||
|
)
|
||||||
|
.fetch(&mut *conn)
|
||||||
|
.try_collect::<Vec<_>>()
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
let items = items
|
||||||
|
.into_iter()
|
||||||
|
.map(|item| item.try_into())
|
||||||
|
.collect::<anyhow::Result<Vec<_>>>()
|
||||||
|
.map_err(|e| {
|
||||||
|
error!("Conversion from DTO failed: {e}. Invalidly stored data?");
|
||||||
|
e
|
||||||
|
})?;
|
||||||
|
Ok(items)
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn get_summary_dto(pool: &DbPool) -> anyhow::Result<Vec<SummaryDto>> {
|
||||||
|
let mut conn = pool.acquire().await?;
|
||||||
|
Ok(sqlx::query_as!(
|
||||||
|
SummaryDto,
|
||||||
|
r#"SELECT
|
||||||
|
key as "key!",
|
||||||
|
value_json as "value_json!",
|
||||||
|
last_updated_utc as "last_updated_utc!"
|
||||||
|
FROM summary"#
|
||||||
|
)
|
||||||
|
.fetch(&mut *conn)
|
||||||
|
.try_collect::<Vec<_>>()
|
||||||
|
.await?)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) async fn get_summary(pool: &DbPool) -> HttpResult<NetworkSummary> {
|
||||||
|
let items = get_summary_dto(pool).await.map_err(|err| {
|
||||||
|
tracing::error!("Couldn't get Summary from DB: {err}");
|
||||||
|
HttpError::internal()
|
||||||
|
})?;
|
||||||
|
from_summary_dto(items).await
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn from_summary_dto(items: Vec<SummaryDto>) -> HttpResult<NetworkSummary> {
|
||||||
|
const MIXNODES_BONDED_COUNT: &str = "mixnodes.bonded.count";
|
||||||
|
const MIXNODES_BONDED_ACTIVE: &str = "mixnodes.bonded.active";
|
||||||
|
const MIXNODES_BONDED_INACTIVE: &str = "mixnodes.bonded.inactive";
|
||||||
|
const MIXNODES_BONDED_RESERVE: &str = "mixnodes.bonded.reserve";
|
||||||
|
const MIXNODES_BLACKLISTED_COUNT: &str = "mixnodes.blacklisted.count";
|
||||||
|
const GATEWAYS_BONDED_COUNT: &str = "gateways.bonded.count";
|
||||||
|
const GATEWAYS_EXPLORER_COUNT: &str = "gateways.explorer.count";
|
||||||
|
const GATEWAYS_BLACKLISTED_COUNT: &str = "gateways.blacklisted.count";
|
||||||
|
const MIXNODES_HISTORICAL_COUNT: &str = "mixnodes.historical.count";
|
||||||
|
const GATEWAYS_HISTORICAL_COUNT: &str = "gateways.historical.count";
|
||||||
|
|
||||||
|
// convert database rows into a map by key
|
||||||
|
let mut map = HashMap::new();
|
||||||
|
for item in items {
|
||||||
|
map.insert(item.key.clone(), item);
|
||||||
|
}
|
||||||
|
|
||||||
|
// check we have all the keys we are expecting, and build up a map of errors for missing one
|
||||||
|
let keys = [
|
||||||
|
GATEWAYS_BONDED_COUNT,
|
||||||
|
GATEWAYS_EXPLORER_COUNT,
|
||||||
|
GATEWAYS_HISTORICAL_COUNT,
|
||||||
|
GATEWAYS_BLACKLISTED_COUNT,
|
||||||
|
MIXNODES_BLACKLISTED_COUNT,
|
||||||
|
MIXNODES_BONDED_ACTIVE,
|
||||||
|
MIXNODES_BONDED_COUNT,
|
||||||
|
MIXNODES_BONDED_INACTIVE,
|
||||||
|
MIXNODES_BONDED_RESERVE,
|
||||||
|
MIXNODES_HISTORICAL_COUNT,
|
||||||
|
];
|
||||||
|
|
||||||
|
let mut errors: Vec<&str> = vec![];
|
||||||
|
for key in keys {
|
||||||
|
if !map.contains_key(key) {
|
||||||
|
errors.push(key);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// return an error if anything is missing, with a nice list
|
||||||
|
if !errors.is_empty() {
|
||||||
|
tracing::error!("Summary value missing: {}", errors.join(", "));
|
||||||
|
return Err(HttpError::internal());
|
||||||
|
}
|
||||||
|
|
||||||
|
// strip the options and use default values (anything missing is trapped above)
|
||||||
|
let mixnodes_bonded_count: SummaryDto =
|
||||||
|
map.get(MIXNODES_BONDED_COUNT).cloned().unwrap_or_default();
|
||||||
|
let mixnodes_bonded_active: SummaryDto =
|
||||||
|
map.get(MIXNODES_BONDED_ACTIVE).cloned().unwrap_or_default();
|
||||||
|
let mixnodes_bonded_inactive: SummaryDto = map
|
||||||
|
.get(MIXNODES_BONDED_INACTIVE)
|
||||||
|
.cloned()
|
||||||
|
.unwrap_or_default();
|
||||||
|
let mixnodes_bonded_reserve: SummaryDto = map
|
||||||
|
.get(MIXNODES_BONDED_RESERVE)
|
||||||
|
.cloned()
|
||||||
|
.unwrap_or_default();
|
||||||
|
let mixnodes_blacklisted_count: SummaryDto = map
|
||||||
|
.get(MIXNODES_BLACKLISTED_COUNT)
|
||||||
|
.cloned()
|
||||||
|
.unwrap_or_default();
|
||||||
|
let gateways_bonded_count: SummaryDto =
|
||||||
|
map.get(GATEWAYS_BONDED_COUNT).cloned().unwrap_or_default();
|
||||||
|
let gateways_explorer_count: SummaryDto = map
|
||||||
|
.get(GATEWAYS_EXPLORER_COUNT)
|
||||||
|
.cloned()
|
||||||
|
.unwrap_or_default();
|
||||||
|
let mixnodes_historical_count: SummaryDto = map
|
||||||
|
.get(MIXNODES_HISTORICAL_COUNT)
|
||||||
|
.cloned()
|
||||||
|
.unwrap_or_default();
|
||||||
|
let gateways_historical_count: SummaryDto = map
|
||||||
|
.get(GATEWAYS_HISTORICAL_COUNT)
|
||||||
|
.cloned()
|
||||||
|
.unwrap_or_default();
|
||||||
|
let gateways_blacklisted_count: SummaryDto = map
|
||||||
|
.get(GATEWAYS_BLACKLISTED_COUNT)
|
||||||
|
.cloned()
|
||||||
|
.unwrap_or_default();
|
||||||
|
|
||||||
|
Ok(NetworkSummary {
|
||||||
|
mixnodes: MixnodeSummary {
|
||||||
|
bonded: MixnodeSummaryBonded {
|
||||||
|
count: to_count_i32(&mixnodes_bonded_count),
|
||||||
|
active: to_count_i32(&mixnodes_bonded_active),
|
||||||
|
reserve: to_count_i32(&mixnodes_bonded_reserve),
|
||||||
|
inactive: to_count_i32(&mixnodes_bonded_inactive),
|
||||||
|
last_updated_utc: to_timestamp(&mixnodes_bonded_count),
|
||||||
|
},
|
||||||
|
blacklisted: MixnodeSummaryBlacklisted {
|
||||||
|
count: to_count_i32(&mixnodes_blacklisted_count),
|
||||||
|
last_updated_utc: to_timestamp(&mixnodes_blacklisted_count),
|
||||||
|
},
|
||||||
|
historical: MixnodeSummaryHistorical {
|
||||||
|
count: to_count_i32(&mixnodes_historical_count),
|
||||||
|
last_updated_utc: to_timestamp(&mixnodes_historical_count),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
gateways: GatewaySummary {
|
||||||
|
bonded: GatewaySummaryBonded {
|
||||||
|
count: to_count_i32(&gateways_bonded_count),
|
||||||
|
last_updated_utc: to_timestamp(&gateways_bonded_count),
|
||||||
|
},
|
||||||
|
blacklisted: GatewaySummaryBlacklisted {
|
||||||
|
count: to_count_i32(&gateways_blacklisted_count),
|
||||||
|
last_updated_utc: to_timestamp(&gateways_blacklisted_count),
|
||||||
|
},
|
||||||
|
historical: GatewaySummaryHistorical {
|
||||||
|
count: to_count_i32(&gateways_historical_count),
|
||||||
|
last_updated_utc: to_timestamp(&gateways_historical_count),
|
||||||
|
},
|
||||||
|
explorer: GatewaySummaryExplorer {
|
||||||
|
count: to_count_i32(&gateways_explorer_count),
|
||||||
|
last_updated_utc: to_timestamp(&gateways_explorer_count),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn to_count_i32(value: &SummaryDto) -> i32 {
|
||||||
|
value.value_json.parse::<i32>().unwrap_or_default()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn to_timestamp(value: &SummaryDto) -> String {
|
||||||
|
timestamp_as_utc(value.last_updated_utc as u64).to_rfc3339()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn timestamp_as_utc(unix_timestamp: u64) -> DateTime<Utc> {
|
||||||
|
let d = std::time::UNIX_EPOCH + std::time::Duration::from_secs(unix_timestamp);
|
||||||
|
d.into()
|
||||||
|
}
|
||||||
@@ -0,0 +1,126 @@
|
|||||||
|
use crate::http::models::TestrunAssignment;
|
||||||
|
use crate::{
|
||||||
|
db::models::{TestRunDto, TestRunStatus},
|
||||||
|
testruns::now_utc,
|
||||||
|
};
|
||||||
|
use anyhow::Context;
|
||||||
|
use sqlx::{pool::PoolConnection, Sqlite};
|
||||||
|
|
||||||
|
pub(crate) async fn get_testrun_by_id(
|
||||||
|
conn: &mut PoolConnection<Sqlite>,
|
||||||
|
testrun_id: i64,
|
||||||
|
) -> anyhow::Result<TestRunDto> {
|
||||||
|
sqlx::query_as!(
|
||||||
|
TestRunDto,
|
||||||
|
r#"SELECT
|
||||||
|
id as "id!",
|
||||||
|
gateway_id as "gateway_id!",
|
||||||
|
status as "status!",
|
||||||
|
timestamp_utc as "timestamp_utc!",
|
||||||
|
ip_address as "ip_address!",
|
||||||
|
log as "log!"
|
||||||
|
FROM testruns
|
||||||
|
WHERE id = ?
|
||||||
|
ORDER BY timestamp_utc"#,
|
||||||
|
testrun_id
|
||||||
|
)
|
||||||
|
.fetch_one(conn.as_mut())
|
||||||
|
.await
|
||||||
|
.context(format!("Couldn't retrieve testrun {testrun_id}"))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) async fn get_oldest_testrun_and_make_it_pending(
|
||||||
|
// TODO dz accept mut reference, repeat in all similar functions
|
||||||
|
conn: PoolConnection<Sqlite>,
|
||||||
|
) -> anyhow::Result<Option<TestrunAssignment>> {
|
||||||
|
let mut conn = conn;
|
||||||
|
let assignment = sqlx::query_as!(
|
||||||
|
TestrunAssignment,
|
||||||
|
r#"UPDATE testruns
|
||||||
|
SET status = ?
|
||||||
|
WHERE rowid =
|
||||||
|
(
|
||||||
|
SELECT rowid
|
||||||
|
FROM testruns
|
||||||
|
WHERE status = ?
|
||||||
|
ORDER BY timestamp_utc asc
|
||||||
|
LIMIT 1
|
||||||
|
)
|
||||||
|
RETURNING
|
||||||
|
id as "testrun_id!",
|
||||||
|
gateway_id as "gateway_pk_id!"
|
||||||
|
"#,
|
||||||
|
TestRunStatus::InProgress as i64,
|
||||||
|
TestRunStatus::Pending as i64,
|
||||||
|
)
|
||||||
|
.fetch_optional(&mut *conn)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
Ok(assignment)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) async fn update_testrun_status(
|
||||||
|
conn: &mut PoolConnection<Sqlite>,
|
||||||
|
testrun_id: i64,
|
||||||
|
status: TestRunStatus,
|
||||||
|
) -> anyhow::Result<()> {
|
||||||
|
let status = status as u32;
|
||||||
|
sqlx::query!(
|
||||||
|
"UPDATE testruns SET status = ? WHERE id = ?",
|
||||||
|
status,
|
||||||
|
testrun_id
|
||||||
|
)
|
||||||
|
.execute(conn.as_mut())
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) async fn update_gateway_last_probe_log(
|
||||||
|
conn: &mut PoolConnection<Sqlite>,
|
||||||
|
gateway_pk: i64,
|
||||||
|
log: &str,
|
||||||
|
) -> anyhow::Result<()> {
|
||||||
|
sqlx::query!(
|
||||||
|
"UPDATE gateways SET last_probe_log = ? WHERE id = ?",
|
||||||
|
log,
|
||||||
|
gateway_pk
|
||||||
|
)
|
||||||
|
.execute(conn.as_mut())
|
||||||
|
.await
|
||||||
|
.map(drop)
|
||||||
|
.map_err(From::from)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) async fn update_gateway_last_probe_result(
|
||||||
|
conn: &mut PoolConnection<Sqlite>,
|
||||||
|
gateway_pk: i64,
|
||||||
|
result: &str,
|
||||||
|
) -> anyhow::Result<()> {
|
||||||
|
sqlx::query!(
|
||||||
|
"UPDATE gateways SET last_probe_result = ? WHERE id = ?",
|
||||||
|
result,
|
||||||
|
gateway_pk
|
||||||
|
)
|
||||||
|
.execute(conn.as_mut())
|
||||||
|
.await
|
||||||
|
.map(drop)
|
||||||
|
.map_err(From::from)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) async fn update_gateway_score(
|
||||||
|
conn: &mut PoolConnection<Sqlite>,
|
||||||
|
gateway_pk: i64,
|
||||||
|
) -> anyhow::Result<()> {
|
||||||
|
let now = now_utc().timestamp();
|
||||||
|
sqlx::query!(
|
||||||
|
"UPDATE gateways SET last_testrun_utc = ?, last_updated_utc = ? WHERE id = ?",
|
||||||
|
now,
|
||||||
|
now,
|
||||||
|
gateway_pk
|
||||||
|
)
|
||||||
|
.execute(conn.as_mut())
|
||||||
|
.await
|
||||||
|
.map(drop)
|
||||||
|
.map_err(From::from)
|
||||||
|
}
|
||||||
@@ -0,0 +1,110 @@
|
|||||||
|
use axum::{
|
||||||
|
extract::{Path, Query, State},
|
||||||
|
Json, Router,
|
||||||
|
};
|
||||||
|
use serde::Deserialize;
|
||||||
|
use utoipa::IntoParams;
|
||||||
|
|
||||||
|
use crate::http::{
|
||||||
|
error::{HttpError, HttpResult},
|
||||||
|
models::{Gateway, GatewaySkinny},
|
||||||
|
state::AppState,
|
||||||
|
PagedResult, Pagination,
|
||||||
|
};
|
||||||
|
|
||||||
|
pub(crate) fn routes() -> Router<AppState> {
|
||||||
|
Router::new()
|
||||||
|
.route("/", axum::routing::get(gateways))
|
||||||
|
.route("/skinny", axum::routing::get(gateways_skinny))
|
||||||
|
.route("/:identity_key", axum::routing::get(get_gateway))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[utoipa::path(
|
||||||
|
tag = "Gateways",
|
||||||
|
get,
|
||||||
|
params(
|
||||||
|
Pagination
|
||||||
|
),
|
||||||
|
path = "/v2/gateways",
|
||||||
|
responses(
|
||||||
|
(status = 200, body = PagedGateway)
|
||||||
|
)
|
||||||
|
)]
|
||||||
|
async fn gateways(
|
||||||
|
Query(pagination): Query<Pagination>,
|
||||||
|
State(state): State<AppState>,
|
||||||
|
) -> HttpResult<Json<PagedResult<Gateway>>> {
|
||||||
|
let db = state.db_pool();
|
||||||
|
let res = state.cache().get_gateway_list(db).await;
|
||||||
|
|
||||||
|
Ok(Json(PagedResult::paginate(pagination, res)))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[utoipa::path(
|
||||||
|
tag = "Gateways",
|
||||||
|
get,
|
||||||
|
params(
|
||||||
|
Pagination
|
||||||
|
),
|
||||||
|
path = "/v2/gateways/skinny",
|
||||||
|
responses(
|
||||||
|
(status = 200, body = PagedGatewaySkinny)
|
||||||
|
)
|
||||||
|
)]
|
||||||
|
async fn gateways_skinny(
|
||||||
|
Query(pagination): Query<Pagination>,
|
||||||
|
State(state): State<AppState>,
|
||||||
|
) -> HttpResult<Json<PagedResult<GatewaySkinny>>> {
|
||||||
|
let db = state.db_pool();
|
||||||
|
let res = state.cache().get_gateway_list(db).await;
|
||||||
|
let res: Vec<GatewaySkinny> = res
|
||||||
|
.iter()
|
||||||
|
.filter(|g| g.bonded)
|
||||||
|
.map(|g| GatewaySkinny {
|
||||||
|
gateway_identity_key: g.gateway_identity_key.clone(),
|
||||||
|
self_described: g.self_described.clone(),
|
||||||
|
performance: g.performance,
|
||||||
|
explorer_pretty_bond: g.explorer_pretty_bond.clone(),
|
||||||
|
last_probe_result: g.last_probe_result.clone(),
|
||||||
|
last_testrun_utc: g.last_testrun_utc.clone(),
|
||||||
|
last_updated_utc: g.last_updated_utc.clone(),
|
||||||
|
routing_score: g.routing_score,
|
||||||
|
config_score: g.config_score,
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
Ok(Json(PagedResult::paginate(pagination, res)))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Deserialize, IntoParams)]
|
||||||
|
#[into_params(parameter_in = Path)]
|
||||||
|
struct IdentityKeyParam {
|
||||||
|
identity_key: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[utoipa::path(
|
||||||
|
tag = "Gateways",
|
||||||
|
get,
|
||||||
|
params(
|
||||||
|
IdentityKeyParam
|
||||||
|
),
|
||||||
|
path = "/v2/gateways/{identity_key}",
|
||||||
|
responses(
|
||||||
|
(status = 200, body = Gateway)
|
||||||
|
)
|
||||||
|
)]
|
||||||
|
async fn get_gateway(
|
||||||
|
Path(IdentityKeyParam { identity_key }): Path<IdentityKeyParam>,
|
||||||
|
State(state): State<AppState>,
|
||||||
|
) -> HttpResult<Json<Gateway>> {
|
||||||
|
let db = state.db_pool();
|
||||||
|
let res = state.cache().get_gateway_list(db).await;
|
||||||
|
|
||||||
|
match res
|
||||||
|
.iter()
|
||||||
|
.find(|item| item.gateway_identity_key == identity_key)
|
||||||
|
{
|
||||||
|
Some(res) => Ok(Json(res.clone())),
|
||||||
|
None => Err(HttpError::invalid_input(identity_key)),
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,91 @@
|
|||||||
|
use axum::{
|
||||||
|
extract::{Path, Query, State},
|
||||||
|
Json, Router,
|
||||||
|
};
|
||||||
|
use serde::Deserialize;
|
||||||
|
use tracing::instrument;
|
||||||
|
use utoipa::IntoParams;
|
||||||
|
|
||||||
|
use crate::http::{
|
||||||
|
error::{HttpError, HttpResult},
|
||||||
|
models::{DailyStats, Mixnode},
|
||||||
|
state::AppState,
|
||||||
|
PagedResult, Pagination,
|
||||||
|
};
|
||||||
|
|
||||||
|
pub(crate) fn routes() -> Router<AppState> {
|
||||||
|
Router::new()
|
||||||
|
.route("/", axum::routing::get(mixnodes))
|
||||||
|
.route("/:mix_id", axum::routing::get(get_mixnodes))
|
||||||
|
.route("/stats", axum::routing::get(get_stats))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[utoipa::path(
|
||||||
|
tag = "Mixnodes",
|
||||||
|
get,
|
||||||
|
params(
|
||||||
|
Pagination
|
||||||
|
),
|
||||||
|
path = "/v2/mixnodes",
|
||||||
|
responses(
|
||||||
|
(status = 200, body = PagedMixnode)
|
||||||
|
)
|
||||||
|
)]
|
||||||
|
#[instrument(level = tracing::Level::DEBUG, skip_all, fields(page=pagination.page, size=pagination.size))]
|
||||||
|
async fn mixnodes(
|
||||||
|
Query(pagination): Query<Pagination>,
|
||||||
|
State(state): State<AppState>,
|
||||||
|
) -> HttpResult<Json<PagedResult<Mixnode>>> {
|
||||||
|
let db = state.db_pool();
|
||||||
|
let res = state.cache().get_mixnodes_list(db).await;
|
||||||
|
|
||||||
|
Ok(Json(PagedResult::paginate(pagination, res)))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Deserialize, IntoParams)]
|
||||||
|
#[into_params(parameter_in = Path)]
|
||||||
|
struct MixIdParam {
|
||||||
|
mix_id: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[utoipa::path(
|
||||||
|
tag = "Mixnodes",
|
||||||
|
get,
|
||||||
|
params(
|
||||||
|
MixIdParam
|
||||||
|
),
|
||||||
|
path = "/v2/mixnodes/{mix_id}",
|
||||||
|
responses(
|
||||||
|
(status = 200, body = Mixnode)
|
||||||
|
)
|
||||||
|
)]
|
||||||
|
#[instrument(level = tracing::Level::DEBUG, skip_all, fields(mix_id = mix_id))]
|
||||||
|
async fn get_mixnodes(
|
||||||
|
Path(MixIdParam { mix_id }): Path<MixIdParam>,
|
||||||
|
State(state): State<AppState>,
|
||||||
|
) -> HttpResult<Json<Mixnode>> {
|
||||||
|
match mix_id.parse::<u32>() {
|
||||||
|
Ok(parsed_mix_id) => {
|
||||||
|
let res = state.cache().get_mixnodes_list(state.db_pool()).await;
|
||||||
|
|
||||||
|
match res.iter().find(|item| item.mix_id == parsed_mix_id) {
|
||||||
|
Some(res) => Ok(Json(res.clone())),
|
||||||
|
None => Err(HttpError::invalid_input(mix_id)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(_e) => Err(HttpError::invalid_input(mix_id)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[utoipa::path(
|
||||||
|
tag = "Mixnodes",
|
||||||
|
get,
|
||||||
|
path = "/v2/mixnodes/stats",
|
||||||
|
responses(
|
||||||
|
(status = 200, body = Vec<DailyStats>)
|
||||||
|
)
|
||||||
|
)]
|
||||||
|
async fn get_stats(State(state): State<AppState>) -> HttpResult<Json<Vec<DailyStats>>> {
|
||||||
|
let stats = state.cache().get_mixnode_stats(state.db_pool()).await;
|
||||||
|
Ok(Json(stats))
|
||||||
|
}
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user