Compare commits
60 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| f16704a0c5 | |||
| 66e4021530 | |||
| 1f089ca6ad | |||
| ae30b0d803 | |||
| 2cdf2ec0c9 | |||
| 2eb1862eaa | |||
| 4383aa84b8 | |||
| 27ac34522c | |||
| 5e0d1bb14e | |||
| c16746a47b | |||
| a21052b72e | |||
| 92e9da7be5 | |||
| 143b336978 | |||
| d4293c9bae | |||
| e2d1806e49 | |||
| 469f85fc49 | |||
| 1202a2f5f4 | |||
| 6030bf6c95 | |||
| 09a771f58f | |||
| 676a909aee | |||
| e37145422c | |||
| 4ad52accc0 | |||
| 784fae2204 | |||
| 8aa5711bee | |||
| 07022314fc | |||
| 76c3081470 | |||
| d399161d31 | |||
| 27fb4ae0cc | |||
| 74392a2886 | |||
| 457c478a03 | |||
| 5e95992427 | |||
| d7eecd481c | |||
| e08fc4894b | |||
| fabd48b7ea | |||
| 894e0bd1bf | |||
| f86e088663 | |||
| f76300669a | |||
| 333ace1f97 | |||
| 487bf6732e | |||
| 5d4a0fef55 | |||
| 1627146c0e | |||
| ae40a00b8f | |||
| 7f3c0470e0 | |||
| 1bc26ed79f | |||
| 60fa5cfeb8 | |||
| 3b7088aeea | |||
| 803850be74 | |||
| 3dc62a9a60 | |||
| 5753b79997 | |||
| 2a6aa13ecd | |||
| d5c9e1d8cb | |||
| 87751894d9 | |||
| c8c3928575 | |||
| 2fa8da8117 | |||
| 4548ef4d05 | |||
| 7f147ee2b0 | |||
| 48bcd7e802 | |||
| aad028be3f | |||
| 6db3b34bcb | |||
| f9383578da |
@@ -5,7 +5,7 @@ on:
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-20.04-16-core
|
||||
runs-on: arc-ubuntu-20.04
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Install Dependencies (Linux)
|
||||
@@ -99,24 +99,3 @@ jobs:
|
||||
run: vercel deploy --prebuilt --prod --token=${{ secrets.VERCEL_TOKEN }}
|
||||
working-directory: dist/docs
|
||||
continue-on-error: false
|
||||
|
||||
- name: Matrix - Node Install
|
||||
run: npm install
|
||||
working-directory: .github/workflows/support-files
|
||||
- name: Matrix - Send Notification
|
||||
env:
|
||||
NYM_NOTIFICATION_KIND: cd-docs
|
||||
NYM_PROJECT_NAME: "Docs CD"
|
||||
NYM_CI_WWW_BASE: "${{ secrets.NYM_CD_WWW_BASE }}"
|
||||
NYM_CI_WWW_LOCATION: "${{ env.GITHUB_REF_SLUG }}"
|
||||
GIT_COMMIT_MESSAGE: "${{ github.event.head_commit.message }}"
|
||||
GIT_BRANCH: "${GITHUB_REF##*/}"
|
||||
MATRIX_SERVER: "${{ secrets.MATRIX_SERVER }}"
|
||||
MATRIX_ROOM: "${{ secrets.MATRIX_ROOM_DOCS }}"
|
||||
MATRIX_USER_ID: "${{ secrets.MATRIX_USER_ID }}"
|
||||
MATRIX_TOKEN: "${{ secrets.MATRIX_TOKEN }}"
|
||||
MATRIX_DEVICE_ID: "${{ secrets.MATRIX_DEVICE_ID }}"
|
||||
IS_SUCCESS: "${{ job.status == 'success' }}"
|
||||
uses: docker://keybaseio/client:stable-node
|
||||
with:
|
||||
args: .github/workflows/support-files/notifications/entry_point.sh
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
name: ci-build-ts
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
pull_request:
|
||||
paths:
|
||||
- "ts-packages/**"
|
||||
@@ -9,7 +10,7 @@ on:
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-20.04-16-core
|
||||
runs-on: arc-ubuntu-20.04
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Install rsync
|
||||
@@ -45,23 +46,3 @@ jobs:
|
||||
REMOTE_USER: ${{ secrets.CI_WWW_REMOTE_USER }}
|
||||
TARGET: ${{ secrets.CI_WWW_REMOTE_TARGET }}/ts-${{ env.GITHUB_REF_SLUG }}-example
|
||||
EXCLUDE: "/dist/, /node_modules/"
|
||||
- name: Matrix - Node Install
|
||||
run: npm install
|
||||
working-directory: .github/workflows/support-files
|
||||
- name: Matrix - Send Notification
|
||||
env:
|
||||
NYM_NOTIFICATION_KIND: ts-packages
|
||||
NYM_PROJECT_NAME: "ts-packages"
|
||||
NYM_CI_WWW_BASE: "${{ secrets.NYM_CI_WWW_BASE }}"
|
||||
NYM_CI_WWW_LOCATION: "ts-${{ env.GITHUB_REF_SLUG }}"
|
||||
GIT_COMMIT_MESSAGE: "${{ github.event.head_commit.message }}"
|
||||
GIT_BRANCH: "${GITHUB_REF##*/}"
|
||||
IS_SUCCESS: "${{ job.status == 'success' }}"
|
||||
MATRIX_SERVER: "${{ secrets.MATRIX_SERVER }}"
|
||||
MATRIX_ROOM: "${{ secrets.MATRIX_ROOM }}"
|
||||
MATRIX_USER_ID: "${{ secrets.MATRIX_USER_ID }}"
|
||||
MATRIX_TOKEN: "${{ secrets.MATRIX_TOKEN }}"
|
||||
MATRIX_DEVICE_ID: "${{ secrets.MATRIX_DEVICE_ID }}"
|
||||
uses: docker://keybaseio/client:stable-node
|
||||
with:
|
||||
args: .github/workflows/support-files/notifications/entry_point.sh
|
||||
|
||||
@@ -26,6 +26,7 @@ on:
|
||||
- "nym-api/**"
|
||||
- "nym-node/**"
|
||||
- "nym-outfox/**"
|
||||
- 'nym-data-observatory/**'
|
||||
- "nym-validator-rewarder/**"
|
||||
- "sdk/rust/nym-sdk/**"
|
||||
- "service-providers/**"
|
||||
@@ -96,6 +97,7 @@ jobs:
|
||||
target/release/nym-socks5-client
|
||||
target/release/nym-api
|
||||
target/release/nym-network-requester
|
||||
target/release/nym-data-observatory
|
||||
target/release/nym-cli
|
||||
target/release/nymvisor
|
||||
target/release/nym-node
|
||||
@@ -113,6 +115,7 @@ jobs:
|
||||
cp target/release/nym-socks5-client $OUTPUT_DIR
|
||||
cp target/release/nym-api $OUTPUT_DIR
|
||||
cp target/release/nym-network-requester $OUTPUT_DIR
|
||||
cp target/release/nym-data-observatory $OUTPUT_DIR
|
||||
cp target/release/nymvisor $OUTPUT_DIR
|
||||
cp target/release/nym-node $OUTPUT_DIR
|
||||
cp target/release/nym-cli $OUTPUT_DIR
|
||||
|
||||
@@ -16,6 +16,7 @@ on:
|
||||
- 'nym-api/**'
|
||||
- 'nym-node/**'
|
||||
- 'nym-outfox/**'
|
||||
- 'nym-data-observatory/**'
|
||||
- 'nym-validator-rewarder/**'
|
||||
- 'tools/**'
|
||||
- 'wasm/**'
|
||||
|
||||
@@ -22,4 +22,4 @@ jobs:
|
||||
with:
|
||||
log-level: warn
|
||||
command: check ${{ matrix.checks }}
|
||||
argument: --all-features
|
||||
arguments: --all-features
|
||||
|
||||
@@ -16,7 +16,7 @@ jobs:
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
platform: arc-ubuntu-20.04
|
||||
platform: [ arc-ubuntu-20.04 ]
|
||||
|
||||
runs-on: ${{ matrix.platform }}
|
||||
env:
|
||||
|
||||
@@ -10,7 +10,7 @@ on:
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-20.04-16-core
|
||||
runs-on: arc-ubuntu-20.04
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Install Dependencies (Linux)
|
||||
@@ -70,24 +70,3 @@ jobs:
|
||||
REMOTE_USER: ${{ secrets.CI_WWW_REMOTE_USER }}
|
||||
TARGET: ${{ secrets.CI_WWW_REMOTE_TARGET }}/docs-${{ env.GITHUB_REF_SLUG }}
|
||||
EXCLUDE: "/node_modules/"
|
||||
|
||||
- name: Matrix - Node Install
|
||||
run: npm install
|
||||
working-directory: .github/workflows/support-files
|
||||
- name: Matrix - Send Notification
|
||||
env:
|
||||
NYM_NOTIFICATION_KIND: ci-docs
|
||||
NYM_PROJECT_NAME: "Docs CI"
|
||||
NYM_CI_WWW_BASE: "${{ secrets.NYM_CI_WWW_BASE }}"
|
||||
NYM_CI_WWW_LOCATION: "docs-${{ env.GITHUB_REF_SLUG }}"
|
||||
GIT_COMMIT_MESSAGE: "${{ github.event.head_commit.message }}"
|
||||
GIT_BRANCH: "${GITHUB_REF##*/}"
|
||||
MATRIX_SERVER: "${{ secrets.MATRIX_SERVER }}"
|
||||
MATRIX_ROOM: "${{ secrets.MATRIX_ROOM_DOCS }}"
|
||||
MATRIX_USER_ID: "${{ secrets.MATRIX_USER_ID }}"
|
||||
MATRIX_TOKEN: "${{ secrets.MATRIX_TOKEN }}"
|
||||
MATRIX_DEVICE_ID: "${{ secrets.MATRIX_DEVICE_ID }}"
|
||||
IS_SUCCESS: "${{ job.status == 'success' }}"
|
||||
uses: docker://keybaseio/client:stable-node
|
||||
with:
|
||||
args: .github/workflows/support-files/notifications/entry_point.sh
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
name: ci-lint-typescript
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
pull_request:
|
||||
paths:
|
||||
- "ts-packages/**"
|
||||
@@ -14,7 +15,7 @@ on:
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-20.04-16-core
|
||||
runs-on: arc-ubuntu-20.04
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: rlespinasse/github-slug-action@v3.x
|
||||
@@ -53,24 +54,3 @@ jobs:
|
||||
run: yarn lint
|
||||
- name: Typecheck with tsc
|
||||
run: yarn tsc
|
||||
|
||||
- name: Matrix - Node Install
|
||||
run: npm install
|
||||
working-directory: .github/workflows/support-files
|
||||
- name: Matrix - Send Notification
|
||||
env:
|
||||
NYM_NOTIFICATION_KIND: ts-packages
|
||||
NYM_PROJECT_NAME: "ts-packages"
|
||||
NYM_CI_WWW_BASE: "${{ secrets.NYM_CI_WWW_BASE }}"
|
||||
NYM_CI_WWW_LOCATION: "ts-${{ env.GITHUB_REF_SLUG }}"
|
||||
GIT_COMMIT_MESSAGE: "${{ github.event.head_commit.message }}"
|
||||
GIT_BRANCH: "${GITHUB_REF##*/}"
|
||||
IS_SUCCESS: "${{ job.status == 'success' }}"
|
||||
MATRIX_SERVER: "${{ secrets.MATRIX_SERVER }}"
|
||||
MATRIX_ROOM: "${{ secrets.MATRIX_ROOM }}"
|
||||
MATRIX_USER_ID: "${{ secrets.MATRIX_USER_ID }}"
|
||||
MATRIX_TOKEN: "${{ secrets.MATRIX_TOKEN }}"
|
||||
MATRIX_DEVICE_ID: "${{ secrets.MATRIX_DEVICE_ID }}"
|
||||
uses: docker://keybaseio/client:stable-node
|
||||
with:
|
||||
args: .github/workflows/support-files/notifications/entry_point.sh
|
||||
|
||||
@@ -4,7 +4,7 @@ on:
|
||||
|
||||
jobs:
|
||||
publish:
|
||||
runs-on: ubuntu-20.04-16-core
|
||||
runs-on: arc-ubuntu-20.04
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
|
||||
@@ -4,6 +4,82 @@ Post 1.0.0 release, the changelog format is based on [Keep a Changelog](https://
|
||||
|
||||
## [Unreleased]
|
||||
|
||||
## [2024.11-wedel] (2024-09-23)
|
||||
|
||||
- Backport #4894 to fix ci ([#4899])
|
||||
- Bugfix/ticketbook false double spending ([#4892])
|
||||
- fix: allow updating globally stored signatures ([#4891])
|
||||
- [DOCs/operators]: Document changelog for patch/2024.10-caramello ([#4886])
|
||||
- [DOCs/operators]: Post release docs updates ([#4874])
|
||||
- Bump defguard to github latest version ([#4872])
|
||||
- chore: removed completed queued mixnet migration ([#4865])
|
||||
- Disable push trigger and add missing paths in ci-build ([#4864])
|
||||
- Fix linux conditional in ci-build.yml ([#4863])
|
||||
- Remove golang workaround in ci-sdk-wasm ([#4858])
|
||||
- Revert runner for ci-docs ([#4855])
|
||||
- Move credential verification into common crate ([#4853])
|
||||
- Fix test failure in ipr request size ([#4844])
|
||||
- Start switching over jobs to arc-ubuntu-20.04 ([#4843])
|
||||
- Use ecash credential type for bandwidth value ([#4840])
|
||||
- Create nym-repo-setup debian package and nym-vpn meta package ([#4837])
|
||||
- Remove serde_crate named import ([#4832])
|
||||
- Run cargo autoinherit following last weeks dependabot updates ([#4831])
|
||||
- revamped ticketbook serialisation and exposed additional cli methods ([#4827])
|
||||
- Expose wireguard details on self described endpoint ([#4825])
|
||||
- Remove unused wireguard flag from SDK ([#4823])
|
||||
- Add `axum` server to `nym-api` ([#4803])
|
||||
- Run cargo-autoinherit for a few new crates ([#4801])
|
||||
- Update dependabot ([#4796])
|
||||
- Fix clippy for unwrap_or_default ([#4783])
|
||||
- Enable dependabot version upgrades for root rust workspace ([#4778])
|
||||
- Persist used wireguard private IPs ([#4771])
|
||||
- Avoid race on ip and registration structures ([#4766])
|
||||
- docs/hotfix ([#4765])
|
||||
- chore: remove repetitive words ([#4763])
|
||||
- Make gateway latency check generic ([#4759])
|
||||
- Remove duplicate stat count for retransmissions ([#4756])
|
||||
- Update peer refresh value ([#4754])
|
||||
- Remove deprecated mark_as_success and use new disarm ([#4751])
|
||||
- Add get_mixnodes_described to validator_client ([#4725])
|
||||
- New Network Monitor ([#4610])
|
||||
|
||||
[#4899]: https://github.com/nymtech/nym/pull/4899
|
||||
[#4892]: https://github.com/nymtech/nym/pull/4892
|
||||
[#4891]: https://github.com/nymtech/nym/pull/4891
|
||||
[#4886]: https://github.com/nymtech/nym/pull/4886
|
||||
[#4874]: https://github.com/nymtech/nym/pull/4874
|
||||
[#4872]: https://github.com/nymtech/nym/pull/4872
|
||||
[#4865]: https://github.com/nymtech/nym/pull/4865
|
||||
[#4864]: https://github.com/nymtech/nym/pull/4864
|
||||
[#4863]: https://github.com/nymtech/nym/pull/4863
|
||||
[#4858]: https://github.com/nymtech/nym/pull/4858
|
||||
[#4855]: https://github.com/nymtech/nym/pull/4855
|
||||
[#4853]: https://github.com/nymtech/nym/pull/4853
|
||||
[#4844]: https://github.com/nymtech/nym/pull/4844
|
||||
[#4843]: https://github.com/nymtech/nym/pull/4843
|
||||
[#4840]: https://github.com/nymtech/nym/pull/4840
|
||||
[#4837]: https://github.com/nymtech/nym/pull/4837
|
||||
[#4832]: https://github.com/nymtech/nym/pull/4832
|
||||
[#4831]: https://github.com/nymtech/nym/pull/4831
|
||||
[#4827]: https://github.com/nymtech/nym/pull/4827
|
||||
[#4825]: https://github.com/nymtech/nym/pull/4825
|
||||
[#4823]: https://github.com/nymtech/nym/pull/4823
|
||||
[#4803]: https://github.com/nymtech/nym/pull/4803
|
||||
[#4801]: https://github.com/nymtech/nym/pull/4801
|
||||
[#4796]: https://github.com/nymtech/nym/pull/4796
|
||||
[#4783]: https://github.com/nymtech/nym/pull/4783
|
||||
[#4778]: https://github.com/nymtech/nym/pull/4778
|
||||
[#4771]: https://github.com/nymtech/nym/pull/4771
|
||||
[#4766]: https://github.com/nymtech/nym/pull/4766
|
||||
[#4765]: https://github.com/nymtech/nym/pull/4765
|
||||
[#4763]: https://github.com/nymtech/nym/pull/4763
|
||||
[#4759]: https://github.com/nymtech/nym/pull/4759
|
||||
[#4756]: https://github.com/nymtech/nym/pull/4756
|
||||
[#4754]: https://github.com/nymtech/nym/pull/4754
|
||||
[#4751]: https://github.com/nymtech/nym/pull/4751
|
||||
[#4725]: https://github.com/nymtech/nym/pull/4725
|
||||
[#4610]: https://github.com/nymtech/nym/pull/4610
|
||||
|
||||
## [2024.10-caramello] (2024-09-10)
|
||||
|
||||
- Backport 4844 and 4845 ([#4857])
|
||||
|
||||
Generated
+604
-110
File diff suppressed because it is too large
Load Diff
+20
-9
@@ -103,6 +103,9 @@ members = [
|
||||
"mixnode",
|
||||
"sdk/lib/socks5-listener",
|
||||
"sdk/rust/nym-sdk",
|
||||
"sdk/ffi/shared",
|
||||
"sdk/ffi/go",
|
||||
"sdk/ffi/cpp",
|
||||
"service-providers/authenticator",
|
||||
"service-providers/common",
|
||||
"service-providers/ip-packet-router",
|
||||
@@ -111,11 +114,13 @@ members = [
|
||||
"nym-api",
|
||||
"nym-browser-extension/storage",
|
||||
"nym-api/nym-api-requests",
|
||||
"nym-data-observatory",
|
||||
"nym-node",
|
||||
"nym-node/nym-node-http-api",
|
||||
"nym-node/nym-node-requests",
|
||||
"nym-outfox",
|
||||
"nym-validator-rewarder",
|
||||
"tools/echo-server",
|
||||
"tools/internal/ssl-inject",
|
||||
# "tools/internal/sdk-version-bump",
|
||||
"tools/internal/testnet-manager",
|
||||
@@ -130,6 +135,11 @@ members = [
|
||||
"wasm/mix-fetch",
|
||||
"wasm/node-tester",
|
||||
"wasm/zknym-lib",
|
||||
"tools/internal/testnet-manager",
|
||||
"tools/internal/testnet-manager/dkg-bypass-contract",
|
||||
"tools/echo-server",
|
||||
"tools/internal/contract-state-importer/importer-cli",
|
||||
"tools/internal/contract-state-importer/importer-contract",
|
||||
]
|
||||
|
||||
default-members = [
|
||||
@@ -139,6 +149,7 @@ default-members = [
|
||||
"gateway",
|
||||
"mixnode",
|
||||
"nym-api",
|
||||
"nym-data-observatory",
|
||||
"nym-node",
|
||||
"nym-validator-rewarder",
|
||||
"service-providers/authenticator",
|
||||
@@ -153,7 +164,6 @@ exclude = [
|
||||
"nym-wallet",
|
||||
"nym-vpn/ui/src-tauri",
|
||||
"cpu-cycles",
|
||||
"sdk/ffi/cpp",
|
||||
]
|
||||
|
||||
[workspace.package]
|
||||
@@ -174,9 +184,9 @@ aes-gcm-siv = "0.11.1"
|
||||
aead = "0.5.2"
|
||||
anyhow = "1.0.89"
|
||||
argon2 = "0.5.0"
|
||||
async-trait = "0.1.82"
|
||||
async-trait = "0.1.83"
|
||||
axum = "0.7.5"
|
||||
axum-extra = "0.9.3"
|
||||
axum-extra = "0.9.4"
|
||||
base64 = "0.22.1"
|
||||
bincode = "1.3.3"
|
||||
bip39 = { version = "2.0.0", features = ["zeroize"] }
|
||||
@@ -189,7 +199,7 @@ blake3 = "1.5.4"
|
||||
bloomfilter = "1.0.14"
|
||||
bs58 = "0.5.1"
|
||||
bytecodec = "0.4.15"
|
||||
bytes = "1.7.1"
|
||||
bytes = "1.7.2"
|
||||
cargo_metadata = "0.18.1"
|
||||
celes = "2.4.0"
|
||||
cfg-if = "1.0.0"
|
||||
@@ -197,7 +207,7 @@ chacha20 = "0.9.0"
|
||||
chacha20poly1305 = "0.10.1"
|
||||
chrono = "0.4.31"
|
||||
cipher = "0.4.3"
|
||||
clap = "4.5.17"
|
||||
clap = "4.5.18"
|
||||
clap_complete = "4.5"
|
||||
clap_complete_fig = "4.5"
|
||||
colored = "2.0"
|
||||
@@ -213,7 +223,8 @@ ctr = "0.9.1"
|
||||
cupid = "0.6.1"
|
||||
curve25519-dalek = "4.1"
|
||||
dashmap = "5.5.3"
|
||||
defguard_wireguard_rs = "0.4.2"
|
||||
# We want https://github.com/DefGuard/wireguard-rs/pull/64 , but there's no crates.io release being pushed out anymore
|
||||
defguard_wireguard_rs = { git = "https://github.com/DefGuard/wireguard-rs.git", rev = "v0.4.7" }
|
||||
digest = "0.10.7"
|
||||
dirs = "5.0"
|
||||
doc-comment = "0.3"
|
||||
@@ -223,7 +234,7 @@ ed25519-dalek = "2.1"
|
||||
etherparse = "0.13.0"
|
||||
eyre = "0.6.9"
|
||||
fastrand = "2.1.1"
|
||||
flate2 = "1.0.33"
|
||||
flate2 = "1.0.34"
|
||||
futures = "0.3.28"
|
||||
generic-array = "0.14.7"
|
||||
getrandom = "0.2.10"
|
||||
@@ -298,9 +309,9 @@ subtle-encoding = "0.5"
|
||||
syn = "1"
|
||||
sysinfo = "0.30.13"
|
||||
tap = "1.0.1"
|
||||
tar = "0.4.41"
|
||||
tar = "0.4.42"
|
||||
tempfile = "3.5.0"
|
||||
thiserror = "1.0.63"
|
||||
thiserror = "1.0.64"
|
||||
time = "0.3.30"
|
||||
tokio = "1.39"
|
||||
tokio-stream = "0.1.16"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "nym-client"
|
||||
version = "1.1.40"
|
||||
version = "1.1.41"
|
||||
authors = ["Dave Hrycyszyn <futurechimp@users.noreply.github.com>", "Jędrzej Stuczyński <andrew@nymtech.net>"]
|
||||
description = "Implementation of the Nym Client"
|
||||
edition = "2021"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "nym-socks5-client"
|
||||
version = "1.1.40"
|
||||
version = "1.1.41"
|
||||
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"
|
||||
edition = "2021"
|
||||
|
||||
@@ -6,6 +6,7 @@ pub mod v2;
|
||||
|
||||
mod error;
|
||||
|
||||
pub use error::Error;
|
||||
pub use v2 as latest;
|
||||
|
||||
pub const CURRENT_VERSION: u8 = 2;
|
||||
|
||||
@@ -10,4 +10,4 @@ pub use registration::{ClientMac, GatewayClient, InitMessage, Nonce};
|
||||
#[cfg(feature = "verify")]
|
||||
pub use registration::HmacSha256;
|
||||
|
||||
const VERSION: u8 = 1;
|
||||
pub const VERSION: u8 = 1;
|
||||
|
||||
@@ -26,7 +26,7 @@ pub type HmacSha256 = Hmac<Sha256>;
|
||||
pub type Nonce = u64;
|
||||
pub type Taken = Option<SystemTime>;
|
||||
|
||||
pub const BANDWIDTH_CAP_PER_DAY: u64 = 1024 * 1024 * 1024; // 1 GB
|
||||
pub const BANDWIDTH_CAP_PER_DAY: i64 = 1024 * 1024 * 1024; // 1 GB
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone)]
|
||||
pub struct InitMessage {
|
||||
|
||||
@@ -62,8 +62,113 @@ impl From<v1::registration::GatewayClient> for v2::registration::GatewayClient {
|
||||
}
|
||||
}
|
||||
|
||||
impl From<v2::registration::GatewayClient> for v1::registration::GatewayClient {
|
||||
fn from(gw_client: v2::registration::GatewayClient) -> Self {
|
||||
Self {
|
||||
pub_key: gw_client.pub_key,
|
||||
private_ip: gw_client.private_ip,
|
||||
mac: gw_client.mac.into(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<v1::registration::ClientMac> for v2::registration::ClientMac {
|
||||
fn from(mac: v1::registration::ClientMac) -> Self {
|
||||
Self::new(mac.to_vec())
|
||||
}
|
||||
}
|
||||
|
||||
impl From<v2::registration::ClientMac> for v1::registration::ClientMac {
|
||||
fn from(mac: v2::registration::ClientMac) -> Self {
|
||||
Self::new(mac.to_vec())
|
||||
}
|
||||
}
|
||||
|
||||
impl From<v2::response::AuthenticatorResponse> for v1::response::AuthenticatorResponse {
|
||||
fn from(authenticator_response: v2::response::AuthenticatorResponse) -> Self {
|
||||
Self {
|
||||
version: authenticator_response.protocol.version,
|
||||
data: authenticator_response.data.into(),
|
||||
reply_to: authenticator_response.reply_to,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<v2::response::AuthenticatorResponseData> for v1::response::AuthenticatorResponseData {
|
||||
fn from(authenticator_response_data: v2::response::AuthenticatorResponseData) -> Self {
|
||||
match authenticator_response_data {
|
||||
v2::response::AuthenticatorResponseData::PendingRegistration(
|
||||
pending_registration_response,
|
||||
) => v1::response::AuthenticatorResponseData::PendingRegistration(
|
||||
pending_registration_response.into(),
|
||||
),
|
||||
v2::response::AuthenticatorResponseData::Registered(registered_response) => {
|
||||
v1::response::AuthenticatorResponseData::Registered(registered_response.into())
|
||||
}
|
||||
v2::response::AuthenticatorResponseData::RemainingBandwidth(
|
||||
remaining_bandwidth_response,
|
||||
) => v1::response::AuthenticatorResponseData::RemainingBandwidth(
|
||||
remaining_bandwidth_response.into(),
|
||||
),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<v2::response::PendingRegistrationResponse> for v1::response::PendingRegistrationResponse {
|
||||
fn from(value: v2::response::PendingRegistrationResponse) -> Self {
|
||||
Self {
|
||||
request_id: value.request_id,
|
||||
reply_to: value.reply_to,
|
||||
reply: value.reply.into(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<v2::response::RegisteredResponse> for v1::response::RegisteredResponse {
|
||||
fn from(value: v2::response::RegisteredResponse) -> Self {
|
||||
Self {
|
||||
request_id: value.request_id,
|
||||
reply_to: value.reply_to,
|
||||
reply: value.reply.into(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<v2::response::RemainingBandwidthResponse> for v1::response::RemainingBandwidthResponse {
|
||||
fn from(value: v2::response::RemainingBandwidthResponse) -> Self {
|
||||
Self {
|
||||
request_id: value.request_id,
|
||||
reply_to: value.reply_to,
|
||||
reply: value.reply.map(Into::into),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<v2::registration::RegistrationData> for v1::registration::RegistrationData {
|
||||
fn from(value: v2::registration::RegistrationData) -> Self {
|
||||
Self {
|
||||
nonce: value.nonce,
|
||||
gateway_data: value.gateway_data.into(),
|
||||
wg_port: value.wg_port,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<v2::registration::RegistredData> for v1::registration::RegistredData {
|
||||
fn from(value: v2::registration::RegistredData) -> Self {
|
||||
Self {
|
||||
pub_key: value.pub_key,
|
||||
private_ip: value.private_ip,
|
||||
wg_port: value.wg_port,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<v2::registration::RemainingBandwidthData> for v1::registration::RemainingBandwidthData {
|
||||
fn from(value: v2::registration::RemainingBandwidthData) -> Self {
|
||||
Self {
|
||||
available_bandwidth: value.available_bandwidth as u64,
|
||||
suspended: false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,4 +6,4 @@ pub mod registration;
|
||||
pub mod request;
|
||||
pub mod response;
|
||||
|
||||
const VERSION: u8 = 2;
|
||||
pub const VERSION: u8 = 2;
|
||||
|
||||
@@ -66,7 +66,7 @@ pub struct RegistredData {
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone)]
|
||||
pub struct RemainingBandwidthData {
|
||||
pub available_bandwidth: u64,
|
||||
pub available_bandwidth: i64,
|
||||
}
|
||||
|
||||
/// Client that wants to register sends its PublicKey bytes mac digest encrypted with a DH shared secret.
|
||||
|
||||
@@ -2,21 +2,37 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
use si_scale::helpers::bibytes2;
|
||||
use std::sync::atomic::{AtomicI64, Ordering};
|
||||
use std::sync::atomic::{AtomicBool, AtomicI64, Ordering};
|
||||
use std::sync::Arc;
|
||||
use std::time::Duration;
|
||||
use time::OffsetDateTime;
|
||||
|
||||
#[derive(Clone, Default)]
|
||||
pub(crate) struct BandwidthClaimGuard {
|
||||
inner: Arc<ClientBandwidthInner>,
|
||||
}
|
||||
|
||||
impl Drop for BandwidthClaimGuard {
|
||||
fn drop(&mut self) {
|
||||
let old = self.inner.claiming_more.swap(false, Ordering::SeqCst);
|
||||
assert!(
|
||||
old,
|
||||
"critical failure: there were multiple BandwidthClaimGuard existing"
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct ClientBandwidth {
|
||||
inner: Arc<ClientBandwidthInner>,
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
struct ClientBandwidthInner {
|
||||
/// the actual bandwidth amount (in bytes) available
|
||||
available: AtomicI64,
|
||||
|
||||
/// flag to indicate whether this client is currently in the process of claiming additional bandwidth
|
||||
claiming_more: AtomicBool,
|
||||
|
||||
/// defines the timestamp when the bandwidth information has been logged to the logs stream
|
||||
last_logged_ts: AtomicI64,
|
||||
|
||||
@@ -29,11 +45,28 @@ impl ClientBandwidth {
|
||||
ClientBandwidth {
|
||||
inner: Arc::new(ClientBandwidthInner {
|
||||
available: AtomicI64::new(0),
|
||||
claiming_more: AtomicBool::new(false),
|
||||
last_logged_ts: AtomicI64::new(0),
|
||||
last_updated_ts: AtomicI64::new(0),
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn begin_bandwidth_claim(&self) -> Option<BandwidthClaimGuard> {
|
||||
if self
|
||||
.inner
|
||||
.claiming_more
|
||||
.compare_exchange(false, true, Ordering::SeqCst, Ordering::SeqCst)
|
||||
.is_ok()
|
||||
{
|
||||
Some(BandwidthClaimGuard {
|
||||
inner: self.inner.clone(),
|
||||
})
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn remaining(&self) -> i64 {
|
||||
self.inner.available.load(Ordering::Acquire)
|
||||
}
|
||||
|
||||
@@ -724,6 +724,11 @@ impl<C, St> GatewayClient<C, St> {
|
||||
return Err(GatewayClientError::NoBandwidthControllerAvailable);
|
||||
}
|
||||
|
||||
let Some(_claim_guard) = self.bandwidth.begin_bandwidth_claim() else {
|
||||
debug!("there's already an existing bandwidth claim ongoing");
|
||||
return Ok(());
|
||||
};
|
||||
|
||||
warn!("Not enough bandwidth. Trying to get more bandwidth, this might take a while");
|
||||
if !self.cfg.bandwidth.require_tickets {
|
||||
info!("The client is running in disabled credentials mode - attempting to claim bandwidth without a credential");
|
||||
|
||||
@@ -20,6 +20,7 @@ nym-coconut-bandwidth-contract-common = { path = "../../cosmwasm-smart-contracts
|
||||
nym-ecash-contract-common = { path = "../../cosmwasm-smart-contracts/ecash-contract" }
|
||||
nym-multisig-contract-common = { path = "../../cosmwasm-smart-contracts/multisig-contract" }
|
||||
nym-group-contract-common = { path = "../../cosmwasm-smart-contracts/group-contract" }
|
||||
nym-serde-helpers = { path = "../../serde-helpers", features = ["hex", "base64"] }
|
||||
serde = { workspace = true, features = ["derive"] }
|
||||
serde_json = { workspace = true }
|
||||
nym-http-api-client = { path = "../../../common/http-api-client" }
|
||||
|
||||
+44
-10
@@ -5,7 +5,7 @@ use crate::nyxd;
|
||||
use crate::nyxd::coin::Coin;
|
||||
use crate::nyxd::cosmwasm_client::helpers::{create_pagination, next_page_key};
|
||||
use crate::nyxd::cosmwasm_client::types::{
|
||||
Account, CodeDetails, Contract, ContractCodeId, SequenceResponse, SimulateResponse,
|
||||
Account, CodeDetails, Contract, ContractCodeId, Model, SequenceResponse, SimulateResponse,
|
||||
};
|
||||
use crate::nyxd::error::NyxdError;
|
||||
use crate::nyxd::Query;
|
||||
@@ -21,11 +21,11 @@ use cosmrs::proto::cosmos::tx::v1beta1::{
|
||||
SimulateRequest, SimulateResponse as ProtoSimulateResponse,
|
||||
};
|
||||
use cosmrs::proto::cosmwasm::wasm::v1::{
|
||||
QueryCodeRequest, QueryCodeResponse, QueryCodesRequest, QueryCodesResponse,
|
||||
QueryContractHistoryRequest, QueryContractHistoryResponse, QueryContractInfoRequest,
|
||||
QueryContractInfoResponse, QueryContractsByCodeRequest, QueryContractsByCodeResponse,
|
||||
QueryRawContractStateRequest, QueryRawContractStateResponse, QuerySmartContractStateRequest,
|
||||
QuerySmartContractStateResponse,
|
||||
QueryAllContractStateRequest, QueryAllContractStateResponse, QueryCodeRequest,
|
||||
QueryCodeResponse, QueryCodesRequest, QueryCodesResponse, QueryContractHistoryRequest,
|
||||
QueryContractHistoryResponse, QueryContractInfoRequest, QueryContractInfoResponse,
|
||||
QueryContractsByCodeRequest, QueryContractsByCodeResponse, QueryRawContractStateRequest,
|
||||
QueryRawContractStateResponse, QuerySmartContractStateRequest, QuerySmartContractStateResponse,
|
||||
};
|
||||
use cosmrs::tendermint::{block, chain, Hash};
|
||||
use cosmrs::{AccountId, Coin as CosmosCoin, Tx};
|
||||
@@ -218,17 +218,19 @@ pub trait CosmWasmClient: TendermintRpcClient {
|
||||
|
||||
loop {
|
||||
let mut res = self
|
||||
.tx_search(query.clone(), false, page, 100, Order::Ascending)
|
||||
.tx_search(query.clone(), false, page, per_page, Order::Ascending)
|
||||
.await?;
|
||||
|
||||
results.append(&mut res.txs);
|
||||
// sanity check for if tendermint's maximum per_page was modified -
|
||||
// we don't want to accidentally be stuck in an infinite loop
|
||||
if res.total_count == 0 || res.txs.is_empty() {
|
||||
let early_break = res.total_count == 0 || res.txs.is_empty();
|
||||
results.append(&mut res.txs);
|
||||
|
||||
if early_break {
|
||||
break;
|
||||
}
|
||||
|
||||
if res.total_count >= per_page {
|
||||
if res.total_count > results.len() as u32 {
|
||||
page += 1
|
||||
} else {
|
||||
break;
|
||||
@@ -442,6 +444,38 @@ pub trait CosmWasmClient: TendermintRpcClient {
|
||||
.collect::<Result<_, _>>()?)
|
||||
}
|
||||
|
||||
async fn query_all_contract_state(&self, address: &AccountId) -> Result<Vec<Model>, NyxdError> {
|
||||
let path = Some("/cosmwasm.wasm.v1.Query/AllContractState".to_owned());
|
||||
|
||||
let mut models = Vec::new();
|
||||
let mut pagination = None;
|
||||
|
||||
loop {
|
||||
let req = QueryAllContractStateRequest {
|
||||
address: address.to_string(),
|
||||
pagination,
|
||||
};
|
||||
|
||||
let mut res = self
|
||||
.make_abci_query::<_, QueryAllContractStateResponse>(path.clone(), req)
|
||||
.await?;
|
||||
|
||||
let empty_response = res.models.is_empty();
|
||||
models.append(&mut res.models);
|
||||
|
||||
if empty_response {
|
||||
break;
|
||||
}
|
||||
if let Some(next_key) = next_page_key(res.pagination) {
|
||||
pagination = Some(create_pagination(next_key))
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(models.into_iter().map(Into::into).collect())
|
||||
}
|
||||
|
||||
async fn query_contract_raw(
|
||||
&self,
|
||||
address: &AccountId,
|
||||
|
||||
@@ -27,13 +27,34 @@ use cosmrs::vesting::{
|
||||
};
|
||||
use cosmrs::{AccountId, Any, Coin as CosmosCoin};
|
||||
use prost::Message;
|
||||
use serde::Serialize;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
pub use cosmrs::abci::GasInfo;
|
||||
pub use cosmrs::abci::MsgResponse;
|
||||
|
||||
pub type ContractCodeId = u64;
|
||||
|
||||
// yet another thing to put in cosmrs
|
||||
#[derive(Serialize, Deserialize)]
|
||||
pub struct Model {
|
||||
#[serde(with = "nym_serde_helpers::hex")]
|
||||
pub key: Vec<u8>,
|
||||
|
||||
#[serde(with = "nym_serde_helpers::base64")]
|
||||
pub value: Vec<u8>,
|
||||
}
|
||||
|
||||
// follow the cosmwasm serialisation format, i.e. hex for key and base64 for value
|
||||
|
||||
impl From<cosmrs::proto::cosmwasm::wasm::v1::Model> for Model {
|
||||
fn from(model: cosmrs::proto::cosmwasm::wasm::v1::Model) -> Self {
|
||||
Model {
|
||||
key: model.key,
|
||||
value: model.value,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
pub struct EmptyMsg {}
|
||||
|
||||
|
||||
@@ -7,13 +7,14 @@ pub mod execute_contract;
|
||||
pub mod generators;
|
||||
pub mod init_contract;
|
||||
pub mod migrate_contract;
|
||||
pub mod raw_contract_state;
|
||||
pub mod upload_contract;
|
||||
|
||||
#[derive(Debug, Args)]
|
||||
#[clap(args_conflicts_with_subcommands = true, subcommand_required = true)]
|
||||
pub struct Cosmwasm {
|
||||
#[clap(subcommand)]
|
||||
pub command: Option<CosmwasmCommands>,
|
||||
pub command: CosmwasmCommands,
|
||||
}
|
||||
|
||||
#[derive(Debug, Subcommand)]
|
||||
@@ -28,4 +29,6 @@ pub enum CosmwasmCommands {
|
||||
Migrate(crate::validator::cosmwasm::migrate_contract::Args),
|
||||
/// Execute a WASM smart contract method
|
||||
Execute(crate::validator::cosmwasm::execute_contract::Args),
|
||||
/// Obtain raw contract state of a cosmwasm smart contract
|
||||
RawContractState(crate::validator::cosmwasm::raw_contract_state::Args),
|
||||
}
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
// Copyright 2024 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
use crate::context::QueryClient;
|
||||
use clap::Parser;
|
||||
use cosmrs::AccountId;
|
||||
use log::trace;
|
||||
use nym_validator_client::nyxd::CosmWasmClient;
|
||||
use std::fs;
|
||||
use std::fs::File;
|
||||
use std::path::PathBuf;
|
||||
|
||||
#[derive(Debug, Parser)]
|
||||
pub struct Args {
|
||||
#[clap(long, value_parser)]
|
||||
#[clap(help = "The address of contract to get the state of")]
|
||||
pub contract: AccountId,
|
||||
|
||||
#[clap(short, long)]
|
||||
#[clap(help = "Output file for the retrieved contract state")]
|
||||
pub output: PathBuf,
|
||||
}
|
||||
|
||||
pub async fn execute(args: Args, client: QueryClient) -> anyhow::Result<()> {
|
||||
trace!("args: {args:?}");
|
||||
|
||||
let output = File::create(&args.output)?;
|
||||
let raw = client.query_all_contract_state(&args.contract).await?;
|
||||
|
||||
serde_json::to_writer(output, &raw)?;
|
||||
println!(
|
||||
"wrote {} key-value from {} pairs into '{}'",
|
||||
raw.len(),
|
||||
args.contract,
|
||||
fs::canonicalize(args.output)?.display()
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -3,12 +3,13 @@
|
||||
|
||||
use cosmwasm_std::{Decimal, StdError, StdResult, Uint128};
|
||||
|
||||
#[track_caller]
|
||||
pub fn compare_decimals(a: Decimal, b: Decimal, epsilon: Option<Decimal>) {
|
||||
let epsilon = epsilon.unwrap_or_else(|| Decimal::from_ratio(1u128, 100_000_000u128));
|
||||
if a > b {
|
||||
assert!(a - b < epsilon, "{a} != {b}")
|
||||
assert!(a - b < epsilon, "{a} != {b}, delta: {}", a - b)
|
||||
} else {
|
||||
assert!(b - a < epsilon, "{a} != {b}")
|
||||
assert!(b - a < epsilon, "{a} != {b}, delta: {}", b - a)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -532,7 +532,7 @@ pub enum QueryMsg {
|
||||
/// Gets the basic list of all unbonded mixnodes that belonged to a particular owner.
|
||||
#[cfg_attr(feature = "schema", returns(PagedUnbondedMixnodesResponse))]
|
||||
GetUnbondedMixNodesByOwner {
|
||||
/// The address of the owner of the the mixnodes used for the query.
|
||||
/// The address of the owner of the mixnodes used for the query.
|
||||
owner: String,
|
||||
|
||||
/// Controls the maximum number of entries returned by the query. Note that too large values will be overwritten by a saner default.
|
||||
@@ -783,7 +783,26 @@ pub enum QueryMsg {
|
||||
},
|
||||
}
|
||||
|
||||
#[cw_serde]
|
||||
pub struct AffectedDelegator {
|
||||
pub address: String,
|
||||
pub missing_ratio: Decimal,
|
||||
}
|
||||
|
||||
#[cw_serde]
|
||||
pub struct AffectedNode {
|
||||
pub mix_id: MixId,
|
||||
pub delegators: Vec<AffectedDelegator>,
|
||||
}
|
||||
|
||||
impl AffectedNode {
|
||||
pub fn total_ratio(&self) -> Decimal {
|
||||
self.delegators.iter().map(|d| d.missing_ratio).sum()
|
||||
}
|
||||
}
|
||||
|
||||
#[cw_serde]
|
||||
pub struct MigrateMsg {
|
||||
pub vesting_contract_address: Option<String>,
|
||||
pub fix_nodes: Option<Vec<AffectedNode>>,
|
||||
}
|
||||
|
||||
@@ -171,10 +171,20 @@ impl SqliteEcashTicketbookManager {
|
||||
data: &[u8],
|
||||
) -> Result<(), sqlx::Error> {
|
||||
sqlx::query!(
|
||||
"INSERT INTO master_verification_key(epoch_id, serialised_key, serialization_revision) VALUES (?, ?, ?)",
|
||||
r#"
|
||||
INSERT OR IGNORE INTO master_verification_key(epoch_id, serialised_key, serialization_revision) VALUES (?, ?, ?);
|
||||
UPDATE master_verification_key
|
||||
SET
|
||||
serialised_key = ?,
|
||||
serialization_revision = ?
|
||||
WHERE epoch_id = ?
|
||||
"#,
|
||||
epoch_id,
|
||||
data,
|
||||
serialisation_revision
|
||||
serialisation_revision,
|
||||
data,
|
||||
serialisation_revision,
|
||||
epoch_id
|
||||
)
|
||||
.execute(&self.connection_pool)
|
||||
.await?;
|
||||
@@ -204,10 +214,20 @@ impl SqliteEcashTicketbookManager {
|
||||
data: &[u8],
|
||||
) -> Result<(), sqlx::Error> {
|
||||
sqlx::query!(
|
||||
"INSERT INTO coin_indices_signatures(epoch_id, serialised_signatures, serialization_revision) VALUES (?, ?, ?)",
|
||||
r#"
|
||||
INSERT OR IGNORE INTO coin_indices_signatures(epoch_id, serialised_signatures, serialization_revision) VALUES (?, ?, ?);
|
||||
UPDATE coin_indices_signatures
|
||||
SET
|
||||
serialised_signatures = ?,
|
||||
serialization_revision = ?
|
||||
WHERE epoch_id = ?
|
||||
"#,
|
||||
epoch_id,
|
||||
data,
|
||||
serialisation_revision
|
||||
serialisation_revision,
|
||||
data,
|
||||
serialisation_revision,
|
||||
epoch_id,
|
||||
)
|
||||
.execute(&self.connection_pool)
|
||||
.await?;
|
||||
@@ -240,13 +260,21 @@ impl SqliteEcashTicketbookManager {
|
||||
) -> Result<(), sqlx::Error> {
|
||||
sqlx::query!(
|
||||
r#"
|
||||
INSERT INTO expiration_date_signatures(expiration_date, epoch_id, serialised_signatures, serialization_revision)
|
||||
VALUES (?, ?, ?, ?)
|
||||
INSERT OR IGNORE INTO expiration_date_signatures(expiration_date, epoch_id, serialised_signatures, serialization_revision)
|
||||
VALUES (?, ?, ?, ?);
|
||||
UPDATE expiration_date_signatures
|
||||
SET
|
||||
serialised_signatures = ?,
|
||||
serialization_revision = ?
|
||||
WHERE expiration_date = ?
|
||||
"#,
|
||||
expiration_date,
|
||||
epoch_id,
|
||||
data,
|
||||
serialisation_revision
|
||||
serialisation_revision,
|
||||
data,
|
||||
serialisation_revision,
|
||||
expiration_date
|
||||
)
|
||||
.execute(&self.connection_pool)
|
||||
.await?;
|
||||
|
||||
@@ -1,18 +1,17 @@
|
||||
// Copyright 2024 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
use crate::error::*;
|
||||
use crate::BandwidthFlushingBehaviourConfig;
|
||||
use crate::ClientBandwidth;
|
||||
use nym_credentials::ecash::utils::ecash_today;
|
||||
use nym_credentials_interface::{AvailableBandwidth, Bandwidth};
|
||||
use nym_credentials_interface::Bandwidth;
|
||||
use nym_gateway_requests::ServerResponse;
|
||||
use nym_gateway_storage::Storage;
|
||||
use si_scale::helpers::bibytes2;
|
||||
use time::OffsetDateTime;
|
||||
use tracing::*;
|
||||
|
||||
use crate::error::*;
|
||||
use crate::BandwidthFlushingBehaviourConfig;
|
||||
use crate::ClientBandwidth;
|
||||
|
||||
const FREE_TESTNET_BANDWIDTH_VALUE: Bandwidth = Bandwidth::new_unchecked(64 * 1024 * 1024 * 1024); // 64GB
|
||||
|
||||
#[derive(Clone)]
|
||||
@@ -41,13 +40,13 @@ impl<S: Storage + Clone + 'static> BandwidthStorageManager<S> {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn available_bandwidth(&self) -> AvailableBandwidth {
|
||||
self.client_bandwidth.bandwidth
|
||||
pub async fn available_bandwidth(&self) -> i64 {
|
||||
self.client_bandwidth.available().await
|
||||
}
|
||||
|
||||
async fn sync_expiration(&mut self) -> Result<()> {
|
||||
self.storage
|
||||
.set_expiration(self.client_id, self.client_bandwidth.bandwidth.expiration)
|
||||
.set_expiration(self.client_id, self.client_bandwidth.expiration().await)
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
@@ -61,17 +60,17 @@ impl<S: Storage + Clone + 'static> BandwidthStorageManager<S> {
|
||||
|
||||
self.increase_bandwidth(FREE_TESTNET_BANDWIDTH_VALUE, ecash_today())
|
||||
.await?;
|
||||
let available_total = self.client_bandwidth.bandwidth.bytes;
|
||||
let available_total = self.client_bandwidth.available().await;
|
||||
|
||||
Ok(ServerResponse::Bandwidth { available_total })
|
||||
}
|
||||
|
||||
#[instrument(skip_all)]
|
||||
pub async fn try_use_bandwidth(&mut self, required_bandwidth: i64) -> Result<i64> {
|
||||
if self.client_bandwidth.bandwidth.expired() {
|
||||
if self.client_bandwidth.expired().await {
|
||||
self.expire_bandwidth().await?;
|
||||
}
|
||||
let available_bandwidth = self.client_bandwidth.bandwidth.bytes;
|
||||
let available_bandwidth = self.client_bandwidth.available().await;
|
||||
|
||||
if available_bandwidth < required_bandwidth {
|
||||
return Err(Error::OutOfBandwidth {
|
||||
@@ -90,8 +89,7 @@ impl<S: Storage + Clone + 'static> BandwidthStorageManager<S> {
|
||||
|
||||
async fn expire_bandwidth(&mut self) -> Result<()> {
|
||||
self.storage.reset_bandwidth(self.client_id).await?;
|
||||
self.client_bandwidth.bandwidth = Default::default();
|
||||
self.client_bandwidth.update_sync_data();
|
||||
self.client_bandwidth.expire_bandwidth().await;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -101,31 +99,31 @@ impl<S: Storage + Clone + 'static> BandwidthStorageManager<S> {
|
||||
///
|
||||
/// * `amount`: amount to decrease the available bandwidth by.
|
||||
async fn consume_bandwidth(&mut self, amount: i64) -> Result<()> {
|
||||
self.client_bandwidth.bandwidth.bytes -= amount;
|
||||
self.client_bandwidth.bytes_delta_since_sync -= amount;
|
||||
self.client_bandwidth.decrease_bandwidth(amount).await;
|
||||
|
||||
// since we're going to be operating on a fair use policy anyway, even if we crash and let extra few packets
|
||||
// through, that's completely fine
|
||||
if self.client_bandwidth.should_sync(self.bandwidth_cfg) {
|
||||
self.sync_bandwidth().await?;
|
||||
if self.client_bandwidth.should_sync(self.bandwidth_cfg).await {
|
||||
self.sync_storage_bandwidth().await?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[instrument(level = "trace", skip_all)]
|
||||
async fn sync_bandwidth(&mut self) -> Result<()> {
|
||||
async fn sync_storage_bandwidth(&mut self) -> Result<()> {
|
||||
trace!("syncing client bandwidth with the underlying storage");
|
||||
let updated = self
|
||||
.storage
|
||||
.increase_bandwidth(self.client_id, self.client_bandwidth.bytes_delta_since_sync)
|
||||
.increase_bandwidth(
|
||||
self.client_id,
|
||||
self.client_bandwidth.delta_since_sync().await,
|
||||
)
|
||||
.await?;
|
||||
|
||||
trace!(updated);
|
||||
|
||||
self.client_bandwidth.bandwidth.bytes = updated;
|
||||
|
||||
self.client_bandwidth.update_sync_data();
|
||||
self.client_bandwidth
|
||||
.resync_bandwidth_with_storage(updated)
|
||||
.await;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -140,13 +138,14 @@ impl<S: Storage + Clone + 'static> BandwidthStorageManager<S> {
|
||||
bandwidth: Bandwidth,
|
||||
expiration: OffsetDateTime,
|
||||
) -> Result<()> {
|
||||
self.client_bandwidth.bandwidth.bytes += bandwidth.value() as i64;
|
||||
self.client_bandwidth.bytes_delta_since_sync += bandwidth.value() as i64;
|
||||
self.client_bandwidth.bandwidth.expiration = expiration;
|
||||
self.client_bandwidth
|
||||
.increase_bandwidth(bandwidth.value() as i64, expiration)
|
||||
.await;
|
||||
|
||||
// any increases to bandwidth should get flushed immediately
|
||||
// (we don't want to accidentally miss somebody claiming a gigabyte voucher)
|
||||
self.sync_expiration().await?;
|
||||
self.sync_bandwidth().await
|
||||
self.sync_storage_bandwidth().await?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
// Copyright 2024 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
use std::time::Duration;
|
||||
|
||||
use nym_credentials::ecash::utils::ecash_today;
|
||||
use nym_credentials_interface::AvailableBandwidth;
|
||||
use std::sync::Arc;
|
||||
use std::time::Duration;
|
||||
use time::OffsetDateTime;
|
||||
use tokio::sync::RwLock;
|
||||
|
||||
const DEFAULT_CLIENT_BANDWIDTH_MAX_FLUSHING_RATE: Duration = Duration::from_millis(5);
|
||||
const DEFAULT_CLIENT_BANDWIDTH_MAX_DELTA_FLUSHING_AMOUNT: i64 = 512 * 1024; // 512kB
|
||||
@@ -28,10 +30,15 @@ impl Default for BandwidthFlushingBehaviourConfig {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct ClientBandwidth {
|
||||
inner: Arc<RwLock<ClientBandwidthInner>>,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
struct ClientBandwidthInner {
|
||||
pub(crate) bandwidth: AvailableBandwidth,
|
||||
pub(crate) last_flushed: OffsetDateTime,
|
||||
pub(crate) last_synced: OffsetDateTime,
|
||||
|
||||
/// the number of bytes the client had during the last sync.
|
||||
/// it is used to determine whether the current value should be synced with the storage
|
||||
@@ -43,28 +50,74 @@ pub struct ClientBandwidth {
|
||||
impl ClientBandwidth {
|
||||
pub fn new(bandwidth: AvailableBandwidth) -> ClientBandwidth {
|
||||
ClientBandwidth {
|
||||
bandwidth,
|
||||
last_flushed: OffsetDateTime::now_utc(),
|
||||
bytes_at_last_sync: bandwidth.bytes,
|
||||
bytes_delta_since_sync: 0,
|
||||
inner: Arc::new(RwLock::new(ClientBandwidthInner {
|
||||
bandwidth,
|
||||
last_synced: OffsetDateTime::now_utc(),
|
||||
bytes_at_last_sync: bandwidth.bytes,
|
||||
bytes_delta_since_sync: 0,
|
||||
})),
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn should_sync(&self, cfg: BandwidthFlushingBehaviourConfig) -> bool {
|
||||
if self.bytes_delta_since_sync.abs() >= cfg.client_bandwidth_max_delta_flushing_amount {
|
||||
pub(crate) async fn should_sync(&self, cfg: BandwidthFlushingBehaviourConfig) -> bool {
|
||||
let guard = self.inner.read().await;
|
||||
|
||||
if guard.bytes_delta_since_sync.abs() >= cfg.client_bandwidth_max_delta_flushing_amount {
|
||||
return true;
|
||||
}
|
||||
|
||||
if self.last_flushed + cfg.client_bandwidth_max_flushing_rate < OffsetDateTime::now_utc() {
|
||||
if guard.last_synced + cfg.client_bandwidth_max_flushing_rate < OffsetDateTime::now_utc() {
|
||||
return true;
|
||||
}
|
||||
|
||||
false
|
||||
}
|
||||
|
||||
pub(crate) fn update_sync_data(&mut self) {
|
||||
self.last_flushed = OffsetDateTime::now_utc();
|
||||
self.bytes_at_last_sync = self.bandwidth.bytes;
|
||||
self.bytes_delta_since_sync = 0;
|
||||
pub(crate) async fn available(&self) -> i64 {
|
||||
self.inner.read().await.bandwidth.bytes
|
||||
}
|
||||
|
||||
pub(crate) async fn delta_since_sync(&self) -> i64 {
|
||||
self.inner.read().await.bytes_delta_since_sync
|
||||
}
|
||||
pub(crate) async fn expiration(&self) -> OffsetDateTime {
|
||||
self.inner.read().await.bandwidth.expiration
|
||||
}
|
||||
|
||||
pub(crate) async fn expired(&self) -> bool {
|
||||
self.expiration().await < ecash_today()
|
||||
}
|
||||
|
||||
pub(crate) async fn decrease_bandwidth(&self, decrease: i64) {
|
||||
let mut guard = self.inner.write().await;
|
||||
|
||||
guard.bandwidth.bytes -= decrease;
|
||||
guard.bytes_delta_since_sync -= decrease;
|
||||
}
|
||||
|
||||
pub(crate) async fn increase_bandwidth(&self, increase: i64, new_expiration: OffsetDateTime) {
|
||||
let mut guard = self.inner.write().await;
|
||||
|
||||
guard.bandwidth.bytes += increase;
|
||||
guard.bandwidth.expiration = new_expiration;
|
||||
guard.bytes_delta_since_sync += increase;
|
||||
}
|
||||
|
||||
pub(crate) async fn expire_bandwidth(&self) {
|
||||
let mut guard = self.inner.write().await;
|
||||
|
||||
guard.bandwidth = AvailableBandwidth::default();
|
||||
guard.last_synced = OffsetDateTime::now_utc();
|
||||
guard.bytes_at_last_sync = 0;
|
||||
guard.bytes_delta_since_sync = 0;
|
||||
}
|
||||
|
||||
pub(crate) async fn resync_bandwidth_with_storage(&self, stored: i64) {
|
||||
let mut guard = self.inner.write().await;
|
||||
|
||||
guard.bandwidth.bytes = stored;
|
||||
guard.bytes_at_last_sync = stored;
|
||||
guard.bytes_delta_since_sync = 0;
|
||||
guard.last_synced = OffsetDateTime::now_utc();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -150,7 +150,7 @@ impl<S: Storage + Clone + 'static> CredentialVerifier<S> {
|
||||
Ok(self
|
||||
.bandwidth_storage_manager
|
||||
.client_bandwidth
|
||||
.bandwidth
|
||||
.bytes)
|
||||
.available()
|
||||
.await)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -67,19 +67,3 @@ impl From<v7::response::InfoLevel> for v6::response::InfoLevel {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<v7::response::UnrequestedDisconnectReason> for v6::response::UnrequestedDisconnectReason {
|
||||
fn from(reason: v7::response::UnrequestedDisconnectReason) -> Self {
|
||||
match reason {
|
||||
v7::response::UnrequestedDisconnectReason::ClientMixnetTrafficTimeout => {
|
||||
v6::response::UnrequestedDisconnectReason::ClientMixnetTrafficTimeout
|
||||
}
|
||||
v7::response::UnrequestedDisconnectReason::ClientTunTrafficTimeout => {
|
||||
v6::response::UnrequestedDisconnectReason::ClientTunTrafficTimeout
|
||||
}
|
||||
v7::response::UnrequestedDisconnectReason::Other(reason) => {
|
||||
v6::response::UnrequestedDisconnectReason::Other(reason)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,4 +2,4 @@ pub mod conversion;
|
||||
pub mod request;
|
||||
pub mod response;
|
||||
|
||||
const VERSION: u8 = 6;
|
||||
pub const VERSION: u8 = 6;
|
||||
|
||||
@@ -3,4 +3,4 @@ pub mod request;
|
||||
pub mod response;
|
||||
pub mod signature;
|
||||
|
||||
const VERSION: u8 = 7;
|
||||
pub const VERSION: u8 = 7;
|
||||
|
||||
@@ -2,30 +2,17 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
use nym_sphinx_acknowledgements::surb_ack::SurbAckRecoveryError;
|
||||
use nym_sphinx_addressing::nodes::NymNodeRoutingAddressError;
|
||||
use nym_sphinx_types::{NymPacketError, OutfoxError, SphinxError};
|
||||
use nym_sphinx_framing::processing::PacketProcessingError;
|
||||
use thiserror::Error;
|
||||
|
||||
#[derive(Error, Debug)]
|
||||
pub enum MixProcessingError {
|
||||
#[error("failed to process received packet: {0}")]
|
||||
NymPacketProcessingError(#[from] NymPacketError),
|
||||
|
||||
#[error("failed to process received sphinx packet: {0}")]
|
||||
SphinxProcessingError(#[from] SphinxError),
|
||||
|
||||
#[error("the forward hop address was malformed: {0}")]
|
||||
InvalidForwardHopAddress(#[from] NymNodeRoutingAddressError),
|
||||
|
||||
#[error("the final hop did not contain a SURB-Ack")]
|
||||
NoSurbAckInFinalHop,
|
||||
|
||||
#[error("failed to recover the expected SURB-Ack packet: {0}")]
|
||||
MalformedSurbAck(#[from] SurbAckRecoveryError),
|
||||
|
||||
#[error("the received packet was set to use the very old and very much deprecated 'VPN' mode")]
|
||||
ReceivedOldTypeVpnPacket,
|
||||
|
||||
#[error("failed to process received outfox packet: {0}")]
|
||||
OutfoxProcessingError(#[from] OutfoxError),
|
||||
#[error("failed to process received Nym packet: {0}")]
|
||||
NymPacketProcessingError(#[from] PacketProcessingError),
|
||||
}
|
||||
|
||||
@@ -1,38 +1,9 @@
|
||||
// Copyright 2021 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
use crate::packet_processor::error::MixProcessingError;
|
||||
use log::*;
|
||||
use nym_metrics::nanos;
|
||||
use nym_sphinx_acknowledgements::surb_ack::SurbAck;
|
||||
use nym_sphinx_addressing::nodes::NymNodeRoutingAddress;
|
||||
use nym_sphinx_forwarding::packet::MixPacket;
|
||||
use nym_sphinx_framing::packet::FramedNymPacket;
|
||||
use nym_sphinx_params::{PacketSize, PacketType};
|
||||
use nym_sphinx_types::{
|
||||
Delay as SphinxDelay, DestinationAddressBytes, NodeAddressBytes, NymPacket, NymProcessedPacket,
|
||||
PrivateKey, ProcessedPacket,
|
||||
};
|
||||
|
||||
use std::sync::Arc;
|
||||
|
||||
type ForwardAck = MixPacket;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct ProcessedFinalHop {
|
||||
pub destination: DestinationAddressBytes,
|
||||
pub forward_ack: Option<ForwardAck>,
|
||||
pub message: Vec<u8>,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum MixProcessingResult {
|
||||
/// Contains unwrapped data that should first get delayed before being sent to next hop.
|
||||
ForwardHop(MixPacket, Option<SphinxDelay>),
|
||||
|
||||
/// Contains all data extracted out of the final hop packet that could be forwarded to the destination.
|
||||
FinalHop(ProcessedFinalHop),
|
||||
}
|
||||
use nym_sphinx_types::PrivateKey;
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct SphinxPacketProcessor {
|
||||
@@ -48,280 +19,7 @@ impl SphinxPacketProcessor {
|
||||
}
|
||||
}
|
||||
|
||||
/// Performs a fresh sphinx unwrapping using no cache.
|
||||
fn perform_initial_packet_processing(
|
||||
&self,
|
||||
packet: NymPacket,
|
||||
) -> Result<NymProcessedPacket, MixProcessingError> {
|
||||
nanos!("perform_initial_packet_processing", {
|
||||
packet.process(&self.sphinx_key).map_err(|err| {
|
||||
debug!("Failed to unwrap NymPacket packet: {err}");
|
||||
MixProcessingError::NymPacketProcessingError(err)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
/// Takes the received framed packet and tries to unwrap it from the sphinx encryption.
|
||||
fn perform_initial_unwrapping(
|
||||
&self,
|
||||
received: FramedNymPacket,
|
||||
) -> Result<NymProcessedPacket, MixProcessingError> {
|
||||
nanos!("perform_initial_unwrapping", {
|
||||
let packet = received.into_inner();
|
||||
self.perform_initial_packet_processing(packet)
|
||||
})
|
||||
}
|
||||
|
||||
/// Processed received forward hop packet - tries to extract next hop address, sets delay
|
||||
/// and packs all the data in a way that can be easily sent to the next hop.
|
||||
fn process_forward_hop(
|
||||
&self,
|
||||
packet: NymPacket,
|
||||
forward_address: NodeAddressBytes,
|
||||
delay: SphinxDelay,
|
||||
packet_type: PacketType,
|
||||
) -> Result<MixProcessingResult, MixProcessingError> {
|
||||
let next_hop_address = NymNodeRoutingAddress::try_from(forward_address)?;
|
||||
|
||||
let mix_packet = MixPacket::new(next_hop_address, packet, packet_type);
|
||||
Ok(MixProcessingResult::ForwardHop(mix_packet, Some(delay)))
|
||||
}
|
||||
|
||||
/// Split data extracted from the final hop sphinx packet into a SURBAck and message
|
||||
/// that should get delivered to a client.
|
||||
fn split_hop_data_into_ack_and_message(
|
||||
&self,
|
||||
mut extracted_data: Vec<u8>,
|
||||
packet_type: PacketType,
|
||||
) -> Result<(Vec<u8>, Vec<u8>), MixProcessingError> {
|
||||
let ack_len = SurbAck::len(Some(packet_type));
|
||||
|
||||
// in theory it's impossible for this to fail since it managed to go into correct `match`
|
||||
// branch at the caller
|
||||
if extracted_data.len() < ack_len {
|
||||
return Err(MixProcessingError::NoSurbAckInFinalHop);
|
||||
}
|
||||
|
||||
let message = extracted_data.split_off(ack_len);
|
||||
let ack_data = extracted_data;
|
||||
Ok((ack_data, message))
|
||||
}
|
||||
|
||||
/// Tries to extract a SURBAck that could be sent back into the mix network and message
|
||||
/// that should get delivered to a client from received Sphinx packet.
|
||||
fn split_into_ack_and_message(
|
||||
&self,
|
||||
data: Vec<u8>,
|
||||
packet_size: PacketSize,
|
||||
packet_type: PacketType,
|
||||
) -> Result<(Option<MixPacket>, Vec<u8>), MixProcessingError> {
|
||||
match packet_size {
|
||||
PacketSize::AckPacket | PacketSize::OutfoxAckPacket => {
|
||||
trace!("received an ack packet!");
|
||||
Ok((None, data))
|
||||
}
|
||||
PacketSize::RegularPacket
|
||||
| PacketSize::ExtendedPacket8
|
||||
| PacketSize::ExtendedPacket16
|
||||
| PacketSize::ExtendedPacket32
|
||||
| PacketSize::OutfoxRegularPacket => {
|
||||
trace!("received a normal packet!");
|
||||
let (ack_data, message) =
|
||||
self.split_hop_data_into_ack_and_message(data, packet_type)?;
|
||||
let (ack_first_hop, ack_packet) =
|
||||
match SurbAck::try_recover_first_hop_packet(&ack_data, packet_type) {
|
||||
Ok((first_hop, packet)) => (first_hop, packet),
|
||||
Err(err) => {
|
||||
info!("Failed to recover first hop from ack data: {err}");
|
||||
return Err(err.into());
|
||||
}
|
||||
};
|
||||
let forward_ack = MixPacket::new(ack_first_hop, ack_packet, packet_type);
|
||||
Ok((Some(forward_ack), message))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Processed received final hop packet - tries to extract SURBAck out of it (assuming the
|
||||
/// packet itself is not an ACK) and splits it from the message that should get delivered
|
||||
/// to the destination.
|
||||
fn process_final_hop(
|
||||
&self,
|
||||
destination: DestinationAddressBytes,
|
||||
payload: Vec<u8>,
|
||||
packet_size: PacketSize,
|
||||
packet_type: PacketType,
|
||||
) -> Result<MixProcessingResult, MixProcessingError> {
|
||||
let (forward_ack, message) =
|
||||
self.split_into_ack_and_message(payload, packet_size, packet_type)?;
|
||||
|
||||
Ok(MixProcessingResult::FinalHop(ProcessedFinalHop {
|
||||
destination,
|
||||
forward_ack,
|
||||
message,
|
||||
}))
|
||||
}
|
||||
|
||||
/// Performs final processing for the unwrapped packet based on whether it was a forward hop
|
||||
/// or a final hop.
|
||||
fn perform_final_processing(
|
||||
&self,
|
||||
packet: NymProcessedPacket,
|
||||
packet_size: PacketSize,
|
||||
packet_type: PacketType,
|
||||
) -> Result<MixProcessingResult, MixProcessingError> {
|
||||
match packet {
|
||||
NymProcessedPacket::Sphinx(packet) => {
|
||||
match packet {
|
||||
ProcessedPacket::ForwardHop(packet, address, delay) => self
|
||||
.process_forward_hop(
|
||||
NymPacket::Sphinx(*packet),
|
||||
address,
|
||||
delay,
|
||||
packet_type,
|
||||
),
|
||||
// right now there's no use for the surb_id included in the header - probably it should get removed from the
|
||||
// sphinx all together?
|
||||
ProcessedPacket::FinalHop(destination, _, payload) => self.process_final_hop(
|
||||
destination,
|
||||
payload.recover_plaintext()?,
|
||||
packet_size,
|
||||
packet_type,
|
||||
),
|
||||
}
|
||||
}
|
||||
NymProcessedPacket::Outfox(packet) => {
|
||||
let next_address = *packet.next_address();
|
||||
let packet = packet.into_packet();
|
||||
if packet.is_final_hop() {
|
||||
self.process_final_hop(
|
||||
DestinationAddressBytes::from_bytes(next_address),
|
||||
packet.recover_plaintext()?.to_vec(),
|
||||
packet_size,
|
||||
packet_type,
|
||||
)
|
||||
} else {
|
||||
let mix_packet = MixPacket::new(
|
||||
NymNodeRoutingAddress::try_from_bytes(&next_address)?,
|
||||
NymPacket::Outfox(packet),
|
||||
PacketType::Outfox,
|
||||
);
|
||||
Ok(MixProcessingResult::ForwardHop(mix_packet, None))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn process_received(
|
||||
&self,
|
||||
received: FramedNymPacket,
|
||||
) -> Result<MixProcessingResult, MixProcessingError> {
|
||||
// explicit packet size will help to correctly parse final hop
|
||||
nanos!("process_received", {
|
||||
let packet_size = received.packet_size();
|
||||
let packet_type = received.packet_type();
|
||||
|
||||
// unwrap the sphinx packet and if possible and appropriate, cache keys
|
||||
let processed_packet = self.perform_initial_unwrapping(received)?;
|
||||
|
||||
// for forward packets, extract next hop and set delay (but do NOT delay here)
|
||||
// for final packets, extract SURBAck
|
||||
let final_processing_result =
|
||||
self.perform_final_processing(processed_packet, packet_size, packet_type);
|
||||
|
||||
if final_processing_result.is_err() {
|
||||
error!("{:?}", final_processing_result)
|
||||
}
|
||||
|
||||
final_processing_result
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: what more could we realistically test here?
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use nym_sphinx_types::crypto::keygen;
|
||||
|
||||
fn fixture() -> SphinxPacketProcessor {
|
||||
let local_keys = keygen();
|
||||
SphinxPacketProcessor::new(local_keys.0)
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn splitting_hop_data_works_for_sufficiently_long_payload() {
|
||||
let processor = fixture();
|
||||
|
||||
let short_data = vec![42u8];
|
||||
assert!(processor
|
||||
.split_hop_data_into_ack_and_message(short_data, PacketType::Mix)
|
||||
.is_err());
|
||||
|
||||
let sufficient_data = vec![42u8; SurbAck::len(Some(PacketType::Mix))];
|
||||
let (ack, data) = processor
|
||||
.split_hop_data_into_ack_and_message(sufficient_data.clone(), PacketType::Mix)
|
||||
.unwrap();
|
||||
assert_eq!(sufficient_data, ack);
|
||||
assert!(data.is_empty());
|
||||
|
||||
let long_data = vec![42u8; SurbAck::len(Some(PacketType::Mix)) * 5];
|
||||
let (ack, data) = processor
|
||||
.split_hop_data_into_ack_and_message(long_data, PacketType::Mix)
|
||||
.unwrap();
|
||||
assert_eq!(ack.len(), SurbAck::len(Some(PacketType::Mix)));
|
||||
assert_eq!(data.len(), SurbAck::len(Some(PacketType::Mix)) * 4)
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn splitting_hop_data_works_for_sufficiently_long_payload_outfox() {
|
||||
let processor = fixture();
|
||||
|
||||
let short_data = vec![42u8];
|
||||
assert!(processor
|
||||
.split_hop_data_into_ack_and_message(short_data, PacketType::Outfox)
|
||||
.is_err());
|
||||
|
||||
let sufficient_data = vec![42u8; SurbAck::len(Some(PacketType::Outfox))];
|
||||
let (ack, data) = processor
|
||||
.split_hop_data_into_ack_and_message(sufficient_data.clone(), PacketType::Outfox)
|
||||
.unwrap();
|
||||
assert_eq!(sufficient_data, ack);
|
||||
assert!(data.is_empty());
|
||||
|
||||
let long_data = vec![42u8; SurbAck::len(Some(PacketType::Outfox)) * 5];
|
||||
let (ack, data) = processor
|
||||
.split_hop_data_into_ack_and_message(long_data, PacketType::Outfox)
|
||||
.unwrap();
|
||||
assert_eq!(ack.len(), SurbAck::len(Some(PacketType::Outfox)));
|
||||
assert_eq!(data.len(), SurbAck::len(Some(PacketType::Outfox)) * 4)
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn splitting_into_ack_and_message_returns_whole_data_for_ack() {
|
||||
let processor = fixture();
|
||||
|
||||
let data = vec![42u8; SurbAck::len(Some(PacketType::Mix)) + 10];
|
||||
let (ack, message) = processor
|
||||
.split_into_ack_and_message(data.clone(), PacketSize::AckPacket, PacketType::Mix)
|
||||
.unwrap();
|
||||
assert!(ack.is_none());
|
||||
assert_eq!(data, message)
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn splitting_into_ack_and_message_returns_whole_data_for_ack_outfox() {
|
||||
let processor = fixture();
|
||||
|
||||
let data = vec![42u8; SurbAck::len(Some(PacketType::Outfox)) + 10];
|
||||
let (ack, message) = processor
|
||||
.split_into_ack_and_message(
|
||||
data.clone(),
|
||||
PacketSize::OutfoxAckPacket,
|
||||
PacketType::Outfox,
|
||||
)
|
||||
.unwrap();
|
||||
assert!(ack.is_none());
|
||||
assert_eq!(data, message)
|
||||
pub fn sphinx_key(&self) -> &PrivateKey {
|
||||
&self.sphinx_key
|
||||
}
|
||||
}
|
||||
|
||||
@@ -32,7 +32,7 @@ pub const NYXD_URL: &str = "https://rpc.nymtech.net";
|
||||
pub const NYM_API: &str = "https://validator.nymtech.net/api/";
|
||||
pub const NYXD_WS: &str = "wss://rpc.nymtech.net/websocket";
|
||||
pub const EXPLORER_API: &str = "https://explorer.nymtech.net/api/";
|
||||
pub const NYM_VPN_API: &str = "https://nymvpn.net/api/";
|
||||
pub const NYM_VPN_API: &str = "https://nymvpn.com/api/";
|
||||
|
||||
// I'm making clippy mad on purpose, because that url HAS TO be updated and deployed before merging
|
||||
pub const EXIT_POLICY_URL: &str =
|
||||
|
||||
@@ -11,7 +11,14 @@ repository = { workspace = true }
|
||||
bytes = { workspace = true }
|
||||
tokio-util = { workspace = true, features = ["codec"] }
|
||||
thiserror = { workspace = true }
|
||||
log = { workspace = true }
|
||||
|
||||
nym-sphinx-types = { path = "../types", features = ["sphinx", "outfox"] }
|
||||
nym-sphinx-params = { path = "../params", features = ["sphinx", "outfox"] }
|
||||
nym-sphinx-forwarding = { path = "../forwarding" }
|
||||
nym-metrics = { path = "../../nym-metrics" }
|
||||
nym-sphinx-addressing = { path = "../addressing" }
|
||||
nym-sphinx-acknowledgements = { path = "../acknowledgements" }
|
||||
|
||||
[dev-dependencies]
|
||||
tokio = { workspace = true, features = ["full"] }
|
||||
|
||||
@@ -3,3 +3,4 @@
|
||||
|
||||
pub mod codec;
|
||||
pub mod packet;
|
||||
pub mod processing;
|
||||
|
||||
@@ -0,0 +1,284 @@
|
||||
use log::{debug, error, info, trace};
|
||||
use nym_sphinx_acknowledgements::surb_ack::{SurbAck, SurbAckRecoveryError};
|
||||
use nym_sphinx_addressing::nodes::{NymNodeRoutingAddress, NymNodeRoutingAddressError};
|
||||
use nym_sphinx_params::{PacketSize, PacketType};
|
||||
use nym_sphinx_types::{
|
||||
Delay as SphinxDelay, DestinationAddressBytes, NodeAddressBytes, NymPacket, NymPacketError,
|
||||
NymProcessedPacket, OutfoxError, PrivateKey, ProcessedPacket, SphinxError,
|
||||
};
|
||||
use thiserror::Error;
|
||||
|
||||
use crate::packet::FramedNymPacket;
|
||||
use nym_metrics::nanos;
|
||||
use nym_sphinx_forwarding::packet::MixPacket;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum MixProcessingResult {
|
||||
/// Contains unwrapped data that should first get delayed before being sent to next hop.
|
||||
ForwardHop(MixPacket, Option<SphinxDelay>),
|
||||
|
||||
/// Contains all data extracted out of the final hop packet that could be forwarded to the destination.
|
||||
FinalHop(ProcessedFinalHop),
|
||||
}
|
||||
|
||||
type ForwardAck = MixPacket;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct ProcessedFinalHop {
|
||||
pub destination: DestinationAddressBytes,
|
||||
pub forward_ack: Option<ForwardAck>,
|
||||
pub message: Vec<u8>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Error)]
|
||||
pub enum PacketProcessingError {
|
||||
#[error("failed to process received packet: {0}")]
|
||||
NymPacketProcessingError(#[from] NymPacketError),
|
||||
|
||||
#[error("failed to process received sphinx packet: {0}")]
|
||||
SphinxProcessingError(#[from] SphinxError),
|
||||
|
||||
#[error("the forward hop address was malformed: {0}")]
|
||||
InvalidForwardHopAddress(#[from] NymNodeRoutingAddressError),
|
||||
|
||||
#[error("the final hop did not contain a SURB-Ack")]
|
||||
NoSurbAckInFinalHop,
|
||||
|
||||
#[error("failed to recover the expected SURB-Ack packet: {0}")]
|
||||
MalformedSurbAck(#[from] SurbAckRecoveryError),
|
||||
|
||||
#[error("the received packet was set to use the very old and very much deprecated 'VPN' mode")]
|
||||
ReceivedOldTypeVpnPacket,
|
||||
|
||||
#[error("failed to process received outfox packet: {0}")]
|
||||
OutfoxProcessingError(#[from] OutfoxError),
|
||||
}
|
||||
|
||||
pub fn process_framed_packet(
|
||||
received: FramedNymPacket,
|
||||
sphinx_key: &PrivateKey,
|
||||
) -> Result<MixProcessingResult, PacketProcessingError> {
|
||||
nanos!("process_received", {
|
||||
let packet_size = received.packet_size();
|
||||
let packet_type = received.packet_type();
|
||||
|
||||
// unwrap the sphinx packet and if possible and appropriate, cache keys
|
||||
let processed_packet = perform_framed_unwrapping(received, sphinx_key)?;
|
||||
|
||||
// for forward packets, extract next hop and set delay (but do NOT delay here)
|
||||
// for final packets, extract SURBAck
|
||||
let final_processing_result =
|
||||
perform_final_processing(processed_packet, packet_size, packet_type);
|
||||
|
||||
if final_processing_result.is_err() {
|
||||
error!("{:?}", final_processing_result)
|
||||
}
|
||||
|
||||
final_processing_result
|
||||
})
|
||||
}
|
||||
|
||||
fn perform_framed_unwrapping(
|
||||
received: FramedNymPacket,
|
||||
sphinx_key: &PrivateKey,
|
||||
) -> Result<NymProcessedPacket, PacketProcessingError> {
|
||||
nanos!("perform_initial_unwrapping", {
|
||||
let packet = received.into_inner();
|
||||
perform_framed_packet_processing(packet, sphinx_key)
|
||||
})
|
||||
}
|
||||
|
||||
fn perform_framed_packet_processing(
|
||||
packet: NymPacket,
|
||||
sphinx_key: &PrivateKey,
|
||||
) -> Result<NymProcessedPacket, PacketProcessingError> {
|
||||
nanos!("perform_initial_packet_processing", {
|
||||
packet.process(sphinx_key).map_err(|err| {
|
||||
debug!("Failed to unwrap NymPacket packet: {err}");
|
||||
PacketProcessingError::NymPacketProcessingError(err)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
fn perform_final_processing(
|
||||
packet: NymProcessedPacket,
|
||||
packet_size: PacketSize,
|
||||
packet_type: PacketType,
|
||||
) -> Result<MixProcessingResult, PacketProcessingError> {
|
||||
match packet {
|
||||
NymProcessedPacket::Sphinx(packet) => {
|
||||
match packet {
|
||||
ProcessedPacket::ForwardHop(packet, address, delay) => {
|
||||
process_forward_hop(NymPacket::Sphinx(*packet), address, delay, packet_type)
|
||||
}
|
||||
// right now there's no use for the surb_id included in the header - probably it should get removed from the
|
||||
// sphinx all together?
|
||||
ProcessedPacket::FinalHop(destination, _, payload) => process_final_hop(
|
||||
destination,
|
||||
payload.recover_plaintext()?,
|
||||
packet_size,
|
||||
packet_type,
|
||||
),
|
||||
}
|
||||
}
|
||||
NymProcessedPacket::Outfox(packet) => {
|
||||
let next_address = *packet.next_address();
|
||||
let packet = packet.into_packet();
|
||||
if packet.is_final_hop() {
|
||||
process_final_hop(
|
||||
DestinationAddressBytes::from_bytes(next_address),
|
||||
packet.recover_plaintext()?.to_vec(),
|
||||
packet_size,
|
||||
packet_type,
|
||||
)
|
||||
} else {
|
||||
let mix_packet = MixPacket::new(
|
||||
NymNodeRoutingAddress::try_from_bytes(&next_address)?,
|
||||
NymPacket::Outfox(packet),
|
||||
PacketType::Outfox,
|
||||
);
|
||||
Ok(MixProcessingResult::ForwardHop(mix_packet, None))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn process_final_hop(
|
||||
destination: DestinationAddressBytes,
|
||||
payload: Vec<u8>,
|
||||
packet_size: PacketSize,
|
||||
packet_type: PacketType,
|
||||
) -> Result<MixProcessingResult, PacketProcessingError> {
|
||||
let (forward_ack, message) = split_into_ack_and_message(payload, packet_size, packet_type)?;
|
||||
|
||||
Ok(MixProcessingResult::FinalHop(ProcessedFinalHop {
|
||||
destination,
|
||||
forward_ack,
|
||||
message,
|
||||
}))
|
||||
}
|
||||
|
||||
fn split_into_ack_and_message(
|
||||
data: Vec<u8>,
|
||||
packet_size: PacketSize,
|
||||
packet_type: PacketType,
|
||||
) -> Result<(Option<MixPacket>, Vec<u8>), PacketProcessingError> {
|
||||
match packet_size {
|
||||
PacketSize::AckPacket | PacketSize::OutfoxAckPacket => {
|
||||
trace!("received an ack packet!");
|
||||
Ok((None, data))
|
||||
}
|
||||
PacketSize::RegularPacket
|
||||
| PacketSize::ExtendedPacket8
|
||||
| PacketSize::ExtendedPacket16
|
||||
| PacketSize::ExtendedPacket32
|
||||
| PacketSize::OutfoxRegularPacket => {
|
||||
trace!("received a normal packet!");
|
||||
let (ack_data, message) = split_hop_data_into_ack_and_message(data, packet_type)?;
|
||||
let (ack_first_hop, ack_packet) =
|
||||
match SurbAck::try_recover_first_hop_packet(&ack_data, packet_type) {
|
||||
Ok((first_hop, packet)) => (first_hop, packet),
|
||||
Err(err) => {
|
||||
info!("Failed to recover first hop from ack data: {err}");
|
||||
return Err(err.into());
|
||||
}
|
||||
};
|
||||
let forward_ack = MixPacket::new(ack_first_hop, ack_packet, packet_type);
|
||||
Ok((Some(forward_ack), message))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn split_hop_data_into_ack_and_message(
|
||||
mut extracted_data: Vec<u8>,
|
||||
packet_type: PacketType,
|
||||
) -> Result<(Vec<u8>, Vec<u8>), PacketProcessingError> {
|
||||
let ack_len = SurbAck::len(Some(packet_type));
|
||||
|
||||
// in theory it's impossible for this to fail since it managed to go into correct `match`
|
||||
// branch at the caller
|
||||
if extracted_data.len() < ack_len {
|
||||
return Err(PacketProcessingError::NoSurbAckInFinalHop);
|
||||
}
|
||||
|
||||
let message = extracted_data.split_off(ack_len);
|
||||
let ack_data = extracted_data;
|
||||
Ok((ack_data, message))
|
||||
}
|
||||
|
||||
fn process_forward_hop(
|
||||
packet: NymPacket,
|
||||
forward_address: NodeAddressBytes,
|
||||
delay: SphinxDelay,
|
||||
packet_type: PacketType,
|
||||
) -> Result<MixProcessingResult, PacketProcessingError> {
|
||||
let next_hop_address = NymNodeRoutingAddress::try_from(forward_address)?;
|
||||
|
||||
let mix_packet = MixPacket::new(next_hop_address, packet, packet_type);
|
||||
Ok(MixProcessingResult::ForwardHop(mix_packet, Some(delay)))
|
||||
}
|
||||
|
||||
// TODO: what more could we realistically test here?
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[tokio::test]
|
||||
async fn splitting_hop_data_works_for_sufficiently_long_payload() {
|
||||
let short_data = vec![42u8];
|
||||
assert!(split_hop_data_into_ack_and_message(short_data, PacketType::Mix).is_err());
|
||||
|
||||
let sufficient_data = vec![42u8; SurbAck::len(Some(PacketType::Mix))];
|
||||
let (ack, data) =
|
||||
split_hop_data_into_ack_and_message(sufficient_data.clone(), PacketType::Mix).unwrap();
|
||||
assert_eq!(sufficient_data, ack);
|
||||
assert!(data.is_empty());
|
||||
|
||||
let long_data: Vec<u8> = vec![42u8; SurbAck::len(Some(PacketType::Mix)) * 5];
|
||||
let (ack, data) = split_hop_data_into_ack_and_message(long_data, PacketType::Mix).unwrap();
|
||||
assert_eq!(ack.len(), SurbAck::len(Some(PacketType::Mix)));
|
||||
assert_eq!(data.len(), SurbAck::len(Some(PacketType::Mix)) * 4)
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn splitting_hop_data_works_for_sufficiently_long_payload_outfox() {
|
||||
let short_data = vec![42u8];
|
||||
assert!(split_hop_data_into_ack_and_message(short_data, PacketType::Outfox).is_err());
|
||||
|
||||
let sufficient_data = vec![42u8; SurbAck::len(Some(PacketType::Outfox))];
|
||||
let (ack, data) =
|
||||
split_hop_data_into_ack_and_message(sufficient_data.clone(), PacketType::Outfox)
|
||||
.unwrap();
|
||||
assert_eq!(sufficient_data, ack);
|
||||
assert!(data.is_empty());
|
||||
|
||||
let long_data = vec![42u8; SurbAck::len(Some(PacketType::Outfox)) * 5];
|
||||
let (ack, data) =
|
||||
split_hop_data_into_ack_and_message(long_data, PacketType::Outfox).unwrap();
|
||||
assert_eq!(ack.len(), SurbAck::len(Some(PacketType::Outfox)));
|
||||
assert_eq!(data.len(), SurbAck::len(Some(PacketType::Outfox)) * 4)
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn splitting_into_ack_and_message_returns_whole_data_for_ack() {
|
||||
let data = vec![42u8; SurbAck::len(Some(PacketType::Mix)) + 10];
|
||||
let (ack, message) =
|
||||
split_into_ack_and_message(data.clone(), PacketSize::AckPacket, PacketType::Mix)
|
||||
.unwrap();
|
||||
assert!(ack.is_none());
|
||||
assert_eq!(data, message)
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn splitting_into_ack_and_message_returns_whole_data_for_ack_outfox() {
|
||||
let data = vec![42u8; SurbAck::len(Some(PacketType::Outfox)) + 10];
|
||||
let (ack, message) = split_into_ack_and_message(
|
||||
data.clone(),
|
||||
PacketSize::OutfoxAckPacket,
|
||||
PacketType::Outfox,
|
||||
)
|
||||
.unwrap();
|
||||
assert!(ack.is_none());
|
||||
assert_eq!(data, message)
|
||||
}
|
||||
}
|
||||
@@ -10,6 +10,7 @@ use crate::rpc_client::RpcClient;
|
||||
use crate::storage::{persist_block, ScraperStorage};
|
||||
use crate::PruningOptions;
|
||||
use futures::StreamExt;
|
||||
use std::cmp::max;
|
||||
use std::collections::{BTreeMap, HashSet, VecDeque};
|
||||
use std::ops::{Add, Range};
|
||||
use std::sync::Arc;
|
||||
@@ -99,7 +100,15 @@ impl BlockProcessor {
|
||||
})
|
||||
}
|
||||
|
||||
async fn process_block(&mut self, block: BlockToProcess) -> Result<(), ScraperError> {
|
||||
pub fn with_pruning(mut self, pruning_options: PruningOptions) -> Self {
|
||||
self.pruning_options = pruning_options;
|
||||
self
|
||||
}
|
||||
|
||||
pub(super) async fn process_block(
|
||||
&mut self,
|
||||
block: BlockToProcess,
|
||||
) -> Result<(), ScraperError> {
|
||||
info!("processing block at height {}", block.height);
|
||||
|
||||
let full_info = self.rpc_client.try_get_full_details(block).await?;
|
||||
@@ -169,6 +178,10 @@ impl BlockProcessor {
|
||||
self.msg_modules = modules;
|
||||
}
|
||||
|
||||
pub(super) fn last_process_height(&self) -> u32 {
|
||||
self.last_processed_height
|
||||
}
|
||||
|
||||
async fn maybe_request_missing_blocks(&mut self) -> Result<(), ScraperError> {
|
||||
// we're still processing, so we're good
|
||||
if self.last_processed_at.elapsed() < MAX_MISSING_BLOCKS_DELAY {
|
||||
@@ -254,6 +267,7 @@ impl BlockProcessor {
|
||||
}
|
||||
|
||||
if to_prune == 0 {
|
||||
self.last_pruned_height = self.last_processed_height;
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
@@ -353,7 +367,14 @@ impl BlockProcessor {
|
||||
self.maybe_prune_storage().await?;
|
||||
|
||||
let latest_block = self.rpc_client.current_block_height().await? as u32;
|
||||
|
||||
if latest_block > self.last_processed_height && self.last_processed_height != 0 {
|
||||
// in case we were offline for a while,
|
||||
// make sure we don't request blocks we'd have to prune anyway
|
||||
let keep_recent = self.pruning_options.strategy_keep_recent();
|
||||
let last_to_keep = latest_block - keep_recent;
|
||||
self.last_processed_height = max(self.last_processed_height, last_to_keep);
|
||||
|
||||
let request_range = self.last_processed_height + 1..latest_block + 1;
|
||||
info!("we need to request {request_range:?} to resync");
|
||||
self.request_missing_blocks(request_range).await?;
|
||||
|
||||
@@ -16,7 +16,7 @@ pub enum ScraperError {
|
||||
#[error("failed to perform startup SQL migration: {0}")]
|
||||
StartupMigrationFailure(#[from] sqlx::migrate::MigrateError),
|
||||
|
||||
#[error("can't add any modules to the scraper as it's already running")]
|
||||
#[error("the block scraper is already running")]
|
||||
ScraperAlreadyRunning,
|
||||
|
||||
#[error("failed to establish websocket connection to {url}: {source}")]
|
||||
|
||||
@@ -1,21 +1,25 @@
|
||||
// Copyright 2023 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
use crate::block_processor::types::BlockToProcess;
|
||||
use crate::block_processor::BlockProcessor;
|
||||
use crate::block_requester::BlockRequester;
|
||||
use crate::block_requester::{BlockRequest, BlockRequester};
|
||||
use crate::error::ScraperError;
|
||||
use crate::modules::{BlockModule, MsgModule, TxModule};
|
||||
use crate::rpc_client::RpcClient;
|
||||
use crate::scraper::subscriber::ChainSubscriber;
|
||||
use crate::storage::ScraperStorage;
|
||||
use crate::PruningOptions;
|
||||
use futures::future::join_all;
|
||||
use std::path::PathBuf;
|
||||
use std::sync::Arc;
|
||||
use tokio::sync::mpsc::{channel, unbounded_channel};
|
||||
use tokio::sync::mpsc::{
|
||||
channel, unbounded_channel, Receiver, Sender, UnboundedReceiver, UnboundedSender,
|
||||
};
|
||||
use tokio::sync::Notify;
|
||||
use tokio_util::sync::CancellationToken;
|
||||
use tokio_util::task::TaskTracker;
|
||||
use tracing::info;
|
||||
use tracing::{error, info};
|
||||
use url::Url;
|
||||
|
||||
mod subscriber;
|
||||
@@ -115,6 +119,7 @@ pub struct NyxdScraper {
|
||||
cancel_token: CancellationToken,
|
||||
startup_sync: Arc<Notify>,
|
||||
pub storage: ScraperStorage,
|
||||
rpc_client: RpcClient,
|
||||
}
|
||||
|
||||
impl NyxdScraper {
|
||||
@@ -125,6 +130,7 @@ impl NyxdScraper {
|
||||
pub async fn new(config: Config) -> Result<Self, ScraperError> {
|
||||
config.pruning_options.validate()?;
|
||||
let storage = ScraperStorage::init(&config.database_path).await?;
|
||||
let rpc_client = RpcClient::new(&config.rpc_url)?;
|
||||
|
||||
Ok(NyxdScraper {
|
||||
config,
|
||||
@@ -132,6 +138,7 @@ impl NyxdScraper {
|
||||
cancel_token: CancellationToken::new(),
|
||||
startup_sync: Arc::new(Default::default()),
|
||||
storage,
|
||||
rpc_client,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -151,36 +158,156 @@ impl NyxdScraper {
|
||||
self.task_tracker.close();
|
||||
}
|
||||
|
||||
pub async fn start(&self) -> Result<(), ScraperError> {
|
||||
let (processing_tx, processing_rx) = unbounded_channel();
|
||||
let (req_tx, req_rx) = channel(5);
|
||||
pub async fn process_single_block(&self, height: u32) -> Result<(), ScraperError> {
|
||||
info!(height = height, "attempting to process a single block");
|
||||
if !self.task_tracker.is_empty() {
|
||||
return Err(ScraperError::ScraperAlreadyRunning);
|
||||
}
|
||||
|
||||
let rpc_client = RpcClient::new(&self.config.rpc_url)?;
|
||||
let (_, processing_rx) = unbounded_channel();
|
||||
let (req_tx, _) = channel(5);
|
||||
|
||||
// create the tasks
|
||||
let block_requester = BlockRequester::new(
|
||||
let mut block_processor = self
|
||||
.new_block_processor(req_tx.clone(), processing_rx)
|
||||
.await?
|
||||
.with_pruning(PruningOptions::nothing());
|
||||
|
||||
let block = self.rpc_client.get_basic_block_details(height).await?;
|
||||
|
||||
block_processor.process_block(block.into()).await
|
||||
}
|
||||
|
||||
pub async fn process_block_range(
|
||||
&self,
|
||||
starting_height: Option<u32>,
|
||||
end_height: Option<u32>,
|
||||
) -> Result<(), ScraperError> {
|
||||
if !self.task_tracker.is_empty() {
|
||||
return Err(ScraperError::ScraperAlreadyRunning);
|
||||
}
|
||||
|
||||
let (_, processing_rx) = unbounded_channel();
|
||||
let (req_tx, _) = channel(5);
|
||||
|
||||
let mut block_processor = self
|
||||
.new_block_processor(req_tx.clone(), processing_rx)
|
||||
.await?
|
||||
.with_pruning(PruningOptions::nothing());
|
||||
|
||||
let current_height = self.rpc_client.current_block_height().await? as u32;
|
||||
let last_processed = block_processor.last_process_height();
|
||||
|
||||
let starting_height = match starting_height {
|
||||
// always attempt to use whatever the user has provided
|
||||
Some(explicit) => explicit,
|
||||
None => {
|
||||
// otherwise, attempt to resume where we last stopped
|
||||
// and if we haven't processed anything, start from the current height
|
||||
if last_processed != 0 {
|
||||
last_processed
|
||||
} else {
|
||||
current_height
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
let end_height = match end_height {
|
||||
// always attempt to use whatever the user has provided
|
||||
Some(explicit) => explicit,
|
||||
None => {
|
||||
// otherwise, attempt to either go from the start height to the height right
|
||||
// before the final processed block held in the storage (in case there are gaps)
|
||||
// or finally, just go to the current block height
|
||||
if last_processed > starting_height {
|
||||
last_processed - 1
|
||||
} else {
|
||||
current_height
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
info!(
|
||||
starting_height = starting_height,
|
||||
end_height = end_height,
|
||||
"attempting to process block range"
|
||||
);
|
||||
|
||||
let range = (starting_height..=end_height).collect::<Vec<_>>();
|
||||
|
||||
// the most likely bottleneck here are going to be the chain queries,
|
||||
// so batch multiple requests
|
||||
for batch in range.chunks(4) {
|
||||
let batch_result = join_all(
|
||||
batch
|
||||
.iter()
|
||||
.map(|height| self.rpc_client.get_basic_block_details(*height)),
|
||||
)
|
||||
.await;
|
||||
for result in batch_result {
|
||||
match result {
|
||||
Ok(block) => block_processor.process_block(block.into()).await?,
|
||||
Err(err) => {
|
||||
error!("failed to retrieve the block: {err}. stopping...");
|
||||
return Err(err);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn new_block_requester(
|
||||
&self,
|
||||
req_rx: Receiver<BlockRequest>,
|
||||
processing_tx: UnboundedSender<BlockToProcess>,
|
||||
) -> BlockRequester {
|
||||
BlockRequester::new(
|
||||
self.cancel_token.clone(),
|
||||
rpc_client.clone(),
|
||||
self.rpc_client.clone(),
|
||||
req_rx,
|
||||
processing_tx.clone(),
|
||||
);
|
||||
let block_processor = BlockProcessor::new(
|
||||
)
|
||||
}
|
||||
|
||||
async fn new_block_processor(
|
||||
&self,
|
||||
req_tx: Sender<BlockRequest>,
|
||||
processing_rx: UnboundedReceiver<BlockToProcess>,
|
||||
) -> Result<BlockProcessor, ScraperError> {
|
||||
BlockProcessor::new(
|
||||
self.config.pruning_options,
|
||||
self.cancel_token.clone(),
|
||||
self.startup_sync.clone(),
|
||||
processing_rx,
|
||||
req_tx,
|
||||
self.storage.clone(),
|
||||
rpc_client,
|
||||
self.rpc_client.clone(),
|
||||
)
|
||||
.await?;
|
||||
let chain_subscriber = ChainSubscriber::new(
|
||||
.await
|
||||
}
|
||||
|
||||
async fn new_chain_subscriber(
|
||||
&self,
|
||||
processing_tx: UnboundedSender<BlockToProcess>,
|
||||
) -> Result<ChainSubscriber, ScraperError> {
|
||||
ChainSubscriber::new(
|
||||
&self.config.websocket_url,
|
||||
self.cancel_token.clone(),
|
||||
self.task_tracker.clone(),
|
||||
processing_tx,
|
||||
)
|
||||
.await?;
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn start(&self) -> Result<(), ScraperError> {
|
||||
let (processing_tx, processing_rx) = unbounded_channel();
|
||||
let (req_tx, req_rx) = channel(5);
|
||||
|
||||
// create the tasks
|
||||
let block_requester = self.new_block_requester(req_rx, processing_tx.clone());
|
||||
let block_processor = self.new_block_processor(req_tx, processing_rx).await?;
|
||||
let chain_subscriber = self.new_chain_subscriber(processing_tx).await?;
|
||||
|
||||
// spawn them
|
||||
self.start_tasks(block_requester, block_processor, chain_subscriber);
|
||||
|
||||
@@ -16,7 +16,7 @@ use url::Url;
|
||||
|
||||
const MAX_FAILURES: usize = 10;
|
||||
const MAX_RECONNECTION_ATTEMPTS: usize = 8;
|
||||
const SOCKET_FAILURE_RESET: Duration = Duration::hours(2);
|
||||
const SOCKET_FAILURE_RESET: Duration = Duration::minutes(15);
|
||||
|
||||
pub struct ChainSubscriber {
|
||||
cancel: CancellationToken,
|
||||
|
||||
@@ -435,9 +435,12 @@ where
|
||||
trace!("update_last_processed");
|
||||
let start = Instant::now();
|
||||
|
||||
sqlx::query!("UPDATE metadata SET last_processed_height = ?", height)
|
||||
.execute(executor)
|
||||
.await?;
|
||||
sqlx::query!(
|
||||
"UPDATE metadata SET last_processed_height = MAX(last_processed_height, ?)",
|
||||
height
|
||||
)
|
||||
.execute(executor)
|
||||
.await?;
|
||||
log_db_operation_time("update_last_processed", start);
|
||||
|
||||
Ok(())
|
||||
|
||||
@@ -13,11 +13,13 @@ license.workspace = true
|
||||
[dependencies]
|
||||
|
||||
serde = { workspace = true }
|
||||
hex = { workspace = true, optional = true }
|
||||
bs58 = { workspace = true, optional = true }
|
||||
base64 = { workspace = true, optional = true }
|
||||
time = { workspace = true, features = ["formatting", "parsing"], optional = true }
|
||||
|
||||
[features]
|
||||
hex = ["dep:hex"]
|
||||
bs58 = ["dep:bs58"]
|
||||
base64 = ["dep:base64"]
|
||||
date = ["time"]
|
||||
@@ -32,6 +32,20 @@ pub mod bs58 {
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "hex")]
|
||||
pub mod hex {
|
||||
use serde::{Deserialize, Deserializer, Serializer};
|
||||
|
||||
pub fn serialize<S: Serializer>(bytes: &[u8], serializer: S) -> Result<S::Ok, S::Error> {
|
||||
serializer.serialize_str(&::hex::encode(bytes))
|
||||
}
|
||||
|
||||
pub fn deserialize<'de, D: Deserializer<'de>>(deserializer: D) -> Result<Vec<u8>, D::Error> {
|
||||
let s = String::deserialize(deserializer)?;
|
||||
::hex::decode(&s).map_err(serde::de::Error::custom)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "date")]
|
||||
pub mod date {
|
||||
use serde::ser::Error;
|
||||
|
||||
@@ -114,6 +114,7 @@ pub async fn start_wireguard<St: nym_gateway_storage::Storage + Clone + 'static>
|
||||
address: wireguard_data.inner.config().private_ip.to_string(),
|
||||
port: wireguard_data.inner.config().announced_port as u32,
|
||||
peers,
|
||||
mtu: None,
|
||||
};
|
||||
wg_api.configure_interface(&interface_config)?;
|
||||
|
||||
|
||||
@@ -226,7 +226,7 @@ impl<St: Storage + Clone + 'static> PeerController<St> {
|
||||
.read()
|
||||
.await
|
||||
.available_bandwidth()
|
||||
.bytes as u64
|
||||
.await
|
||||
} else {
|
||||
let peer = self
|
||||
.host_information
|
||||
@@ -236,7 +236,7 @@ impl<St: Storage + Clone + 'static> PeerController<St> {
|
||||
.get(key)
|
||||
.ok_or(Error::PeerMismatch)?
|
||||
.clone();
|
||||
BANDWIDTH_CAP_PER_DAY.saturating_sub(peer.rx_bytes + peer.tx_bytes)
|
||||
BANDWIDTH_CAP_PER_DAY.saturating_sub((peer.rx_bytes + peer.tx_bytes) as i64)
|
||||
};
|
||||
|
||||
Ok(Some(RemainingBandwidthData {
|
||||
|
||||
Generated
+8
-6
@@ -1283,6 +1283,7 @@ dependencies = [
|
||||
"nym-crypto",
|
||||
"nym-mixnet-contract-common",
|
||||
"nym-vesting-contract-common",
|
||||
"rand",
|
||||
"rand_chacha",
|
||||
"serde",
|
||||
"thiserror",
|
||||
@@ -1748,11 +1749,12 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "serde_json"
|
||||
version = "1.0.116"
|
||||
version = "1.0.128"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3e17db7126d17feb94eb3fad46bf1a96b034e8aacbc2e775fe81505f8b0b2813"
|
||||
checksum = "6ff5456707a1de34e7e37f2a6fd3d3f808c318259cbd01ab6377795054b483d8"
|
||||
dependencies = [
|
||||
"itoa",
|
||||
"memchr",
|
||||
"ryu",
|
||||
"serde",
|
||||
]
|
||||
@@ -1935,18 +1937,18 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "thiserror"
|
||||
version = "1.0.63"
|
||||
version = "1.0.64"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c0342370b38b6a11b6cc11d6a805569958d54cfa061a29969c3b5ce2ea405724"
|
||||
checksum = "d50af8abc119fb8bb6dbabcfa89656f46f84aa0ac7688088608076ad2b459a84"
|
||||
dependencies = [
|
||||
"thiserror-impl",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "thiserror-impl"
|
||||
version = "1.0.63"
|
||||
version = "1.0.64"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a4558b58466b9ad7ca0f102865eccc95938dca1a74a856f2b57b6629050da261"
|
||||
checksum = "08904e7672f5eb876eaaf87e0ce17857500934f4981c4a0ab2b4aa98baac7fc3"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
|
||||
@@ -308,6 +308,7 @@ pub fn instantiate_contracts(
|
||||
mixnet_contract_address.clone(),
|
||||
&nym_mixnet_contract_common::MigrateMsg {
|
||||
vesting_contract_address: Some(vesting_contract_address.to_string()),
|
||||
fix_nodes: None,
|
||||
},
|
||||
mixnet_code_id,
|
||||
)
|
||||
|
||||
@@ -45,6 +45,7 @@ time = { version = "0.3", features = ["macros"] }
|
||||
|
||||
[dev-dependencies]
|
||||
rand_chacha = "0.3"
|
||||
rand = "0.8.5"
|
||||
nym-crypto = { path = "../../common/crypto", features = ["asymmetric", "rand"] }
|
||||
|
||||
[features]
|
||||
|
||||
@@ -2150,7 +2150,7 @@
|
||||
"minimum": 0.0
|
||||
},
|
||||
"owner": {
|
||||
"description": "The address of the owner of the the mixnodes used for the query.",
|
||||
"description": "The address of the owner of the mixnodes used for the query.",
|
||||
"type": "string"
|
||||
},
|
||||
"start_after": {
|
||||
@@ -2961,6 +2961,15 @@
|
||||
"title": "MigrateMsg",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"fix_nodes": {
|
||||
"type": [
|
||||
"array",
|
||||
"null"
|
||||
],
|
||||
"items": {
|
||||
"$ref": "#/definitions/AffectedNode"
|
||||
}
|
||||
},
|
||||
"vesting_contract_address": {
|
||||
"type": [
|
||||
"string",
|
||||
@@ -2968,7 +2977,50 @@
|
||||
]
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
"additionalProperties": false,
|
||||
"definitions": {
|
||||
"AffectedDelegator": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"address",
|
||||
"missing_ratio"
|
||||
],
|
||||
"properties": {
|
||||
"address": {
|
||||
"type": "string"
|
||||
},
|
||||
"missing_ratio": {
|
||||
"$ref": "#/definitions/Decimal"
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
},
|
||||
"AffectedNode": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"delegators",
|
||||
"mix_id"
|
||||
],
|
||||
"properties": {
|
||||
"delegators": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/definitions/AffectedDelegator"
|
||||
}
|
||||
},
|
||||
"mix_id": {
|
||||
"type": "integer",
|
||||
"format": "uint32",
|
||||
"minimum": 0.0
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
},
|
||||
"Decimal": {
|
||||
"description": "A fixed-point decimal value with 18 fractional digits, i.e. Decimal(1_000_000_000_000_000_000) == 1.0\n\nThe greatest possible value that can be represented is 340282366920938463463.374607431768211455 (which is (2^128 - 1) / 10^18)",
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"sudo": null,
|
||||
"responses": {
|
||||
|
||||
@@ -3,6 +3,15 @@
|
||||
"title": "MigrateMsg",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"fix_nodes": {
|
||||
"type": [
|
||||
"array",
|
||||
"null"
|
||||
],
|
||||
"items": {
|
||||
"$ref": "#/definitions/AffectedNode"
|
||||
}
|
||||
},
|
||||
"vesting_contract_address": {
|
||||
"type": [
|
||||
"string",
|
||||
@@ -10,5 +19,48 @@
|
||||
]
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
"additionalProperties": false,
|
||||
"definitions": {
|
||||
"AffectedDelegator": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"address",
|
||||
"missing_ratio"
|
||||
],
|
||||
"properties": {
|
||||
"address": {
|
||||
"type": "string"
|
||||
},
|
||||
"missing_ratio": {
|
||||
"$ref": "#/definitions/Decimal"
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
},
|
||||
"AffectedNode": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"delegators",
|
||||
"mix_id"
|
||||
],
|
||||
"properties": {
|
||||
"delegators": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/definitions/AffectedDelegator"
|
||||
}
|
||||
},
|
||||
"mix_id": {
|
||||
"type": "integer",
|
||||
"format": "uint32",
|
||||
"minimum": 0.0
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
},
|
||||
"Decimal": {
|
||||
"description": "A fixed-point decimal value with 18 fractional digits, i.e. Decimal(1_000_000_000_000_000_000) == 1.0\n\nThe greatest possible value that can be represented is 340282366920938463463.374607431768211455 (which is (2^128 - 1) / 10^18)",
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -438,7 +438,7 @@
|
||||
"minimum": 0.0
|
||||
},
|
||||
"owner": {
|
||||
"description": "The address of the owner of the the mixnodes used for the query.",
|
||||
"description": "The address of the owner of the mixnodes used for the query.",
|
||||
"type": "string"
|
||||
},
|
||||
"start_after": {
|
||||
|
||||
@@ -5,6 +5,7 @@ use crate::constants::{INITIAL_GATEWAY_PLEDGE_AMOUNT, INITIAL_MIXNODE_PLEDGE_AMO
|
||||
use crate::interval::storage as interval_storage;
|
||||
use crate::mixnet_contract_settings::storage as mixnet_params_storage;
|
||||
use crate::mixnodes::storage as mixnode_storage;
|
||||
use crate::queued_migrations;
|
||||
use crate::rewards::storage as rewards_storage;
|
||||
use cosmwasm_std::{
|
||||
entry_point, to_binary, Addr, Coin, Deps, DepsMut, Env, MessageInfo, QueryResponse, Response,
|
||||
@@ -272,7 +273,7 @@ pub fn execute(
|
||||
crate::vesting_migration::try_migrate_vested_mixnode(deps, info)
|
||||
}
|
||||
ExecuteMsg::MigrateVestedDelegation { mix_id } => {
|
||||
crate::vesting_migration::try_migrate_vested_delegation(deps, info, mix_id)
|
||||
crate::vesting_migration::try_migrate_vested_delegation(deps, env, info, mix_id)
|
||||
}
|
||||
|
||||
// legacy vesting
|
||||
@@ -538,15 +539,13 @@ pub fn query(
|
||||
|
||||
#[entry_point]
|
||||
pub fn migrate(
|
||||
mut deps: DepsMut<'_>,
|
||||
_env: Env,
|
||||
deps: DepsMut<'_>,
|
||||
env: Env,
|
||||
msg: MigrateMsg,
|
||||
) -> Result<Response, MixnetContractError> {
|
||||
set_build_information!(deps.storage)?;
|
||||
cw2::ensure_from_older_version(deps.storage, CONTRACT_NAME, CONTRACT_VERSION)?;
|
||||
|
||||
crate::queued_migrations::explicit_contract_admin(deps.branch())?;
|
||||
|
||||
// due to circular dependency on contract addresses (i.e. mixnet contract requiring vesting contract address
|
||||
// and vesting contract requiring the mixnet contract address), if we ever want to deploy any new fresh
|
||||
// environment, one of the contracts will HAVE TO go through a migration
|
||||
@@ -557,7 +556,13 @@ pub fn migrate(
|
||||
mixnet_params_storage::CONTRACT_STATE.save(deps.storage, ¤t_state)?;
|
||||
}
|
||||
|
||||
Ok(Default::default())
|
||||
let mut response = Response::new();
|
||||
|
||||
if let Some(nodes_to_fix) = msg.fix_nodes {
|
||||
queued_migrations::restore_vested_delegations(&mut response, deps, env, nodes_to_fix)?;
|
||||
}
|
||||
|
||||
Ok(response)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
|
||||
@@ -1,20 +1,669 @@
|
||||
// Copyright 2022-2023 - Nym Technologies SA <contact@nymtech.net>
|
||||
// Copyright 2022-2024 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
use crate::mixnet_contract_settings::storage as mixnet_params_storage;
|
||||
use cosmwasm_std::DepsMut;
|
||||
use crate::delegations::storage::delegations;
|
||||
use crate::rewards::storage::MIXNODE_REWARDING;
|
||||
use cosmwasm_std::{Addr, Decimal, DepsMut, Env, Event, Order, Response};
|
||||
use mixnet_contract_common::error::MixnetContractError;
|
||||
use mixnet_contract_common::helpers::IntoBaseDecimal;
|
||||
use mixnet_contract_common::rewarding::helpers::truncate_reward;
|
||||
use mixnet_contract_common::{AffectedNode, Delegation};
|
||||
use std::collections::BTreeMap;
|
||||
|
||||
pub(crate) fn explicit_contract_admin(deps: DepsMut) -> Result<(), MixnetContractError> {
|
||||
// we need to read the deprecated field to migrate it over
|
||||
#[allow(deprecated)]
|
||||
// SAFETY: this value should ALWAYS exist on the first execution of this migration;
|
||||
// as a matter of fact, it should ALWAYS continue existing until another migration
|
||||
#[allow(clippy::expect_used)]
|
||||
let existing_admin = mixnet_params_storage::CONTRACT_STATE
|
||||
.load(deps.storage)?
|
||||
.owner
|
||||
.expect("the contract state is corrupt - there's no admin set");
|
||||
mixnet_params_storage::ADMIN.set(deps, Some(existing_admin))?;
|
||||
fn fix_affected_node(
|
||||
response: &mut Response,
|
||||
deps: DepsMut<'_>,
|
||||
env: &Env,
|
||||
node: AffectedNode,
|
||||
) -> Result<(), MixnetContractError> {
|
||||
let total_ratio = node.total_ratio();
|
||||
let one = Decimal::one();
|
||||
|
||||
// the total ratio has to be equal to 1 (or be extremely close to it, because it can be affected by rounding)
|
||||
// if it doesn't it means we passed an invalid migrate msg and we HAVE TO fail the migration if that's the case
|
||||
let epsilon = Decimal::from_ratio(1u128, 100_000_000u128);
|
||||
|
||||
if total_ratio > one {
|
||||
if total_ratio - one >= epsilon {
|
||||
return Err(MixnetContractError::FailedMigration {
|
||||
comment: format!(
|
||||
"the total delegation ratio for node {} does not sum up to 1",
|
||||
node.mix_id
|
||||
),
|
||||
});
|
||||
}
|
||||
} else if one - total_ratio >= epsilon {
|
||||
return Err(MixnetContractError::FailedMigration {
|
||||
comment: format!(
|
||||
"the total delegation ratio for node {} does not sum up to 1",
|
||||
node.mix_id
|
||||
),
|
||||
});
|
||||
}
|
||||
|
||||
let mut total_accounted_for = Decimal::zero();
|
||||
let mut mix_rewarding = MIXNODE_REWARDING.load(deps.storage, node.mix_id)?;
|
||||
|
||||
let mut cached_delegations = BTreeMap::new();
|
||||
|
||||
// determine all the stake accounted for, i.e. all delegations and their pending rewards
|
||||
for entry in delegations()
|
||||
.idx
|
||||
.mixnode
|
||||
.prefix(node.mix_id)
|
||||
.range(deps.storage, None, None, Order::Ascending)
|
||||
.map(|record| record.map(|r| r.1))
|
||||
{
|
||||
let delegation = entry?;
|
||||
let base_delegation = delegation.dec_amount()?;
|
||||
let pending_reward = mix_rewarding.determine_delegation_reward(&delegation)?;
|
||||
|
||||
// cache the delegation and reward for the lookup in the next loop
|
||||
if node
|
||||
.delegators
|
||||
.iter()
|
||||
.any(|d| d.address == delegation.owner.as_str())
|
||||
{
|
||||
cached_delegations.insert(delegation.owner.to_string(), (delegation, pending_reward));
|
||||
}
|
||||
|
||||
total_accounted_for += base_delegation;
|
||||
total_accounted_for += pending_reward;
|
||||
}
|
||||
|
||||
// sanity check
|
||||
assert!(cached_delegations.len() <= node.delegators.len());
|
||||
|
||||
// the missing stake equals to the difference between total node delegation (which includes all rewards, etc.)
|
||||
// and the value we managed to just account for
|
||||
let node_missing = mix_rewarding.delegates - total_accounted_for;
|
||||
|
||||
let mut distributed = Decimal::zero();
|
||||
|
||||
// finally split the missing stake among the affected delegators according to the ratios
|
||||
// provided in the migration which were very painstakingly determined by scraping different
|
||||
// sources of chain data
|
||||
for delegator in node.delegators {
|
||||
let restored = node_missing * delegator.missing_ratio;
|
||||
distributed += restored;
|
||||
|
||||
// we have two scenarios to cover here:
|
||||
// 1. somebody performed vested migration and then undelegated the tokens (*sigh*)
|
||||
// - in that case we have to create brand-new delegation with the restored amount
|
||||
// 2. the delegation still exists
|
||||
// - in that case we have to increase the existing delegation. essentially treat it as if somebody delegated extra tokens
|
||||
|
||||
if let Some((old_liquid_delegation, pending_reward)) =
|
||||
cached_delegations.remove(&delegator.address)
|
||||
{
|
||||
// delegation still exists
|
||||
|
||||
assert!(old_liquid_delegation.proxy.is_none());
|
||||
|
||||
let old_liquid = old_liquid_delegation.dec_amount()? + pending_reward;
|
||||
let updated_amount_dec = old_liquid + restored;
|
||||
let updated_amount =
|
||||
truncate_reward(updated_amount_dec, &old_liquid_delegation.amount.denom);
|
||||
|
||||
// take the truncation into consideration for the purposes of future accounting
|
||||
let truncated_delta = updated_amount_dec - updated_amount.amount.into_base_decimal()?;
|
||||
mix_rewarding.delegates -= truncated_delta;
|
||||
|
||||
// just emit EVERYTHING we can. just in case
|
||||
response.events.push(
|
||||
Event::new("delegation_restoration")
|
||||
.add_attribute("delegator", delegator.address)
|
||||
.add_attribute("delegator_ratio", delegator.missing_ratio.to_string())
|
||||
.add_attribute("mix_id", node.mix_id.to_string())
|
||||
.add_attribute("restored_amount_dec", restored.to_string())
|
||||
.add_attribute("node_delegates", mix_rewarding.delegates.to_string())
|
||||
.add_attribute("total_node_delegations", total_accounted_for.to_string())
|
||||
.add_attribute("total_missing_delegations", node_missing.to_string())
|
||||
.add_attribute("updated_amount_dec", updated_amount_dec.to_string())
|
||||
.add_attribute("updated_amount", updated_amount.to_string())
|
||||
.add_attribute("liquid_delegation_existed", "true")
|
||||
.add_attribute(
|
||||
"old_liquid_delegation_unit_reward",
|
||||
old_liquid_delegation.cumulative_reward_ratio.to_string(),
|
||||
)
|
||||
.add_attribute(
|
||||
"old_liquid_delegation_amount",
|
||||
old_liquid_delegation.amount.to_string(),
|
||||
)
|
||||
.add_attribute(
|
||||
"old_liquid_delegation_pending_reward",
|
||||
pending_reward.to_string(),
|
||||
)
|
||||
.add_attribute("truncated_amount", truncated_delta.to_string()),
|
||||
);
|
||||
|
||||
// create new delegation with the updated amount
|
||||
// and also, what's very important, with correct unit reward amount
|
||||
let updated_delegation = Delegation::new(
|
||||
old_liquid_delegation.owner.clone(),
|
||||
node.mix_id,
|
||||
mix_rewarding.total_unit_reward,
|
||||
updated_amount,
|
||||
env.block.height,
|
||||
);
|
||||
|
||||
// replace the value stored under the existing key
|
||||
let delegation_storage_key = old_liquid_delegation.storage_key();
|
||||
delegations().replace(
|
||||
deps.storage,
|
||||
delegation_storage_key,
|
||||
Some(&updated_delegation),
|
||||
Some(&old_liquid_delegation),
|
||||
)?;
|
||||
} else {
|
||||
let restored_amount = truncate_reward(restored, "unym");
|
||||
|
||||
// take the truncation into consideration for the purposes of future accounting
|
||||
let truncated_delta = restored - restored_amount.amount.into_base_decimal()?;
|
||||
mix_rewarding.delegates -= truncated_delta;
|
||||
|
||||
// delegation is now gone - create a new one with the restored amount
|
||||
let delegation = Delegation::new(
|
||||
Addr::unchecked(&delegator.address),
|
||||
node.mix_id,
|
||||
mix_rewarding.total_unit_reward,
|
||||
restored_amount,
|
||||
env.block.height,
|
||||
);
|
||||
|
||||
let delegation_storage_key = delegation.storage_key();
|
||||
delegations().save(deps.storage, delegation_storage_key, &delegation)?;
|
||||
|
||||
response.events.push(
|
||||
Event::new("delegation_restoration")
|
||||
.add_attribute("delegator", delegator.address)
|
||||
.add_attribute("delegator_ratio", delegator.missing_ratio.to_string())
|
||||
.add_attribute("mix_id", node.mix_id.to_string())
|
||||
.add_attribute("restored_amount_dec", restored.to_string())
|
||||
.add_attribute("node_delegates", mix_rewarding.delegates.to_string())
|
||||
.add_attribute("total_node_delegations", total_accounted_for.to_string())
|
||||
.add_attribute("total_missing_delegations", node_missing.to_string())
|
||||
.add_attribute("updated_amount_dec", restored.to_string())
|
||||
.add_attribute("updated_amount", delegation.amount.to_string())
|
||||
.add_attribute("liquid_delegation_existed", "false")
|
||||
.add_attribute("truncated_amount", truncated_delta.to_string()),
|
||||
);
|
||||
}
|
||||
|
||||
// the vested and liquid delegations got combined into one
|
||||
mix_rewarding.unique_delegations -= 1;
|
||||
MIXNODE_REWARDING.save(deps.storage, node.mix_id, &mix_rewarding)?;
|
||||
}
|
||||
|
||||
response.events.push(
|
||||
Event::new("node_delegation_restoration")
|
||||
.add_attribute("mix_id", node.mix_id.to_string())
|
||||
.add_attribute("node_delegates", mix_rewarding.delegates.to_string())
|
||||
.add_attribute("total_node_delegations", total_accounted_for.to_string())
|
||||
.add_attribute("total_missing_delegations", node_missing.to_string())
|
||||
.add_attribute("total_redistributed", distributed.to_string()),
|
||||
);
|
||||
|
||||
// another sanity check
|
||||
assert!(distributed <= node_missing);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn restore_vested_delegations(
|
||||
response: &mut Response,
|
||||
mut deps: DepsMut<'_>,
|
||||
env: Env,
|
||||
affected_nodes: Vec<AffectedNode>,
|
||||
) -> Result<(), MixnetContractError> {
|
||||
for node in affected_nodes {
|
||||
fix_affected_node(response, deps.branch(), &env, node)?
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[cfg(test)]
|
||||
mod restoring_vested_delegations {
|
||||
use super::*;
|
||||
use crate::support::tests::test_helpers::{assert_eq_with_leeway, TestSetup};
|
||||
use crate::vesting_migration::try_migrate_vested_delegation;
|
||||
use cosmwasm_std::testing::mock_info;
|
||||
use cosmwasm_std::Uint128;
|
||||
use mixnet_contract_common::reward_params::Performance;
|
||||
use mixnet_contract_common::rewarding::helpers::truncate_reward_amount;
|
||||
use mixnet_contract_common::AffectedDelegator;
|
||||
use nym_contracts_common::truncate_decimal;
|
||||
use rand::RngCore;
|
||||
|
||||
#[test]
|
||||
fn for_node_with_single_affected_delegator_without_undelegating() {
|
||||
let mut test = TestSetup::new_complex();
|
||||
|
||||
let problematic_delegator = "n1foomp";
|
||||
let problematic_delegator_twin = "n1bar";
|
||||
let mix_id = 4;
|
||||
|
||||
// "accidentally" overwrite the delegation
|
||||
let liquid_storage_key = Delegation::generate_storage_key(
|
||||
mix_id,
|
||||
&Addr::unchecked(problematic_delegator),
|
||||
None,
|
||||
);
|
||||
let vested_storage_key = Delegation::generate_storage_key(
|
||||
mix_id,
|
||||
&Addr::unchecked(problematic_delegator),
|
||||
Some(&test.vesting_contract()),
|
||||
);
|
||||
let vested_delegation = delegations()
|
||||
.load(test.deps().storage, vested_storage_key.clone())
|
||||
.unwrap();
|
||||
let mut bad_liquid_delegation = vested_delegation.clone();
|
||||
bad_liquid_delegation.proxy = None;
|
||||
|
||||
delegations()
|
||||
.remove(test.deps_mut().storage, vested_storage_key)
|
||||
.unwrap();
|
||||
delegations()
|
||||
.save(
|
||||
test.deps_mut().storage,
|
||||
liquid_storage_key,
|
||||
&bad_liquid_delegation,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
// go through few rewarding cycles...
|
||||
let all_nodes = test.all_mixnodes();
|
||||
for _ in 0..100 {
|
||||
test.skip_to_next_epoch_end();
|
||||
test.force_change_rewarded_set(all_nodes.clone());
|
||||
test.start_epoch_transition();
|
||||
|
||||
// reward each node
|
||||
for node in &all_nodes {
|
||||
let performance = test.rng.next_u64() % 100;
|
||||
test.reward_with_distribution(
|
||||
*node,
|
||||
Performance::from_percentage_value(performance).unwrap(),
|
||||
);
|
||||
}
|
||||
|
||||
test.set_epoch_in_progress_state();
|
||||
}
|
||||
|
||||
// restoring problematic delegator should be equivalent to the delegator twin just migrating
|
||||
let env = test.env();
|
||||
fix_affected_node(
|
||||
&mut Response::new(),
|
||||
test.deps_mut(),
|
||||
&env,
|
||||
AffectedNode {
|
||||
mix_id,
|
||||
delegators: vec![AffectedDelegator {
|
||||
address: problematic_delegator.to_string(),
|
||||
missing_ratio: Decimal::one(),
|
||||
}],
|
||||
},
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
try_migrate_vested_delegation(
|
||||
test.deps_mut(),
|
||||
env,
|
||||
mock_info(problematic_delegator_twin, &[]),
|
||||
mix_id,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let liquid_storage_key = Delegation::generate_storage_key(
|
||||
mix_id,
|
||||
&Addr::unchecked(problematic_delegator),
|
||||
None,
|
||||
);
|
||||
let liquid_storage_key_twin = Delegation::generate_storage_key(
|
||||
mix_id,
|
||||
&Addr::unchecked(problematic_delegator_twin),
|
||||
None,
|
||||
);
|
||||
|
||||
let liquid_delegation = delegations()
|
||||
.load(test.deps().storage, liquid_storage_key)
|
||||
.unwrap();
|
||||
let liquid_delegation_alt = delegations()
|
||||
.load(test.deps().storage, liquid_storage_key_twin)
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
liquid_delegation.cumulative_reward_ratio,
|
||||
liquid_delegation_alt.cumulative_reward_ratio
|
||||
);
|
||||
assert_eq_with_leeway(
|
||||
liquid_delegation.amount.amount,
|
||||
liquid_delegation_alt.amount.amount,
|
||||
Uint128::one(),
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn for_node_with_single_affected_delegator_after_undelegating() {
|
||||
let mut test = TestSetup::new_complex();
|
||||
|
||||
let problematic_delegator = "n1foomp";
|
||||
let problematic_delegator_twin = "n1bar";
|
||||
let mix_id = 4;
|
||||
|
||||
// "accidentally" overwrite the delegation
|
||||
let liquid_storage_key = Delegation::generate_storage_key(
|
||||
mix_id,
|
||||
&Addr::unchecked(problematic_delegator),
|
||||
None,
|
||||
);
|
||||
let vested_storage_key = Delegation::generate_storage_key(
|
||||
mix_id,
|
||||
&Addr::unchecked(problematic_delegator),
|
||||
Some(&test.vesting_contract()),
|
||||
);
|
||||
let vested_delegation = delegations()
|
||||
.load(test.deps().storage, vested_storage_key.clone())
|
||||
.unwrap();
|
||||
let mut bad_liquid_delegation = vested_delegation.clone();
|
||||
bad_liquid_delegation.proxy = None;
|
||||
|
||||
delegations()
|
||||
.remove(test.deps_mut().storage, vested_storage_key)
|
||||
.unwrap();
|
||||
delegations()
|
||||
.save(
|
||||
test.deps_mut().storage,
|
||||
liquid_storage_key,
|
||||
&bad_liquid_delegation,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
// go through few rewarding cycles...
|
||||
let all_nodes = test.all_mixnodes();
|
||||
for _ in 0..100 {
|
||||
test.skip_to_next_epoch_end();
|
||||
test.force_change_rewarded_set(all_nodes.clone());
|
||||
test.start_epoch_transition();
|
||||
|
||||
// reward each node
|
||||
for node in &all_nodes {
|
||||
let performance = test.rng.next_u64() % 100;
|
||||
test.reward_with_distribution(
|
||||
*node,
|
||||
Performance::from_percentage_value(performance).unwrap(),
|
||||
);
|
||||
}
|
||||
|
||||
test.set_epoch_in_progress_state();
|
||||
}
|
||||
|
||||
// they got scared and undelegated (the removed part is their vested delegation)
|
||||
test.remove_immediate_delegation(problematic_delegator, mix_id);
|
||||
|
||||
// go through some more rewarding
|
||||
for _ in 0..100 {
|
||||
test.skip_to_next_epoch_end();
|
||||
test.force_change_rewarded_set(all_nodes.clone());
|
||||
test.start_epoch_transition();
|
||||
|
||||
// reward each node
|
||||
for node in &all_nodes {
|
||||
let performance = test.rng.next_u64() % 100;
|
||||
test.reward_with_distribution(
|
||||
*node,
|
||||
Performance::from_percentage_value(performance).unwrap(),
|
||||
);
|
||||
}
|
||||
|
||||
test.set_epoch_in_progress_state();
|
||||
}
|
||||
|
||||
// the restored amount should be equivalent to the liquid part (+ rewards) of the twin delegator
|
||||
let env = test.env();
|
||||
fix_affected_node(
|
||||
&mut Response::new(),
|
||||
test.deps_mut(),
|
||||
&env,
|
||||
AffectedNode {
|
||||
mix_id,
|
||||
delegators: vec![AffectedDelegator {
|
||||
address: problematic_delegator.to_string(),
|
||||
missing_ratio: Decimal::one(),
|
||||
}],
|
||||
},
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let liquid_storage_key = Delegation::generate_storage_key(
|
||||
mix_id,
|
||||
&Addr::unchecked(problematic_delegator),
|
||||
None,
|
||||
);
|
||||
let liquid_storage_key_twin = Delegation::generate_storage_key(
|
||||
mix_id,
|
||||
&Addr::unchecked(problematic_delegator_twin),
|
||||
None,
|
||||
);
|
||||
|
||||
let liquid_delegation = delegations()
|
||||
.load(test.deps().storage, liquid_storage_key)
|
||||
.unwrap();
|
||||
let liquid_delegation_alt = delegations()
|
||||
.load(test.deps().storage, liquid_storage_key_twin)
|
||||
.unwrap();
|
||||
let mix_info = test.mix_rewarding(mix_id);
|
||||
let pending_twin_reward = mix_info
|
||||
.determine_delegation_reward(&liquid_delegation_alt)
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(
|
||||
liquid_delegation.cumulative_reward_ratio,
|
||||
mix_info.total_unit_reward
|
||||
);
|
||||
assert_eq_with_leeway(
|
||||
liquid_delegation.amount.amount,
|
||||
liquid_delegation_alt.amount.amount + truncate_reward_amount(pending_twin_reward),
|
||||
Uint128::one(),
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn for_node_with_multiple_affected_delegators() {
|
||||
let mut test = TestSetup::new_complex();
|
||||
|
||||
// some random delegator
|
||||
let problematic_delegator = "n1foomp";
|
||||
|
||||
// another delegator that made DIFFERENT delegations as the previous ones BUT to the same node
|
||||
let problematic_delegator_alt_twin = "n1whatever";
|
||||
|
||||
let mix_id = 4;
|
||||
let mix_info_start = test.mix_rewarding(mix_id);
|
||||
|
||||
// "accidentally" overwrite the delegations
|
||||
let liquid_storage_key1 = Delegation::generate_storage_key(
|
||||
mix_id,
|
||||
&Addr::unchecked(problematic_delegator),
|
||||
None,
|
||||
);
|
||||
let vested_storage_key1 = Delegation::generate_storage_key(
|
||||
mix_id,
|
||||
&Addr::unchecked(problematic_delegator),
|
||||
Some(&test.vesting_contract()),
|
||||
);
|
||||
let liquid_delegation1 = delegations()
|
||||
.load(test.deps().storage, liquid_storage_key1.clone())
|
||||
.unwrap();
|
||||
let vested_delegation1 = delegations()
|
||||
.load(test.deps().storage, vested_storage_key1.clone())
|
||||
.unwrap();
|
||||
|
||||
// keep track of the 'lost' tokens for test assertions
|
||||
let lost1 = liquid_delegation1.dec_amount().unwrap()
|
||||
+ mix_info_start
|
||||
.determine_delegation_reward(&liquid_delegation1)
|
||||
.unwrap();
|
||||
|
||||
let mut bad_liquid_delegation1 = vested_delegation1.clone();
|
||||
bad_liquid_delegation1.proxy = None;
|
||||
|
||||
delegations()
|
||||
.remove(test.deps_mut().storage, vested_storage_key1)
|
||||
.unwrap();
|
||||
delegations()
|
||||
.save(
|
||||
test.deps_mut().storage,
|
||||
liquid_storage_key1.clone(),
|
||||
&bad_liquid_delegation1,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let liquid_storage_key2 = Delegation::generate_storage_key(
|
||||
mix_id,
|
||||
&Addr::unchecked(problematic_delegator_alt_twin),
|
||||
None,
|
||||
);
|
||||
let vested_storage_key2 = Delegation::generate_storage_key(
|
||||
mix_id,
|
||||
&Addr::unchecked(problematic_delegator_alt_twin),
|
||||
Some(&test.vesting_contract()),
|
||||
);
|
||||
let liquid_delegation2 = delegations()
|
||||
.load(test.deps().storage, liquid_storage_key2.clone())
|
||||
.unwrap();
|
||||
let vested_delegation2 = delegations()
|
||||
.load(test.deps().storage, vested_storage_key2.clone())
|
||||
.unwrap();
|
||||
let lost2 = liquid_delegation2.dec_amount().unwrap()
|
||||
+ mix_info_start
|
||||
.determine_delegation_reward(&liquid_delegation2)
|
||||
.unwrap();
|
||||
|
||||
let mut bad_liquid_delegation2 = vested_delegation2.clone();
|
||||
bad_liquid_delegation2.proxy = None;
|
||||
|
||||
delegations()
|
||||
.remove(test.deps_mut().storage, vested_storage_key2)
|
||||
.unwrap();
|
||||
delegations()
|
||||
.save(
|
||||
test.deps_mut().storage,
|
||||
liquid_storage_key2.clone(),
|
||||
&bad_liquid_delegation2,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
// go through few rewarding cycles...
|
||||
let all_nodes = test.all_mixnodes();
|
||||
|
||||
for _ in 0..100 {
|
||||
test.skip_to_next_epoch_end();
|
||||
test.force_change_rewarded_set(all_nodes.clone());
|
||||
test.start_epoch_transition();
|
||||
|
||||
// reward each node
|
||||
for node in &all_nodes {
|
||||
let performance = test.rng.next_u64() % 100;
|
||||
test.reward_with_distribution(
|
||||
*node,
|
||||
Performance::from_percentage_value(performance).unwrap(),
|
||||
);
|
||||
}
|
||||
|
||||
test.set_epoch_in_progress_state();
|
||||
}
|
||||
|
||||
// those ratios got determined externally. in this test we unfortunately use purely artificial values
|
||||
let ratio1: Decimal = "0.45326524362".parse().unwrap();
|
||||
let ratio2 = Decimal::one() - ratio1;
|
||||
|
||||
let mix_info = test.mix_rewarding(mix_id);
|
||||
let liquid_delegation_before = delegations()
|
||||
.load(test.deps().storage, liquid_storage_key1.clone())
|
||||
.unwrap();
|
||||
let liquid_reward_before = mix_info
|
||||
.determine_delegation_reward(&liquid_delegation_before)
|
||||
.unwrap();
|
||||
|
||||
let liquid_delegation_alt_before = delegations()
|
||||
.load(test.deps().storage, liquid_storage_key2.clone())
|
||||
.unwrap();
|
||||
let liquid_reward_alt_before = mix_info
|
||||
.determine_delegation_reward(&liquid_delegation_alt_before)
|
||||
.unwrap();
|
||||
|
||||
let env = test.env();
|
||||
let mut res = Response::new();
|
||||
fix_affected_node(
|
||||
&mut res,
|
||||
test.deps_mut(),
|
||||
&env,
|
||||
AffectedNode {
|
||||
mix_id,
|
||||
delegators: vec![
|
||||
AffectedDelegator {
|
||||
address: problematic_delegator.to_string(),
|
||||
missing_ratio: ratio1,
|
||||
},
|
||||
AffectedDelegator {
|
||||
address: problematic_delegator_alt_twin.to_string(),
|
||||
missing_ratio: ratio2,
|
||||
},
|
||||
],
|
||||
},
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let liquid_delegation = delegations()
|
||||
.load(test.deps().storage, liquid_storage_key1)
|
||||
.unwrap();
|
||||
let liquid_delegation_alt = delegations()
|
||||
.load(test.deps().storage, liquid_storage_key2)
|
||||
.unwrap();
|
||||
|
||||
// the total amount recovered must be equal to what has been lost (approximately)
|
||||
let total_lost = lost1 + lost2;
|
||||
// determine the compounded rewards on the lost tokens
|
||||
// (just unroll `MixNodeRewarding::determine_delegation_reward(...)`)
|
||||
let starting_ratio = mix_info_start.total_unit_reward;
|
||||
let ending_ratio = mix_info.full_reward_ratio();
|
||||
let adjust = starting_ratio + mix_info.unit_delegation;
|
||||
let compounded_lost_reward = (ending_ratio - starting_ratio) * total_lost / adjust;
|
||||
|
||||
let before = liquid_delegation_before.dec_amount().unwrap()
|
||||
+ liquid_delegation_alt_before.dec_amount().unwrap()
|
||||
+ liquid_reward_before
|
||||
+ liquid_reward_alt_before;
|
||||
|
||||
let after = liquid_delegation.amount.amount + liquid_delegation_alt.amount.amount;
|
||||
let expected_before = truncate_decimal(total_lost + compounded_lost_reward + before);
|
||||
|
||||
assert_eq_with_leeway(after, expected_before, Uint128::one());
|
||||
|
||||
test.ensure_delegation_sync(mix_id);
|
||||
|
||||
// more rewarding
|
||||
for _ in 0..100 {
|
||||
test.skip_to_next_epoch_end();
|
||||
test.force_change_rewarded_set(all_nodes.clone());
|
||||
test.start_epoch_transition();
|
||||
|
||||
// reward each node
|
||||
for node in &all_nodes {
|
||||
let performance = test.rng.next_u64() % 100;
|
||||
test.reward_with_distribution(
|
||||
*node,
|
||||
Performance::from_percentage_value(performance).unwrap(),
|
||||
);
|
||||
}
|
||||
|
||||
test.set_epoch_in_progress_state();
|
||||
}
|
||||
|
||||
test.ensure_delegation_sync(mix_id);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,6 +12,7 @@ pub mod test_helpers {
|
||||
use crate::contract::instantiate;
|
||||
use crate::delegations::queries::query_mixnode_delegations_paged;
|
||||
use crate::delegations::storage as delegations_storage;
|
||||
use crate::delegations::storage::delegations;
|
||||
use crate::delegations::transactions::try_delegate_to_mixnode;
|
||||
use crate::families::transactions::{try_create_family, try_join_family};
|
||||
use crate::gateways::transactions::try_add_gateway;
|
||||
@@ -25,7 +26,7 @@ pub mod test_helpers {
|
||||
rewarding_validator_address,
|
||||
};
|
||||
use crate::mixnodes::storage as mixnodes_storage;
|
||||
use crate::mixnodes::storage::mixnode_bonds;
|
||||
use crate::mixnodes::storage::{assign_layer, mixnode_bonds, next_mixnode_id_counter};
|
||||
use crate::mixnodes::transactions::{try_add_mixnode, try_remove_mixnode};
|
||||
use crate::rewards::queries::{
|
||||
query_pending_delegator_reward, query_pending_mixnode_operator_reward,
|
||||
@@ -42,7 +43,7 @@ pub mod test_helpers {
|
||||
use cosmwasm_std::testing::mock_info;
|
||||
use cosmwasm_std::testing::MockApi;
|
||||
use cosmwasm_std::testing::MockQuerier;
|
||||
use cosmwasm_std::{coin, coins, Addr, BankMsg, CosmosMsg, Storage};
|
||||
use cosmwasm_std::{coin, coins, Addr, Api, BankMsg, CosmosMsg, Storage};
|
||||
use cosmwasm_std::{Coin, Order};
|
||||
use cosmwasm_std::{Decimal, Empty, MemoryStorage};
|
||||
use cosmwasm_std::{Deps, OwnedDeps};
|
||||
@@ -52,6 +53,7 @@ pub mod test_helpers {
|
||||
may_find_attribute, MixnetEventType, DELEGATES_REWARD_KEY, OPERATOR_REWARD_KEY,
|
||||
};
|
||||
use mixnet_contract_common::families::FamilyHead;
|
||||
use mixnet_contract_common::helpers::compare_decimals;
|
||||
use mixnet_contract_common::mixnode::{MixNodeRewarding, UnbondedMixnode};
|
||||
use mixnet_contract_common::pending_events::{PendingEpochEventData, PendingIntervalEventData};
|
||||
use mixnet_contract_common::reward_params::{Performance, RewardingParams};
|
||||
@@ -69,19 +71,23 @@ pub mod test_helpers {
|
||||
};
|
||||
use nym_crypto::asymmetric::identity;
|
||||
use nym_crypto::asymmetric::identity::KeyPair;
|
||||
use rand::distributions::WeightedIndex;
|
||||
use rand::prelude::*;
|
||||
use rand_chacha::rand_core::{CryptoRng, RngCore, SeedableRng};
|
||||
use rand_chacha::ChaCha20Rng;
|
||||
use serde::Serialize;
|
||||
use std::time::Duration;
|
||||
|
||||
#[track_caller]
|
||||
pub fn assert_eq_with_leeway(a: Uint128, b: Uint128, leeway: Uint128) {
|
||||
if a > b {
|
||||
assert!(a - b <= leeway)
|
||||
assert!(a - b <= leeway, "{} != {}", a, b)
|
||||
} else {
|
||||
assert!(b - a <= leeway)
|
||||
assert!(b - a <= leeway, "{} != {}", a, b)
|
||||
}
|
||||
}
|
||||
|
||||
#[track_caller]
|
||||
pub fn assert_decimals(a: Decimal, b: Decimal) {
|
||||
let epsilon = Decimal::from_ratio(1u128, 100_000_000u128);
|
||||
if a > b {
|
||||
@@ -120,6 +126,133 @@ pub mod test_helpers {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn new_complex() -> Self {
|
||||
let mut test = TestSetup::new();
|
||||
|
||||
let mut nodes = Vec::new();
|
||||
|
||||
let problematic_delegator = "n1foomp";
|
||||
let problematic_delegator_twin = "n1bar";
|
||||
let problematic_delegator_alt_twin = "n1whatever";
|
||||
|
||||
let choices = [true, false];
|
||||
|
||||
// every epoch there's a 2% chance of somebody bonding a node
|
||||
let bonding_weights = [2, 98];
|
||||
|
||||
// and 15% of making a delegation
|
||||
let delegation_weights = [15, 85];
|
||||
|
||||
// and 1% of making a VESTED delegation
|
||||
let vested_delegation_weights = [1, 99];
|
||||
|
||||
let bonding_dist = WeightedIndex::new(bonding_weights).unwrap();
|
||||
let delegation_dist = WeightedIndex::new(delegation_weights).unwrap();
|
||||
let vested_delegation_dist = WeightedIndex::new(vested_delegation_weights).unwrap();
|
||||
|
||||
// make sure we have at least a single node at the beginning
|
||||
let owner = test.random_address();
|
||||
let mix_id = test.add_dummy_mixnode(&owner, None);
|
||||
nodes.push(mix_id);
|
||||
|
||||
// create a bunch of nodes and delegations and progress through epochs
|
||||
for epoch_id in 0..1000 {
|
||||
// go through 1000 epochs
|
||||
|
||||
let owner = test.random_address();
|
||||
let min_stake = 100_000_000;
|
||||
// u32 has max value of 4B, which is ~4k nym tokens, which is a realistic amount somebody could bond/delegate
|
||||
let variance = test.rng.next_u32();
|
||||
let stake = Uint128::new(min_stake as u128 + variance as u128);
|
||||
|
||||
if choices[bonding_dist.sample(&mut test.rng)] {
|
||||
// bond
|
||||
let mix_id = test.add_dummy_mixnode(&owner, Some(stake));
|
||||
nodes.push(mix_id);
|
||||
}
|
||||
|
||||
if choices[delegation_dist.sample(&mut test.rng)] {
|
||||
// uniformly choose a random node to delegate to
|
||||
let node = nodes.choose(&mut test.rng).unwrap();
|
||||
test.add_immediate_delegation(&owner, stake, *node)
|
||||
}
|
||||
|
||||
if choices[vested_delegation_dist.sample(&mut test.rng)] {
|
||||
// uniformly choose a random node to make vested delegation to
|
||||
let node = nodes.choose(&mut test.rng).unwrap();
|
||||
test.add_immediate_delegation_with_legal_proxy(&owner, stake, *node)
|
||||
}
|
||||
|
||||
// make sure we cover our edge case of somebody having both liquid and vested delegation towards the same node
|
||||
if epoch_id == 123 {
|
||||
test.add_immediate_delegation(problematic_delegator, stake, 4);
|
||||
test.add_immediate_delegation(problematic_delegator_twin, stake, 4);
|
||||
}
|
||||
|
||||
if epoch_id == 666 {
|
||||
test.add_immediate_delegation_with_legal_proxy(problematic_delegator, stake, 4);
|
||||
test.add_immediate_delegation_with_legal_proxy(
|
||||
problematic_delegator_twin,
|
||||
stake,
|
||||
4,
|
||||
);
|
||||
}
|
||||
|
||||
if epoch_id == 234 {
|
||||
test.add_immediate_delegation(problematic_delegator_alt_twin, stake, 4);
|
||||
}
|
||||
|
||||
if epoch_id == 420 {
|
||||
test.add_immediate_delegation_with_legal_proxy(
|
||||
problematic_delegator_alt_twin,
|
||||
stake,
|
||||
4,
|
||||
);
|
||||
}
|
||||
|
||||
test.skip_to_next_epoch_end();
|
||||
test.force_change_rewarded_set(nodes.clone());
|
||||
test.start_epoch_transition();
|
||||
|
||||
// reward each node
|
||||
for node in &nodes {
|
||||
let performance = test.rng.next_u64() % 100;
|
||||
test.reward_with_distribution(
|
||||
*node,
|
||||
Performance::from_percentage_value(performance).unwrap(),
|
||||
);
|
||||
}
|
||||
|
||||
test.set_epoch_in_progress_state();
|
||||
}
|
||||
|
||||
test
|
||||
}
|
||||
|
||||
#[track_caller]
|
||||
pub fn ensure_delegation_sync(&self, mix_id: MixId) {
|
||||
let mix_info = self.mix_rewarding(mix_id);
|
||||
let epsilon = "0.001".parse().unwrap();
|
||||
|
||||
let subtotal: Decimal = delegations()
|
||||
.prefix(mix_id)
|
||||
.range(self.deps().storage, None, None, Order::Ascending)
|
||||
.filter_map(|d| {
|
||||
d.map(|(_, del)| {
|
||||
let pending_rewards = mix_info.determine_delegation_reward(&del).unwrap();
|
||||
pending_rewards + del.dec_amount().unwrap()
|
||||
})
|
||||
.ok()
|
||||
})
|
||||
.sum();
|
||||
|
||||
compare_decimals(mix_info.delegates, subtotal, Some(epsilon))
|
||||
}
|
||||
|
||||
pub fn random_address(&mut self) -> String {
|
||||
format!("n1foomp{}", self.rng.next_u64())
|
||||
}
|
||||
|
||||
pub fn deps(&self) -> Deps<'_> {
|
||||
self.deps.as_ref()
|
||||
}
|
||||
@@ -146,6 +279,20 @@ pub mod test_helpers {
|
||||
self.owner.clone()
|
||||
}
|
||||
|
||||
pub fn vesting_contract(&self) -> Addr {
|
||||
mixnet_params_storage::CONTRACT_STATE
|
||||
.load(self.deps().storage)
|
||||
.unwrap()
|
||||
.vesting_contract_address
|
||||
}
|
||||
|
||||
pub fn all_mixnodes(&self) -> Vec<MixId> {
|
||||
mixnode_bonds()
|
||||
.range(self.deps().storage, None, None, Order::Ascending)
|
||||
.filter_map(|m| m.map(|(_, node)| node.mix_id).ok())
|
||||
.collect::<Vec<_>>()
|
||||
}
|
||||
|
||||
pub fn coin(&self, amount: u128) -> Coin {
|
||||
coin(amount, rewarding_denom(self.deps().storage).unwrap())
|
||||
}
|
||||
@@ -266,6 +413,72 @@ pub mod test_helpers {
|
||||
current_id_counter + 1
|
||||
}
|
||||
|
||||
pub fn add_dummy_mixnode_with_proxy_and_keypair(
|
||||
&mut self,
|
||||
owner: &str,
|
||||
stake: Option<Uint128>,
|
||||
) -> (MixId, identity::KeyPair) {
|
||||
let pledge = self.make_mix_pledge(stake).pop().unwrap();
|
||||
|
||||
let proxy = self.vesting_contract();
|
||||
|
||||
let keypair = identity::KeyPair::new(&mut self.rng);
|
||||
let identity_key = keypair.public_key().to_base58_string();
|
||||
let legit_sphinx_keys = nym_crypto::asymmetric::encryption::KeyPair::new(&mut self.rng);
|
||||
|
||||
let mixnode = MixNode {
|
||||
identity_key,
|
||||
sphinx_key: legit_sphinx_keys.public_key().to_base58_string(),
|
||||
..tests::fixtures::mix_node_fixture()
|
||||
};
|
||||
|
||||
let height = self.env.block.height;
|
||||
let storage = self.deps_mut().storage;
|
||||
|
||||
// manually unroll `save_new_mixnode` to allow for proxy usage
|
||||
let layer = assign_layer(storage).unwrap();
|
||||
let mix_id = next_mixnode_id_counter(storage).unwrap();
|
||||
|
||||
let current_epoch = interval_storage::current_interval(storage)
|
||||
.unwrap()
|
||||
.current_epoch_absolute_id();
|
||||
|
||||
let mixnode_rewarding = MixNodeRewarding::initialise_new(
|
||||
tests::fixtures::mix_node_cost_params_fixture(),
|
||||
&pledge,
|
||||
current_epoch,
|
||||
)
|
||||
.unwrap();
|
||||
let mixnode_bond = MixNodeBond {
|
||||
mix_id,
|
||||
owner: Addr::unchecked(owner),
|
||||
original_pledge: pledge,
|
||||
layer,
|
||||
mix_node: mixnode,
|
||||
proxy: Some(proxy),
|
||||
bonding_height: height,
|
||||
is_unbonding: false,
|
||||
};
|
||||
|
||||
mixnode_bonds()
|
||||
.save(storage, mix_id, &mixnode_bond)
|
||||
.unwrap();
|
||||
rewards_storage::MIXNODE_REWARDING
|
||||
.save(storage, mix_id, &mixnode_rewarding)
|
||||
.unwrap();
|
||||
|
||||
(mix_id, keypair)
|
||||
}
|
||||
|
||||
pub fn add_dummy_mixnode_with_legal_proxy(
|
||||
&mut self,
|
||||
owner: &str,
|
||||
stake: Option<Uint128>,
|
||||
) -> MixId {
|
||||
self.add_dummy_mixnode_with_proxy_and_keypair(owner, stake)
|
||||
.0
|
||||
}
|
||||
|
||||
pub fn add_dummy_gateway(&mut self, sender: &str, stake: Option<Uint128>) -> IdentityKey {
|
||||
let stake = self.make_gateway_pledge(stake);
|
||||
let (gateway, owner_signature) =
|
||||
@@ -454,6 +667,55 @@ pub mod test_helpers {
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
pub fn add_immediate_delegation_with_legal_proxy(
|
||||
&mut self,
|
||||
delegator: &str,
|
||||
amount: impl Into<Uint128>,
|
||||
target: MixId,
|
||||
) {
|
||||
let denom = rewarding_denom(self.deps().storage).unwrap();
|
||||
let amount = Coin {
|
||||
denom,
|
||||
amount: amount.into(),
|
||||
};
|
||||
let proxy = self.vesting_contract();
|
||||
|
||||
let owner = self.deps.api.addr_validate(delegator).unwrap();
|
||||
let storage_key = Delegation::generate_storage_key(target, &owner, Some(&proxy));
|
||||
|
||||
let mut mix_rewarding = self.mix_rewarding(target);
|
||||
|
||||
let mut stored_delegation_amount = amount;
|
||||
|
||||
if let Some(existing_delegation) = delegations_storage::delegations()
|
||||
.may_load(&self.deps.storage, storage_key.clone())
|
||||
.unwrap()
|
||||
{
|
||||
let og_with_reward = mix_rewarding.undelegate(&existing_delegation).unwrap();
|
||||
stored_delegation_amount.amount += og_with_reward.amount;
|
||||
}
|
||||
|
||||
mix_rewarding
|
||||
.add_base_delegation(stored_delegation_amount.amount)
|
||||
.unwrap();
|
||||
|
||||
let delegation = Delegation {
|
||||
owner,
|
||||
mix_id: target,
|
||||
cumulative_reward_ratio: mix_rewarding.total_unit_reward,
|
||||
amount: stored_delegation_amount,
|
||||
height: self.env.block.height,
|
||||
proxy: Some(proxy),
|
||||
};
|
||||
|
||||
delegations_storage::delegations()
|
||||
.save(&mut self.deps.storage, storage_key, &delegation)
|
||||
.unwrap();
|
||||
rewards_storage::MIXNODE_REWARDING
|
||||
.save(&mut self.deps.storage, target, &mix_rewarding)
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
#[allow(unused)]
|
||||
pub fn add_delegation(
|
||||
&mut self,
|
||||
@@ -639,6 +901,14 @@ pub mod test_helpers {
|
||||
|
||||
let res =
|
||||
try_reward_mixnode(self.deps_mut(), env, sender, mix_id, performance).unwrap();
|
||||
|
||||
if performance.is_zero() {
|
||||
return RewardDistribution {
|
||||
operator: Decimal::zero(),
|
||||
delegates: Decimal::zero(),
|
||||
};
|
||||
}
|
||||
|
||||
let operator: Decimal = find_attribute(
|
||||
Some(MixnetEventType::MixnodeRewarding.to_string()),
|
||||
OPERATOR_REWARD_KEY,
|
||||
@@ -749,6 +1019,7 @@ pub mod test_helpers {
|
||||
None
|
||||
}
|
||||
|
||||
#[track_caller]
|
||||
pub fn find_attribute<S: Into<String>>(
|
||||
event_type: Option<S>,
|
||||
attribute: &str,
|
||||
|
||||
@@ -5,10 +5,11 @@ use crate::delegations::storage as delegations_storage;
|
||||
use crate::mixnet_contract_settings::storage as mixnet_params_storage;
|
||||
use crate::mixnodes::helpers::get_mixnode_details_by_owner;
|
||||
use crate::mixnodes::storage as mixnodes_storage;
|
||||
use crate::rewards::storage as rewards_storage;
|
||||
use crate::support::helpers::{
|
||||
ensure_bonded, ensure_epoch_in_progress_state, ensure_no_pending_pledge_changes,
|
||||
};
|
||||
use cosmwasm_std::{wasm_execute, DepsMut, MessageInfo, Response};
|
||||
use cosmwasm_std::{wasm_execute, DepsMut, Env, Event, MessageInfo, Response};
|
||||
use mixnet_contract_common::error::MixnetContractError;
|
||||
use mixnet_contract_common::{Delegation, MixId};
|
||||
use vesting_contract_common::messages::ExecuteMsg as VestingExecuteMsg;
|
||||
@@ -49,42 +50,159 @@ pub(crate) fn try_migrate_vested_mixnode(
|
||||
Some(&mix_details.bond_information),
|
||||
)?;
|
||||
|
||||
Ok(Response::new().add_message(wasm_execute(
|
||||
vesting_contract,
|
||||
&VestingExecuteMsg::TrackMigratedMixnode {
|
||||
owner: info.sender.into_string(),
|
||||
},
|
||||
vec![],
|
||||
)?))
|
||||
Ok(Response::new()
|
||||
.add_event(Event::new("migrate-vested-mixnode").add_attribute("mix_id", mix_id.to_string()))
|
||||
.add_message(wasm_execute(
|
||||
vesting_contract,
|
||||
&VestingExecuteMsg::TrackMigratedMixnode {
|
||||
owner: info.sender.into_string(),
|
||||
},
|
||||
vec![],
|
||||
)?))
|
||||
}
|
||||
|
||||
pub(crate) fn try_migrate_vested_delegation(
|
||||
deps: DepsMut<'_>,
|
||||
env: Env,
|
||||
info: MessageInfo,
|
||||
mix_id: MixId,
|
||||
) -> Result<Response, MixnetContractError> {
|
||||
let mut response = Response::new();
|
||||
|
||||
ensure_epoch_in_progress_state(deps.storage)?;
|
||||
|
||||
let vesting_contract = mixnet_params_storage::vesting_contract_address(deps.storage)?;
|
||||
|
||||
let storage_key =
|
||||
Delegation::generate_storage_key(mix_id, &info.sender, Some(&vesting_contract));
|
||||
let Some(mut delegation) =
|
||||
let Some(vested_delegation) =
|
||||
delegations_storage::delegations().may_load(deps.storage, storage_key.clone())?
|
||||
else {
|
||||
return Err(MixnetContractError::NotAVestingDelegation);
|
||||
};
|
||||
|
||||
// sanity check that's meant to blow up the contract
|
||||
assert_eq!(delegation.proxy, Some(vesting_contract.clone()));
|
||||
assert_eq!(vested_delegation.proxy, Some(vesting_contract.clone()));
|
||||
|
||||
// update the delegation and save it under the correct storage key
|
||||
delegation.proxy = None;
|
||||
let updated_storage_key = Delegation::generate_storage_key(mix_id, &info.sender, None);
|
||||
delegations_storage::delegations().remove(deps.storage, storage_key)?;
|
||||
delegations_storage::delegations().save(deps.storage, updated_storage_key, &delegation)?;
|
||||
let mut updated_delegation = vested_delegation.clone();
|
||||
updated_delegation.proxy = None;
|
||||
|
||||
Ok(Response::new().add_message(wasm_execute(
|
||||
let new_storage_key = Delegation::generate_storage_key(mix_id, &info.sender, None);
|
||||
|
||||
// remove the old (vested) delegation
|
||||
delegations_storage::delegations().remove(deps.storage, storage_key)?;
|
||||
|
||||
// check if there was already a delegation present under that key (i.e. an old liquid one)
|
||||
if let Some(existing_liquid_delegation) =
|
||||
delegations_storage::delegations().may_load(deps.storage, new_storage_key.clone())?
|
||||
{
|
||||
// treat it as adding extra stake to the existing delegation, so we need to update the unit reward value
|
||||
// as well as retrieve any pending rewards
|
||||
// it replicates part of code from `pending_events::delegate`,
|
||||
// but without some checks that'd be redundant in this instance
|
||||
let mut mix_rewarding =
|
||||
rewards_storage::MIXNODE_REWARDING.load(deps.storage, vested_delegation.mix_id)?;
|
||||
|
||||
// calculate rewards separately for the purposes of emitting those in events
|
||||
let pending_liquid_reward =
|
||||
mix_rewarding.determine_delegation_reward(&existing_liquid_delegation)?;
|
||||
let pending_vested_reward =
|
||||
mix_rewarding.determine_delegation_reward(&vested_delegation)?;
|
||||
|
||||
// the calls to 'undelegate' followed by artificial delegate are performed
|
||||
// to keep the internal `.delegates` field in sync
|
||||
// (this is due to the fact delegation only holds values up in `Uint128` and lacks the precision of a `Decimal`
|
||||
// which has to be used for reward accounting)
|
||||
let liquid_delegation_with_reward =
|
||||
mix_rewarding.undelegate(&existing_liquid_delegation)?;
|
||||
let vested_delegation_with_reward = mix_rewarding.undelegate(&vested_delegation)?;
|
||||
|
||||
// updated delegation amount consists of:
|
||||
// - delegated vested tokens
|
||||
// - delegated liquid tokens
|
||||
// - pending rewards earned by the delegated vested tokens
|
||||
// - pending rewards earned by the delegated liquid tokens
|
||||
let mut updated_total = liquid_delegation_with_reward.clone();
|
||||
updated_total.amount += vested_delegation_with_reward.amount;
|
||||
mix_rewarding.add_base_delegation(updated_total.amount)?;
|
||||
|
||||
updated_delegation.amount = updated_total;
|
||||
updated_delegation.height = env.block.height;
|
||||
updated_delegation.cumulative_reward_ratio = mix_rewarding.total_unit_reward;
|
||||
|
||||
rewards_storage::MIXNODE_REWARDING.save(
|
||||
deps.storage,
|
||||
vested_delegation.mix_id,
|
||||
&mix_rewarding,
|
||||
)?;
|
||||
|
||||
// replace the old delegation with the new one
|
||||
delegations_storage::delegations().replace(
|
||||
deps.storage,
|
||||
new_storage_key,
|
||||
Some(&updated_delegation),
|
||||
Some(&existing_liquid_delegation),
|
||||
)?;
|
||||
|
||||
// just emit EVERYTHING we can. just in case
|
||||
response.events.push(
|
||||
Event::new("migrate-vested-delegation")
|
||||
.add_attribute("mix_id", mix_id.to_string())
|
||||
.add_attribute("existing_liquid", "true")
|
||||
.add_attribute(
|
||||
"old_vested_unit_reward",
|
||||
vested_delegation.cumulative_reward_ratio.to_string(),
|
||||
)
|
||||
.add_attribute(
|
||||
"old_vested_delegation_amount",
|
||||
vested_delegation.amount.to_string(),
|
||||
)
|
||||
.add_attribute(
|
||||
"old_liquid_unit_reward",
|
||||
existing_liquid_delegation
|
||||
.cumulative_reward_ratio
|
||||
.to_string(),
|
||||
)
|
||||
.add_attribute(
|
||||
"old_liquid_delegation_amount",
|
||||
existing_liquid_delegation.amount.to_string(),
|
||||
)
|
||||
.add_attribute(
|
||||
"new_unit_reward",
|
||||
updated_delegation.cumulative_reward_ratio.to_string(),
|
||||
)
|
||||
.add_attribute(
|
||||
"new_delegation_amount",
|
||||
updated_delegation.amount.to_string(),
|
||||
)
|
||||
.add_attribute("applied_liquid_reward", pending_liquid_reward.to_string())
|
||||
.add_attribute("applied_vested_reward", pending_vested_reward.to_string()),
|
||||
)
|
||||
} else {
|
||||
// otherwise, this is as simple as resaving the updated value under the new key
|
||||
delegations_storage::delegations().save(
|
||||
deps.storage,
|
||||
new_storage_key,
|
||||
&updated_delegation,
|
||||
)?;
|
||||
|
||||
response.events.push(
|
||||
Event::new("migrate-vested-delegation")
|
||||
.add_attribute("mix_id", mix_id.to_string())
|
||||
.add_attribute("existing_liquid", "false")
|
||||
.add_attribute(
|
||||
"old_vested_unit_reward",
|
||||
vested_delegation.cumulative_reward_ratio.to_string(),
|
||||
)
|
||||
.add_attribute(
|
||||
"old_vested_delegation_amount",
|
||||
vested_delegation.amount.to_string(),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
Ok(response.add_message(wasm_execute(
|
||||
vesting_contract,
|
||||
&VestingExecuteMsg::TrackMigratedDelegation {
|
||||
owner: info.sender.into_string(),
|
||||
@@ -93,3 +211,354 @@ pub(crate) fn try_migrate_vested_delegation(
|
||||
vec![],
|
||||
)?))
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[cfg(test)]
|
||||
mod migrating_vested_mixnode {
|
||||
use super::*;
|
||||
use crate::mixnodes::helpers::get_mixnode_details_by_id;
|
||||
use crate::support::tests::test_helpers::TestSetup;
|
||||
use cosmwasm_std::testing::mock_info;
|
||||
use cosmwasm_std::{from_binary, Addr, CosmosMsg, WasmMsg};
|
||||
|
||||
#[test]
|
||||
fn with_no_bonded_nodes() {
|
||||
let mut test = TestSetup::new();
|
||||
|
||||
let sender = mock_info("owner", &[]);
|
||||
let deps = test.deps_mut();
|
||||
|
||||
// nothing happens
|
||||
let res = try_migrate_vested_mixnode(deps, sender).unwrap_err();
|
||||
assert_eq!(
|
||||
res,
|
||||
MixnetContractError::NoAssociatedMixNodeBond {
|
||||
owner: Addr::unchecked("owner")
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn with_liquid_node_bonded() {
|
||||
let mut test = TestSetup::new();
|
||||
test.add_dummy_mixnode("owner", None);
|
||||
|
||||
let sender = mock_info("owner", &[]);
|
||||
let deps = test.deps_mut();
|
||||
|
||||
// nothing happens
|
||||
let res = try_migrate_vested_mixnode(deps, sender).unwrap_err();
|
||||
assert_eq!(res, MixnetContractError::NotAVestingMixnode)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn with_vested_node_bonded() {
|
||||
let mut test = TestSetup::new();
|
||||
let mix_id = test.add_dummy_mixnode_with_legal_proxy("owner", None);
|
||||
|
||||
let sender = mock_info("owner", &[]);
|
||||
let deps = test.deps_mut();
|
||||
|
||||
let existing_node = get_mixnode_details_by_id(deps.storage, mix_id)
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
assert!(existing_node.bond_information.proxy.is_some());
|
||||
|
||||
let mut expected = existing_node.clone();
|
||||
expected.bond_information.proxy = None;
|
||||
|
||||
// node is simply resaved with proxy data removed and a track message is sent into the vesting contract
|
||||
let res = try_migrate_vested_mixnode(deps, sender).unwrap();
|
||||
let CosmosMsg::Wasm(WasmMsg::Execute { msg, .. }) = &res.messages[0].msg else {
|
||||
panic!("no track message present")
|
||||
};
|
||||
|
||||
assert_eq!(
|
||||
from_binary::<VestingExecuteMsg>(msg).unwrap(),
|
||||
VestingExecuteMsg::TrackMigratedMixnode {
|
||||
owner: "owner".to_string()
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod migrating_vested_delegation {
|
||||
use super::*;
|
||||
use crate::delegations::storage::delegations;
|
||||
use crate::support::tests::test_helpers::{assert_eq_with_leeway, TestSetup};
|
||||
use cosmwasm_std::testing::mock_info;
|
||||
use cosmwasm_std::{from_binary, Addr, CosmosMsg, Order, Uint128, WasmMsg};
|
||||
use mixnet_contract_common::helpers::compare_decimals;
|
||||
use mixnet_contract_common::reward_params::Performance;
|
||||
use mixnet_contract_common::rewarding::helpers::truncate_reward;
|
||||
use rand::RngCore;
|
||||
|
||||
#[test]
|
||||
fn with_no_delegation() {
|
||||
let mut test = TestSetup::new_complex();
|
||||
let env = test.env();
|
||||
|
||||
let sender = mock_info("owner-without-any-delegations", &[]);
|
||||
|
||||
// it simply fails for there is nothing to migrate
|
||||
let res = try_migrate_vested_delegation(test.deps_mut(), env, sender, 42).unwrap_err();
|
||||
assert_eq!(res, MixnetContractError::NotAVestingDelegation);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn with_just_liquid_delegation() {
|
||||
let mut test = TestSetup::new_complex();
|
||||
let env = test.env();
|
||||
|
||||
// find a valid delegation
|
||||
let delegation = delegations()
|
||||
.range(test.deps().storage, None, None, Order::Ascending)
|
||||
.filter_map(|d| d.map(|(_, del)| del).ok())
|
||||
.find(|d| d.proxy.is_none())
|
||||
.unwrap();
|
||||
|
||||
// make sure we haven't chosen somebody that also has a vested delegation because that would have invalidated the test
|
||||
assert!(!delegations()
|
||||
.range(test.deps().storage, None, None, Order::Ascending)
|
||||
.filter_map(|d| d.map(|(_, del)| del).ok())
|
||||
.any(|d| d.proxy.is_some() && d.owner.as_str() == delegation.owner.as_str()));
|
||||
|
||||
let sender = mock_info(delegation.owner.as_str(), &[]);
|
||||
let mix_id = delegation.mix_id;
|
||||
|
||||
// it also fails because the method is only allowed for vested delegations
|
||||
let res =
|
||||
try_migrate_vested_delegation(test.deps_mut(), env, sender, mix_id).unwrap_err();
|
||||
assert_eq!(res, MixnetContractError::NotAVestingDelegation);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn with_just_vested_delegation() {
|
||||
let mut test = TestSetup::new_complex();
|
||||
let env = test.env();
|
||||
|
||||
// find a valid delegation
|
||||
let delegation = delegations()
|
||||
.range(test.deps().storage, None, None, Order::Ascending)
|
||||
.filter_map(|d| d.map(|(_, del)| del).ok())
|
||||
.find(|d| d.proxy.is_some())
|
||||
.unwrap();
|
||||
|
||||
// make sure we haven't chosen somebody that also has a liquid delegation because that would have invalidated the test
|
||||
assert!(!delegations()
|
||||
.range(test.deps().storage, None, None, Order::Ascending)
|
||||
.filter_map(|d| d.map(|(_, del)| del).ok())
|
||||
.any(|d| d.proxy.is_none() && d.owner.as_str() == delegation.owner.as_str()));
|
||||
|
||||
let storage_key = delegation.storage_key();
|
||||
let mut expected_liquid = delegation.clone();
|
||||
expected_liquid.proxy = None;
|
||||
let expected_new_storage_key = expected_liquid.storage_key();
|
||||
|
||||
let sender = mock_info(delegation.owner.as_str(), &[]);
|
||||
let mix_id = delegation.mix_id;
|
||||
|
||||
// a track message is sent into the vesting contract
|
||||
let res = try_migrate_vested_delegation(test.deps_mut(), env, sender, mix_id).unwrap();
|
||||
let CosmosMsg::Wasm(WasmMsg::Execute { msg, .. }) = &res.messages[0].msg else {
|
||||
panic!("no track message present")
|
||||
};
|
||||
|
||||
assert_eq!(
|
||||
from_binary::<VestingExecuteMsg>(msg).unwrap(),
|
||||
VestingExecuteMsg::TrackMigratedDelegation {
|
||||
owner: delegation.owner.to_string(),
|
||||
mix_id,
|
||||
}
|
||||
);
|
||||
|
||||
// the entry is gone from the old storage key
|
||||
assert!(delegations()
|
||||
.may_load(test.deps().storage, storage_key)
|
||||
.unwrap()
|
||||
.is_none());
|
||||
|
||||
// and is resaved (without proxy) under the new key
|
||||
assert_eq!(
|
||||
expected_liquid,
|
||||
delegations()
|
||||
.load(test.deps().storage, expected_new_storage_key)
|
||||
.unwrap()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn with_both_liquid_and_vested_delegation() {
|
||||
let mut test = TestSetup::new_complex();
|
||||
let env = test.env();
|
||||
|
||||
let problematic_delegator = "n1foomp";
|
||||
let problematic_delegator_twin = "n1bar";
|
||||
let mix_id = 4;
|
||||
|
||||
let liquid_storage_key = Delegation::generate_storage_key(
|
||||
mix_id,
|
||||
&Addr::unchecked(problematic_delegator),
|
||||
None,
|
||||
);
|
||||
let vested_storage_key = Delegation::generate_storage_key(
|
||||
mix_id,
|
||||
&Addr::unchecked(problematic_delegator),
|
||||
Some(&test.vesting_contract()),
|
||||
);
|
||||
|
||||
let liquid_delegation = delegations()
|
||||
.load(test.deps().storage, liquid_storage_key.clone())
|
||||
.unwrap();
|
||||
let vested_delegation = delegations()
|
||||
.load(test.deps().storage, vested_storage_key.clone())
|
||||
.unwrap();
|
||||
let mix_info = test.mix_rewarding(mix_id);
|
||||
let unclaimed_liquid_reward = mix_info
|
||||
.determine_delegation_reward(&liquid_delegation)
|
||||
.unwrap();
|
||||
let unclaimed_vested_reward = mix_info
|
||||
.determine_delegation_reward(&vested_delegation)
|
||||
.unwrap();
|
||||
|
||||
// sanity check before doing anything
|
||||
test.ensure_delegation_sync(mix_id);
|
||||
|
||||
// a track message is sent into the vesting contract
|
||||
let sender = mock_info(problematic_delegator, &[]);
|
||||
let res = try_migrate_vested_delegation(test.deps_mut(), env, sender, mix_id).unwrap();
|
||||
let CosmosMsg::Wasm(WasmMsg::Execute { msg, .. }) = &res.messages[0].msg else {
|
||||
panic!("no track message present")
|
||||
};
|
||||
|
||||
assert_eq!(
|
||||
from_binary::<VestingExecuteMsg>(msg).unwrap(),
|
||||
VestingExecuteMsg::TrackMigratedDelegation {
|
||||
owner: problematic_delegator.to_string(),
|
||||
mix_id,
|
||||
}
|
||||
);
|
||||
|
||||
let updated_mix_info = test.mix_rewarding(mix_id);
|
||||
assert_eq!(
|
||||
mix_info.unique_delegations - 1,
|
||||
updated_mix_info.unique_delegations
|
||||
);
|
||||
|
||||
// the vested delegation is gone
|
||||
assert!(delegations()
|
||||
.may_load(test.deps().storage, vested_storage_key)
|
||||
.unwrap()
|
||||
.is_none());
|
||||
|
||||
let updated_liquid_delegation = delegations()
|
||||
.load(test.deps().storage, liquid_storage_key.clone())
|
||||
.unwrap();
|
||||
|
||||
assert!(updated_liquid_delegation.proxy.is_none());
|
||||
assert_eq!(
|
||||
updated_liquid_delegation.cumulative_reward_ratio,
|
||||
updated_mix_info.total_unit_reward
|
||||
);
|
||||
|
||||
let expected_amount = truncate_reward(
|
||||
vested_delegation.dec_amount().unwrap()
|
||||
+ liquid_delegation.dec_amount().unwrap()
|
||||
+ unclaimed_liquid_reward
|
||||
+ unclaimed_vested_reward,
|
||||
"unym",
|
||||
);
|
||||
// due to rounding we can expect and tolerate a single token of difference
|
||||
assert_eq_with_leeway(
|
||||
updated_liquid_delegation.amount.amount,
|
||||
expected_amount.amount,
|
||||
Uint128::one(),
|
||||
);
|
||||
|
||||
// this assertion must still hold
|
||||
test.ensure_delegation_sync(mix_id);
|
||||
|
||||
// go through few more rewarding epochs to make sure the rewards and accounting
|
||||
// would be the same as if the delegations remained separate
|
||||
let all_nodes = test.all_mixnodes();
|
||||
|
||||
let twin_liquid_storage_key = Delegation::generate_storage_key(
|
||||
mix_id,
|
||||
&Addr::unchecked(problematic_delegator_twin),
|
||||
None,
|
||||
);
|
||||
let twin_vested_storage_key = Delegation::generate_storage_key(
|
||||
mix_id,
|
||||
&Addr::unchecked(problematic_delegator_twin),
|
||||
Some(&test.vesting_contract()),
|
||||
);
|
||||
|
||||
let twin_liquid_delegation = delegations()
|
||||
.load(test.deps().storage, twin_liquid_storage_key.clone())
|
||||
.unwrap();
|
||||
let twin_vested_delegation = delegations()
|
||||
.load(test.deps().storage, twin_vested_storage_key.clone())
|
||||
.unwrap();
|
||||
|
||||
let info = test.mix_rewarding(mix_id);
|
||||
|
||||
let unclaimed_rewards_twin_liquid = info
|
||||
.determine_delegation_reward(&twin_liquid_delegation)
|
||||
.unwrap();
|
||||
let unclaimed_rewards_twin_vested = info
|
||||
.determine_delegation_reward(&twin_vested_delegation)
|
||||
.unwrap();
|
||||
|
||||
for _ in 0..100 {
|
||||
test.skip_to_next_epoch_end();
|
||||
test.force_change_rewarded_set(all_nodes.clone());
|
||||
test.start_epoch_transition();
|
||||
|
||||
// reward each node
|
||||
for node in &all_nodes {
|
||||
let performance = test.rng.next_u64() % 100;
|
||||
test.reward_with_distribution(
|
||||
*node,
|
||||
Performance::from_percentage_value(performance).unwrap(),
|
||||
);
|
||||
}
|
||||
|
||||
test.set_epoch_in_progress_state();
|
||||
}
|
||||
|
||||
// this assertion must still hold
|
||||
test.ensure_delegation_sync(mix_id);
|
||||
|
||||
let info = test.mix_rewarding(mix_id);
|
||||
|
||||
let current_liquid = delegations()
|
||||
.load(test.deps().storage, liquid_storage_key)
|
||||
.unwrap();
|
||||
let rewards = info.determine_delegation_reward(¤t_liquid).unwrap();
|
||||
|
||||
let twin_liquid_delegation = delegations()
|
||||
.load(test.deps().storage, twin_liquid_storage_key.clone())
|
||||
.unwrap();
|
||||
let twin_vested_delegation = delegations()
|
||||
.load(test.deps().storage, twin_vested_storage_key.clone())
|
||||
.unwrap();
|
||||
|
||||
let rewards_twin_liquid = info
|
||||
.determine_delegation_reward(&twin_liquid_delegation)
|
||||
.unwrap();
|
||||
let rewards_twin_vested = info
|
||||
.determine_delegation_reward(&twin_vested_delegation)
|
||||
.unwrap();
|
||||
|
||||
let new_rewards_twin = rewards_twin_liquid + rewards_twin_vested
|
||||
- unclaimed_rewards_twin_liquid
|
||||
- unclaimed_rewards_twin_vested;
|
||||
|
||||
compare_decimals(rewards, new_rewards_twin, Some("0.01".parse().unwrap()))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,6 +2,242 @@
|
||||
|
||||
This page displays a full list of all the changes during our release cycle from [`v2024.3-eclipse`](https://github.com/nymtech/nym/blob/nym-binaries-v2024.3-eclipse/CHANGELOG.md) onwards. Operators can find here the newest updates together with links to relevant documentation. The list is sorted so that the newest changes appear first.
|
||||
|
||||
## `v2024.11-wedel`
|
||||
|
||||
- [Release binaries](https://github.com/nymtech/nym/releases/tag/nym-binaries-v2024.11-wedel)
|
||||
- [Release CHANGELOG.md](https://github.com/nymtech/nym/blob/nym-binaries-v2024.11-wedel/CHANGELOG.md)
|
||||
- [`nym-node`](nodes/nym-node.md) version `1.1.8`
|
||||
|
||||
```sh
|
||||
Binary Name: nym-node
|
||||
Build Timestamp: 2024-09-27T11:02:37.073944654Z
|
||||
Build Version: 1.1.8
|
||||
Commit SHA: c3ec970a377adb25d57be5428551fada2ec55128
|
||||
Commit Date: 2024-09-26T08:24:53.000000000+02:00
|
||||
Commit Branch: master
|
||||
rustc Version: 1.80.1
|
||||
rustc Channel: stable
|
||||
cargo Profile: release
|
||||
```
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
- [New Network Monitor](https://github.com/nymtech/nym/pull/4610): Monitors the Nym network by sending itself packages across the mixnet. Network monitor is running two tokio tasks, one manages mixnet clients and another manages monitoring itself. Monitor is designed to be driven externally, via an `HTTP api`. This means that it does not do any monitoring unless driven by something like [`locust`](https://locust.io/). This allows us to tailor the load externally, potentially distributing it across multiple monitors. Includes a dockerised setup for automatically spinning up monitor and driving it with locust.
|
||||
- *Note: NNM is not deployed on mainnet yet!*
|
||||
|
||||
- [Add get_mixnodes_described to validator_client](https://github.com/nymtech/nym/pull/4725)
|
||||
|
||||
- [Remove deprecated mark_as_success and use new disarm](https://github.com/nymtech/nym/pull/4751): Update function name to keep terminology consistent with tokio `CancellationToken DropGuard`.
|
||||
|
||||
- [Update peer refresh value](https://github.com/nymtech/nym/pull/4754): `lso` expose the value by moving it to wireguard types, and separate the refresh time to the database sync time, so that more probable and needed actions happen faster (refresh) and more improbable ones don't overload the system (peer suspended or stale)
|
||||
~~~admonish example collapsible=true title='Testing steps performed'
|
||||
- **Noted** that the constants `DEFAULT_PEER_TIMEOUT` and `DEFAULT_PEER_TIMEOUT_CHECK` have been moved to `common/wireguard-types/src/lib.rs` and are now being used across modules for consistency
|
||||
- **Observed** that the `peer_controller.rs` now separates the in-memory updates from the storage sync operations to reduce system load
|
||||
- **Identified** that in-memory updates of peer bandwidth usage happen every `DEFAULT_PEER_TIMEOUT_CHECK` (every 5 seconds), while storage updates occur every 5 * `DEFAULT_PEER_TIMEOUT_CHECK` (every 25 seconds)
|
||||
|
||||
**Checked System Load and Performance:**
|
||||
|
||||
- **Monitored** system resource usage (CPU, memory, I/O) during the test to assess the impact of the changes
|
||||
- **Confirmed** that the separation of in-memory updates and storage syncs resulted in reduced system load, particularly I/O operations, compared to previous versions where storage updates occurred more frequently
|
||||
- **Ensured** that the system remained responsive and no performance bottlenecks were introduced
|
||||
|
||||
- **Efficiency Improvement:** The separation of in-memory updates and storage syncs effectively reduced unnecessary database writes, improving system efficiency without compromising data accuracy
|
||||
~~~
|
||||
|
||||
- [Remove duplicate stat count for retransmissions](https://github.com/nymtech/nym/pull/4756)
|
||||
|
||||
- [Make gateway latency check generic](https://github.com/nymtech/nym/pull/4759): Replace concrete gateway type with trait in latency check, so we can make use of it in the vpn client.
|
||||
~~~admonish example collapsible=true title='Testing steps performed'
|
||||
- Initialised new `nym-client` with the `--latency-based-selection` flag and ensured it still works as normal.
|
||||
~~~
|
||||
|
||||
- [chore: remove repetitive words](https://github.com/nymtech/nym/pull/4763)
|
||||
|
||||
- [Avoid race on ip and registration structures](https://github.com/nymtech/nym/pull/4766): To avoid a state where the ip is being cleared out before the registration is also cleared out, couple the two structures under the same lock, since they are anyway very inter-dependent.
|
||||
~~~admonish example collapsible=true title='Testing steps performed'
|
||||
1. - **Checked out** the release/2024.10-wedel branch containing the fix for the race condition on IP and registration structures
|
||||
- **Deployed** the on a controlled test environment to prevent interference
|
||||
|
||||
2. **Monitored Logs:**
|
||||
|
||||
- **Enabled** debug logging to capture all events
|
||||
- **Monitored** logs in real-time to observe the handling of concurrent registration requests
|
||||
- **Checked** for any error messages, warnings, or indications of race conditions
|
||||
|
||||
3. **Verified Client Responses:**
|
||||
|
||||
- Ensured that all clients received appropriate responses:
|
||||
- Successful registration with assigned IP and registration data
|
||||
- Appropriate error messages if no IPs were available or if other issues occurred
|
||||
- Confirmed that no clients were left in an inconsistent state (e.g., assigned an IP but not fully registered)
|
||||
|
||||
4. **Validated Normal Operation:**
|
||||
- **Conducted standard registration processes** with individual clients to confirm that regular functionality is unaffected via `nym-vpn-cli`
|
||||
- Ensured that authenticated clients could communicate over the network as expected
|
||||
~~~
|
||||
|
||||
- [Persist used wireguard private IPs](https://github.com/nymtech/nym/pull/4771)
|
||||
|
||||
- [Enable dependabot version upgrades for root rust workspace](https://github.com/nymtech/nym/pull/4778)
|
||||
|
||||
- [Fix clippy for `unwrap_or_default`](https://github.com/nymtech/nym/pull/4783): Fix nightly build for [beta toolchain](https://github.com/nymtech/nym/actions/runs/10552082396/job/29230401668)
|
||||
|
||||
- [Update dependabot](https://github.com/nymtech/nym/pull/4796): Bump max number of dependabot rust PRs to 10. Add readme entry to workspace package.
|
||||
|
||||
- [Run `cargo-autoinherit` for a few new crates](https://github.com/nymtech/nym/pull/4801): Run cargo-autoinherit for a few new crates - Sort crates list.
|
||||
|
||||
- [Add `axum` server to `nym-api`](https://github.com/nymtech/nym/pull/4803): Summary PR to add axum functionality behind a feature flag `axum`, alongside rocket.
|
||||
|
||||
- [Remove unused wireguard flag from SDK](https://github.com/nymtech/nym/pull/4823)
|
||||
|
||||
- [Expose wireguard details on self described endpoint](https://github.com/nymtech/nym/pull/4825)
|
||||
~~~admonish example collapsible=true title='Testing steps performed'
|
||||
Wireguard details are now visible at the nym-node endpoint `/api/v1/gateway/client-interfaces` as well as on the nym-api self-described endpoint `/api/v1/gateways/described`, above the existing data displaying mixnet_websocket information.
|
||||
|
||||
An example of what will be shown is:
|
||||
```json
|
||||
"wireguard": {
|
||||
"port": 51822,
|
||||
"public_key": "<some public key here>"
|
||||
}
|
||||
```
|
||||
~~~
|
||||
|
||||
- [Revamped ticketbook serialisation and exposed additional cli methods](https://github.com/nymtech/nym/pull/4827): `wip` branch that includes changes needed for `vpn-api` alongside additional `ecash utils`
|
||||
~~~admonish example collapsible=true title='Testing steps performed'
|
||||
Checked the following commands:
|
||||
```sh
|
||||
show-ticket-books # which displays the information about all ticketbooks associated to the client
|
||||
import-ticket-book # which imports a normal ticketbook to the client alongside `--full` flag
|
||||
```
|
||||
|
||||
On the cli, the following were added: `import-coin-index-signatures`, `import-expiration-date-signatures` and `import-master-verification-key`.
|
||||
~~~
|
||||
|
||||
- [Run cargo autoinherit following last weeks dependabot updates](https://github.com/nymtech/nym/pull/4831)
|
||||
|
||||
- [Remove serde_crate named import](https://github.com/nymtech/nym/pull/4832)
|
||||
|
||||
- [Create nym-repo-setup debian package and nym-vpn meta package](https://github.com/nymtech/nym/pull/4837): Create nym-repo-setup debian package that sets up the nymtech debian repo on the system it's installed on. It does 2 things:
|
||||
|
||||
1. Copy the keyring to `/usr/share/keyrings/nymtech.gpg`
|
||||
2. Copy the repo spec to `/etc/apt/sources.list.d/nymtech.list`
|
||||
- Also create a meta package `nym-vpn` which only purpose is to depend on the daemon and UI.
|
||||
|
||||
~~~admonish example collapsible=true title='Usage'
|
||||
1. Install with
|
||||
```sh
|
||||
sudo dpkg -i ./nym-repo-setup.deb
|
||||
```
|
||||
2. Once it's installed, it should be possible to install the vpn client with
|
||||
```sh
|
||||
sudo apt install nym-vpnc
|
||||
```
|
||||
3. To reemove the repo, use
|
||||
```sh
|
||||
sudo apt remove nym-repo-setup
|
||||
```
|
||||
|
||||
NOTE: removing the repo will not remove any installed nym-vpn packages
|
||||
~~~
|
||||
|
||||
~~~admonish example collapsible=true title='Testing steps performed'
|
||||
|
||||
1. **Downloaded** the `nym-repo-setup.deb` package to a Debian-based test system
|
||||
|
||||
2. **Installed** the repository setup package using the command:
|
||||
```bash
|
||||
sudo dpkg -i ./nym-repo-setup.deb
|
||||
```
|
||||
|
||||
3. **Verified** that the GPG keyring was copied to `/usr/share/keyrings/nymtech.gpg`:
|
||||
```bash
|
||||
ls -l /usr/share/keyrings/nymtech.gpg
|
||||
```
|
||||
|
||||
4. **Checked** that the repository specification was added to `/etc/apt/sources.list.d/nymtech.list`:
|
||||
```bash
|
||||
cat /etc/apt/sources.list.d/nymtech.list
|
||||
```
|
||||
|
||||
5. **Updated** the package list:
|
||||
```bash
|
||||
sudo apt update
|
||||
```
|
||||
|
||||
6. **Installed** the VPN client meta-package:
|
||||
```bash
|
||||
sudo apt install nym-vpnc
|
||||
```
|
||||
|
||||
7. **Confirmed** that the `nym-vpnc` package and its dependencies (daemon and UI) were installed successfully
|
||||
|
||||
8. **Tested** the VPN client to ensure it operates as expected
|
||||
|
||||
9. **Removed** the repository setup package:
|
||||
```bash
|
||||
sudo apt remove nym-repo-setup
|
||||
```
|
||||
|
||||
10. **Verified** that the repository specification file `/etc/apt/sources.list.d/nymtech.list` was removed
|
||||
|
||||
11. **Ensured** that the installed `nym-vpnc` packages remained installed and functional after removing the repo setup package
|
||||
~~~
|
||||
|
||||
- [Use ecash credential type for bandwidth value](https://github.com/nymtech/nym/pull/4840)
|
||||
|
||||
- [Start switching over jobs to arc-ubuntu-20.04](https://github.com/nymtech/nym/pull/4843)
|
||||
|
||||
~~~admonish example collapsible=true title='`ci-binary-config-checker`'
|
||||
```
|
||||
- ci-build-upload-binaries
|
||||
- ci-build
|
||||
- ci-cargo-deny
|
||||
- ci-contracts-schema
|
||||
- ci-contracts-upload-binaries
|
||||
- ci-contracts
|
||||
- ci-docs
|
||||
- ci-nym-wallet-rust
|
||||
- ci-sdk-wasm
|
||||
```
|
||||
~~~
|
||||
|
||||
- [Move credential verification into common crate](https://github.com/nymtech/nym/pull/4853)
|
||||
|
||||
- [Revert runner for `ci-docs`](https://github.com/nymtech/nym/pull/4855)
|
||||
|
||||
- [Remove `golang` workaround in `ci-sdk-wasm`](https://github.com/nymtech/nym/pull/4858)
|
||||
|
||||
- [Fix linux conditional in `ci-build.yml`](https://github.com/nymtech/nym/pull/4863)
|
||||
|
||||
- [Disable push trigger and add missing paths in `ci-build`](https://github.com/nymtech/nym/pull/4864)
|
||||
|
||||
- [chore: removed completed queued mixnet migration](https://github.com/nymtech/nym/pull/4865)
|
||||
|
||||
- [Bump defguard to github latest version](https://github.com/nymtech/nym/pull/4872)
|
||||
|
||||
- [Backport #4894 to fix ci](https://github.com/nymtech/nym/pull/4899)
|
||||
|
||||
### Bugfix
|
||||
|
||||
- [Fix test failure in ipr request size](https://github.com/nymtech/nym/pull/4844): Nightly build started failing due to a unit test using `now()`, changing the serialized size. Fixed to use a fixed date.
|
||||
|
||||
- [Fix clippy for nym-wallet and latest rustc](https://github.com/nymtech/nym/pull/4845)
|
||||
|
||||
- [Allow updating globally stored signatures](https://github.com/nymtech/nym/pull/4891)
|
||||
|
||||
- [Bugfix/ticketbook false double spending](https://github.com/nymtech/nym/pull/4892)
|
||||
~~~admonish example collapsible=true title='Testing steps performed'
|
||||
Tested running a client in mixnet mode, with a standard ticketbook, as well as a client using an imported ticketbook. The double spending bug is no longer an issue, bandwidth is consumed properly, and upon consumption of one ticket another ticket is properly obtained.
|
||||
~~~
|
||||
|
||||
### Operators Guide, Tooling & Updates
|
||||
|
||||
- [WSS setup guide updates](https://github.com/nymtech/nym/commit/05d6652177fb77324f8c38b3d8a547d07e729fec): Operators setting up WSS and reverse proxy on Gateways have now cleaner and simpler guide to configure their VPS.
|
||||
|
||||
- [Updat hostname instruction for WSS](https://github.com/nymtech/nym/commit/7146c4c012ba7012dc74edc8510bbf377dc32fba): Adding a hostname instruction for clarity
|
||||
|
||||
## `nym-node` patch from `release/2024.10-caramello`
|
||||
|
||||
- [Patch release binaries](https://github.com/nymtech/nym/releases/tag/nym-binaries-v2024.10-caramello-patch)
|
||||
|
||||
@@ -25,7 +25,7 @@ We don't recommend this setup because it's really difficult to get a static IP a
|
||||
|
||||
### What's the Sphinx packet size?
|
||||
|
||||
The sizes are shown in the configs [here](https://github.com/nymtech/nym/blob/1ba6444e722e7757f1175a296bed6e31e25b8db8/common/nymsphinx/params/src/packet_sizes.rs#L12) (default is the one clients use, the others are for research purposes, not to be used in production as this would fragment the anonymity set). More info can be found [here](https://github.com/nymtech/nym/blob/4844ac953a12b29fa27688609ec193f1d560c996/common/nymsphinx/anonymous-replies/src/reply_surb.rs#L80).
|
||||
The sizes are shown in the configs [here](https://github.com/nymtech/nym/blob/develop/common/nymsphinx/params/src/packet_sizes.rs#L32) (default is the one clients use, the others are for research purposes, not to be used in production as this would fragment the anonymity set). More info can be found [here](https://github.com/nymtech/nym/blob/develop/common/nymsphinx/anonymous-replies/src/reply_surb.rs#L80).
|
||||
|
||||
### Why a Mix Node and a Gateway cannot be bonded with the same wallet?
|
||||
|
||||
|
||||
@@ -2,6 +2,8 @@
|
||||
|
||||
### What determines the rewards when running a `nym-node --mode mixnode`?
|
||||
|
||||
> **Visit [nymtech.net/about/token](https://nymtech.net/about/token) to find live information, graphs and dashboards about NYM token.**
|
||||
|
||||
The stake required for a Mix Node to achieve maximum rewards is called Mix Node saturation point. This is calculated from the staking supply (all circulating supply + part of unlocked tokens). The target level of staking is to have 40% of the staking supply locked in Mix Nodes.
|
||||
|
||||
The node stake saturation point, which we denote by Nsat, is given by the stake supply, target level of staking divided between the rewarded nodes.
|
||||
@@ -20,13 +22,3 @@ The rewarded nodes are the nodes which will receive some rewards by the end of t
|
||||
|
||||
|
||||
For more detailed calculation, read our blog post [Nym Token Economics update](https://blog.nymtech.net/nym-token-economics-update-fedff0ed5267). More info on staking can be found [here](https://blog.nymtech.net/staking-in-nym-introducing-mainnet-mixmining-f9bb1cbc7c36). And [here](https://blog.nymtech.net/want-to-stake-in-nym-here-is-how-to-choose-a-mix-node-to-delegate-nym-to-c3b862add165) is more info on how to choose a Mix Node for delegation. And finally an [update](https://blog.nymtech.net/quarterly-token-economic-parameter-update-b2862948710f) on token economics from July 2023.
|
||||
|
||||
<!--
|
||||
<iframe src="https://status.notrustverify.ch/d-solo/CW3L7dVVk/nym-mixnet?orgId=1&from=1703074829887&to=1705666829887&panelId=31" width="850" height="400" frameborder="0"></iframe>
|
||||
-->
|
||||
|
||||
<iframe src="https://dashboard.notrustverify.ch/d-solo/l71MWkX7k/ntv-mixnode?orgId=1&from=1710949572440&to=1713537972440&panelId=18" width="850" height="400" frameborder="0"></iframe>
|
||||
|
||||
*More graphs and stats at [stats.notrustverify.ch](https://status.notrustverify.ch/d/CW3L7dVVk/nym-mixnet?orgId=1&from=1703074861988&to=1705666862004).*
|
||||
|
||||
|
||||
|
||||
+4
-7
@@ -11,16 +11,13 @@ STAKE_DENOM_DISPLAY=nyx
|
||||
DENOMS_EXPONENT=6
|
||||
|
||||
MIXNET_CONTRACT_ADDRESS=n1hm4y6fzgxgu688jgf7ek66px6xkrtmn3gyk8fax3eawhp68c2d5qujz296
|
||||
ECASH_CONTRACT_ADDRESS=n14y2x8a60knc5jjfeztt84kw8x8l5pwdgnqg256v0p9v4p7t2q6eswxyusw
|
||||
GROUP_CONTRACT_ADDRESS=n1qp35fcj0v9u3trhaps5v9q0lc42t4m6aty2wryss75ee8zuqnsqqdcreyq
|
||||
MULTISIG_CONTRACT_ADDRESS=n1qa4hswlcjmttulj0q9qa46jf64f93pecl6tydcsjldfe0hy5ju0sdmwzya
|
||||
COCONUT_DKG_CONTRACT_ADDRESS=n1ayrk6wp6w5lf6njtnfjwljmtcc9vevv5sxwkz7uq24rp2pw67t0qhmmxdd
|
||||
ECASH_CONTRACT_ADDRESS=n13xspq62y9gq6nueqmywxcdv2yep4p6nzv98w2889k25v3nhdy2dq2rkrk7
|
||||
GROUP_CONTRACT_ADDRESS=n13l7rwuwktklrwskc7m6lv70zws07en85uma28j7dxwsz9y5hvvhspl7a2t
|
||||
MULTISIG_CONTRACT_ADDRESS=n138c9pyf7f3hyx0j3t6vmsz7ultnw2wj0lu6hzndep9z5grgq9haqlc25k0
|
||||
COCONUT_DKG_CONTRACT_ADDRESS=n1pk8jgr6y4c5k93gz7qf3xc0hvygmp7csk88c2tf8l39tkq6834wq2a6dtr
|
||||
VESTING_CONTRACT_ADDRESS=n1jlzdxnyces4hrhqz68dqk28mrw5jgwtcfq0c2funcwrmw0dx9l9s8nnnvj
|
||||
REWARDING_VALIDATOR_ADDRESS=n1rfvpsynktze6wvn6ldskj8xgwfzzk5v6pnff39
|
||||
|
||||
EXPLORER_API=https://qa-network-explorer.qa.nymte.ch/api
|
||||
NYXD="https://qa-validator.qa.nymte.ch"
|
||||
NYM_API="https://qa-nym-api.qa.nymte.ch/api"
|
||||
|
||||
DKG_TIME_CONFIGURATION="600,300,300,60,60,1209600"
|
||||
EXIT_POLICY="https://nymtech.net/.wellknown/network-requester/exit-policy.txt"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "explorer-api"
|
||||
version = "1.1.39"
|
||||
version = "1.1.40"
|
||||
edition = "2021"
|
||||
license.workspace = true
|
||||
|
||||
|
||||
@@ -27,7 +27,7 @@ use nym_gateway_storage::{error::StorageError, Storage};
|
||||
use nym_sphinx::forwarding::packet::MixPacket;
|
||||
use nym_task::TaskClient;
|
||||
use nym_validator_client::coconut::EcashApiError;
|
||||
use rand::{CryptoRng, Rng};
|
||||
use rand::{random, CryptoRng, Rng};
|
||||
use std::{process, time::Duration};
|
||||
use thiserror::Error;
|
||||
use tokio::io::{AsyncRead, AsyncWrite};
|
||||
@@ -236,11 +236,7 @@ where
|
||||
enc_credential: Vec<u8>,
|
||||
iv: Vec<u8>,
|
||||
) -> Result<ServerResponse, RequestHandlingError> {
|
||||
// TODO: change it into a span field instead once we move to tracing
|
||||
debug!(
|
||||
"handling e-cash bandwidth request from {}",
|
||||
self.client.address
|
||||
);
|
||||
debug!("handling e-cash bandwidth request");
|
||||
|
||||
let credential = ClientControlRequest::try_from_enc_ecash_credential(
|
||||
enc_credential,
|
||||
@@ -253,7 +249,11 @@ where
|
||||
self.bandwidth_storage_manager.clone(),
|
||||
);
|
||||
|
||||
let available_total = verifier.verify().await?;
|
||||
let available_total = verifier
|
||||
.verify()
|
||||
.await
|
||||
.inspect_err(|verification_failure| debug!("{verification_failure}"))?;
|
||||
trace!("available total bandwidth: {available_total}");
|
||||
|
||||
Ok(ServerResponse::Bandwidth { available_total })
|
||||
}
|
||||
@@ -340,20 +340,17 @@ where
|
||||
&mut self,
|
||||
ciphertext: Vec<u8>,
|
||||
nonce: Vec<u8>,
|
||||
) -> Message {
|
||||
) -> Result<ServerResponse, RequestHandlingError> {
|
||||
let Ok(req) = ClientRequest::decrypt(&ciphertext, &nonce, &self.client.shared_keys) else {
|
||||
return RequestHandlingError::InvalidEncryptedTextRequest.into_error_message();
|
||||
return Err(RequestHandlingError::InvalidEncryptedTextRequest);
|
||||
};
|
||||
|
||||
match req {
|
||||
ClientRequest::UpgradeKey {
|
||||
hkdf_salt,
|
||||
derived_key_digest,
|
||||
} => self
|
||||
.handle_key_upgrade(hkdf_salt, derived_key_digest)
|
||||
.await
|
||||
.into_ws_message(),
|
||||
_ => RequestHandlingError::UnknownEncryptedTextRequest.into_error_message(),
|
||||
} => self.handle_key_upgrade(hkdf_salt, derived_key_digest).await,
|
||||
_ => Err(RequestHandlingError::UnknownEncryptedTextRequest),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -366,59 +363,64 @@ where
|
||||
/// * `raw_request`: raw message to handle.
|
||||
async fn handle_text(&mut self, raw_request: String) -> Message {
|
||||
trace!("text request");
|
||||
match ClientControlRequest::try_from(raw_request) {
|
||||
Err(e) => RequestHandlingError::InvalidTextRequest(e).into_error_message(),
|
||||
Ok(request) => match request {
|
||||
ClientControlRequest::EncryptedRequest { ciphertext, nonce } => {
|
||||
self.handle_encrypted_text_request(ciphertext, nonce).await
|
||||
}
|
||||
ClientControlRequest::EcashCredential { enc_credential, iv } => self
|
||||
.handle_ecash_bandwidth(enc_credential, iv)
|
||||
.await
|
||||
.into_ws_message(),
|
||||
ClientControlRequest::BandwidthCredential { .. } => {
|
||||
RequestHandlingError::IllegalRequest {
|
||||
additional_context: "coconut credential are not longer supported".into(),
|
||||
}
|
||||
.into_error_message()
|
||||
}
|
||||
ClientControlRequest::BandwidthCredentialV2 { .. } => {
|
||||
RequestHandlingError::IllegalRequest {
|
||||
additional_context: "coconut credential are not longer supported".into(),
|
||||
}
|
||||
.into_error_message()
|
||||
}
|
||||
ClientControlRequest::ClaimFreeTestnetBandwidth => self
|
||||
.bandwidth_storage_manager
|
||||
.handle_claim_testnet_bandwidth()
|
||||
.await
|
||||
.map_err(|e| e.into())
|
||||
.into_ws_message(),
|
||||
ClientControlRequest::SupportedProtocol { .. } => self
|
||||
.inner
|
||||
.handle_supported_protocol_request()
|
||||
.into_ws_message(),
|
||||
other @ ClientControlRequest::Authenticate { .. } => {
|
||||
RequestHandlingError::IllegalRequest {
|
||||
additional_context: format!(
|
||||
"received illegal message of type {} in an authenticated client",
|
||||
other.name()
|
||||
),
|
||||
}
|
||||
.into_error_message()
|
||||
}
|
||||
other @ ClientControlRequest::RegisterHandshakeInitRequest { .. } => {
|
||||
RequestHandlingError::IllegalRequest {
|
||||
additional_context: format!(
|
||||
"received illegal message of type {} in an authenticated client",
|
||||
other.name()
|
||||
),
|
||||
}
|
||||
.into_error_message()
|
||||
}
|
||||
_ => RequestHandlingError::UnknownTextRequest.into_error_message(),
|
||||
},
|
||||
|
||||
let request = match ClientControlRequest::try_from(raw_request) {
|
||||
Ok(req) => {
|
||||
debug!("received request of type {}", req.name());
|
||||
req
|
||||
}
|
||||
Err(err) => {
|
||||
debug!("request was malformed: {err}");
|
||||
return RequestHandlingError::InvalidTextRequest(err).into_error_message();
|
||||
}
|
||||
};
|
||||
|
||||
match request {
|
||||
ClientControlRequest::EncryptedRequest { ciphertext, nonce } => {
|
||||
self.handle_encrypted_text_request(ciphertext, nonce).await
|
||||
}
|
||||
ClientControlRequest::EcashCredential { enc_credential, iv } => {
|
||||
self.handle_ecash_bandwidth(enc_credential, iv).await
|
||||
}
|
||||
ClientControlRequest::BandwidthCredential { .. } => {
|
||||
Err(RequestHandlingError::IllegalRequest {
|
||||
additional_context: "coconut credential are not longer supported".into(),
|
||||
})
|
||||
}
|
||||
ClientControlRequest::BandwidthCredentialV2 { .. } => {
|
||||
Err(RequestHandlingError::IllegalRequest {
|
||||
additional_context: "coconut credential are not longer supported".into(),
|
||||
})
|
||||
}
|
||||
ClientControlRequest::ClaimFreeTestnetBandwidth => self
|
||||
.bandwidth_storage_manager
|
||||
.handle_claim_testnet_bandwidth()
|
||||
.await
|
||||
.map_err(|e| e.into()),
|
||||
ClientControlRequest::SupportedProtocol { .. } => {
|
||||
Ok(self.inner.handle_supported_protocol_request())
|
||||
}
|
||||
other @ ClientControlRequest::Authenticate { .. } => {
|
||||
Err(RequestHandlingError::IllegalRequest {
|
||||
additional_context: format!(
|
||||
"received illegal message of type {} in an authenticated client",
|
||||
other.name()
|
||||
),
|
||||
})
|
||||
}
|
||||
other @ ClientControlRequest::RegisterHandshakeInitRequest { .. } => {
|
||||
Err(RequestHandlingError::IllegalRequest {
|
||||
additional_context: format!(
|
||||
"received illegal message of type {} in an authenticated client",
|
||||
other.name()
|
||||
),
|
||||
})
|
||||
}
|
||||
_ => Err(RequestHandlingError::UnknownTextRequest),
|
||||
}
|
||||
.inspect(|res| debug!(response = ?res, "success"))
|
||||
.inspect_err(|err| debug!(error = %err, "failure"))
|
||||
.into_ws_message()
|
||||
}
|
||||
|
||||
/// Handles pong message received from the client.
|
||||
@@ -452,12 +454,13 @@ where
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `raw_request`: raw received websocket message.
|
||||
#[instrument(level = "debug", skip_all,
|
||||
fields(
|
||||
client = %self.client.address.as_base58_string()
|
||||
)
|
||||
)]
|
||||
async fn handle_request(&mut self, raw_request: Message) -> Option<Message> {
|
||||
// TODO: this should be added via tracing
|
||||
debug!(
|
||||
"handling request from {}",
|
||||
self.client.address.as_base58_string()
|
||||
);
|
||||
trace!("new request");
|
||||
|
||||
// apparently tungstenite auto-handles ping/pong/close messages so for now let's ignore
|
||||
// them and let's test that claim. If that's not the case, just copy code from
|
||||
@@ -478,8 +481,8 @@ where
|
||||
where
|
||||
S: AsyncRead + AsyncWrite + Unpin,
|
||||
{
|
||||
let tag: u64 = rand::thread_rng().gen();
|
||||
debug!("Got request to ping our connection: {}", tag);
|
||||
let tag: u64 = random();
|
||||
debug!("got request to ping our connection: {tag}");
|
||||
self.inner
|
||||
.send_websocket_message(Message::Ping(tag.to_be_bytes().to_vec()))
|
||||
.await?;
|
||||
|
||||
@@ -420,7 +420,7 @@ where
|
||||
// we can't handle clients with higher protocol than ours
|
||||
// (perhaps we could try to negotiate downgrade on our end? sounds like a nice future improvement)
|
||||
if client_protocol_version <= CURRENT_PROTOCOL_VERSION {
|
||||
info!("the client is using exactly the same (or older) protocol version as we are. We're good to continue!");
|
||||
debug!("the client is using exactly the same (or older) protocol version as we are. We're good to continue!");
|
||||
Ok(CURRENT_PROTOCOL_VERSION)
|
||||
} else {
|
||||
let err = InitialAuthenticationError::IncompatibleProtocol {
|
||||
|
||||
@@ -8,10 +8,10 @@ use futures::channel::mpsc::SendError;
|
||||
use futures::StreamExt;
|
||||
use nym_gateway_storage::{error::StorageError, Storage};
|
||||
use nym_mixnet_client::forwarder::MixForwardingSender;
|
||||
use nym_mixnode_common::packet_processor::processor::ProcessedFinalHop;
|
||||
use nym_sphinx::forwarding::packet::MixPacket;
|
||||
use nym_sphinx::framing::codec::NymCodec;
|
||||
use nym_sphinx::framing::packet::FramedNymPacket;
|
||||
use nym_sphinx::framing::processing::ProcessedFinalHop;
|
||||
use nym_sphinx::DestinationAddressBytes;
|
||||
use nym_task::TaskClient;
|
||||
use std::collections::HashMap;
|
||||
@@ -21,6 +21,8 @@ use tokio::net::TcpStream;
|
||||
use tokio_util::codec::Framed;
|
||||
use tracing::*;
|
||||
|
||||
use super::packet_processing::process_packet;
|
||||
|
||||
// defines errors that warrant a panic if not thrown in the context of a shutdown
|
||||
#[derive(Debug, Error)]
|
||||
enum CriticalPacketProcessingError {
|
||||
@@ -184,14 +186,14 @@ impl<St: Storage> ConnectionHandler<St> {
|
||||
// question: can it also be per connection vs global?
|
||||
//
|
||||
|
||||
let processed_final_hop = match self.packet_processor.process_received(framed_sphinx_packet)
|
||||
{
|
||||
Err(err) => {
|
||||
debug!("We failed to process received sphinx packet - {err}");
|
||||
return Ok(());
|
||||
}
|
||||
Ok(processed_final_hop) => processed_final_hop,
|
||||
};
|
||||
let processed_final_hop =
|
||||
match process_packet(framed_sphinx_packet, self.packet_processor.sphinx_key()) {
|
||||
Err(err) => {
|
||||
debug!("We failed to process received sphinx packet - {err}");
|
||||
return Ok(());
|
||||
}
|
||||
Ok(processed_final_hop) => processed_final_hop,
|
||||
};
|
||||
|
||||
self.handle_processed_packet(processed_final_hop).await
|
||||
}
|
||||
|
||||
@@ -3,18 +3,24 @@
|
||||
|
||||
use nym_crypto::asymmetric::encryption;
|
||||
use nym_mixnode_common::packet_processor::error::MixProcessingError;
|
||||
pub use nym_mixnode_common::packet_processor::processor::MixProcessingResult;
|
||||
use nym_mixnode_common::packet_processor::processor::{ProcessedFinalHop, SphinxPacketProcessor};
|
||||
use nym_mixnode_common::packet_processor::processor::SphinxPacketProcessor;
|
||||
use nym_sphinx::framing::packet::FramedNymPacket;
|
||||
use nym_sphinx::framing::processing::{
|
||||
process_framed_packet, MixProcessingResult, PacketProcessingError, ProcessedFinalHop,
|
||||
};
|
||||
use nym_sphinx::PrivateKey;
|
||||
use thiserror::Error;
|
||||
|
||||
#[derive(Error, Debug)]
|
||||
pub enum GatewayProcessingError {
|
||||
#[error("failed to process received mix packet - {0}")]
|
||||
PacketProcessingError(#[from] MixProcessingError),
|
||||
PacketProcessing(#[from] MixProcessingError),
|
||||
|
||||
#[error("received a forward hop mix packet")]
|
||||
ForwardHopReceivedError,
|
||||
ForwardHopReceived,
|
||||
|
||||
#[error("failed to process received sphinx packet: {0}")]
|
||||
NymPacketProcessing(#[from] PacketProcessingError),
|
||||
}
|
||||
|
||||
// PacketProcessor contains all data required to correctly unwrap and store sphinx packets
|
||||
@@ -24,21 +30,23 @@ pub struct PacketProcessor {
|
||||
}
|
||||
|
||||
impl PacketProcessor {
|
||||
pub fn sphinx_key(&self) -> &PrivateKey {
|
||||
self.inner_processor.sphinx_key()
|
||||
}
|
||||
|
||||
pub(crate) fn new(encryption_key: &encryption::PrivateKey) -> Self {
|
||||
PacketProcessor {
|
||||
inner_processor: SphinxPacketProcessor::new(encryption_key.into()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn process_received(
|
||||
&self,
|
||||
received: FramedNymPacket,
|
||||
) -> Result<ProcessedFinalHop, GatewayProcessingError> {
|
||||
match self.inner_processor.process_received(received)? {
|
||||
MixProcessingResult::ForwardHop(..) => {
|
||||
Err(GatewayProcessingError::ForwardHopReceivedError)
|
||||
}
|
||||
MixProcessingResult::FinalHop(processed_final) => Ok(processed_final),
|
||||
}
|
||||
pub(crate) fn process_packet(
|
||||
received: FramedNymPacket,
|
||||
sphinx_key: &nym_sphinx::PrivateKey,
|
||||
) -> Result<ProcessedFinalHop, GatewayProcessingError> {
|
||||
match process_framed_packet(received, sphinx_key)? {
|
||||
MixProcessingResult::ForwardHop(..) => Err(GatewayProcessingError::ForwardHopReceived),
|
||||
MixProcessingResult::FinalHop(processed_final) => Ok(processed_final),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,9 +1,7 @@
|
||||
// Copyright 2020 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: GPL-3.0-only
|
||||
|
||||
use crate::node::listener::connection_handler::packet_processing::{
|
||||
MixProcessingResult, PacketProcessor,
|
||||
};
|
||||
use crate::node::listener::connection_handler::packet_processing::PacketProcessor;
|
||||
use crate::node::packet_delayforwarder::PacketDelayForwardSender;
|
||||
use crate::node::TaskClient;
|
||||
use futures::StreamExt;
|
||||
@@ -13,7 +11,9 @@ use nym_metrics::nanos;
|
||||
use nym_sphinx::forwarding::packet::MixPacket;
|
||||
use nym_sphinx::framing::codec::NymCodec;
|
||||
use nym_sphinx::framing::packet::FramedNymPacket;
|
||||
use nym_sphinx::framing::processing::MixProcessingResult;
|
||||
use nym_sphinx::Delay as SphinxDelay;
|
||||
use packet_processing::process_received_packet;
|
||||
use std::net::SocketAddr;
|
||||
use tokio::net::TcpStream;
|
||||
use tokio::time::Instant;
|
||||
@@ -38,6 +38,10 @@ impl ConnectionHandler {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn packet_processor(&self) -> &PacketProcessor {
|
||||
&self.packet_processor
|
||||
}
|
||||
|
||||
fn delay_and_forward_packet(&self, mix_packet: MixPacket, delay: Option<SphinxDelay>) {
|
||||
// determine instant at which packet should get forwarded. this way we minimise effect of
|
||||
// being stuck in the queue [of the channel] to get inserted into the delay queue
|
||||
@@ -60,7 +64,10 @@ impl ConnectionHandler {
|
||||
// all processing such, key caching, etc. was done.
|
||||
// however, if it was a forward hop, we still need to delay it
|
||||
nanos!("handle_received_packet", {
|
||||
match self.packet_processor.process_received(framed_sphinx_packet) {
|
||||
self.packet_processor
|
||||
.node_stats_update_sender()
|
||||
.report_received();
|
||||
match process_received_packet(framed_sphinx_packet, self.packet_processor().inner()) {
|
||||
Err(err) => debug!("We failed to process received sphinx packet - {err}"),
|
||||
Ok(res) => match res {
|
||||
MixProcessingResult::ForwardHop(forward_packet, delay) => {
|
||||
|
||||
@@ -4,13 +4,13 @@
|
||||
use crate::node::node_statistics;
|
||||
use nym_crypto::asymmetric::encryption;
|
||||
use nym_mixnode_common::packet_processor::error::MixProcessingError;
|
||||
pub use nym_mixnode_common::packet_processor::processor::MixProcessingResult;
|
||||
use nym_mixnode_common::packet_processor::processor::SphinxPacketProcessor;
|
||||
use nym_sphinx::framing::packet::FramedNymPacket;
|
||||
use nym_sphinx::framing::processing::{process_framed_packet, MixProcessingResult};
|
||||
|
||||
// PacketProcessor contains all data required to correctly unwrap and forward sphinx packets
|
||||
#[derive(Clone)]
|
||||
pub struct PacketProcessor {
|
||||
pub(crate) struct PacketProcessor {
|
||||
/// Responsible for performing unwrapping
|
||||
inner_processor: SphinxPacketProcessor,
|
||||
|
||||
@@ -29,11 +29,18 @@ impl PacketProcessor {
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn process_received(
|
||||
&self,
|
||||
received: FramedNymPacket,
|
||||
) -> Result<MixProcessingResult, MixProcessingError> {
|
||||
self.node_stats_update_sender.report_received();
|
||||
self.inner_processor.process_received(received)
|
||||
pub fn inner(&self) -> &SphinxPacketProcessor {
|
||||
&self.inner_processor
|
||||
}
|
||||
|
||||
pub fn node_stats_update_sender(&self) -> &node_statistics::UpdateSender {
|
||||
&self.node_stats_update_sender
|
||||
}
|
||||
}
|
||||
|
||||
pub fn process_received_packet(
|
||||
packet: FramedNymPacket,
|
||||
inner_processor: &SphinxPacketProcessor,
|
||||
) -> Result<MixProcessingResult, MixProcessingError> {
|
||||
Ok(process_framed_packet(packet, inner_processor.sphinx_key())?)
|
||||
}
|
||||
|
||||
+2
-1
@@ -4,7 +4,7 @@
|
||||
[package]
|
||||
name = "nym-api"
|
||||
license = "GPL-3.0"
|
||||
version = "1.1.43"
|
||||
version = "1.1.44"
|
||||
authors = [
|
||||
"Dave Hrycyszyn <futurechimp@users.noreply.github.com>",
|
||||
"Jędrzej Stuczyński <andrew@nymtech.net>",
|
||||
@@ -131,6 +131,7 @@ nym-http-api-common = { path = "../common/http-api-common", features = ["utoipa"
|
||||
|
||||
[features]
|
||||
no-reward = []
|
||||
v2-performance = []
|
||||
generate-ts = ["ts-rs"]
|
||||
axum = ["dep:axum",
|
||||
"axum-extra",
|
||||
|
||||
@@ -395,18 +395,33 @@ impl StorageManager {
|
||||
start: i64,
|
||||
end: i64,
|
||||
) -> Result<Option<f32>, sqlx::Error> {
|
||||
let result = sqlx::query!(
|
||||
r#"
|
||||
SELECT AVG(reliability) as "reliability: f32" FROM mixnode_status
|
||||
WHERE mixnode_details_id= ? AND timestamp >= ? AND timestamp <= ?
|
||||
"#,
|
||||
id,
|
||||
start,
|
||||
end
|
||||
)
|
||||
.fetch_one(&self.connection_pool)
|
||||
.await?;
|
||||
Ok(result.reliability)
|
||||
if cfg!(feature = "v2-performance") {
|
||||
let result = sqlx::query!(
|
||||
r#"
|
||||
SELECT AVG(reliability) as "reliability: f32" FROM mixnode_status_v2
|
||||
WHERE mixnode_details_id= ? AND timestamp >= ? AND timestamp <= ?
|
||||
"#,
|
||||
id,
|
||||
start,
|
||||
end
|
||||
)
|
||||
.fetch_one(&self.connection_pool)
|
||||
.await?;
|
||||
Ok(result.reliability)
|
||||
} else {
|
||||
let result = sqlx::query!(
|
||||
r#"
|
||||
SELECT AVG(reliability) as "reliability: f32" FROM mixnode_status
|
||||
WHERE mixnode_details_id= ? AND timestamp >= ? AND timestamp <= ?
|
||||
"#,
|
||||
id,
|
||||
start,
|
||||
end
|
||||
)
|
||||
.fetch_one(&self.connection_pool)
|
||||
.await?;
|
||||
Ok(result.reliability)
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) async fn get_gateway_average_reliability_in_interval(
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
# Copyright 2024 - Nym Technologies SA <contact@nymtech.net>
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
[package]
|
||||
name = "nym-data-observatory"
|
||||
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 }
|
||||
axum = { workspace = true, features = ["tokio"] }
|
||||
chrono = { workspace = true }
|
||||
nym-bin-common = { path = "../common/bin-common" }
|
||||
nym-network-defaults = { path = "../common/network-defaults" }
|
||||
nym-task = { path = "../common/task" }
|
||||
nym-node-requests = { path = "../nym-node/nym-node-requests", features = ["openapi"] }
|
||||
serde = { workspace = true, features = ["derive"] }
|
||||
serde_json = { workspace = true }
|
||||
sqlx = { workspace = true, features = ["runtime-tokio-rustls", "postgres", "offline"] }
|
||||
tokio = { workspace = true, features = ["process"] }
|
||||
tokio-util = { workspace = true }
|
||||
tracing = { workspace = true }
|
||||
tracing-subscriber = { workspace = true, features = ["env-filter"] }
|
||||
tower-http = { workspace = true, features = ["cors", "trace"] }
|
||||
utoipa = { workspace = true, features = ["axum_extras", "time"] }
|
||||
utoipa-swagger-ui = { workspace = true, features = ["axum"] }
|
||||
utoipauto = { workspace = true }
|
||||
|
||||
|
||||
[build-dependencies]
|
||||
anyhow = { workspace = true }
|
||||
tokio = { workspace = true, features = ["macros" ] }
|
||||
sqlx = { workspace = true, features = ["runtime-tokio-rustls", "postgres"] }
|
||||
@@ -0,0 +1,93 @@
|
||||
# Making `sqlx` work
|
||||
|
||||
Some of the errors encountered and possible solutions.
|
||||
|
||||
## `The cargo feature offline has to be enabled to use SQLX_OFFLINE`
|
||||
|
||||
Did you enable `offline` **cargo feature** of `sqlx` dependency in your
|
||||
`Cargo.toml`?
|
||||
|
||||
Also, it may happen if you have a version mismatch beetween
|
||||
|
||||
- `sqlx` cargo dependency in `Cargo.toml`
|
||||
- `sqlx-cli` installed by cargo
|
||||
|
||||
To ensure correct version, do
|
||||
|
||||
```
|
||||
cargo uninstall sqlx-cli
|
||||
cargo install --version <version> sqlx-cli
|
||||
```
|
||||
|
||||
where `<version>` matches the version of sqlx in your `Cargo.toml`.
|
||||
|
||||
## `Error: failed to connect to database: password authentication failed`
|
||||
|
||||
If it's in-code, make sure you don't "double authenticate", i.e.
|
||||
|
||||
- if username and password are already specified in `DATABASE_URL`
|
||||
- then, you don't have to use
|
||||
|
||||
```rust
|
||||
ConnectOptions::from_str(&database_Url)
|
||||
.username() // unnecessary
|
||||
.password() // unnecessary
|
||||
```
|
||||
|
||||
If it's outside of code (i.e. when running `cargo check`)
|
||||
|
||||
- make sure password doesn't have any special characters that could be
|
||||
interpreted by the command line/shell weirdly, like `$#\` etc.
|
||||
|
||||
## Cannot generate `sqlx-data.json`
|
||||
|
||||
In order for `sqlx` to generate schema for "offline" work (without DB
|
||||
connection), as of `v0.6.3` you first **need an active DB connection**.
|
||||
|
||||
So make sure
|
||||
|
||||
- DB is running
|
||||
- `DATABASE_URL` is set correctly
|
||||
- `SQLX_OFFLINE` isn't exported to true
|
||||
|
||||
Then run `cargo sqlx prepare`
|
||||
|
||||
After you have the file, you can ignore `DATABASE_URL` and terminate the DB
|
||||
instance. This file represents the DB schema, so when your migrations change,
|
||||
you'll need to re-generate it
|
||||
|
||||
Make sure to commit the file to VCS if you want to avoid re-doing this again on
|
||||
each machine (e.g. other developers, CI).
|
||||
|
||||
## Generated `sqlx-data.json` looks like this
|
||||
|
||||
```json
|
||||
{
|
||||
"db": "PostgreSQL"
|
||||
}
|
||||
```
|
||||
|
||||
after running `cargo sqlx prepare`
|
||||
|
||||
### Similar to:
|
||||
|
||||
```
|
||||
warning: no queries found; do you have the `offline` feature enabled
|
||||
```
|
||||
|
||||
### Possible solutions
|
||||
|
||||
- does your `sqlx-cli` version match `sqlx` version from `Cargo.toml`?
|
||||
- do you have `offline` cargo feature enabled?
|
||||
- make sure to `cargo clean` after these updates
|
||||
|
||||
## Any many, many more
|
||||
|
||||
- `EOF while parsing a value at line`
|
||||
- `failed to find data for query`
|
||||
|
||||
### Possible solutions
|
||||
|
||||
- Usually a DB connection issue
|
||||
- Retry everything
|
||||
- Throw in a `cargo clean -p <your package>` for good measure
|
||||
@@ -0,0 +1,58 @@
|
||||
use anyhow::Result;
|
||||
use sqlx::{Connection, PgConnection};
|
||||
use std::io::Write;
|
||||
use std::{collections::HashMap, fs::File};
|
||||
|
||||
const POSTGRES_USER: &str = "nym";
|
||||
const POSTGRES_PASSWORD: &str = "password123";
|
||||
const POSTGRES_DB: &str = "data_obs_db";
|
||||
|
||||
/// if schema changes, rerun `cargo sqlx prepare` with a running DB
|
||||
/// https://github.com/launchbadge/sqlx/blob/main/sqlx-cli/README.md#enable-building-in-offline-mode-with-query
|
||||
#[tokio::main]
|
||||
async fn main() -> Result<()> {
|
||||
let db_url =
|
||||
format!("postgresql://{POSTGRES_USER}:{POSTGRES_PASSWORD}@localhost:5432/{POSTGRES_DB}");
|
||||
|
||||
// if a live DB is reachable, use that
|
||||
if PgConnection::connect(&db_url).await.is_ok() {
|
||||
export_db_variables(&db_url)?;
|
||||
println!("cargo::rustc-env=SQLX_OFFLINE=false");
|
||||
run_migrations(&db_url).await?;
|
||||
} else {
|
||||
// by default, run in offline mode
|
||||
println!("cargo::rustc-env=SQLX_OFFLINE=true");
|
||||
}
|
||||
|
||||
rerun_if_changed();
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn export_db_variables(db_url: &str) -> Result<()> {
|
||||
let mut map = HashMap::new();
|
||||
map.insert("POSTGRES_USER", POSTGRES_USER);
|
||||
map.insert("POSTGRES_PASSWORD", POSTGRES_PASSWORD);
|
||||
map.insert("POSTGRES_DB", POSTGRES_DB);
|
||||
map.insert("DATABASE_URL", db_url);
|
||||
|
||||
let mut file = File::create(".env")?;
|
||||
for (var, value) in map.iter() {
|
||||
println!("cargo::rustc-env={}={}", var, value);
|
||||
writeln!(file, "{}={}", var, value).expect("Failed to write to dotenv file");
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn run_migrations(db_url: &str) -> Result<()> {
|
||||
let mut conn = PgConnection::connect(db_url).await?;
|
||||
sqlx::migrate!("./migrations").run(&mut conn).await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn rerun_if_changed() {
|
||||
println!("cargo::rerun-if-changed=migrations");
|
||||
println!("cargo::rerun-if-changed=src/db/queries");
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
services:
|
||||
postgres:
|
||||
image: postgres:13
|
||||
container_name: nym-data-observatory-pg
|
||||
env_file:
|
||||
- .env
|
||||
ports:
|
||||
- "5432:5432"
|
||||
volumes:
|
||||
- pgdata:/var/lib/postgresql/data
|
||||
|
||||
volumes:
|
||||
pgdata:
|
||||
@@ -0,0 +1,6 @@
|
||||
CREATE TABLE responses (
|
||||
id SERIAL PRIMARY KEY,
|
||||
joke_id VARCHAR NOT NULL UNIQUE,
|
||||
joke TEXT NOT NULL,
|
||||
date_created INTEGER NOT NULL
|
||||
);
|
||||
Executable
+12
@@ -0,0 +1,12 @@
|
||||
#!/bin/bash
|
||||
|
||||
source .env
|
||||
|
||||
# Launching a container in such a way that it's destroyed after you detach from the terminal:
|
||||
docker compose up
|
||||
|
||||
# docker exec -it nym-data-observatory-pg /bin/bash
|
||||
# psql -U youruser -d yourdb
|
||||
|
||||
echo "Tearing down containers to have a clean slate"
|
||||
docker compose down -v
|
||||
@@ -0,0 +1,79 @@
|
||||
{
|
||||
"db": "PostgreSQL",
|
||||
"249faa11b88b749f50342bb5c9cc41d20896db543eed74a6f320c041bcbb723d": {
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"nullable": [],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Varchar",
|
||||
"Text",
|
||||
"Int4"
|
||||
]
|
||||
}
|
||||
},
|
||||
"query": "INSERT INTO responses\n (joke_id, joke, date_created)\n VALUES\n ($1, $2, $3)\n ON CONFLICT(joke_id) DO UPDATE SET\n joke=excluded.joke,\n date_created=excluded.date_created;"
|
||||
},
|
||||
"aff7fbd06728004d2f2226d20c32f1482df00de2dc1d2b4debbb2e12553d997b": {
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"name": "joke_id",
|
||||
"ordinal": 0,
|
||||
"type_info": "Varchar"
|
||||
},
|
||||
{
|
||||
"name": "joke",
|
||||
"ordinal": 1,
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"name": "date_created",
|
||||
"ordinal": 2,
|
||||
"type_info": "Int4"
|
||||
}
|
||||
],
|
||||
"nullable": [
|
||||
false,
|
||||
false,
|
||||
false
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Text"
|
||||
]
|
||||
}
|
||||
},
|
||||
"query": "SELECT joke_id, joke, date_created FROM responses WHERE joke_id = $1"
|
||||
},
|
||||
"e53f479f8cead3dc8aa1875e5d450ad69686cf6a109e37d6c3f0623c3e9f91d0": {
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"name": "joke_id",
|
||||
"ordinal": 0,
|
||||
"type_info": "Varchar"
|
||||
},
|
||||
{
|
||||
"name": "joke",
|
||||
"ordinal": 1,
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"name": "date_created",
|
||||
"ordinal": 2,
|
||||
"type_info": "Int4"
|
||||
}
|
||||
],
|
||||
"nullable": [
|
||||
false,
|
||||
false,
|
||||
false
|
||||
],
|
||||
"parameters": {
|
||||
"Left": []
|
||||
}
|
||||
},
|
||||
"query": "SELECT joke_id, joke, date_created FROM responses"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
use core::str;
|
||||
use serde::Deserialize;
|
||||
use tokio::process::Command;
|
||||
use tokio::task::JoinHandle;
|
||||
use tokio::time::Duration;
|
||||
|
||||
use crate::db::{self, DbPool};
|
||||
|
||||
const REFRESH_DELAY: Duration = Duration::from_secs(15);
|
||||
const FAILURE_RETRY_DELAY: Duration = Duration::from_secs(60 * 2);
|
||||
|
||||
pub(crate) async fn spawn_in_background(db_pool: DbPool) -> JoinHandle<()> {
|
||||
loop {
|
||||
tracing::info!("Running in a loop 🏃");
|
||||
|
||||
if let Err(e) = some_network_action(&db_pool).await {
|
||||
tracing::error!(
|
||||
"❌ Run failed: {e}, retrying in {}s...",
|
||||
FAILURE_RETRY_DELAY.as_secs()
|
||||
);
|
||||
tokio::time::sleep(FAILURE_RETRY_DELAY).await;
|
||||
} else {
|
||||
tracing::info!(
|
||||
"✅ Run successful, sleeping for {}s...",
|
||||
REFRESH_DELAY.as_secs()
|
||||
);
|
||||
tokio::time::sleep(REFRESH_DELAY).await;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Debug)]
|
||||
pub(crate) struct Response {
|
||||
#[serde(rename(deserialize = "id"))]
|
||||
pub(crate) joke_id: String,
|
||||
pub(crate) joke: String,
|
||||
#[serde(rename(deserialize = "status"))]
|
||||
pub(crate) _status: u16,
|
||||
}
|
||||
|
||||
async fn some_network_action(pool: &DbPool) -> anyhow::Result<()> {
|
||||
// for demonstration purposes only. You should use reqwest if you need it
|
||||
let output = Command::new("curl")
|
||||
.arg("-H")
|
||||
.arg("Accept: application/json")
|
||||
.arg("https://icanhazdadjoke.com/")
|
||||
.output()
|
||||
.await?;
|
||||
|
||||
if !output.status.success() {
|
||||
anyhow::bail!("Curl command failed with status: {}", output.status);
|
||||
}
|
||||
|
||||
let response_str = str::from_utf8(&output.stdout)?;
|
||||
let joke_response: Response = serde_json::from_str(response_str)?;
|
||||
|
||||
tracing::info!("{:?}", joke_response.joke);
|
||||
db::queries::insert_joke(pool, joke_response.into()).await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
use anyhow::{anyhow, Result};
|
||||
use sqlx::{migrate::Migrator, postgres::PgConnectOptions, ConnectOptions, PgPool};
|
||||
use std::str::FromStr;
|
||||
|
||||
pub(crate) mod models;
|
||||
pub(crate) mod queries;
|
||||
|
||||
pub(crate) const DATABASE_URL_ENV_VAR: &str = "DATABASE_URL";
|
||||
|
||||
static MIGRATOR: Migrator = sqlx::migrate!("./migrations");
|
||||
|
||||
pub(crate) type DbPool = PgPool;
|
||||
|
||||
pub(crate) struct Storage {
|
||||
pool: DbPool,
|
||||
}
|
||||
|
||||
impl Storage {
|
||||
pub async fn init() -> Result<Self> {
|
||||
let connection_url = std::env::var(DATABASE_URL_ENV_VAR).map_err(anyhow::Error::from)?;
|
||||
let connect_options = {
|
||||
let mut connect_options = PgConnectOptions::from_str(&connection_url)?;
|
||||
let connect_options = connect_options.disable_statement_logging();
|
||||
(*connect_options).clone()
|
||||
};
|
||||
|
||||
let pool = DbPool::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 async fn pool_owned(&self) -> DbPool {
|
||||
self.pool.clone()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
use serde::{Deserialize, Serialize};
|
||||
use utoipa::ToSchema;
|
||||
|
||||
use crate::background_task::Response;
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, ToSchema)]
|
||||
pub(crate) struct JokeDto {
|
||||
pub(crate) joke_id: String,
|
||||
pub(crate) joke: String,
|
||||
pub(crate) date_created: i32,
|
||||
}
|
||||
|
||||
impl From<Response> for JokeDto {
|
||||
fn from(value: Response) -> Self {
|
||||
Self {
|
||||
joke_id: value.joke_id,
|
||||
joke: value.joke,
|
||||
// casting not smart, can implicitly panic, don't do this in prod
|
||||
date_created: chrono::offset::Utc::now().timestamp() as i32,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
use crate::db::{models::JokeDto, DbPool};
|
||||
|
||||
pub(crate) async fn insert_joke(pool: &DbPool, joke: JokeDto) -> anyhow::Result<()> {
|
||||
let mut conn = pool.acquire().await?;
|
||||
sqlx::query!(
|
||||
"INSERT INTO responses
|
||||
(joke_id, joke, date_created)
|
||||
VALUES
|
||||
($1, $2, $3)
|
||||
ON CONFLICT(joke_id) DO UPDATE SET
|
||||
joke=excluded.joke,
|
||||
date_created=excluded.date_created;",
|
||||
joke.joke_id,
|
||||
joke.joke,
|
||||
joke.date_created as i32,
|
||||
)
|
||||
.execute(&mut *conn)
|
||||
.await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(crate) async fn select_joke_by_id(pool: &DbPool, joke_id: &str) -> anyhow::Result<JokeDto> {
|
||||
sqlx::query_as!(
|
||||
JokeDto,
|
||||
"SELECT joke_id, joke, date_created FROM responses WHERE joke_id = $1",
|
||||
joke_id
|
||||
)
|
||||
.fetch_one(pool)
|
||||
.await
|
||||
.map_err(anyhow::Error::from)
|
||||
}
|
||||
|
||||
pub(crate) async fn select_all(pool: &DbPool) -> anyhow::Result<Vec<JokeDto>> {
|
||||
sqlx::query_as!(JokeDto, "SELECT joke_id, joke, date_created FROM responses",)
|
||||
.fetch_all(pool)
|
||||
.await
|
||||
.map_err(anyhow::Error::from)
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
// group queries in files by theme
|
||||
mod joke;
|
||||
|
||||
// re-exporting allows us to access all queries via `queries::bla``
|
||||
pub(crate) use joke::{insert_joke, select_all, select_joke_by_id};
|
||||
@@ -0,0 +1,78 @@
|
||||
use axum::{
|
||||
extract::{Path, State},
|
||||
Json, Router,
|
||||
};
|
||||
use serde::Deserialize;
|
||||
use utoipa::IntoParams;
|
||||
|
||||
use crate::{
|
||||
db::{
|
||||
models::JokeDto,
|
||||
queries::{self, select_joke_by_id},
|
||||
},
|
||||
http::{
|
||||
error::{Error, HttpResult},
|
||||
state::AppState,
|
||||
},
|
||||
};
|
||||
|
||||
pub(crate) fn routes() -> Router<AppState> {
|
||||
Router::new()
|
||||
.route("/", axum::routing::get(jokes))
|
||||
.route("/:joke_id", axum::routing::get(joke_by_id))
|
||||
.route("/fetch_another", axum::routing::get(fetch_another))
|
||||
}
|
||||
|
||||
#[utoipa::path(
|
||||
tag = "Dad Jokes",
|
||||
get,
|
||||
path = "/v1/jokes",
|
||||
responses(
|
||||
(status = 200, body = Vec<JokeDto>)
|
||||
)
|
||||
)]
|
||||
async fn jokes(State(state): State<AppState>) -> HttpResult<Json<Vec<JokeDto>>> {
|
||||
queries::select_all(state.db_pool())
|
||||
.await
|
||||
.map(Json::from)
|
||||
.map_err(|_| Error::internal())
|
||||
}
|
||||
|
||||
#[derive(Deserialize, IntoParams)]
|
||||
#[into_params(parameter_in = Path)]
|
||||
struct JokeIdParam {
|
||||
joke_id: String,
|
||||
}
|
||||
|
||||
#[utoipa::path(
|
||||
tag = "Dad Jokes",
|
||||
get,
|
||||
params(
|
||||
JokeIdParam
|
||||
),
|
||||
path = "/v1/jokes/{joke_id}",
|
||||
responses(
|
||||
(status = 200, body = JokeDto)
|
||||
)
|
||||
)]
|
||||
async fn joke_by_id(
|
||||
Path(JokeIdParam { joke_id }): Path<JokeIdParam>,
|
||||
State(state): State<AppState>,
|
||||
) -> HttpResult<Json<JokeDto>> {
|
||||
select_joke_by_id(state.db_pool(), &joke_id)
|
||||
.await
|
||||
.map(Json::from)
|
||||
.map_err(|_| Error::not_found(joke_id))
|
||||
}
|
||||
|
||||
#[utoipa::path(
|
||||
tag = "Dad Jokes",
|
||||
get,
|
||||
path = "/v1/jokes/fetch_another",
|
||||
responses(
|
||||
(status = 200, body = String)
|
||||
)
|
||||
)]
|
||||
async fn fetch_another(State(_state): State<AppState>) -> HttpResult<Json<String>> {
|
||||
Ok(Json(String::from("Done boss, check the DB")))
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
use axum::{extract::State, Json, Router};
|
||||
|
||||
use crate::http::{error::HttpResult, state::AppState};
|
||||
|
||||
pub(crate) fn routes() -> Router<AppState> {
|
||||
Router::new().route("/", axum::routing::get(mixnodes))
|
||||
}
|
||||
|
||||
#[utoipa::path(
|
||||
tag = "Mixnodes",
|
||||
get,
|
||||
path = "/v1/mixnodes",
|
||||
responses(
|
||||
(status = 200, body = String)
|
||||
)
|
||||
)]
|
||||
async fn mixnodes(State(_state): State<AppState>) -> HttpResult<Json<serde_json::Value>> {
|
||||
Ok(Json(
|
||||
serde_json::json!({"message": "😎 Nothing to see here, move along 😎"}),
|
||||
))
|
||||
}
|
||||
@@ -0,0 +1,79 @@
|
||||
use anyhow::anyhow;
|
||||
use axum::{response::Redirect, Router};
|
||||
use tokio::net::ToSocketAddrs;
|
||||
use tower_http::{cors::CorsLayer, trace::TraceLayer};
|
||||
use utoipa::OpenApi;
|
||||
use utoipa_swagger_ui::SwaggerUi;
|
||||
|
||||
use crate::http::{api_docs, server::HttpServer, state::AppState};
|
||||
|
||||
pub(crate) mod jokes;
|
||||
pub(crate) mod mixnodes;
|
||||
|
||||
pub(crate) struct RouterBuilder {
|
||||
unfinished_router: Router<AppState>,
|
||||
}
|
||||
|
||||
impl RouterBuilder {
|
||||
pub(crate) fn with_default_routes() -> Self {
|
||||
let router = Router::new()
|
||||
.merge(
|
||||
SwaggerUi::new("/swagger")
|
||||
.url("/api-docs/openapi.json", api_docs::ApiDoc::openapi()),
|
||||
)
|
||||
.route(
|
||||
"/",
|
||||
axum::routing::get(|| async { Redirect::permanent("/swagger") }),
|
||||
)
|
||||
.nest(
|
||||
"/v1",
|
||||
Router::new()
|
||||
.nest("/jokes", jokes::routes())
|
||||
.nest("/mixnodes", mixnodes::routes()),
|
||||
);
|
||||
|
||||
Self {
|
||||
unfinished_router: router,
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn with_state(self, state: AppState) -> RouterWithState {
|
||||
RouterWithState {
|
||||
router: self.finalize_routes().with_state(state),
|
||||
}
|
||||
}
|
||||
|
||||
fn finalize_routes(self) -> Router<AppState> {
|
||||
// layers added later wrap earlier layers
|
||||
self.unfinished_router
|
||||
// CORS layer needs to wrap other API layers
|
||||
.layer(setup_cors())
|
||||
// logger should be outermost layer
|
||||
.layer(TraceLayer::new_for_http())
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) struct RouterWithState {
|
||||
router: Router,
|
||||
}
|
||||
|
||||
impl RouterWithState {
|
||||
pub(crate) async fn build_server<A: ToSocketAddrs>(
|
||||
self,
|
||||
bind_address: A,
|
||||
) -> anyhow::Result<HttpServer> {
|
||||
tokio::net::TcpListener::bind(bind_address)
|
||||
.await
|
||||
.map(|listener| HttpServer::new(self.router, listener))
|
||||
.map_err(|err| anyhow!("Couldn't bind to address due to {}", err))
|
||||
}
|
||||
}
|
||||
|
||||
fn setup_cors() -> CorsLayer {
|
||||
use axum::http::Method;
|
||||
CorsLayer::new()
|
||||
.allow_origin(tower_http::cors::Any)
|
||||
.allow_methods([Method::POST, Method::GET, Method::PATCH, Method::OPTIONS])
|
||||
.allow_headers(tower_http::cors::Any)
|
||||
.allow_credentials(false)
|
||||
}
|
||||
@@ -0,0 +1,91 @@
|
||||
use axum::Router;
|
||||
use core::net::SocketAddr;
|
||||
use tokio::{net::TcpListener, task::JoinHandle};
|
||||
use tokio_util::sync::{CancellationToken, WaitForCancellationFutureOwned};
|
||||
|
||||
use crate::{
|
||||
db::DbPool,
|
||||
http::{api::RouterBuilder, state::AppState},
|
||||
};
|
||||
|
||||
/// Return handles that allow for graceful shutdown of server + awaiting its
|
||||
/// background tokio task
|
||||
pub(crate) async fn start_http_api(
|
||||
db_pool: DbPool,
|
||||
http_port: u16,
|
||||
nym_http_cache_ttl: u64,
|
||||
) -> anyhow::Result<ShutdownHandles> {
|
||||
let router_builder = RouterBuilder::with_default_routes();
|
||||
|
||||
let state = AppState::new(db_pool, nym_http_cache_ttl);
|
||||
let router = router_builder.with_state(state);
|
||||
|
||||
let bind_addr = format!("0.0.0.0:{}", http_port);
|
||||
let server = router.build_server(bind_addr).await?;
|
||||
|
||||
Ok(start_server(server))
|
||||
}
|
||||
|
||||
fn start_server(server: HttpServer) -> ShutdownHandles {
|
||||
// one copy is stored to trigger a graceful shutdown later
|
||||
let shutdown_button = CancellationToken::new();
|
||||
// other copy is given to server to listen for a shutdown
|
||||
let shutdown_receiver = shutdown_button.clone();
|
||||
let shutdown_receiver = shutdown_receiver.cancelled_owned();
|
||||
|
||||
let server_handle = tokio::spawn(async move { server.run(shutdown_receiver).await });
|
||||
|
||||
ShutdownHandles {
|
||||
server_handle,
|
||||
shutdown_button,
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) struct ShutdownHandles {
|
||||
server_handle: JoinHandle<std::io::Result<()>>,
|
||||
shutdown_button: CancellationToken,
|
||||
}
|
||||
|
||||
impl ShutdownHandles {
|
||||
/// Send graceful shutdown signal to server and wait for server task to complete
|
||||
pub(crate) async fn shutdown(self) -> anyhow::Result<()> {
|
||||
self.shutdown_button.cancel();
|
||||
|
||||
match self.server_handle.await {
|
||||
Ok(Ok(_)) => {
|
||||
tracing::info!("HTTP server shut down without errors");
|
||||
}
|
||||
Ok(Err(err)) => {
|
||||
tracing::error!("HTTP server terminated with: {err}");
|
||||
anyhow::bail!(err)
|
||||
}
|
||||
Err(err) => {
|
||||
tracing::error!("Server task panicked: {err}");
|
||||
}
|
||||
};
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) struct HttpServer {
|
||||
router: Router,
|
||||
listener: TcpListener,
|
||||
}
|
||||
|
||||
impl HttpServer {
|
||||
pub(crate) fn new(router: Router, listener: TcpListener) -> Self {
|
||||
Self { router, listener }
|
||||
}
|
||||
|
||||
pub(crate) async fn run(self, receiver: WaitForCancellationFutureOwned) -> std::io::Result<()> {
|
||||
// into_make_service_with_connect_info allows us to see client ip address
|
||||
axum::serve(
|
||||
self.listener,
|
||||
self.router
|
||||
.into_make_service_with_connect_info::<SocketAddr>(),
|
||||
)
|
||||
.with_graceful_shutdown(receiver)
|
||||
.await
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
use utoipa::OpenApi;
|
||||
use utoipauto::utoipauto;
|
||||
|
||||
// manually import external structs which are behind feature flags because they
|
||||
// can't be automatically discovered
|
||||
// https://github.com/ProbablyClem/utoipauto/issues/13#issuecomment-1974911829
|
||||
#[utoipauto(paths = "./nym-data-observatory/src")]
|
||||
#[derive(OpenApi)]
|
||||
#[openapi(info(title = "Nym API"), tags(), components(schemas()))]
|
||||
pub(super) struct ApiDoc;
|
||||
@@ -0,0 +1,23 @@
|
||||
use crate::read_env_var;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub(crate) struct Config {
|
||||
http_port: u16,
|
||||
}
|
||||
|
||||
const HTTP_PORT_DEFAULT: u16 = 8000;
|
||||
|
||||
impl Config {
|
||||
pub(crate) fn from_env() -> Self {
|
||||
Self {
|
||||
http_port: read_env_var("HTTP_PORT")
|
||||
.unwrap_or(HTTP_PORT_DEFAULT.to_string())
|
||||
.parse()
|
||||
.unwrap_or(HTTP_PORT_DEFAULT),
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn http_port(&self) -> u16 {
|
||||
self.http_port
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
pub(crate) type HttpResult<T> = Result<T, Error>;
|
||||
|
||||
pub(crate) struct Error {
|
||||
message: String,
|
||||
status: axum::http::StatusCode,
|
||||
}
|
||||
|
||||
impl Error {
|
||||
pub(crate) fn not_found(message: String) -> Self {
|
||||
Self {
|
||||
message,
|
||||
status: axum::http::StatusCode::NOT_FOUND,
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn internal() -> Self {
|
||||
Self {
|
||||
message: String::from("Internal server error"),
|
||||
status: axum::http::StatusCode::INTERNAL_SERVER_ERROR,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl axum::response::IntoResponse for Error {
|
||||
fn into_response(self) -> axum::response::Response {
|
||||
(self.status, self.message).into_response()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
pub(crate) mod api;
|
||||
pub(crate) mod api_docs;
|
||||
pub(crate) mod config;
|
||||
pub(crate) mod error;
|
||||
pub(crate) mod server;
|
||||
pub(crate) mod state;
|
||||
@@ -0,0 +1,91 @@
|
||||
use axum::Router;
|
||||
use core::net::SocketAddr;
|
||||
use tokio::{net::TcpListener, task::JoinHandle};
|
||||
use tokio_util::sync::{CancellationToken, WaitForCancellationFutureOwned};
|
||||
|
||||
use crate::{
|
||||
db::DbPool,
|
||||
http::{api::RouterBuilder, state::AppState},
|
||||
};
|
||||
|
||||
/// Return handles that allow for graceful shutdown of server + awaiting its
|
||||
/// background tokio task
|
||||
pub(crate) async fn start_http_api(
|
||||
db_pool: DbPool,
|
||||
http_port: u16,
|
||||
) -> anyhow::Result<ShutdownHandles> {
|
||||
let router_builder = RouterBuilder::with_default_routes();
|
||||
|
||||
let state = AppState::new(db_pool);
|
||||
let router = router_builder.with_state(state);
|
||||
|
||||
let bind_addr = format!("0.0.0.0:{}", http_port);
|
||||
let server = router.build_server(bind_addr).await?;
|
||||
|
||||
Ok(start_server(server))
|
||||
}
|
||||
|
||||
fn start_server(server: HttpServer) -> ShutdownHandles {
|
||||
// one copy is stored to trigger a graceful shutdown later
|
||||
let shutdown_button = CancellationToken::new();
|
||||
// other copy is given to server to listen for a shutdown
|
||||
let shutdown_receiver = shutdown_button.clone();
|
||||
let shutdown_receiver = shutdown_receiver.cancelled_owned();
|
||||
|
||||
let server_handle = tokio::spawn(async move { server.run(shutdown_receiver).await });
|
||||
|
||||
ShutdownHandles {
|
||||
server_handle,
|
||||
shutdown_button,
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) struct ShutdownHandles {
|
||||
server_handle: JoinHandle<std::io::Result<()>>,
|
||||
shutdown_button: CancellationToken,
|
||||
}
|
||||
|
||||
impl ShutdownHandles {
|
||||
/// Send graceful shutdown signal to server and wait for server task to complete
|
||||
pub(crate) async fn shutdown(self) -> anyhow::Result<()> {
|
||||
self.shutdown_button.cancel();
|
||||
|
||||
match self.server_handle.await {
|
||||
Ok(Ok(_)) => {
|
||||
tracing::info!("HTTP server shut down without errors");
|
||||
}
|
||||
Ok(Err(err)) => {
|
||||
tracing::error!("HTTP server terminated with: {err}");
|
||||
anyhow::bail!(err)
|
||||
}
|
||||
Err(err) => {
|
||||
tracing::error!("Server task panicked: {err}");
|
||||
}
|
||||
};
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) struct HttpServer {
|
||||
router: Router,
|
||||
listener: TcpListener,
|
||||
}
|
||||
|
||||
impl HttpServer {
|
||||
pub(crate) fn new(router: Router, listener: TcpListener) -> Self {
|
||||
Self { router, listener }
|
||||
}
|
||||
|
||||
pub(crate) async fn run(self, receiver: WaitForCancellationFutureOwned) -> std::io::Result<()> {
|
||||
// into_make_service_with_connect_info allows us to see client ip address
|
||||
// in middleware, for logging, TLS, routing etc.
|
||||
axum::serve(
|
||||
self.listener,
|
||||
self.router
|
||||
.into_make_service_with_connect_info::<SocketAddr>(),
|
||||
)
|
||||
.with_graceful_shutdown(receiver)
|
||||
.await
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
use crate::db::DbPool;
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub(crate) struct AppState {
|
||||
db_pool: DbPool,
|
||||
}
|
||||
|
||||
impl AppState {
|
||||
pub(crate) fn new(db_pool: DbPool) -> Self {
|
||||
Self { db_pool }
|
||||
}
|
||||
|
||||
pub(crate) fn db_pool(&self) -> &DbPool {
|
||||
&self.db_pool
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
use tracing::level_filters::LevelFilter;
|
||||
use tracing_subscriber::{filter::Directive, EnvFilter};
|
||||
|
||||
pub(crate) fn setup_tracing_logger() {
|
||||
fn directive_checked(directive: String) -> Directive {
|
||||
directive.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)
|
||||
// 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 = [
|
||||
"nym_bin_common",
|
||||
"nym_explorer_client",
|
||||
"nym_network_defaults",
|
||||
"nym_validator_client",
|
||||
"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)));
|
||||
}
|
||||
|
||||
log_builder.with_env_filter(filter).init();
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
use nym_network_defaults::setup_env;
|
||||
use nym_task::signal::wait_for_signal;
|
||||
|
||||
use crate::http::config;
|
||||
|
||||
mod background_task;
|
||||
mod db;
|
||||
mod http;
|
||||
mod logging;
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() -> anyhow::Result<()> {
|
||||
logging::setup_tracing_logger();
|
||||
|
||||
// if dotenv file is present, load its values
|
||||
// otherwise, default to mainnet
|
||||
setup_env(Some(".env"));
|
||||
|
||||
let conf = config::Config::from_env();
|
||||
tracing::debug!("Using config:\n{:?}", conf);
|
||||
|
||||
let storage = db::Storage::init().await?;
|
||||
let db_pool = storage.pool_owned().await;
|
||||
tokio::spawn(async move {
|
||||
background_task::spawn_in_background(db_pool).await;
|
||||
tracing::info!("Started task");
|
||||
});
|
||||
|
||||
let shutdown_handles =
|
||||
http::server::start_http_api(storage.pool_owned().await, conf.http_port())
|
||||
.await
|
||||
.expect("Failed to start server");
|
||||
tracing::info!("Started HTTP server on port {}", conf.http_port());
|
||||
|
||||
wait_for_signal().await;
|
||||
|
||||
if let Err(err) = shutdown_handles.shutdown().await {
|
||||
tracing::error!("{err}");
|
||||
};
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// TODO dz move this to common
|
||||
fn read_env_var(env_var: &str) -> anyhow::Result<String> {
|
||||
std::env::var(env_var)
|
||||
.map_err(|_| anyhow::anyhow!("You need to set {}", env_var))
|
||||
.map(|value| {
|
||||
tracing::trace!("{}={}", env_var, value);
|
||||
value
|
||||
})
|
||||
}
|
||||
+1
-1
@@ -3,7 +3,7 @@
|
||||
|
||||
[package]
|
||||
name = "nym-node"
|
||||
version = "1.1.7"
|
||||
version = "1.1.8"
|
||||
authors.workspace = true
|
||||
repository.workspace = true
|
||||
homepage.workspace = true
|
||||
|
||||
@@ -29,7 +29,7 @@ rand = { workspace = true }
|
||||
fastrand = { workspace = true }
|
||||
|
||||
nym-crypto = { path = "../../common/crypto", features = ["asymmetric", "rand"] }
|
||||
nym-http-api-common = { path = "../../common/http-api-common" }
|
||||
nym-http-api-common = { path = "../../common/http-api-common", features = ["utoipa"] }
|
||||
nym-node-requests = { path = "../nym-node-requests", default-features = false, features = [
|
||||
"openapi",
|
||||
] }
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user