Compare commits

..

27 Commits

Author SHA1 Message Date
Andrej Mihajlov 6bbdb462d0 test: remove file after closing for a test 2025-07-08 14:44:02 +03:00
Andrej Mihajlov 2cbfdd22c9 Fix missing await on self.close_pool_inner() 2025-07-08 14:44:02 +03:00
Andrej Mihajlov 4551d79080 Hide tokio behind feature 2025-07-08 14:44:01 +03:00
Andrej Mihajlov c1418e995d Hide tokio and sqlx behind not(wasm32) 2025-07-08 14:43:45 +03:00
Andrej Mihajlov a6220fd6fc Exclude sqlx-pool-guard from wasm builds 2025-07-08 14:43:45 +03:00
Andrej Mihajlov ab6615d432 Log all tracing output just in case 2025-07-08 14:43:45 +03:00
Andrej Mihajlov 416c2ce6f3 Include proc_pidinfo on iOS 2025-07-08 14:43:45 +03:00
Andrej Mihajlov 423997955b Switch to tracing 2025-07-08 14:43:45 +03:00
Andrej Mihajlov dd5b18f08f Use log here 2025-07-08 14:43:45 +03:00
Andrej Mihajlov 6c58ea8f7c Expose database path 2025-07-08 14:43:45 +03:00
Andrej Mihajlov a5d2ca139b Clean up 2025-07-08 14:43:45 +03:00
Andrej Mihajlov 0a2fd8e30d Improve windows 2025-07-08 14:43:45 +03:00
Andrej Mihajlov d00fb5c486 Cleanup 2025-07-08 14:43:45 +03:00
Andrej Mihajlov dde747b867 Revert 2025-07-08 14:43:45 +03:00
Andrej Mihajlov e6387e080f Use sqlite pool guard 2025-07-08 14:43:45 +03:00
Andrej Mihajlov a70a408098 Remove logs 2025-07-08 14:43:45 +03:00
Andrej Mihajlov 9d19b31984 Document 2025-07-08 14:43:45 +03:00
Andrej Mihajlov 9f14a55de6 Add Windows impl 2025-07-08 14:43:45 +03:00
Andrej Mihajlov cd6494c9f7 Add Windows implementation 2025-07-08 14:43:45 +03:00
Andrej Mihajlov 101a364935 Update Linux impl 2025-07-08 14:43:44 +03:00
Andrej Mihajlov 835e297069 Open file watch 2025-07-08 14:43:44 +03:00
Andrej Mihajlov 8354ae5344 Close sqlite pool before erroring 2025-07-08 14:43:44 +03:00
benedetta davico 0d420fb0a5 remove explorer-api in workflow 2025-06-10 11:01:24 +02:00
benedettadavico fce195fdba update changelog 2025-06-10 10:28:47 +02:00
Bogdan-Ștefan Neacşu 3a528d3b89 No autoremoval of peers (#5831)
* No autoremoval

* Remove startup_timestamp
2025-06-06 12:48:34 +03:00
benedettadavico e9bb9792ab bump binaries 2025-06-04 14:42:04 +02:00
Bogdan-Ștefan Neacşu 88d4a9b111 Set cached storage counters to 0 (#5812)
* Set cached storage counters to 0

* u64 to i64 log possible error

* Check addition too
2025-06-04 12:11:46 +03:00
414 changed files with 5617 additions and 22622 deletions
@@ -1,57 +0,0 @@
name: ci-check-nym-stats-api-version
on:
pull_request:
paths:
- "nym-statistics-api/**"
env:
WORKING_DIRECTORY: "nym-statistics-api"
jobs:
check-if-tag-exists:
runs-on: arc-ubuntu-22.04-dind
steps:
- name: Checkout repo
uses: actions/checkout@v4
- name: Get version from cargo.toml
uses: mikefarah/yq@v4.45.4
id: get_version
with:
cmd: yq -oy '.package.version' ${{ env.WORKING_DIRECTORY }}/Cargo.toml
- name: Check if git tag exists
run: |
TAG=${{ env.WORKING_DIRECTORY }}-${{ steps.get_version.outputs.result }}
if [[ -z "$TAG" ]]; then
echo "Tag is empty"
exit 1
fi
git ls-remote --tags origin | awk '{print $2}'
if git ls-remote --tags origin | awk '{print $2}' | grep -q "refs/tags/$TAG$" ; then
echo "Tag '$TAG' ALREADY EXISTS on the remote"
exit 1
else
echo "Tag '$TAG' does not exist on the remote"
fi
- name: Check if harbor tag exists
run: |
TAG=${{ steps.get_version.outputs.result }}
registry=https://harbor.nymte.ch
repo_name=nym/nym-statistics-api
if [[ -z $TAG ]]; then
echo "Tag is empty"
exit 1
fi
curl -su ${{ secrets.HARBOR_ROBOT_USERNAME }}:${{ secrets.HARBOR_ROBOT_SECRET }} "$registry/v2/$repo_name/tags/list" | jq
exists=$(curl -su ${{ secrets.HARBOR_ROBOT_USERNAME }}:${{ secrets.HARBOR_ROBOT_SECRET }} "$registry/v2/$repo_name/tags/list" | jq --arg tag $TAG '.tags | contains([$tag])' )
if [[ $exists = "true" ]]; then
echo "Version '$TAG' defined in Cargo.toml ALREADY EXISTS as tag in harbor repo"
exit 1
elif [[ $exists = "false" ]]; then
echo "Version '$TAG' doesn't exist on the remote"
else
echo "Unknown output '$exists'"
exit 1
fi
@@ -56,7 +56,6 @@ jobs:
cp contracts/target/wasm32-unknown-unknown/release/cw3_flex_multisig.wasm $OUTPUT_DIR
cp contracts/target/wasm32-unknown-unknown/release/cw4_group.wasm $OUTPUT_DIR
cp contracts/target/wasm32-unknown-unknown/release/nym_ecash.wasm $OUTPUT_DIR
cp contracts/target/wasm32-unknown-unknown/release/nym_pool_contract.wasm $OUTPUT_DIR
- name: Deploy branch to CI www
continue-on-error: true
@@ -66,7 +66,6 @@ jobs:
with:
name: my-artifact
path: |
target/release/explorer-api
target/release/nym-client
target/release/nym-socks5-client
target/release/nym-api
@@ -82,7 +81,6 @@ jobs:
if: github.event_name == 'release'
with:
files: |
target/release/explorer-api
target/release/nym-client
target/release/nym-socks5-client
target/release/nym-api
@@ -1,42 +0,0 @@
name: Build and upload Nym Statistics API container to harbor.nymte.ch
on:
workflow_dispatch:
env:
WORKING_DIRECTORY: "nym-statistics-api"
CONTAINER_NAME: "nym-statistics-api"
jobs:
build-container:
runs-on: arc-ubuntu-22.04-dind
steps:
- name: Login to Harbor
uses: docker/login-action@v3
with:
registry: harbor.nymte.ch
username: ${{ secrets.HARBOR_ROBOT_USERNAME }}
password: ${{ secrets.HARBOR_ROBOT_SECRET }}
- name: Checkout repo
uses: actions/checkout@v4
- name: Configure git identity
run: |
git config --global user.email "lawrence@nymtech.net"
git config --global user.name "Lawrence Stalder"
- name: Get version from cargo.toml
uses: mikefarah/yq@v4.45.4
id: get_version
with:
cmd: yq -oy '.package.version' ${{ env.WORKING_DIRECTORY }}/Cargo.toml
- name: Create tag
run: |
git tag -a ${{ env.WORKING_DIRECTORY }}-${{ steps.get_version.outputs.result }} -m "Version ${{ steps.get_version.outputs.result }}"
git push origin ${{ env.WORKING_DIRECTORY }}-${{ steps.get_version.outputs.result }}
- name: BuildAndPushImageOnHarbor
run: |
docker build -f ${{ env.WORKING_DIRECTORY }}/Dockerfile . -t harbor.nymte.ch/nym/${{ env.CONTAINER_NAME }}:${{ steps.get_version.outputs.result }} -t harbor.nymte.ch/nym/${{ env.CONTAINER_NAME }}:latest
docker push harbor.nymte.ch/nym/${{ env.CONTAINER_NAME }} --all-tags
+24
View File
@@ -4,6 +4,30 @@ Post 1.0.0 release, the changelog format is based on [Keep a Changelog](https://
## [Unreleased]
## [2025.11-cheddar] (2025-06-10)
- No autoremoval of peers ([#5831])
- Set cached storage counters to 0 ([#5812])
- hack: temporarily use next.config.js instead of next.config.ts ([#5805])
- chore: resolve 1.87 clippy warnings ([#5802])
- Nym Statistics API ([#5800])
- QoL: RequestPath trait for http-api-client ([#5788])
- Fix contains ticketbook function that always returned true ([#5787])
- swap a decode into a fromrow to please future postgres feature ([#5785])
- Make address cache configurable ([#5784])
- Track wireguard credential retries ([#5783])
[#5831]: https://github.com/nymtech/nym/pull/5831
[#5812]: https://github.com/nymtech/nym/pull/5812
[#5805]: https://github.com/nymtech/nym/pull/5805
[#5802]: https://github.com/nymtech/nym/pull/5802
[#5800]: https://github.com/nymtech/nym/pull/5800
[#5788]: https://github.com/nymtech/nym/pull/5788
[#5787]: https://github.com/nymtech/nym/pull/5787
[#5785]: https://github.com/nymtech/nym/pull/5785
[#5784]: https://github.com/nymtech/nym/pull/5784
[#5783]: https://github.com/nymtech/nym/pull/5783
## [2025.10-brie] (2025-05-27)
- Backport PR 5779 ([#5801])
Generated
+202 -84
View File
@@ -96,6 +96,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e89da841a80418a9b391ebaea17f5c112ffaaa96f621d2c285b5174da76b9011"
dependencies = [
"cfg-if",
"getrandom 0.2.15",
"once_cell",
"version_check",
"zerocopy 0.7.35",
@@ -2678,12 +2679,6 @@ version = "1.0.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1"
[[package]]
name = "foldhash"
version = "0.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2"
[[package]]
name = "form_urlencoded"
version = "1.2.1"
@@ -3126,19 +3121,14 @@ name = "hashbrown"
version = "0.15.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bf151400ff0baff5465007dd2f3e717f3fe502074ca563069ce3a6629d07b289"
dependencies = [
"allocator-api2",
"equivalent",
"foldhash",
]
[[package]]
name = "hashlink"
version = "0.10.0"
version = "0.8.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7382cf6263419f2d8df38c55d7da83da5c18aef87fc7a7fc1fb1e344edfe14c1"
checksum = "e8094feaf31ff591f651a2664fb9cfd92bba7a60ce3197265e9482ebe753c8f7"
dependencies = [
"hashbrown 0.15.2",
"hashbrown 0.14.5",
]
[[package]]
@@ -3192,6 +3182,9 @@ name = "heck"
version = "0.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8"
dependencies = [
"unicode-segmentation",
]
[[package]]
name = "heck"
@@ -4169,9 +4162,9 @@ dependencies = [
[[package]]
name = "libsqlite3-sys"
version = "0.30.1"
version = "0.27.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2e99fb7a497b1e3339bc746195567ed8d3e24945ecd636e3619d20b9de9e9149"
checksum = "cf4e226dcd58b4be396f7bd3c20da8fdee2911400705297ba7d2d7cc2c30f716"
dependencies = [
"cc",
"pkg-config",
@@ -4795,7 +4788,7 @@ checksum = "830b246a0e5f20af87141b25c173cd1b609bd7779a4617d6ec582abaf90870f3"
[[package]]
name = "nym-api"
version = "1.1.59"
version = "1.1.60"
dependencies = [
"anyhow",
"async-trait",
@@ -4912,7 +4905,6 @@ dependencies = [
"tendermint-rpc",
"thiserror 2.0.12",
"time",
"tracing",
"ts-rs",
"utoipa",
]
@@ -5051,7 +5043,7 @@ dependencies = [
[[package]]
name = "nym-cli"
version = "1.1.56"
version = "1.1.57"
dependencies = [
"anyhow",
"base64 0.22.1",
@@ -5133,7 +5125,7 @@ dependencies = [
[[package]]
name = "nym-client"
version = "1.1.56"
version = "1.1.57"
dependencies = [
"bs58",
"clap",
@@ -5273,6 +5265,7 @@ dependencies = [
"nym-sphinx",
"nym-task",
"sqlx",
"sqlx-pool-guard",
"thiserror 2.0.12",
"time",
"tokio",
@@ -5473,6 +5466,7 @@ dependencies = [
"nym-ecash-time",
"serde",
"sqlx",
"sqlx-pool-guard",
"thiserror 2.0.12",
"tokio",
"zeroize",
@@ -5856,7 +5850,6 @@ dependencies = [
"encoding_rs",
"hickory-resolver",
"http 1.3.1",
"itertools 0.14.0",
"mime",
"nym-bin-common",
"nym-http-api-common",
@@ -6138,7 +6131,7 @@ dependencies = [
[[package]]
name = "nym-network-requester"
version = "1.1.57"
version = "1.1.58"
dependencies = [
"addr",
"anyhow",
@@ -6188,7 +6181,7 @@ dependencies = [
[[package]]
name = "nym-node"
version = "1.12.0"
version = "1.13.0"
dependencies = [
"anyhow",
"arc-swap",
@@ -6247,7 +6240,6 @@ dependencies = [
"nym-wireguard",
"nym-wireguard-types",
"rand 0.8.5",
"rand_chacha 0.3.1",
"serde",
"serde_json",
"sha2 0.10.9",
@@ -6307,7 +6299,7 @@ dependencies = [
[[package]]
name = "nym-node-status-agent"
version = "1.0.0"
version = "1.0.0-rc.2"
dependencies = [
"anyhow",
"clap",
@@ -6323,12 +6315,13 @@ dependencies = [
[[package]]
name = "nym-node-status-api"
version = "2.3.3"
version = "2.3.2"
dependencies = [
"ammonia",
"anyhow",
"axum 0.7.9",
"bip39",
"chrono",
"clap",
"cosmwasm-std",
"envy",
@@ -6374,16 +6367,16 @@ dependencies = [
[[package]]
name = "nym-node-status-client"
version = "0.1.1"
version = "0.1.0"
dependencies = [
"anyhow",
"bincode",
"chrono",
"nym-crypto",
"nym-http-api-client",
"reqwest 0.12.15",
"serde",
"serde_json",
"time",
"tracing",
]
@@ -6486,20 +6479,6 @@ version = "0.3.0"
dependencies = [
"pem",
"tracing",
"zeroize",
]
[[package]]
name = "nym-pool-contract-common"
version = "0.1.0"
dependencies = [
"cosmwasm-schema",
"cosmwasm-std",
"cw-controllers",
"schemars",
"serde",
"thiserror 2.0.12",
"time",
]
[[package]]
@@ -6598,7 +6577,7 @@ dependencies = [
[[package]]
name = "nym-socks5-client"
version = "1.1.56"
version = "1.1.57"
dependencies = [
"bs58",
"clap",
@@ -6759,6 +6738,7 @@ dependencies = [
"nym-topology",
"rand 0.8.5",
"rand_chacha 0.3.1",
"serde",
"thiserror 2.0.12",
"tracing",
"wasm-bindgen",
@@ -6802,8 +6782,8 @@ dependencies = [
name = "nym-sphinx-forwarding"
version = "0.1.0"
dependencies = [
"nym-outfox",
"nym-sphinx-addressing",
"nym-sphinx-anonymous-replies",
"nym-sphinx-params",
"nym-sphinx-types",
"thiserror 2.0.12",
@@ -7233,7 +7213,7 @@ dependencies = [
[[package]]
name = "nymvisor"
version = "0.1.21"
version = "0.1.22"
dependencies = [
"anyhow",
"bytes",
@@ -7956,6 +7936,15 @@ dependencies = [
"unicode-ident",
]
[[package]]
name = "proc_pidinfo"
version = "0.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "af53dad2390f8df98dda1e4188322bdf2f91c86cf6001f51d10d64451edf463a"
dependencies = [
"libc",
]
[[package]]
name = "prometheus"
version = "0.14.0"
@@ -9307,9 +9296,6 @@ name = "smallvec"
version = "1.14.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7fcf8323ef1faaee30a44a340193b1ac6814fd9b7b4e88e9d4519a3e4abe1cfd"
dependencies = [
"serde",
]
[[package]]
name = "smawk"
@@ -9395,10 +9381,20 @@ dependencies = [
]
[[package]]
name = "sqlx"
version = "0.8.6"
name = "sqlformat"
version = "0.2.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1fefb893899429669dcdd979aff487bd78f4064e5e7907e4269081e0ef7d97dc"
checksum = "7bba3a93db0cc4f7bdece8bb09e77e2e785c20bfebf79eb8340ed80708048790"
dependencies = [
"nom",
"unicode_categories",
]
[[package]]
name = "sqlx"
version = "0.7.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c9a2ccff1a000a5a59cd33da541d9f2fdcd9e6e8229cc200565942bff36d0aaa"
dependencies = [
"sqlx-core",
"sqlx-macros",
@@ -9409,64 +9405,70 @@ dependencies = [
[[package]]
name = "sqlx-core"
version = "0.8.6"
version = "0.7.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ee6798b1838b6a0f69c007c133b8df5866302197e404e8b6ee8ed3e3a5e68dc6"
checksum = "24ba59a9342a3d9bab6c56c118be528b27c9b60e490080e9711a04dccac83ef6"
dependencies = [
"base64 0.22.1",
"ahash",
"atoi",
"byteorder",
"bytes",
"chrono",
"crc",
"crossbeam-queue",
"either",
"event-listener 5.4.0",
"event-listener 2.5.3",
"futures-channel",
"futures-core",
"futures-intrusive",
"futures-io",
"futures-util",
"hashbrown 0.15.2",
"hashlink",
"hex",
"indexmap 2.7.1",
"log",
"memchr",
"once_cell",
"paste",
"percent-encoding",
"rustls 0.23.25",
"rustls 0.21.12",
"rustls-pemfile 1.0.4",
"serde",
"serde_json",
"sha2 0.10.9",
"smallvec",
"thiserror 2.0.12",
"sqlformat",
"thiserror 1.0.69",
"time",
"tokio",
"tokio-stream",
"tracing",
"url",
"webpki-roots 0.26.8",
"webpki-roots 0.25.4",
]
[[package]]
name = "sqlx-macros"
version = "0.8.6"
version = "0.7.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a2d452988ccaacfbf5e0bdbc348fb91d7c8af5bee192173ac3636b5fb6e6715d"
checksum = "4ea40e2345eb2faa9e1e5e326db8c34711317d2b5e08d0d5741619048a803127"
dependencies = [
"proc-macro2",
"quote",
"sqlx-core",
"sqlx-macros-core",
"syn 2.0.98",
"syn 1.0.109",
]
[[package]]
name = "sqlx-macros-core"
version = "0.8.6"
version = "0.7.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "19a9c1841124ac5a61741f96e1d9e2ec77424bf323962dd894bdb93f37d5219b"
checksum = "5833ef53aaa16d860e92123292f1f6a3d53c34ba8b1969f152ef1a7bb803f3c8"
dependencies = [
"dotenvy",
"either",
"heck 0.5.0",
"heck 0.4.1",
"hex",
"once_cell",
"proc-macro2",
@@ -9478,19 +9480,20 @@ dependencies = [
"sqlx-mysql",
"sqlx-postgres",
"sqlx-sqlite",
"syn 2.0.98",
"syn 1.0.109",
"tempfile",
"tokio",
"url",
]
[[package]]
name = "sqlx-mysql"
version = "0.8.6"
version = "0.7.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "aa003f0038df784eb8fecbbac13affe3da23b45194bd57dba231c8f48199c526"
checksum = "1ed31390216d20e538e447a7a9b959e06ed9fc51c37b514b46eb758016ecd418"
dependencies = [
"atoi",
"base64 0.22.1",
"base64 0.21.7",
"bitflags 2.8.0",
"byteorder",
"bytes",
@@ -9521,20 +9524,33 @@ dependencies = [
"smallvec",
"sqlx-core",
"stringprep",
"thiserror 2.0.12",
"thiserror 1.0.69",
"time",
"tracing",
"whoami",
]
[[package]]
name = "sqlx-pool-guard"
version = "0.1.0"
dependencies = [
"proc_pidinfo",
"sqlx",
"tempfile",
"tokio",
"tracing",
"tracing-subscriber",
"windows 0.61.1",
]
[[package]]
name = "sqlx-postgres"
version = "0.8.6"
version = "0.7.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "db58fcd5a53cf07c184b154801ff91347e4c30d17a3562a635ff028ad5deda46"
checksum = "7c824eb80b894f926f89a0b9da0c7f435d27cdd35b8c655b114e58223918577e"
dependencies = [
"atoi",
"base64 0.22.1",
"base64 0.21.7",
"bitflags 2.8.0",
"byteorder",
"chrono",
@@ -9543,6 +9559,7 @@ dependencies = [
"etcetera",
"futures-channel",
"futures-core",
"futures-io",
"futures-util",
"hex",
"hkdf",
@@ -9560,7 +9577,7 @@ dependencies = [
"smallvec",
"sqlx-core",
"stringprep",
"thiserror 2.0.12",
"thiserror 1.0.69",
"time",
"tracing",
"whoami",
@@ -9568,9 +9585,9 @@ dependencies = [
[[package]]
name = "sqlx-sqlite"
version = "0.8.6"
version = "0.7.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c2d12fe70b2c1b4401038055f90f151b78208de1f9f89a7dbfd41587a10c3eea"
checksum = "b244ef0a8414da0bed4bb1910426e890b19e5e9bccc27ada6b797d05c55ae0aa"
dependencies = [
"atoi",
"chrono",
@@ -9584,12 +9601,11 @@ dependencies = [
"log",
"percent-encoding",
"serde",
"serde_urlencoded",
"sqlx-core",
"thiserror 2.0.12",
"time",
"tracing",
"url",
"urlencoding",
]
[[package]]
@@ -10809,6 +10825,12 @@ version = "0.2.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853"
[[package]]
name = "unicode_categories"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "39ec24b3121d976906ece63c9daad25b85969647682eee313cb5779fdd69e14e"
[[package]]
name = "uniffi"
version = "0.29.2"
@@ -11541,6 +11563,28 @@ dependencies = [
"windows-targets 0.52.6",
]
[[package]]
name = "windows"
version = "0.61.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c5ee8f3d025738cb02bad7868bbb5f8a6327501e870bf51f1b455b0a2454a419"
dependencies = [
"windows-collections",
"windows-core 0.61.2",
"windows-future",
"windows-link",
"windows-numerics",
]
[[package]]
name = "windows-collections"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3beeceb5e5cfd9eb1d76b381630e82c4241ccd0d27f1a39ed41b2760b255c5e8"
dependencies = [
"windows-core 0.61.2",
]
[[package]]
name = "windows-core"
version = "0.52.0"
@@ -11575,6 +11619,30 @@ dependencies = [
"windows-targets 0.52.6",
]
[[package]]
name = "windows-core"
version = "0.61.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c0fdd3ddb90610c7638aa2b3a3ab2904fb9e5cdbecc643ddb3647212781c4ae3"
dependencies = [
"windows-implement 0.60.0",
"windows-interface 0.59.1",
"windows-link",
"windows-result 0.3.4",
"windows-strings 0.4.2",
]
[[package]]
name = "windows-future"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fc6a41e98427b19fe4b73c550f060b59fa592d7d686537eebf9385621bfbad8e"
dependencies = [
"windows-core 0.61.2",
"windows-link",
"windows-threading",
]
[[package]]
name = "windows-implement"
version = "0.57.0"
@@ -11597,6 +11665,17 @@ dependencies = [
"syn 2.0.98",
]
[[package]]
name = "windows-implement"
version = "0.60.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a47fddd13af08290e67f4acabf4b459f647552718f683a7b415d290ac744a836"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.98",
]
[[package]]
name = "windows-interface"
version = "0.57.0"
@@ -11620,10 +11699,31 @@ dependencies = [
]
[[package]]
name = "windows-link"
version = "0.1.0"
name = "windows-interface"
version = "0.59.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6dccfd733ce2b1753b03b6d3c65edf020262ea35e20ccdf3e288043e6dd620e3"
checksum = "bd9211b69f8dcdfa817bfd14bf1c97c9188afa36f4750130fcdf3f400eca9fa8"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.98",
]
[[package]]
name = "windows-link"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "76840935b766e1b0a05c0066835fb9ec80071d4c09a16f6bd5f7e655e3c14c38"
[[package]]
name = "windows-numerics"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9150af68066c4c5c07ddc0ce30421554771e528bde427614c61038bc2c92c2b1"
dependencies = [
"windows-core 0.61.2",
"windows-link",
]
[[package]]
name = "windows-registry"
@@ -11631,7 +11731,7 @@ version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4286ad90ddb45071efd1a66dfa43eb02dd0dfbae1545ad6cc3c51cf34d7e8ba3"
dependencies = [
"windows-result 0.3.1",
"windows-result 0.3.4",
"windows-strings 0.3.1",
"windows-targets 0.53.0",
]
@@ -11656,9 +11756,9 @@ dependencies = [
[[package]]
name = "windows-result"
version = "0.3.1"
version = "0.3.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "06374efe858fab7e4f881500e6e86ec8bc28f9462c47e5a9941a0142ad86b189"
checksum = "56f42bd332cc6c8eac5af113fc0c1fd6a8fd2aa08a0119358686e5160d0586c6"
dependencies = [
"windows-link",
]
@@ -11682,6 +11782,15 @@ dependencies = [
"windows-link",
]
[[package]]
name = "windows-strings"
version = "0.4.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "56e6c93f3a0c3b36176cb1327a4958a0353d5d166c2a35cb268ace15e91d3b57"
dependencies = [
"windows-link",
]
[[package]]
name = "windows-sys"
version = "0.45.0"
@@ -11780,6 +11889,15 @@ dependencies = [
"windows_x86_64_msvc 0.53.0",
]
[[package]]
name = "windows-threading"
version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b66463ad2e0ea3bbf808b7f1d371311c80e115c0b71d60efc142cafbcfb057a6"
dependencies = [
"windows-link",
]
[[package]]
name = "windows_aarch64_gnullvm"
version = "0.42.2"
+6 -7
View File
@@ -33,13 +33,11 @@ members = [
"common/commands",
"common/config",
"common/cosmwasm-smart-contracts/coconut-dkg",
"common/cosmwasm-smart-contracts/contracts-common",
"common/cosmwasm-smart-contracts/easy_addr",
"common/cosmwasm-smart-contracts/contracts-common", "common/cosmwasm-smart-contracts/easy_addr",
"common/cosmwasm-smart-contracts/ecash-contract",
"common/cosmwasm-smart-contracts/group-contract",
"common/cosmwasm-smart-contracts/mixnet-contract",
"common/cosmwasm-smart-contracts/multisig-contract",
"common/cosmwasm-smart-contracts/nym-pool-contract",
"common/cosmwasm-smart-contracts/vesting-contract",
"common/credential-storage",
"common/credential-utils",
@@ -125,6 +123,7 @@ members = [
"service-providers/common",
"service-providers/ip-packet-router",
"service-providers/network-requester",
"sqlx-pool-guard",
"tools/echo-server",
"tools/internal/contract-state-importer/importer-cli",
"tools/internal/contract-state-importer/importer-contract",
@@ -134,8 +133,7 @@ members = [
"tools/internal/testnet-manager",
"tools/internal/testnet-manager",
"tools/internal/testnet-manager/dkg-bypass-contract",
"tools/internal/testnet-manager/dkg-bypass-contract",
"tools/internal/validator-status-check",
"tools/internal/testnet-manager/dkg-bypass-contract", "tools/internal/validator-status-check",
"tools/nym-cli",
"tools/nym-id-cli",
"tools/nym-nr-query",
@@ -286,6 +284,7 @@ petgraph = "0.6.5"
pin-project = "1.1"
pin-project-lite = "0.2.16"
publicsuffix = "2.3.0"
proc_pidinfo = "0.1.3"
quote = "1"
rand = "0.8.5"
rand_chacha = "0.3"
@@ -311,7 +310,7 @@ serde_yaml = "0.9.25"
sha2 = "0.10.9"
si-scale = "0.2.3"
sphinx-packet = "=0.6.0"
sqlx = "0.8.6"
sqlx = "0.7.4"
strum = "0.26"
strum_macros = "0.26"
subtle-encoding = "0.5"
@@ -349,6 +348,7 @@ utoipauto = "0.2"
uuid = "*"
vergen = { version = "=8.3.1", default-features = false }
walkdir = "2"
wasm-bindgen-test = "0.3.49"
x25519-dalek = "2.0.0"
zeroize = "1.7.0"
@@ -396,7 +396,6 @@ serde-wasm-bindgen = "0.6.5"
tsify = "0.4.5"
wasm-bindgen = "0.2.99"
wasm-bindgen-futures = "0.4.49"
wasm-bindgen-test = "0.3.49"
wasmtimer = "0.4.1"
web-sys = "0.3.76"
+1 -1
View File
@@ -133,7 +133,7 @@ clippy: sdk-wasm-lint
# Build contracts ready for deploy
# -----------------------------------------------------------------------------
CONTRACTS=vesting_contract mixnet_contract nym_ecash cw3_flex_multisig cw4_group nym_coconut_dkg nym_pool_contract
CONTRACTS=vesting_contract mixnet_contract nym_ecash cw3_flex_multisig cw4_group nym_coconut_dkg
CONTRACTS_WASM=$(addsuffix .wasm, $(CONTRACTS))
CONTRACTS_OUT_DIR=contracts/target/wasm32-unknown-unknown/release
+1 -1
View File
@@ -1,6 +1,6 @@
[package]
name = "nym-client"
version = "1.1.56"
version = "1.1.57"
authors = ["Dave Hrycyszyn <futurechimp@users.noreply.github.com>", "Jędrzej Stuczyński <andrew@nymtech.net>"]
description = "Implementation of the Nym Client"
edition = "2021"
+1 -1
View File
@@ -1,6 +1,6 @@
[package]
name = "nym-socks5-client"
version = "1.1.56"
version = "1.1.57"
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"
@@ -2,8 +2,7 @@
// SPDX-License-Identifier: Apache-2.0
use crate::BadGateway;
use std::io;
use std::path::PathBuf;
use std::{io, path::PathBuf};
use thiserror::Error;
#[derive(Debug, Error)]
@@ -19,7 +18,6 @@ pub enum StorageError {
#[error("failed to perform sqlx migration: {source}")]
MigrationError {
#[source]
#[from]
source: sqlx::migrate::MigrateError,
},
@@ -32,7 +30,6 @@ pub enum StorageError {
#[error("failed to run the SQL query: {source}")]
QueryError {
#[source]
#[from]
source: sqlx::error::Error,
},
@@ -87,7 +87,7 @@ impl StorageManager {
sqlx::query!("SELECT EXISTS (SELECT 1 FROM registered_gateway WHERE gateway_id_bs58 = ?) AS 'exists'", gateway_id)
.fetch_one(&self.connection_pool)
.await
.map(|result| result.exists == 1)
.map(|result| result.exists == Some(1))
}
pub(crate) async fn maybe_get_registered_gateway(
@@ -1,20 +1,18 @@
// Copyright 2022-2024 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use crate::client::replies::reply_storage::{
fs_backend, CombinedReplyStorage, ReplyStorageBackend,
use crate::{
client::replies::reply_storage::{fs_backend, CombinedReplyStorage, ReplyStorageBackend},
config,
config::Config,
error::ClientCoreError,
};
use crate::config;
use crate::config::Config;
use crate::error::ClientCoreError;
use log::{error, info, trace};
use nym_bandwidth_controller::BandwidthController;
use nym_client_core_gateways_storage::OnDiskGatewaysDetails;
use nym_credential_storage::storage::Storage as CredentialStorage;
use nym_validator_client::nyxd;
use nym_validator_client::QueryHttpRpcNyxdClient;
use std::path::Path;
use std::{fs, io};
use nym_validator_client::{nyxd, QueryHttpRpcNyxdClient};
use std::{io, path::Path};
use time::OffsetDateTime;
use url::Url;
@@ -22,11 +20,11 @@ async fn setup_fresh_backend<P: AsRef<Path>>(
db_path: P,
surb_config: &config::ReplySurbs,
) -> Result<fs_backend::Backend, ClientCoreError> {
info!("creating fresh surb database");
info!("Creating fresh surb database");
let mut storage_backend = match fs_backend::Backend::init(db_path).await {
Ok(backend) => backend,
Err(err) => {
error!("failed to setup persistent storage backend for our reply needs: {err}");
error!("setup_fresh_backend: Failed to setup persistent storage backend for our reply needs: {err}");
return Err(ClientCoreError::SurbStorageError {
source: Box::new(err),
});
@@ -40,14 +38,15 @@ async fn setup_fresh_backend<P: AsRef<Path>>(
surb_config.minimum_reply_surb_storage_threshold,
surb_config.maximum_reply_surb_storage_threshold,
);
storage_backend
.init_fresh(&mem_store)
.await
.map_err(|err| ClientCoreError::SurbStorageError {
source: Box::new(err),
})?;
Ok(storage_backend)
match storage_backend.init_fresh(&mem_store).await {
Ok(()) => Ok(storage_backend),
Err(err) => {
storage_backend.shutdown().await;
Err(ClientCoreError::SurbStorageError {
source: Box::new(err),
})
}
}
}
// fn setup_inactive_backend(surb_config: &config::ReplySurbs) -> fs_backend::Backend {
@@ -58,12 +57,11 @@ async fn setup_fresh_backend<P: AsRef<Path>>(
// )
// }
fn archive_corrupted_database<P: AsRef<Path>>(db_path: P) -> io::Result<()> {
async fn archive_corrupted_database<P: AsRef<Path>>(db_path: P) -> io::Result<()> {
let db_path = db_path.as_ref();
debug_assert!(db_path.exists());
let now = OffsetDateTime::now_utc().unix_timestamp();
let suffix = format!("_{now}.corrupted");
let new_extension =
@@ -72,11 +70,15 @@ fn archive_corrupted_database<P: AsRef<Path>>(db_path: P) -> io::Result<()> {
} else {
suffix
};
let renamed = db_path.with_extension(new_extension);
let mut renamed = db_path.to_owned();
renamed.set_extension(new_extension);
fs::rename(db_path, renamed)
tokio::fs::rename(db_path, &renamed).await.inspect_err(|_| {
error!(
"Failed to rename corrupt database file: {} to {}",
db_path.display(),
renamed.display()
);
})
}
pub async fn setup_fs_reply_surb_backend<P: AsRef<Path>>(
@@ -87,13 +89,12 @@ pub async fn setup_fs_reply_surb_backend<P: AsRef<Path>>(
// the existing one
let db_path = db_path.as_ref();
if db_path.exists() {
info!("loading existing surb database");
info!("Loading existing surb database");
match fs_backend::Backend::try_load(db_path, surb_config.fresh_sender_tags).await {
Ok(backend) => Ok(backend),
Err(err) => {
error!("failed to setup persistent storage backend for our reply needs: {err}. We're going to create a fresh database instead. This behaviour might change in the future");
archive_corrupted_database(db_path)?;
error!("setup_fs_reply_surb_backend: Failed to setup persistent storage backend for our reply needs: {err}. We're going to create a fresh database instead. This behaviour might change in the future");
archive_corrupted_database(db_path).await?;
setup_fresh_backend(db_path, surb_config).await
}
}
@@ -12,7 +12,7 @@ use crate::client::topology_control::{TopologyAccessor, TopologyReadPermit};
use nym_sphinx::acknowledgements::AckKey;
use nym_sphinx::addressing::clients::Recipient;
use nym_sphinx::anonymous_replies::requests::{AnonymousSenderTag, RepliableMessage, ReplyMessage};
use nym_sphinx::anonymous_replies::ReplySurbWithKeyRotation;
use nym_sphinx::anonymous_replies::{ReplySurb, SurbEncryptionKey};
use nym_sphinx::chunking::fragment::{Fragment, FragmentIdentifier};
use nym_sphinx::message::NymMessage;
use nym_sphinx::params::{PacketSize, PacketType};
@@ -44,10 +44,7 @@ pub enum PreparationError {
}
impl PreparationError {
fn return_surbs(
self,
returned_surbs: Vec<ReplySurbWithKeyRotation>,
) -> SurbWrappedPreparationError {
fn return_surbs(self, returned_surbs: Vec<ReplySurb>) -> SurbWrappedPreparationError {
SurbWrappedPreparationError {
source: self,
returned_surbs: Some(returned_surbs),
@@ -61,7 +58,7 @@ pub struct SurbWrappedPreparationError {
#[source]
source: PreparationError,
returned_surbs: Option<Vec<ReplySurbWithKeyRotation>>,
returned_surbs: Option<Vec<ReplySurb>>,
}
impl<T> From<T> for SurbWrappedPreparationError
@@ -271,10 +268,10 @@ where
}
}
async fn generate_reply_surbs(
async fn generate_reply_surbs_with_keys(
&mut self,
amount: usize,
) -> Result<Vec<ReplySurbWithKeyRotation>, PreparationError> {
) -> Result<(Vec<ReplySurb>, Vec<SurbEncryptionKey>), PreparationError> {
let topology_permit = self.topology_access.get_read_permit().await;
let topology = self.get_topology(&topology_permit)?;
@@ -284,14 +281,19 @@ where
topology,
)?;
Ok(reply_surbs)
let reply_keys = reply_surbs
.iter()
.map(|s| *s.encryption_key())
.collect::<Vec<_>>();
Ok((reply_surbs, reply_keys))
}
pub(crate) async fn try_send_single_surb_message(
&mut self,
target: AnonymousSenderTag,
message: ReplyMessage,
reply_surb: ReplySurbWithKeyRotation,
reply_surb: ReplySurb,
is_extra_surb_request: bool,
) -> Result<(), SurbWrappedPreparationError> {
let msg = NymMessage::new_reply(message);
@@ -345,7 +347,7 @@ where
pub(crate) async fn try_request_additional_reply_surbs(
&mut self,
from: AnonymousSenderTag,
reply_surb: ReplySurbWithKeyRotation,
reply_surb: ReplySurb,
amount: u32,
) -> Result<(), SurbWrappedPreparationError> {
debug!("requesting {amount} reply SURBs from {from}");
@@ -385,7 +387,7 @@ where
&mut self,
target: AnonymousSenderTag,
fragments: Vec<FragmentWithMaxRetransmissions>,
reply_surbs: Vec<ReplySurbWithKeyRotation>,
reply_surbs: Vec<ReplySurb>,
lane: TransmissionLane,
) -> Result<(), SurbWrappedPreparationError> {
// TODO: technically this is performing an unnecessary cloning, but in the grand scheme of things
@@ -402,7 +404,7 @@ where
&mut self,
target: AnonymousSenderTag,
fragments: Vec<(TransmissionLane, FragmentWithMaxRetransmissions)>,
reply_surbs: Vec<ReplySurbWithKeyRotation>,
reply_surbs: Vec<ReplySurb>,
) -> Result<(), SurbWrappedPreparationError> {
let prepared_fragments = self
.prepare_reply_chunks_for_sending(
@@ -539,12 +541,8 @@ where
) -> Result<(), PreparationError> {
debug!("Sending additional reply SURBs with packet type {packet_type}");
let sender_tag = self.get_or_create_sender_tag(&recipient);
let reply_surbs = self.generate_reply_surbs(amount as usize).await?;
let reply_keys = reply_surbs
.iter()
.map(|s| *s.encryption_key())
.collect::<Vec<_>>();
let (reply_surbs, reply_keys) =
self.generate_reply_surbs_with_keys(amount as usize).await?;
let message = NymMessage::new_repliable(RepliableMessage::new_additional_surbs(
self.config.use_legacy_sphinx_format,
@@ -581,12 +579,9 @@ where
) -> Result<(), SurbWrappedPreparationError> {
debug!("Sending message with reply SURBs with packet type {packet_type}");
let sender_tag = self.get_or_create_sender_tag(&recipient);
let reply_surbs = self.generate_reply_surbs(num_reply_surbs as usize).await?;
let reply_keys = reply_surbs
.iter()
.map(|s| *s.encryption_key())
.collect::<Vec<_>>();
let (reply_surbs, reply_keys) = self
.generate_reply_surbs_with_keys(num_reply_surbs as usize)
.await?;
let message = NymMessage::new_repliable(RepliableMessage::new_data(
self.config.use_legacy_sphinx_format,
@@ -634,7 +629,7 @@ where
pub(crate) async fn prepare_reply_chunks_for_sending(
&mut self,
fragments: Vec<Fragment>,
reply_surbs: Vec<ReplySurbWithKeyRotation>,
reply_surbs: Vec<ReplySurb>,
) -> Result<Vec<PreparedFragment>, SurbWrappedPreparationError> {
debug_assert_eq!(
fragments.len(),
@@ -670,7 +665,7 @@ where
pub(crate) async fn try_prepare_single_reply_chunk_for_sending(
&mut self,
reply_surb: ReplySurbWithKeyRotation,
reply_surb: ReplySurb,
chunk: Fragment,
) -> Result<PreparedFragment, SurbWrappedPreparationError> {
let topology_permit = self.topology_access.get_read_permit().await;
@@ -11,7 +11,7 @@ use futures::StreamExt;
use log::{debug, error, info, trace, warn};
use nym_sphinx::addressing::clients::Recipient;
use nym_sphinx::anonymous_replies::requests::AnonymousSenderTag;
use nym_sphinx::anonymous_replies::ReplySurbWithKeyRotation;
use nym_sphinx::anonymous_replies::ReplySurb;
use nym_sphinx::chunking::fragment::FragmentIdentifier;
use nym_task::connections::{ConnectionId, TransmissionLane};
use nym_task::TaskClient;
@@ -499,7 +499,7 @@ where
async fn handle_received_surbs(
&mut self,
from: AnonymousSenderTag,
reply_surbs: Vec<ReplySurbWithKeyRotation>,
reply_surbs: Vec<ReplySurb>,
from_surb_request: bool,
) {
trace!("handling received surbs");
@@ -6,7 +6,7 @@ use futures::channel::{mpsc, oneshot};
use log::error;
use nym_sphinx::addressing::clients::Recipient;
use nym_sphinx::anonymous_replies::requests::AnonymousSenderTag;
use nym_sphinx::anonymous_replies::ReplySurbWithKeyRotation;
use nym_sphinx::anonymous_replies::ReplySurb;
use nym_task::connections::{ConnectionId, TransmissionLane};
use std::sync::Weak;
@@ -81,7 +81,7 @@ impl ReplyControllerSender {
pub(crate) fn send_additional_surbs(
&self,
sender_tag: AnonymousSenderTag,
reply_surbs: Vec<ReplySurbWithKeyRotation>,
reply_surbs: Vec<ReplySurb>,
from_surb_request: bool,
) -> Result<(), ReplyControllerSenderError> {
self.0
@@ -167,7 +167,7 @@ pub enum ReplyControllerMessage {
AdditionalSurbs {
sender_tag: AnonymousSenderTag,
reply_surbs: Vec<ReplySurbWithKeyRotation>,
reply_surbs: Vec<ReplySurb>,
from_surb_request: bool,
},
@@ -4,7 +4,7 @@
use async_trait::async_trait;
use log::{debug, error, warn};
use nym_topology::provider_trait::TopologyProvider;
use nym_topology::{NymTopology, NymTopologyMetadata};
use nym_topology::NymTopology;
use nym_validator_client::UserAgent;
use rand::prelude::SliceRandom;
use rand::thread_rng;
@@ -89,84 +89,55 @@ impl NymApiTopologyProvider {
let rewarded_set_fut = self.validator_client.get_current_rewarded_set();
let topology = if self.config.use_extended_topology {
let all_nodes_fut = self.validator_client.get_all_basic_nodes_with_metadata();
let all_nodes_fut = self.validator_client.get_all_basic_nodes();
// Join rewarded_set_fut and all_nodes_fut concurrently
let (rewarded_set, all_nodes_res) = futures::try_join!(rewarded_set_fut, all_nodes_fut)
let (rewarded_set, all_nodes) = futures::try_join!(rewarded_set_fut, all_nodes_fut)
.inspect_err(|err| error!("failed to get network nodes: {err}"))
.ok()?;
let metadata = all_nodes_res.metadata;
let all_nodes = all_nodes_res.nodes;
debug!(
"there are {} nodes on the network (before filtering)",
all_nodes.len()
);
let nodes_filtered = all_nodes
.into_iter()
.filter(|n| n.performance.round_to_integer() >= self.config.min_node_performance())
.collect::<Vec<_>>();
let mut topology = NymTopology::new_empty(rewarded_set);
topology.add_additional_nodes(all_nodes.iter().filter(|n| {
n.performance.round_to_integer() >= self.config.min_node_performance()
}));
NymTopology::new(
NymTopologyMetadata::new(metadata.rotation_id, metadata.absolute_epoch_id),
rewarded_set,
Vec::new(),
)
.with_skimmed_nodes(&nodes_filtered)
topology
} else {
// if we're not using extended topology, we're only getting active set mixnodes and gateways
let mixnodes_fut = self
.validator_client
.get_all_basic_active_mixing_assigned_nodes_with_metadata();
.get_all_basic_active_mixing_assigned_nodes();
// TODO: we really should be getting ACTIVE gateways only
let gateways_fut = self
.validator_client
.get_all_basic_entry_assigned_nodes_v2();
let gateways_fut = self.validator_client.get_all_basic_entry_assigned_nodes();
let (rewarded_set, mixnodes_res, gateways_res) =
let (rewarded_set, mixnodes, gateways) =
futures::try_join!(rewarded_set_fut, mixnodes_fut, gateways_fut)
.inspect_err(|err| {
error!("failed to get network nodes: {err}");
})
.ok()?;
let metadata = mixnodes_res.metadata;
let mixnodes = mixnodes_res.nodes;
if gateways_res.metadata != metadata {
warn!("inconsistent nodes metadata between mixnodes and gateways calls! {metadata:?} and {:?}", gateways_res.metadata);
return None;
}
let gateways = gateways_res.nodes;
debug!(
"there are {} mixnodes and {} gateways in total (before performance filtering)",
mixnodes.len(),
gateways.len()
);
let mut nodes = Vec::new();
for mix in mixnodes {
if mix.performance.round_to_integer() >= self.config.min_mixnode_performance {
nodes.push(mix)
}
}
for gateway in gateways {
if gateway.performance.round_to_integer() >= self.config.min_gateway_performance {
nodes.push(gateway)
}
}
let mut topology = NymTopology::new_empty(rewarded_set);
topology.add_additional_nodes(mixnodes.iter().filter(|m| {
m.performance.round_to_integer() >= self.config.min_mixnode_performance
}));
topology.add_additional_nodes(gateways.iter().filter(|m| {
m.performance.round_to_integer() >= self.config.min_gateway_performance
}));
NymTopology::new(
NymTopologyMetadata::new(metadata.rotation_id, metadata.absolute_epoch_id),
rewarded_set,
Vec::new(),
)
.with_skimmed_nodes(&nodes)
topology
};
if !topology.is_minimally_routable() {
+1 -1
View File
@@ -107,7 +107,7 @@ pub async fn gateways_for_init<R: Rng>(
log::debug!("Fetching list of gateways from: {nym_api}");
let gateways = client.get_all_basic_entry_assigned_nodes_v2().await?.nodes;
let gateways = client.get_all_basic_entry_assigned_nodes().await?;
info!("nym api reports {} gateways", gateways.len());
log::trace!("Gateways: {:#?}", gateways);
+12 -1
View File
@@ -17,15 +17,26 @@ nym-crypto = { path = "../../crypto", optional = true, default-features = false
nym-sphinx = { path = "../../nymsphinx" }
nym-task = { path = "../../task" }
[target."cfg(not(target_arch = \"wasm32\"))".dependencies.tokio]
workspace = true
features = ["fs"]
[target."cfg(not(target_arch = \"wasm32\"))".dependencies.sqlx]
workspace = true
features = ["runtime-tokio-rustls", "sqlite", "macros", "migrate"]
optional = true
[target."cfg(not(target_arch = \"wasm32\"))".dependencies.sqlx-pool-guard]
path = "../../../sqlx-pool-guard"
[build-dependencies]
tokio = { workspace = true, features = ["rt-multi-thread", "macros"] }
sqlx = { workspace = true, features = ["runtime-tokio-rustls", "sqlite", "macros", "migrate"] }
sqlx = { workspace = true, features = [
"runtime-tokio-rustls",
"sqlite",
"macros",
"migrate",
] }
[features]
fs-surb-storage = ["sqlx", "nym-crypto", "nym-crypto/hashing"]
@@ -1,8 +0,0 @@
/*
* Copyright 2025 - Nym Technologies SA <contact@nymtech.net>
* SPDX-License-Identifier: Apache-2.0
*/
-- default value of 0 implies 'unknown' variant
ALTER TABLE reply_surb
ADD COLUMN encoded_key_rotation TINYINT NOT NULL DEFAULT 0;
@@ -1,8 +1,7 @@
// Copyright 2022 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use std::io;
use std::path::PathBuf;
use std::{io, path::PathBuf};
use thiserror::Error;
#[derive(Debug, Error)]
@@ -30,7 +29,6 @@ pub enum StorageError {
#[error("failed to perform sqlx migration: {source}")]
MigrationError {
#[source]
#[from]
source: sqlx::migrate::MigrateError,
},
@@ -43,7 +41,6 @@ pub enum StorageError {
#[error("failed to run the SQL query: {source}")]
QueryError {
#[source]
#[from]
source: sqlx::error::Error,
},
@@ -15,9 +15,11 @@ use sqlx::{
};
use std::path::Path;
use sqlx_pool_guard::SqlitePoolGuard;
#[derive(Debug, Clone)]
pub struct StorageManager {
pub connection_pool: sqlx::SqlitePool,
connection_pool: SqlitePoolGuard,
}
// all SQL goes here
@@ -37,7 +39,7 @@ impl StorageManager {
.journal_mode(sqlx::sqlite::SqliteJournalMode::Wal)
.synchronous(SqliteSynchronous::Normal)
.auto_vacuum(SqliteAutoVacuum::Incremental)
.filename(database_path)
.filename(&database_path)
.create_if_missing(fresh)
.disable_statement_logging();
@@ -49,11 +51,15 @@ impl StorageManager {
}
};
let connection_pool =
SqlitePoolGuard::new(database_path.as_ref().to_path_buf(), connection_pool);
if let Err(err) = sqlx::migrate!("./fs_surbs_migrations")
.run(&connection_pool)
.run(&*connection_pool)
.await
{
error!("Failed to initialize SQLx database: {err}");
connection_pool.close().await;
return Err(err.into());
}
@@ -61,38 +67,43 @@ impl StorageManager {
Ok(StorageManager { connection_pool })
}
/// Close connection pool waiting for all connections to be closed.
pub async fn close_pool(&self) {
self.connection_pool.close().await;
}
#[allow(dead_code)]
pub async fn status_table_exists(&self) -> Result<bool, sqlx::Error> {
sqlx::query!("SELECT name FROM sqlite_master WHERE type='table' AND name='status'")
.fetch_optional(&self.connection_pool)
.fetch_optional(&*self.connection_pool)
.await
.map(|r| r.is_some())
}
pub async fn create_status_table(&self) -> Result<(), sqlx::Error> {
sqlx::query!("INSERT INTO status(flush_in_progress, previous_flush_timestamp, client_in_use) VALUES (0, 0, 1)")
.execute(&self.connection_pool)
.execute(&*self.connection_pool)
.await?;
Ok(())
}
pub async fn get_flush_status(&self) -> Result<bool, sqlx::Error> {
sqlx::query!("SELECT flush_in_progress FROM status;")
.fetch_one(&self.connection_pool)
.fetch_one(&*self.connection_pool)
.await
.map(|r| r.flush_in_progress > 0)
}
pub async fn set_previous_flush_timestamp(&self, timestamp: i64) -> Result<(), sqlx::Error> {
sqlx::query!("UPDATE status SET previous_flush_timestamp = ?", timestamp)
.execute(&self.connection_pool)
.execute(&*self.connection_pool)
.await?;
Ok(())
}
pub async fn get_previous_flush_timestamp(&self) -> Result<i64, sqlx::Error> {
sqlx::query!("SELECT previous_flush_timestamp FROM status;")
.fetch_one(&self.connection_pool)
.fetch_one(&*self.connection_pool)
.await
.map(|r| r.previous_flush_timestamp)
}
@@ -100,14 +111,14 @@ impl StorageManager {
pub async fn set_flush_status(&self, in_progress: bool) -> Result<(), sqlx::Error> {
let in_progress_int = i64::from(in_progress);
sqlx::query!("UPDATE status SET flush_in_progress = ?", in_progress_int)
.execute(&self.connection_pool)
.execute(&*self.connection_pool)
.await?;
Ok(())
}
pub async fn get_client_in_use_status(&self) -> Result<bool, sqlx::Error> {
sqlx::query!("SELECT client_in_use FROM status;")
.fetch_one(&self.connection_pool)
.fetch_one(&*self.connection_pool)
.await
.map(|r| r.client_in_use > 0)
}
@@ -115,21 +126,21 @@ impl StorageManager {
pub async fn set_client_in_use_status(&self, in_use: bool) -> Result<(), sqlx::Error> {
let in_use_int = i64::from(in_use);
sqlx::query!("UPDATE status SET client_in_use = ?", in_use_int)
.execute(&self.connection_pool)
.execute(&*self.connection_pool)
.await?;
Ok(())
}
pub async fn delete_all_tags(&self) -> Result<(), sqlx::Error> {
sqlx::query!("DELETE FROM sender_tag;")
.execute(&self.connection_pool)
.execute(&*self.connection_pool)
.await?;
Ok(())
}
pub async fn get_tags(&self) -> Result<Vec<StoredSenderTag>, sqlx::Error> {
sqlx::query_as!(StoredSenderTag, "SELECT * FROM sender_tag;",)
.fetch_all(&self.connection_pool)
.fetch_all(&*self.connection_pool)
.await
}
@@ -141,21 +152,21 @@ impl StorageManager {
stored_tag.recipient,
stored_tag.tag
)
.execute(&self.connection_pool)
.execute(&*self.connection_pool)
.await?;
Ok(())
}
pub async fn delete_all_reply_keys(&self) -> Result<(), sqlx::Error> {
sqlx::query!("DELETE FROM reply_key;")
.execute(&self.connection_pool)
.execute(&*self.connection_pool)
.await?;
Ok(())
}
pub async fn get_reply_keys(&self) -> Result<Vec<StoredReplyKey>, sqlx::Error> {
sqlx::query_as!(StoredReplyKey, "SELECT * FROM reply_key;",)
.fetch_all(&self.connection_pool)
.fetch_all(&*self.connection_pool)
.await
}
@@ -171,14 +182,14 @@ impl StorageManager {
stored_reply_key.reply_key,
stored_reply_key.sent_at_timestamp
)
.execute(&self.connection_pool)
.execute(&*self.connection_pool)
.await?;
Ok(())
}
pub async fn get_surb_senders(&self) -> Result<Vec<StoredSurbSender>, sqlx::Error> {
sqlx::query_as!(StoredSurbSender, "SELECT * FROM reply_surb_sender;",)
.fetch_all(&self.connection_pool)
.fetch_all(&*self.connection_pool)
.await
}
@@ -193,7 +204,7 @@ impl StorageManager {
stored_surb_sender.tag,
stored_surb_sender.last_sent_timestamp
)
.execute(&self.connection_pool)
.execute(&*self.connection_pool)
.await?
.last_insert_rowid();
Ok(id)
@@ -205,23 +216,20 @@ impl StorageManager {
) -> Result<Vec<StoredReplySurb>, sqlx::Error> {
sqlx::query_as!(
StoredReplySurb,
r#"
SELECT reply_surb_sender_id, reply_surb, encoded_key_rotation as "encoded_key_rotation: u8" FROM reply_surb
WHERE reply_surb_sender_id = ?
"#,
"SELECT * FROM reply_surb WHERE reply_surb_sender_id = ?",
sender_id
)
.fetch_all(&self.connection_pool)
.fetch_all(&*self.connection_pool)
.await
}
pub async fn delete_all_reply_surb_data(&self) -> Result<(), sqlx::Error> {
sqlx::query!("DELETE FROM reply_surb;")
.execute(&self.connection_pool)
.execute(&*self.connection_pool)
.await?;
sqlx::query!("DELETE FROM reply_surb_sender;")
.execute(&self.connection_pool)
.execute(&*self.connection_pool)
.await?;
Ok(())
@@ -233,13 +241,12 @@ impl StorageManager {
) -> Result<(), sqlx::Error> {
sqlx::query!(
r#"
INSERT INTO reply_surb(reply_surb_sender_id, reply_surb, encoded_key_rotation) VALUES (?, ?, ?);
INSERT INTO reply_surb(reply_surb_sender_id, reply_surb) VALUES (?, ?);
"#,
stored_reply_surb.reply_surb_sender_id,
stored_reply_surb.reply_surb,
stored_reply_surb.encoded_key_rotation
stored_reply_surb.reply_surb
)
.execute(&self.connection_pool)
.execute(&*self.connection_pool)
.await?;
Ok(())
}
@@ -253,7 +260,7 @@ impl StorageManager {
SELECT min_reply_surb_threshold as "min_reply_surb_threshold: u32", max_reply_surb_threshold as "max_reply_surb_threshold: u32" FROM reply_surb_storage_metadata;
"#,
)
.fetch_one(&self.connection_pool)
.fetch_one(&*self.connection_pool)
.await
}
@@ -267,7 +274,7 @@ impl StorageManager {
"#,
metadata.min_reply_surb_threshold,
metadata.max_reply_surb_threshold,
).execute(&self.connection_pool).await?;
).execute(&*self.connection_pool).await?;
Ok(())
}
}
@@ -1,18 +1,21 @@
// Copyright 2022 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use crate::backend::fs_backend::manager::StorageManager;
use crate::backend::fs_backend::models::{
ReplySurbStorageMetadata, StoredReplyKey, StoredReplySurb, StoredSenderTag, StoredSurbSender,
};
use crate::surb_storage::ReceivedReplySurbs;
use crate::{
CombinedReplyStorage, ReceivedReplySurbsMap, ReplyStorageBackend, SentReplyKeys, UsedSenderTags,
backend::fs_backend::{
manager::StorageManager,
models::{
ReplySurbStorageMetadata, StoredReplyKey, StoredReplySurb, StoredSenderTag,
StoredSurbSender,
},
},
surb_storage::ReceivedReplySurbs,
CombinedReplyStorage, ReceivedReplySurbsMap, ReplyStorageBackend, SentReplyKeys,
UsedSenderTags,
};
use async_trait::async_trait;
use log::{debug, error, info, warn};
use nym_sphinx::anonymous_replies::requests::AnonymousSenderTag;
use std::fs;
use std::path::{Path, PathBuf};
use time::OffsetDateTime;
@@ -41,15 +44,17 @@ impl Backend {
}
let manager = StorageManager::init(database_path, true).await?;
manager.create_status_table().await?;
let backend = Backend {
temporary_old_path: None,
database_path: owned_path,
manager,
};
Ok(backend)
match manager.create_status_table().await {
Ok(()) => Ok(Backend {
temporary_old_path: None,
database_path: owned_path,
manager,
}),
Err(err) => {
manager.close_pool().await;
Err(err.into())
}
}
}
pub async fn try_load<P: AsRef<Path>>(
@@ -64,7 +69,28 @@ impl Backend {
}
let manager = StorageManager::init(database_path, false).await?;
match Self::try_load_inner(&manager, fresh_sender_tags).await {
Ok(()) => Ok(Backend {
temporary_old_path: None,
database_path: owned_path,
manager,
}),
Err(e) => {
manager.close_pool().await;
Err(e)
}
}
}
/// Gracefully close sqlite connection pool and drop backend.
pub async fn shutdown(self) {
self.manager.close_pool().await
}
async fn try_load_inner(
manager: &StorageManager,
fresh_sender_tags: bool,
) -> Result<(), StorageError> {
// the database flush wasn't fully finished and thus the data is in inconsistent state
// (we don't really know what's properly saved or what's not)
if manager.get_flush_status().await? {
@@ -126,20 +152,11 @@ impl Backend {
manager.delete_all_tags().await?;
}
Ok(Backend {
temporary_old_path: None,
database_path: owned_path,
// manager: StorageManagerState::Storage(manager),
manager,
})
}
async fn close_pool(&mut self) {
self.manager.connection_pool.close().await;
Ok(())
}
async fn rotate(&mut self) -> Result<(), StorageError> {
self.close_pool().await;
self.manager.close_pool().await;
let new_extension = if let Some(existing_extension) =
self.database_path.extension().and_then(|ext| ext.to_str())
@@ -152,7 +169,8 @@ impl Backend {
let mut temp_old = self.database_path.clone();
temp_old.set_extension(new_extension);
fs::rename(&self.database_path, &temp_old)
tokio::fs::rename(&self.database_path, &temp_old)
.await
.map_err(|err| StorageError::DatabaseRenameError { source: err })?;
self.manager = StorageManager::init(&self.database_path, true).await?;
self.manager.create_status_table().await?;
@@ -161,9 +179,10 @@ impl Backend {
Ok(())
}
fn remove_old(&mut self) -> Result<(), StorageError> {
async fn remove_old(&mut self) -> Result<(), StorageError> {
if let Some(old_path) = self.temporary_old_path.take() {
fs::remove_file(old_path)
tokio::fs::remove_file(old_path)
.await
.map_err(|err| StorageError::DatabaseOldFileRemoveError { source: err })
} else {
warn!("the old database file doesn't seem to exist!");
@@ -335,7 +354,7 @@ impl ReplyStorageBackend for Backend {
self.dump_reply_surb_storage_metadata(surbs_ref).await?;
self.dump_reply_surbs(surbs_ref).await?;
self.remove_old()?;
self.remove_old().await?;
self.end_storage_flush().await
}
@@ -8,10 +8,8 @@ use nym_crypto::Digest;
use nym_sphinx::addressing::clients::{Recipient, RecipientBytes};
use nym_sphinx::anonymous_replies::encryption_key::EncryptionKeyDigest;
use nym_sphinx::anonymous_replies::requests::{AnonymousSenderTag, SENDER_TAG_SIZE};
use nym_sphinx::anonymous_replies::{
ReplySurb, ReplySurbWithKeyRotation, SurbEncryptionKey, SurbEncryptionKeySize,
};
use nym_sphinx::params::{ReplySurbKeyDigestAlgorithm, SphinxKeyRotation};
use nym_sphinx::anonymous_replies::{ReplySurb, SurbEncryptionKey, SurbEncryptionKeySize};
use nym_sphinx::params::ReplySurbKeyDigestAlgorithm;
#[derive(Debug, Clone)]
pub struct StoredSenderTag {
@@ -148,40 +146,24 @@ impl TryFrom<StoredSurbSender> for (AnonymousSenderTag, i64) {
pub struct StoredReplySurb {
pub reply_surb_sender_id: i64,
pub reply_surb: Vec<u8>,
// encodes only whether it's 'even', 'odd' or 'unknown' (default)
// and not the whole id because that's redundant
pub encoded_key_rotation: u8,
}
impl StoredReplySurb {
pub fn new(reply_surb_sender_id: i64, reply_surb: &ReplySurbWithKeyRotation) -> Self {
pub fn new(reply_surb_sender_id: i64, reply_surb: &ReplySurb) -> Self {
StoredReplySurb {
reply_surb_sender_id,
reply_surb: reply_surb.inner_reply_surb().to_bytes(),
encoded_key_rotation: reply_surb.key_rotation() as u8,
reply_surb: reply_surb.to_bytes(),
}
}
}
impl TryFrom<StoredReplySurb> for ReplySurbWithKeyRotation {
impl TryFrom<StoredReplySurb> for ReplySurb {
type Error = StorageError;
fn try_from(value: StoredReplySurb) -> Result<Self, Self::Error> {
let key_rotation =
SphinxKeyRotation::try_from(value.encoded_key_rotation).map_err(|err| {
StorageError::CorruptedData {
details: format!("stored key rotation was malformed: {err}"),
}
})?;
let reply_surb = ReplySurb::from_bytes(&value.reply_surb).map_err(|err| {
StorageError::CorruptedData {
details: format!("failed to recover the reply surb: {err}"),
}
})?;
Ok(reply_surb.with_key_rotation(key_rotation))
ReplySurb::from_bytes(&value.reply_surb).map_err(|err| StorageError::CorruptedData {
details: format!("failed to recover the reply surb: {err}"),
})
}
}
@@ -5,7 +5,7 @@ use dashmap::iter::Iter;
use dashmap::DashMap;
use log::trace;
use nym_sphinx::anonymous_replies::requests::AnonymousSenderTag;
use nym_sphinx::anonymous_replies::ReplySurbWithKeyRotation;
use nym_sphinx::anonymous_replies::ReplySurb;
use std::collections::VecDeque;
use std::sync::atomic::{AtomicUsize, Ordering};
use std::sync::Arc;
@@ -134,7 +134,7 @@ impl ReceivedReplySurbsMap {
&self,
target: &AnonymousSenderTag,
amount: usize,
) -> (Option<Vec<ReplySurbWithKeyRotation>>, usize) {
) -> (Option<Vec<ReplySurb>>, usize) {
if let Some(mut entry) = self.inner.data.get_mut(target) {
let surbs_left = entry.items_left();
if surbs_left < self.min_surb_threshold() + amount {
@@ -150,7 +150,7 @@ impl ReceivedReplySurbsMap {
pub fn get_reply_surb_ignoring_threshold(
&self,
target: &AnonymousSenderTag,
) -> Option<(Option<ReplySurbWithKeyRotation>, usize)> {
) -> Option<(Option<ReplySurb>, usize)> {
self.inner
.data
.get_mut(target)
@@ -160,7 +160,7 @@ impl ReceivedReplySurbsMap {
pub fn get_reply_surb(
&self,
target: &AnonymousSenderTag,
) -> Option<(Option<ReplySurbWithKeyRotation>, usize)> {
) -> Option<(Option<ReplySurb>, usize)> {
self.inner.data.get_mut(target).map(|mut entry| {
let surbs_left = entry.items_left();
if surbs_left < self.min_surb_threshold() {
@@ -171,7 +171,7 @@ impl ReceivedReplySurbsMap {
})
}
pub fn insert_surbs<I: IntoIterator<Item = ReplySurbWithKeyRotation>>(
pub fn insert_surbs<I: IntoIterator<Item = ReplySurb>>(
&self,
target: &AnonymousSenderTag,
surbs: I,
@@ -189,14 +189,14 @@ impl ReceivedReplySurbsMap {
pub struct ReceivedReplySurbs {
// in the future we'd probably want to put extra data here to indicate when the SURBs got received
// so we could invalidate entries from the previous key rotations
data: VecDeque<ReplySurbWithKeyRotation>,
data: VecDeque<ReplySurb>,
pending_reception: u32,
surbs_last_received_at_timestamp: i64,
}
impl ReceivedReplySurbs {
fn new(initial_surbs: VecDeque<ReplySurbWithKeyRotation>) -> Self {
fn new(initial_surbs: VecDeque<ReplySurb>) -> Self {
ReceivedReplySurbs {
data: initial_surbs,
pending_reception: 0,
@@ -206,7 +206,7 @@ impl ReceivedReplySurbs {
#[cfg(all(not(target_arch = "wasm32"), feature = "fs-surb-storage"))]
pub fn new_retrieved(
surbs: Vec<ReplySurbWithKeyRotation>,
surbs: Vec<ReplySurb>,
surbs_last_received_at_timestamp: i64,
) -> ReceivedReplySurbs {
ReceivedReplySurbs {
@@ -217,7 +217,7 @@ impl ReceivedReplySurbs {
}
#[cfg(all(not(target_arch = "wasm32"), feature = "fs-surb-storage"))]
pub fn surbs_ref(&self) -> &VecDeque<ReplySurbWithKeyRotation> {
pub fn surbs_ref(&self) -> &VecDeque<ReplySurb> {
&self.data
}
@@ -243,10 +243,7 @@ impl ReceivedReplySurbs {
self.pending_reception = 0;
}
pub fn get_reply_surbs(
&mut self,
amount: usize,
) -> (Option<Vec<ReplySurbWithKeyRotation>>, usize) {
pub fn get_reply_surbs(&mut self, amount: usize) -> (Option<Vec<ReplySurb>>, usize) {
if self.items_left() < amount {
(None, self.items_left())
} else {
@@ -255,11 +252,11 @@ impl ReceivedReplySurbs {
}
}
pub fn get_reply_surb(&mut self) -> (Option<ReplySurbWithKeyRotation>, usize) {
pub fn get_reply_surb(&mut self) -> (Option<ReplySurb>, usize) {
(self.pop_surb(), self.items_left())
}
fn pop_surb(&mut self) -> Option<ReplySurbWithKeyRotation> {
fn pop_surb(&mut self) -> Option<ReplySurb> {
self.data.pop_front()
}
@@ -268,10 +265,7 @@ impl ReceivedReplySurbs {
}
// realistically we're always going to be getting multiple surbs at once
pub fn insert_reply_surbs<I: IntoIterator<Item = ReplySurbWithKeyRotation>>(
&mut self,
surbs: I,
) {
pub fn insert_reply_surbs<I: IntoIterator<Item = ReplySurb>>(&mut self, surbs: I) {
let mut v = surbs.into_iter().collect::<VecDeque<_>>();
trace!("storing {} surbs in the storage", v.len());
self.data.append(&mut v);
@@ -21,8 +21,8 @@ use nym_crypto::asymmetric::ed25519;
use nym_gateway_requests::registration::handshake::client_handshake;
use nym_gateway_requests::{
BinaryRequest, ClientControlRequest, ClientRequest, GatewayProtocolVersionExt,
GatewayRequestsError, SensitiveServerResponse, ServerResponse, SharedGatewayKey,
SharedSymmetricKey, CREDENTIAL_UPDATE_V2_PROTOCOL_VERSION, CURRENT_PROTOCOL_VERSION,
SensitiveServerResponse, ServerResponse, SharedGatewayKey, SharedSymmetricKey,
CREDENTIAL_UPDATE_V2_PROTOCOL_VERSION, CURRENT_PROTOCOL_VERSION,
};
use nym_sphinx::forwarding::packet::MixPacket;
use nym_statistics_common::clients::connection::ConnectionStatsEvent;
@@ -662,7 +662,6 @@ impl<C, St> GatewayClient<C, St> {
let supports_aes_gcm_siv = gw_protocol.supports_aes256_gcm_siv();
let supports_auth_v2 = gw_protocol.supports_authenticate_v2();
let supports_key_rotation_info = gw_protocol.supports_key_rotation_packet();
if !supports_aes_gcm_siv {
warn!("this gateway is on an old version that doesn't support AES256-GCM-SIV");
@@ -670,9 +669,6 @@ impl<C, St> GatewayClient<C, St> {
if !supports_auth_v2 {
warn!("this gateway is on an old version that doesn't support authentication v2")
}
if !supports_key_rotation_info {
warn!("this gateway is on an old version that doesn't support key rotation packets")
}
if self.authenticated {
debug!("Already authenticated");
@@ -853,22 +849,6 @@ impl<C, St> GatewayClient<C, St> {
}
}
fn mix_packet_to_ws_message(&self, packet: MixPacket) -> Result<Message, GatewayRequestsError> {
// note: into_ws_message encrypts the requests and adds a MAC on it. Perhaps it should
// be more explicit in the naming?
let req = if self.negotiated_protocol.supports_key_rotation_packet() {
BinaryRequest::ForwardSphinxV2 { packet }
} else {
BinaryRequest::ForwardSphinx { packet }
};
req.into_ws_message(
self.shared_key
.as_ref()
.expect("no shared key present even though we're authenticated!"),
)
}
pub async fn batch_send_mix_packets(
&mut self,
packets: Vec<MixPacket>,
@@ -897,7 +877,13 @@ impl<C, St> GatewayClient<C, St> {
let messages: Result<Vec<_>, _> = packets
.into_iter()
.map(|mix_packet| self.mix_packet_to_ws_message(mix_packet))
.map(|mix_packet| {
BinaryRequest::ForwardSphinx { packet: mix_packet }.into_ws_message(
self.shared_key
.as_ref()
.expect("no shared key present even though we're authenticated!"),
)
})
.collect();
if let Err(err) = self
@@ -963,8 +949,13 @@ impl<C, St> GatewayClient<C, St> {
if !self.connection.is_established() {
return Err(GatewayClientError::ConnectionNotEstablished);
}
let msg = self.mix_packet_to_ws_message(mix_packet)?;
// note: into_ws_message encrypts the requests and adds a MAC on it. Perhaps it should
// be more explicit in the naming?
let msg = BinaryRequest::ForwardSphinx { packet: mix_packet }.into_ws_message(
self.shared_key
.as_ref()
.expect("no shared key present even though we're authenticated!"),
)?;
self.send_with_reconnection_on_failure(msg).await
}
+22 -11
View File
@@ -3,9 +3,11 @@
use dashmap::DashMap;
use futures::StreamExt;
use nym_sphinx::forwarding::packet::MixPacket;
use nym_sphinx::addressing::nodes::NymNodeRoutingAddress;
use nym_sphinx::framing::codec::NymCodec;
use nym_sphinx::framing::packet::FramedNymPacket;
use nym_sphinx::params::PacketType;
use nym_sphinx::NymPacket;
use std::io;
use std::net::SocketAddr;
use std::ops::Deref;
@@ -47,7 +49,12 @@ impl Config {
pub trait SendWithoutResponse {
// Without response in this context means we will not listen for anything we might get back (not
// that we should get anything), including any possible io errors
fn send_without_response(&self, packet: MixPacket) -> io::Result<()>;
fn send_without_response(
&self,
address: NymNodeRoutingAddress,
packet: NymPacket,
packet_type: PacketType,
) -> io::Result<()>;
}
pub struct Client {
@@ -58,7 +65,7 @@ pub struct Client {
#[derive(Default, Clone)]
pub struct ActiveConnections {
inner: Arc<DashMap<SocketAddr, ConnectionSender>>,
inner: Arc<DashMap<NymNodeRoutingAddress, ConnectionSender>>,
}
impl ActiveConnections {
@@ -75,7 +82,7 @@ impl ActiveConnections {
}
impl Deref for ActiveConnections {
type Target = DashMap<SocketAddr, ConnectionSender>;
type Target = DashMap<NymNodeRoutingAddress, ConnectionSender>;
fn deref(&self) -> &Self::Target {
&self.inner
}
@@ -189,7 +196,7 @@ impl Client {
}
}
fn make_connection(&self, address: SocketAddr, pending_packet: FramedNymPacket) {
fn make_connection(&self, address: NymNodeRoutingAddress, pending_packet: FramedNymPacket) {
let (sender, receiver) = mpsc::channel(self.config.maximum_connection_buffer_size);
// this CAN'T fail because we just created the channel which has a non-zero capacity
@@ -226,7 +233,7 @@ impl Client {
connections_count.fetch_add(1, Ordering::SeqCst);
ManagedConnection::new(
address,
address.into(),
receiver,
initial_connection_timeout,
current_reconnection_attempt,
@@ -239,14 +246,18 @@ impl Client {
}
impl SendWithoutResponse for Client {
fn send_without_response(&self, packet: MixPacket) -> io::Result<()> {
let address = packet.next_hop_address();
trace!("Sending packet to {address}");
let framed_packet = FramedNymPacket::from(packet);
fn send_without_response(
&self,
address: NymNodeRoutingAddress,
packet: NymPacket,
packet_type: PacketType,
) -> io::Result<()> {
trace!("Sending packet to {address:?}");
let framed_packet = FramedNymPacket::new(packet, packet_type);
let Some(sender) = self.active_connections.get_mut(&address) else {
// there was never a connection to begin with
debug!("establishing initial connection to {address}");
debug!("establishing initial connection to {}", address);
// it's not a 'big' error, but we did not manage to send the packet, but queue the packet
// for sending for as soon as the connection is created
self.make_connection(address, framed_packet);
@@ -25,9 +25,7 @@ use nym_api_requests::models::{
NymNodeDescription, RewardEstimationResponse, StakeSaturationResponse,
};
use nym_api_requests::models::{LegacyDescribedGateway, MixNodeBondAnnotated};
use nym_api_requests::nym_nodes::{
NodesByAddressesResponse, SkimmedNode, SkimmedNodesWithMetadata,
};
use nym_api_requests::nym_nodes::{NodesByAddressesResponse, SkimmedNode};
use nym_coconut_dkg_common::types::EpochId;
use nym_http_api_client::UserAgent;
use nym_mixnet_contract_common::EpochRewardedSet;
@@ -48,46 +46,6 @@ use crate::rpc::http_client;
#[cfg(feature = "http-client")]
use crate::{DirectSigningHttpRpcValidatorClient, HttpRpcClient, QueryHttpRpcValidatorClient};
// a simple helper macro to define to repeatedly call a paged query until a full response is constructed
macro_rules! collect_paged_skimmed_v2 {
( $self:ident, $f: ident ) => {{
// unroll first loop iteration in order to obtain the metadata
let mut page = 0;
let res = $self
.nym_api
.$f(false, Some(page), None, $self.use_bincode)
.await?;
let mut nodes = res.nodes.data;
let metadata = res.metadata;
if res.nodes.pagination.total == nodes.len() {
return Ok(SkimmedNodesWithMetadata::new(nodes, metadata));
}
page += 1;
loop {
let mut res = $self
.nym_api
.$f(false, Some(page), None, $self.use_bincode)
.await?;
if metadata != res.metadata {
return Err(ValidatorClientError::InconsistentPagedMetadata);
}
nodes.append(&mut res.nodes.data);
if nodes.len() < res.nodes.pagination.total {
page += 1
} else {
break;
}
}
Ok(SkimmedNodesWithMetadata::new(nodes, metadata))
}};
}
#[must_use]
#[derive(Debug, Clone)]
pub struct Config {
@@ -242,11 +200,11 @@ impl<C, S> Client<C, S> {
#[allow(deprecated)]
impl<C, S> Client<C, S> {
pub fn api_url(&self) -> &Url {
self.nym_api.current_url().as_ref()
self.nym_api.current_url()
}
pub fn change_nym_api(&mut self, new_endpoint: Url) {
self.nym_api.change_base_urls(vec![new_endpoint.into()])
self.nym_api.change_base_url(new_endpoint)
}
#[deprecated]
@@ -444,11 +402,11 @@ impl NymApiClient {
}
pub fn api_url(&self) -> &Url {
self.nym_api.current_url().as_ref()
self.nym_api.current_url()
}
pub fn change_nym_api(&mut self, new_endpoint: Url) {
self.nym_api.change_base_urls(vec![new_endpoint.into()]);
self.nym_api.change_base_url(new_endpoint);
}
#[deprecated(note = "use get_all_basic_active_mixing_assigned_nodes instead")]
@@ -467,67 +425,103 @@ impl NymApiClient {
/// retrieve basic information for nodes are capable of operating as an entry gateway
/// this includes legacy gateways and nym-nodes
#[deprecated(note = "use get_all_basic_entry_assigned_nodes_with_metadata instead")]
pub async fn get_all_basic_entry_assigned_nodes(
&self,
) -> Result<Vec<SkimmedNode>, ValidatorClientError> {
self.get_all_basic_entry_assigned_nodes_v2()
.await
.map(|res| res.nodes)
}
// TODO: deal with paging in macro or some helper function or something, because it's the same pattern everywhere
let mut page = 0;
let mut nodes = Vec::new();
pub async fn get_all_basic_entry_assigned_nodes_v2(
&self,
) -> Result<SkimmedNodesWithMetadata, ValidatorClientError> {
collect_paged_skimmed_v2!(self, get_basic_entry_assigned_nodes_v2)
loop {
let mut res = self
.nym_api
.get_basic_entry_assigned_nodes(false, Some(page), None, self.use_bincode)
.await?;
nodes.append(&mut res.nodes.data);
if nodes.len() < res.nodes.pagination.total {
page += 1
} else {
break;
}
}
Ok(nodes)
}
/// retrieve basic information for nodes that got assigned 'mixing' node in this epoch
/// this includes legacy mixnodes and nym-nodes
#[deprecated(note = "use get_all_basic_active_mixing_assigned_nodes_with_metadata instead")]
pub async fn get_all_basic_active_mixing_assigned_nodes(
&self,
) -> Result<Vec<SkimmedNode>, ValidatorClientError> {
self.get_all_basic_active_mixing_assigned_nodes_with_metadata()
.await
.map(|res| res.nodes)
}
// TODO: deal with paging in macro or some helper function or something, because it's the same pattern everywhere
let mut page = 0;
let mut nodes = Vec::new();
pub async fn get_all_basic_active_mixing_assigned_nodes_with_metadata(
&self,
) -> Result<SkimmedNodesWithMetadata, ValidatorClientError> {
collect_paged_skimmed_v2!(self, get_basic_active_mixing_assigned_nodes_v2)
loop {
let mut res = self
.nym_api
.get_basic_active_mixing_assigned_nodes(false, Some(page), None, self.use_bincode)
.await?;
nodes.append(&mut res.nodes.data);
if nodes.len() < res.nodes.pagination.total {
page += 1
} else {
break;
}
}
Ok(nodes)
}
/// retrieve basic information for nodes are capable of operating as a mixnode
/// this includes legacy mixnodes and nym-nodes
#[deprecated(note = "use get_all_basic_mixing_capable_nodes_with_metadata instead")]
pub async fn get_all_basic_mixing_capable_nodes(
&self,
) -> Result<Vec<SkimmedNode>, ValidatorClientError> {
self.get_all_basic_mixing_capable_nodes_with_metadata()
.await
.map(|res| res.nodes)
}
// TODO: deal with paging in macro or some helper function or something, because it's the same pattern everywhere
let mut page = 0;
let mut nodes = Vec::new();
pub async fn get_all_basic_mixing_capable_nodes_with_metadata(
&self,
) -> Result<SkimmedNodesWithMetadata, ValidatorClientError> {
collect_paged_skimmed_v2!(self, get_basic_mixing_capable_nodes_v2)
loop {
let mut res = self
.nym_api
.get_basic_mixing_capable_nodes(false, Some(page), None, self.use_bincode)
.await?;
nodes.append(&mut res.nodes.data);
if nodes.len() < res.nodes.pagination.total {
page += 1
} else {
break;
}
}
Ok(nodes)
}
/// retrieve basic information for all bonded nodes on the network
#[deprecated(note = "use get_all_basic_nodes_with_metadata instead")]
pub async fn get_all_basic_nodes(&self) -> Result<Vec<SkimmedNode>, ValidatorClientError> {
self.get_all_basic_nodes_with_metadata()
.await
.map(|res| res.nodes)
}
// TODO: deal with paging in macro or some helper function or something, because it's the same pattern everywhere
let mut page = 0;
let mut nodes = Vec::new();
pub async fn get_all_basic_nodes_with_metadata(
&self,
) -> Result<SkimmedNodesWithMetadata, ValidatorClientError> {
collect_paged_skimmed_v2!(self, get_basic_nodes_v2)
loop {
let mut res = self
.nym_api
.get_basic_nodes(false, Some(page), None, self.use_bincode)
.await?;
nodes.append(&mut res.nodes.data);
if nodes.len() < res.nodes.pagination.total {
page += 1
} else {
break;
}
}
Ok(nodes)
}
pub async fn health(&self) -> Result<ApiHealthResponse, ValidatorClientError> {
@@ -22,9 +22,6 @@ pub enum ValidatorClientError {
#[error("nyxd request failed: {0}")]
NyxdError(#[from] crate::nyxd::error::NyxdError),
#[error("the response metadata has changed between pages")]
InconsistentPagedMetadata,
#[error("No validator API url has been provided")]
NoAPIUrlAvailable,
}
@@ -14,12 +14,11 @@ use nym_api_requests::ecash::models::{
use nym_api_requests::ecash::VerificationKeyResponse;
use nym_api_requests::models::{
AnnotationResponse, ApiHealthResponse, BinaryBuildInformationOwned, ChainStatusResponse,
KeyRotationInfoResponse, LegacyDescribedMixNode, NodePerformanceResponse, NodeRefreshBody,
NymNodeDescription, PerformanceHistoryResponse, RewardedSetResponse,
LegacyDescribedMixNode, NodePerformanceResponse, NodeRefreshBody, NymNodeDescription,
PerformanceHistoryResponse, RewardedSetResponse,
};
use nym_api_requests::nym_nodes::{
NodesByAddressesRequestBody, NodesByAddressesResponse, PaginatedCachedNodesResponseV1,
PaginatedCachedNodesResponseV2,
NodesByAddressesRequestBody, NodesByAddressesResponse, PaginatedCachedNodesResponse,
};
use nym_api_requests::pagination::PaginatedResponse;
pub use nym_api_requests::{
@@ -63,7 +62,7 @@ pub trait NymApiClientExt: ApiClient {
async fn health(&self) -> Result<ApiHealthResponse, NymAPIError> {
self.get_json(
&[
routes::V1_API_VERSION,
routes::API_VERSION,
routes::API_STATUS_ROUTES,
routes::HEALTH,
],
@@ -76,7 +75,7 @@ pub trait NymApiClientExt: ApiClient {
async fn build_information(&self) -> Result<BinaryBuildInformationOwned, NymAPIError> {
self.get_json(
&[
routes::V1_API_VERSION,
routes::API_VERSION,
routes::API_STATUS_ROUTES,
routes::BUILD_INFORMATION,
],
@@ -88,7 +87,7 @@ pub trait NymApiClientExt: ApiClient {
#[deprecated]
#[instrument(level = "debug", skip(self))]
async fn get_mixnodes(&self) -> Result<Vec<MixNodeDetails>, NymAPIError> {
self.get_json(&[routes::V1_API_VERSION, routes::MIXNODES], NO_PARAMS)
self.get_json(&[routes::API_VERSION, routes::MIXNODES], NO_PARAMS)
.await
}
@@ -97,7 +96,7 @@ pub trait NymApiClientExt: ApiClient {
async fn get_mixnodes_detailed(&self) -> Result<Vec<MixNodeBondAnnotated>, NymAPIError> {
self.get_json(
&[
routes::V1_API_VERSION,
routes::API_VERSION,
routes::STATUS,
routes::MIXNODES,
routes::DETAILED,
@@ -112,7 +111,7 @@ pub trait NymApiClientExt: ApiClient {
async fn get_gateways_detailed(&self) -> Result<Vec<GatewayBondAnnotated>, NymAPIError> {
self.get_json(
&[
routes::V1_API_VERSION,
routes::API_VERSION,
routes::STATUS,
routes::GATEWAYS,
routes::DETAILED,
@@ -129,7 +128,7 @@ pub trait NymApiClientExt: ApiClient {
) -> Result<Vec<GatewayBondAnnotated>, NymAPIError> {
self.get_json(
&[
routes::V1_API_VERSION,
routes::API_VERSION,
routes::STATUS,
routes::GATEWAYS,
routes::DETAILED_UNFILTERED,
@@ -146,7 +145,7 @@ pub trait NymApiClientExt: ApiClient {
) -> Result<Vec<MixNodeBondAnnotated>, NymAPIError> {
self.get_json(
&[
routes::V1_API_VERSION,
routes::API_VERSION,
routes::STATUS,
routes::MIXNODES,
routes::DETAILED_UNFILTERED,
@@ -159,7 +158,7 @@ pub trait NymApiClientExt: ApiClient {
#[deprecated]
#[instrument(level = "debug", skip(self))]
async fn get_gateways(&self) -> Result<Vec<GatewayBond>, NymAPIError> {
self.get_json(&[routes::V1_API_VERSION, routes::GATEWAYS], NO_PARAMS)
self.get_json(&[routes::API_VERSION, routes::GATEWAYS], NO_PARAMS)
.await
}
@@ -167,7 +166,7 @@ pub trait NymApiClientExt: ApiClient {
#[instrument(level = "debug", skip(self))]
async fn get_gateways_described(&self) -> Result<Vec<LegacyDescribedGateway>, NymAPIError> {
self.get_json(
&[routes::V1_API_VERSION, routes::GATEWAYS, routes::DESCRIBED],
&[routes::API_VERSION, routes::GATEWAYS, routes::DESCRIBED],
NO_PARAMS,
)
.await
@@ -177,7 +176,7 @@ pub trait NymApiClientExt: ApiClient {
#[instrument(level = "debug", skip(self))]
async fn get_mixnodes_described(&self) -> Result<Vec<LegacyDescribedMixNode>, NymAPIError> {
self.get_json(
&[routes::V1_API_VERSION, routes::MIXNODES, routes::DESCRIBED],
&[routes::API_VERSION, routes::MIXNODES, routes::DESCRIBED],
NO_PARAMS,
)
.await
@@ -202,7 +201,7 @@ pub trait NymApiClientExt: ApiClient {
self.get_json(
&[
routes::V1_API_VERSION,
routes::API_VERSION,
routes::NYM_NODES_ROUTES,
routes::NYM_NODES_PERFORMANCE_HISTORY,
&*node_id.to_string(),
@@ -230,7 +229,7 @@ pub trait NymApiClientExt: ApiClient {
self.get_json(
&[
routes::V1_API_VERSION,
routes::API_VERSION,
routes::NYM_NODES_ROUTES,
routes::NYM_NODES_DESCRIBED,
],
@@ -257,7 +256,7 @@ pub trait NymApiClientExt: ApiClient {
self.get_json(
&[
routes::V1_API_VERSION,
routes::API_VERSION,
routes::NYM_NODES_ROUTES,
routes::NYM_NODES_BONDED,
],
@@ -271,7 +270,7 @@ pub trait NymApiClientExt: ApiClient {
async fn get_basic_mixnodes(&self) -> Result<CachedNodesResponse<SkimmedNode>, NymAPIError> {
self.get_json(
&[
routes::V1_API_VERSION,
routes::API_VERSION,
"unstable",
routes::NYM_NODES_ROUTES,
"mixnodes",
@@ -287,7 +286,7 @@ pub trait NymApiClientExt: ApiClient {
async fn get_basic_gateways(&self) -> Result<CachedNodesResponse<SkimmedNode>, NymAPIError> {
self.get_json(
&[
routes::V1_API_VERSION,
routes::API_VERSION,
"unstable",
routes::NYM_NODES_ROUTES,
"gateways",
@@ -302,7 +301,7 @@ pub trait NymApiClientExt: ApiClient {
async fn get_rewarded_set(&self) -> Result<RewardedSetResponse, NymAPIError> {
self.get_json(
&[
routes::V1_API_VERSION,
routes::API_VERSION,
routes::NYM_NODES_ROUTES,
routes::NYM_NODES_REWARDED_SET,
],
@@ -313,7 +312,6 @@ pub trait NymApiClientExt: ApiClient {
/// retrieve basic information for nodes are capable of operating as an entry gateway
/// this includes legacy gateways and nym-nodes
#[deprecated(note = "use get_basic_entry_assigned_nodes_v2")]
#[instrument(level = "debug", skip(self))]
async fn get_basic_entry_assigned_nodes(
&self,
@@ -321,7 +319,7 @@ pub trait NymApiClientExt: ApiClient {
page: Option<u32>,
per_page: Option<u32>,
use_bincode: bool,
) -> Result<PaginatedCachedNodesResponseV1<SkimmedNode>, NymAPIError> {
) -> Result<PaginatedCachedNodesResponse<SkimmedNode>, NymAPIError> {
let mut params = Vec::new();
if no_legacy {
@@ -342,49 +340,7 @@ pub trait NymApiClientExt: ApiClient {
self.get_response(
&[
routes::V1_API_VERSION,
"unstable",
routes::NYM_NODES_ROUTES,
"skimmed",
"entry-gateways",
"all",
],
&params,
)
.await
}
/// retrieve basic information for nodes are capable of operating as an entry gateway
/// this includes legacy gateways and nym-nodes
#[instrument(level = "debug", skip(self))]
async fn get_basic_entry_assigned_nodes_v2(
&self,
no_legacy: bool,
page: Option<u32>,
per_page: Option<u32>,
use_bincode: bool,
) -> Result<PaginatedCachedNodesResponseV2<SkimmedNode>, NymAPIError> {
let mut params = Vec::new();
if no_legacy {
params.push(("no_legacy", "true".to_string()))
}
if let Some(page) = page {
params.push(("page", page.to_string()))
}
if let Some(per_page) = per_page {
params.push(("per_page", per_page.to_string()))
}
if use_bincode {
params.push(("output", "bincode".to_string()))
}
self.get_response(
&[
routes::V2_API_VERSION,
routes::API_VERSION,
"unstable",
routes::NYM_NODES_ROUTES,
"skimmed",
@@ -398,7 +354,6 @@ pub trait NymApiClientExt: ApiClient {
/// retrieve basic information for nodes that got assigned 'mixing' node in this epoch
/// this includes legacy mixnodes and nym-nodes
#[deprecated(note = "use get_basic_active_mixing_assigned_nodes_v2")]
#[instrument(level = "debug", skip(self))]
async fn get_basic_active_mixing_assigned_nodes(
&self,
@@ -406,7 +361,7 @@ pub trait NymApiClientExt: ApiClient {
page: Option<u32>,
per_page: Option<u32>,
use_bincode: bool,
) -> Result<PaginatedCachedNodesResponseV1<SkimmedNode>, NymAPIError> {
) -> Result<PaginatedCachedNodesResponse<SkimmedNode>, NymAPIError> {
let mut params = Vec::new();
if no_legacy {
@@ -427,7 +382,7 @@ pub trait NymApiClientExt: ApiClient {
self.get_response(
&[
routes::V1_API_VERSION,
routes::API_VERSION,
"unstable",
routes::NYM_NODES_ROUTES,
"skimmed",
@@ -442,56 +397,13 @@ pub trait NymApiClientExt: ApiClient {
/// retrieve basic information for nodes that got assigned 'mixing' node in this epoch
/// this includes legacy mixnodes and nym-nodes
#[instrument(level = "debug", skip(self))]
async fn get_basic_active_mixing_assigned_nodes_v2(
&self,
no_legacy: bool,
page: Option<u32>,
per_page: Option<u32>,
use_bincode: bool,
) -> Result<PaginatedCachedNodesResponseV2<SkimmedNode>, NymAPIError> {
let mut params = Vec::new();
if no_legacy {
params.push(("no_legacy", "true".to_string()))
}
if let Some(page) = page {
params.push(("page", page.to_string()))
}
if let Some(per_page) = per_page {
params.push(("per_page", per_page.to_string()))
}
if use_bincode {
params.push(("output", "bincode".to_string()))
}
self.get_response(
&[
routes::V2_API_VERSION,
"unstable",
routes::NYM_NODES_ROUTES,
"skimmed",
"mixnodes",
"active",
],
&params,
)
.await
}
/// retrieve basic information for nodes that got assigned 'mixing' node in this epoch
/// this includes legacy mixnodes and nym-nodes
#[deprecated(note = "use get_basic_mixing_capable_nodes_v2")]
#[instrument(level = "debug", skip(self))]
async fn get_basic_mixing_capable_nodes(
&self,
no_legacy: bool,
page: Option<u32>,
per_page: Option<u32>,
use_bincode: bool,
) -> Result<PaginatedCachedNodesResponseV1<SkimmedNode>, NymAPIError> {
) -> Result<PaginatedCachedNodesResponse<SkimmedNode>, NymAPIError> {
let mut params = Vec::new();
if no_legacy {
@@ -512,7 +424,7 @@ pub trait NymApiClientExt: ApiClient {
self.get_response(
&[
routes::V1_API_VERSION,
routes::API_VERSION,
"unstable",
routes::NYM_NODES_ROUTES,
"skimmed",
@@ -524,49 +436,6 @@ pub trait NymApiClientExt: ApiClient {
.await
}
/// retrieve basic information for nodes that got assigned 'mixing' node in this epoch
/// this includes legacy mixnodes and nym-nodes
#[instrument(level = "debug", skip(self))]
async fn get_basic_mixing_capable_nodes_v2(
&self,
no_legacy: bool,
page: Option<u32>,
per_page: Option<u32>,
use_bincode: bool,
) -> Result<PaginatedCachedNodesResponseV2<SkimmedNode>, NymAPIError> {
let mut params = Vec::new();
if no_legacy {
params.push(("no_legacy", "true".to_string()))
}
if let Some(page) = page {
params.push(("page", page.to_string()))
}
if let Some(per_page) = per_page {
params.push(("per_page", per_page.to_string()))
}
if use_bincode {
params.push(("output", "bincode".to_string()))
}
self.get_response(
&[
routes::V2_API_VERSION,
"unstable",
routes::NYM_NODES_ROUTES,
"skimmed",
"mixnodes",
"all",
],
&params,
)
.await
}
#[deprecated(note = "use get_basic_nodes_v2")]
#[instrument(level = "debug", skip(self))]
async fn get_basic_nodes(
&self,
@@ -574,7 +443,7 @@ pub trait NymApiClientExt: ApiClient {
page: Option<u32>,
per_page: Option<u32>,
use_bincode: bool,
) -> Result<PaginatedCachedNodesResponseV1<SkimmedNode>, NymAPIError> {
) -> Result<PaginatedCachedNodesResponse<SkimmedNode>, NymAPIError> {
let mut params = Vec::new();
if no_legacy {
@@ -595,45 +464,7 @@ pub trait NymApiClientExt: ApiClient {
self.get_response(
&[
routes::V1_API_VERSION,
"unstable",
routes::NYM_NODES_ROUTES,
"skimmed",
],
&params,
)
.await
}
#[instrument(level = "debug", skip(self))]
async fn get_basic_nodes_v2(
&self,
no_legacy: bool,
page: Option<u32>,
per_page: Option<u32>,
use_bincode: bool,
) -> Result<PaginatedCachedNodesResponseV2<SkimmedNode>, NymAPIError> {
let mut params = Vec::new();
if no_legacy {
params.push(("no_legacy", "true".to_string()))
}
if let Some(page) = page {
params.push(("page", page.to_string()))
}
if let Some(per_page) = per_page {
params.push(("per_page", per_page.to_string()))
}
if use_bincode {
params.push(("output", "bincode".to_string()))
}
self.get_response(
&[
routes::V2_API_VERSION,
routes::API_VERSION,
"unstable",
routes::NYM_NODES_ROUTES,
"skimmed",
@@ -647,7 +478,7 @@ pub trait NymApiClientExt: ApiClient {
#[instrument(level = "debug", skip(self))]
async fn get_active_mixnodes(&self) -> Result<Vec<MixNodeDetails>, NymAPIError> {
self.get_json(
&[routes::V1_API_VERSION, routes::MIXNODES, routes::ACTIVE],
&[routes::API_VERSION, routes::MIXNODES, routes::ACTIVE],
NO_PARAMS,
)
.await
@@ -658,7 +489,7 @@ pub trait NymApiClientExt: ApiClient {
async fn get_active_mixnodes_detailed(&self) -> Result<Vec<MixNodeBondAnnotated>, NymAPIError> {
self.get_json(
&[
routes::V1_API_VERSION,
routes::API_VERSION,
routes::STATUS,
routes::MIXNODES,
routes::ACTIVE,
@@ -673,7 +504,7 @@ pub trait NymApiClientExt: ApiClient {
#[instrument(level = "debug", skip(self))]
async fn get_rewarded_mixnodes(&self) -> Result<Vec<MixNodeDetails>, NymAPIError> {
self.get_json(
&[routes::V1_API_VERSION, routes::MIXNODES, routes::REWARDED],
&[routes::API_VERSION, routes::MIXNODES, routes::REWARDED],
NO_PARAMS,
)
.await
@@ -687,7 +518,7 @@ pub trait NymApiClientExt: ApiClient {
) -> Result<MixnodeStatusReportResponse, NymAPIError> {
self.get_json(
&[
routes::V1_API_VERSION,
routes::API_VERSION,
routes::STATUS,
routes::MIXNODE,
&mix_id.to_string(),
@@ -706,7 +537,7 @@ pub trait NymApiClientExt: ApiClient {
) -> Result<GatewayStatusReportResponse, NymAPIError> {
self.get_json(
&[
routes::V1_API_VERSION,
routes::API_VERSION,
routes::STATUS,
routes::GATEWAY,
identity,
@@ -725,7 +556,7 @@ pub trait NymApiClientExt: ApiClient {
) -> Result<MixnodeUptimeHistoryResponse, NymAPIError> {
self.get_json(
&[
routes::V1_API_VERSION,
routes::API_VERSION,
routes::STATUS,
routes::MIXNODE,
&mix_id.to_string(),
@@ -744,7 +575,7 @@ pub trait NymApiClientExt: ApiClient {
) -> Result<GatewayUptimeHistoryResponse, NymAPIError> {
self.get_json(
&[
routes::V1_API_VERSION,
routes::API_VERSION,
routes::STATUS,
routes::GATEWAY,
identity,
@@ -762,7 +593,7 @@ pub trait NymApiClientExt: ApiClient {
) -> Result<Vec<MixNodeBondAnnotated>, NymAPIError> {
self.get_json(
&[
routes::V1_API_VERSION,
routes::API_VERSION,
routes::STATUS,
routes::MIXNODES,
routes::REWARDED,
@@ -783,7 +614,7 @@ pub trait NymApiClientExt: ApiClient {
if let Some(since) = since {
self.get_json(
&[
routes::V1_API_VERSION,
routes::API_VERSION,
routes::STATUS_ROUTES,
routes::GATEWAY,
identity,
@@ -795,7 +626,7 @@ pub trait NymApiClientExt: ApiClient {
} else {
self.get_json(
&[
routes::V1_API_VERSION,
routes::API_VERSION,
routes::STATUS_ROUTES,
routes::GATEWAY,
identity,
@@ -816,7 +647,7 @@ pub trait NymApiClientExt: ApiClient {
if let Some(since) = since {
self.get_json(
&[
routes::V1_API_VERSION,
routes::API_VERSION,
routes::STATUS_ROUTES,
routes::MIXNODE,
&mix_id.to_string(),
@@ -828,7 +659,7 @@ pub trait NymApiClientExt: ApiClient {
} else {
self.get_json(
&[
routes::V1_API_VERSION,
routes::API_VERSION,
routes::STATUS_ROUTES,
routes::MIXNODE,
&mix_id.to_string(),
@@ -848,7 +679,7 @@ pub trait NymApiClientExt: ApiClient {
) -> Result<MixnodeStatusResponse, NymAPIError> {
self.get_json(
&[
routes::V1_API_VERSION,
routes::API_VERSION,
routes::STATUS_ROUTES,
routes::MIXNODE,
&mix_id.to_string(),
@@ -867,7 +698,7 @@ pub trait NymApiClientExt: ApiClient {
) -> Result<RewardEstimationResponse, NymAPIError> {
self.get_json(
&[
routes::V1_API_VERSION,
routes::API_VERSION,
routes::STATUS_ROUTES,
routes::MIXNODE,
&mix_id.to_string(),
@@ -887,7 +718,7 @@ pub trait NymApiClientExt: ApiClient {
) -> Result<RewardEstimationResponse, NymAPIError> {
self.post_json(
&[
routes::V1_API_VERSION,
routes::API_VERSION,
routes::STATUS_ROUTES,
routes::MIXNODE,
&mix_id.to_string(),
@@ -907,7 +738,7 @@ pub trait NymApiClientExt: ApiClient {
) -> Result<StakeSaturationResponse, NymAPIError> {
self.get_json(
&[
routes::V1_API_VERSION,
routes::API_VERSION,
routes::STATUS_ROUTES,
routes::MIXNODE,
&mix_id.to_string(),
@@ -927,7 +758,7 @@ pub trait NymApiClientExt: ApiClient {
) -> Result<nym_api_requests::models::InclusionProbabilityResponse, NymAPIError> {
self.get_json(
&[
routes::V1_API_VERSION,
routes::API_VERSION,
routes::STATUS_ROUTES,
routes::MIXNODE,
&mix_id.to_string(),
@@ -945,7 +776,7 @@ pub trait NymApiClientExt: ApiClient {
) -> Result<NodePerformanceResponse, NymAPIError> {
self.get_json(
&[
routes::V1_API_VERSION,
routes::API_VERSION,
routes::NYM_NODES_ROUTES,
routes::NYM_NODES_PERFORMANCE,
&node_id.to_string(),
@@ -961,7 +792,7 @@ pub trait NymApiClientExt: ApiClient {
) -> Result<AnnotationResponse, NymAPIError> {
self.get_json(
&[
routes::V1_API_VERSION,
routes::API_VERSION,
routes::NYM_NODES_ROUTES,
routes::NYM_NODES_ANNOTATION,
&node_id.to_string(),
@@ -975,7 +806,7 @@ pub trait NymApiClientExt: ApiClient {
async fn get_mixnode_avg_uptime(&self, mix_id: NodeId) -> Result<UptimeResponse, NymAPIError> {
self.get_json(
&[
routes::V1_API_VERSION,
routes::API_VERSION,
routes::STATUS_ROUTES,
routes::MIXNODE,
&mix_id.to_string(),
@@ -990,11 +821,7 @@ pub trait NymApiClientExt: ApiClient {
#[instrument(level = "debug", skip(self))]
async fn get_mixnodes_blacklisted(&self) -> Result<Vec<NodeId>, NymAPIError> {
self.get_json(
&[
routes::V1_API_VERSION,
routes::MIXNODES,
routes::BLACKLISTED,
],
&[routes::API_VERSION, routes::MIXNODES, routes::BLACKLISTED],
NO_PARAMS,
)
.await
@@ -1004,11 +831,7 @@ pub trait NymApiClientExt: ApiClient {
#[instrument(level = "debug", skip(self))]
async fn get_gateways_blacklisted(&self) -> Result<Vec<IdentityKey>, NymAPIError> {
self.get_json(
&[
routes::V1_API_VERSION,
routes::GATEWAYS,
routes::BLACKLISTED,
],
&[routes::API_VERSION, routes::GATEWAYS, routes::BLACKLISTED],
NO_PARAMS,
)
.await
@@ -1021,7 +844,7 @@ pub trait NymApiClientExt: ApiClient {
) -> Result<BlindedSignatureResponse, NymAPIError> {
self.post_json(
&[
routes::V1_API_VERSION,
routes::API_VERSION,
routes::ECASH_ROUTES,
routes::ECASH_BLIND_SIGN,
],
@@ -1038,7 +861,7 @@ pub trait NymApiClientExt: ApiClient {
) -> Result<EcashTicketVerificationResponse, NymAPIError> {
self.post_json(
&[
routes::V1_API_VERSION,
routes::API_VERSION,
routes::ECASH_ROUTES,
routes::VERIFY_ECASH_TICKET,
],
@@ -1055,7 +878,7 @@ pub trait NymApiClientExt: ApiClient {
) -> Result<EcashBatchTicketRedemptionResponse, NymAPIError> {
self.post_json(
&[
routes::V1_API_VERSION,
routes::API_VERSION,
routes::ECASH_ROUTES,
routes::BATCH_REDEEM_ECASH_TICKETS,
],
@@ -1080,7 +903,7 @@ pub trait NymApiClientExt: ApiClient {
self.get_json(
&[
routes::V1_API_VERSION,
routes::API_VERSION,
routes::ECASH_ROUTES,
routes::PARTIAL_EXPIRATION_DATE_SIGNATURES,
],
@@ -1101,7 +924,7 @@ pub trait NymApiClientExt: ApiClient {
self.get_json(
&[
routes::V1_API_VERSION,
routes::API_VERSION,
routes::ECASH_ROUTES,
routes::PARTIAL_COIN_INDICES_SIGNATURES,
],
@@ -1125,7 +948,7 @@ pub trait NymApiClientExt: ApiClient {
self.get_json(
&[
routes::V1_API_VERSION,
routes::API_VERSION,
routes::ECASH_ROUTES,
routes::GLOBAL_EXPIRATION_DATE_SIGNATURES,
],
@@ -1146,7 +969,7 @@ pub trait NymApiClientExt: ApiClient {
self.get_json(
&[
routes::V1_API_VERSION,
routes::API_VERSION,
routes::ECASH_ROUTES,
routes::GLOBAL_COIN_INDICES_SIGNATURES,
],
@@ -1166,7 +989,7 @@ pub trait NymApiClientExt: ApiClient {
};
self.get_json(
&[
routes::V1_API_VERSION,
routes::API_VERSION,
routes::ECASH_ROUTES,
ecash::MASTER_VERIFICATION_KEY,
],
@@ -1182,7 +1005,7 @@ pub trait NymApiClientExt: ApiClient {
) -> Result<(), NymAPIError> {
self.post_json(
&[
routes::V1_API_VERSION,
routes::API_VERSION,
routes::NYM_NODES_ROUTES,
routes::NYM_NODES_REFRESH_DESCRIBED,
],
@@ -1199,7 +1022,7 @@ pub trait NymApiClientExt: ApiClient {
) -> Result<IssuedTicketbooksForResponse, NymAPIError> {
self.get_json(
&[
routes::V1_API_VERSION,
routes::API_VERSION,
routes::ECASH_ROUTES,
routes::ECASH_ISSUED_TICKETBOOKS_FOR,
&expiration_date.to_string(),
@@ -1216,7 +1039,7 @@ pub trait NymApiClientExt: ApiClient {
) -> Result<IssuedTicketbooksForCountResponse, NymAPIError> {
self.get_json(
&[
routes::V1_API_VERSION,
routes::API_VERSION,
routes::ECASH_ROUTES,
routes::ECASH_ISSUED_TICKETBOOKS_FOR_COUNT,
&expiration_date.to_string(),
@@ -1233,7 +1056,7 @@ pub trait NymApiClientExt: ApiClient {
) -> Result<IssuedTicketbooksChallengeCommitmentResponse, NymAPIError> {
self.post_json(
&[
routes::V1_API_VERSION,
routes::API_VERSION,
routes::ECASH_ROUTES,
routes::ECASH_ISSUED_TICKETBOOKS_CHALLENGE_COMMITMENT,
],
@@ -1250,7 +1073,7 @@ pub trait NymApiClientExt: ApiClient {
) -> Result<IssuedTicketbooksDataResponse, NymAPIError> {
self.post_json(
&[
routes::V1_API_VERSION,
routes::API_VERSION,
routes::ECASH_ROUTES,
routes::ECASH_ISSUED_TICKETBOOKS_DATA,
],
@@ -1266,7 +1089,7 @@ pub trait NymApiClientExt: ApiClient {
) -> Result<NodesByAddressesResponse, NymAPIError> {
self.post_json(
&[
routes::V1_API_VERSION,
routes::API_VERSION,
"unstable",
routes::NYM_NODES_ROUTES,
routes::nym_nodes::BY_ADDRESSES,
@@ -1280,7 +1103,7 @@ pub trait NymApiClientExt: ApiClient {
#[instrument(level = "debug", skip(self))]
async fn get_network_details(&self) -> Result<NymNetworkDetailsResponse, NymAPIError> {
self.get_json(
&[routes::V1_API_VERSION, routes::NETWORK, routes::DETAILS],
&[routes::API_VERSION, routes::NETWORK, routes::DETAILS],
NO_PARAMS,
)
.await
@@ -1289,24 +1112,7 @@ pub trait NymApiClientExt: ApiClient {
#[instrument(level = "debug", skip(self))]
async fn get_chain_status(&self) -> Result<ChainStatusResponse, NymAPIError> {
self.get_json(
&[
routes::V1_API_VERSION,
routes::NETWORK,
routes::CHAIN_STATUS,
],
NO_PARAMS,
)
.await
}
#[instrument(level = "debug", skip(self))]
async fn get_key_rotation_info(&self) -> Result<KeyRotationInfoResponse, NymAPIError> {
self.get_json(
&[
routes::V1_API_VERSION,
routes::EPOCH,
routes::KEY_ROTATION_INFO,
],
&[routes::API_VERSION, routes::NETWORK, routes::CHAIN_STATUS],
NO_PARAMS,
)
.await
@@ -1,8 +1,9 @@
// Copyright 2021 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
pub const V1_API_VERSION: &str = "v1";
pub const V2_API_VERSION: &str = "v2";
use nym_network_defaults::NYM_API_VERSION;
pub const API_VERSION: &str = NYM_API_VERSION;
pub const MIXNODES: &str = "mixnodes";
pub const GATEWAYS: &str = "gateways";
pub const DESCRIBED: &str = "described";
@@ -78,11 +79,3 @@ pub const SERVICE_PROVIDERS: &str = "services";
pub const DETAILS: &str = "details";
pub const CHAIN_STATUS: &str = "chain-status";
pub const NETWORK: &str = "network";
pub const EPOCH: &str = "epoch";
pub use epoch_routes::*;
pub mod epoch_routes {
pub const CURRENT: &str = "current";
pub const KEY_ROTATION_INFO: &str = "key-rotation-info";
}
@@ -12,8 +12,8 @@ use nym_mixnet_contract_common::gateway::{PreassignedGatewayIdsResponse, Preassi
use nym_mixnet_contract_common::nym_node::{
EpochAssignmentResponse, NodeDetailsByIdentityResponse, NodeDetailsResponse,
NodeOwnershipResponse, NodeRewardingDetailsResponse, PagedNymNodeBondsResponse,
PagedNymNodeDetailsResponse, PagedUnbondedNymNodesResponse, RewardedSetMetadata, Role,
RolesMetadataResponse, StakeSaturationResponse, UnbondedNodeResponse, UnbondedNymNode,
PagedNymNodeDetailsResponse, PagedUnbondedNymNodesResponse, Role, RolesMetadataResponse,
StakeSaturationResponse, UnbondedNodeResponse, UnbondedNymNode,
};
use nym_mixnet_contract_common::reward_params::WorkFactor;
use nym_mixnet_contract_common::{
@@ -28,12 +28,12 @@ use nym_mixnet_contract_common::{
ContractBuildInformation, ContractState, ContractStateParams, CurrentIntervalResponse,
CurrentNymNodeVersionResponse, Delegation, EpochEventId, EpochRewardedSet, EpochStatus,
GatewayBond, GatewayBondResponse, GatewayOwnershipResponse, HistoricalNymNodeVersionEntry,
IdentityKey, IdentityKeyRef, IntervalEventId, KeyRotationIdResponse, KeyRotationState,
MixNodeBond, MixNodeDetails, MixOwnershipResponse, MixnodeDetailsByIdentityResponse,
MixnodeDetailsResponse, NodeId, NumberOfPendingEventsResponse, NymNodeBond, NymNodeDetails,
NymNodeVersionHistoryResponse, PagedAllDelegationsResponse, PagedDelegatorDelegationsResponse,
PagedGatewayResponse, PagedMixnodeBondsResponse, PagedNodeDelegationsResponse,
PendingEpochEvent, PendingEpochEventResponse, PendingEpochEventsResponse, PendingIntervalEvent,
IdentityKey, IdentityKeyRef, IntervalEventId, MixNodeBond, MixNodeDetails,
MixOwnershipResponse, MixnodeDetailsByIdentityResponse, MixnodeDetailsResponse, NodeId,
NumberOfPendingEventsResponse, NymNodeBond, NymNodeDetails, NymNodeVersionHistoryResponse,
PagedAllDelegationsResponse, PagedDelegatorDelegationsResponse, PagedGatewayResponse,
PagedMixnodeBondsResponse, PagedNodeDelegationsResponse, PendingEpochEvent,
PendingEpochEventResponse, PendingEpochEventsResponse, PendingIntervalEvent,
PendingIntervalEventResponse, PendingIntervalEventsResponse, QueryMsg as MixnetQueryMsg,
RewardedSet, UnbondedMixnode,
};
@@ -546,16 +546,6 @@ pub trait MixnetQueryClient {
})
.await
}
async fn get_key_rotation_state(&self) -> Result<KeyRotationState, NyxdError> {
self.query_mixnet_contract(MixnetQueryMsg::GetKeyRotationState {})
.await
}
async fn get_key_rotation_id(&self) -> Result<KeyRotationIdResponse, NyxdError> {
self.query_mixnet_contract(MixnetQueryMsg::GetKeyRotationId {})
.await
}
}
// extension trait to the query client to deal with the paged queries
@@ -683,20 +673,12 @@ pub trait MixnetQueryClientExt: MixnetQueryClient {
async fn get_rewarded_set(&self) -> Result<EpochRewardedSet, NyxdError> {
let error_response = |message| Err(NyxdError::extension_query_failure("mixnet", message));
// bypass for catch 22 for fresh contracts. we can't refresh cache because there's no rewarded set,
// but we can't set the rewarded set because we didn't refresh the cache
let metadata = self.get_rewarded_set_metadata().await?;
let is_default = metadata.metadata == RewardedSetMetadata::default();
if !metadata.metadata.fully_assigned && !is_default {
if !metadata.metadata.fully_assigned {
return error_response("the rewarded set hasn't been fully assigned for this epoch");
}
let expected_epoch_id = metadata.metadata.epoch_id;
if is_default {
return Ok(Default::default());
}
// if we have to query those things more frequently, we could do it concurrently,
// but as it stands now, it happens so infrequently it might as well be sequential
let entry = self.get_role_assignment(Role::EntryGateway).await?;
@@ -973,8 +955,6 @@ mod tests {
QueryMsg::GetNymNodeVersionHistory { limit, start_after } => client
.get_nym_node_version_history_paged(start_after, limit)
.ignore(),
QueryMsg::GetKeyRotationState {} => client.get_key_rotation_state().ignore(),
QueryMsg::GetKeyRotationId {} => client.get_key_rotation_id().ignore(),
}
}
}
@@ -138,14 +138,6 @@ impl NyxdClient<HttpClient> {
config,
})
}
pub fn connect_to_default_env<U>(endpoint: U) -> Result<QueryHttpRpcNyxdClient, NyxdError>
where
U: TryInto<HttpClientUrl, Error = TendermintRpcError>,
{
let config = Config::try_from_nym_network_details(&NymNetworkDetails::new_from_env())?;
Self::connect(config, endpoint)
}
}
impl NyxdClient<ReqwestRpcClient> {
@@ -157,7 +157,6 @@ pub async fn generate(args: Args) {
minimum: args.minimum_interval_operating_cost.amount.into(),
maximum: args.maximum_interval_operating_cost.amount.into(),
},
key_validity_in_epochs: None,
};
debug!("instantiate_msg: {:?}", instantiate_msg);
@@ -40,6 +40,3 @@ contract-testing = []
utoipa = ["dep:utoipa"]
schema = ["cw2"]
generate-ts = ['ts-rs']
[lints]
workspace = true
@@ -193,9 +193,6 @@ pub enum MixnetContractError {
#[error("attempted to perform the operation with 0 coins. This is not allowed")]
ZeroCoinAmount,
#[error("key rotation validity below minimum value")]
TooShortRotationInterval,
#[error("this validator ({current_validator}) is not the one responsible for advancing this epoch. It's responsibility of {chosen_validator}.")]
RewardingValidatorMismatch {
current_validator: Addr,
@@ -358,7 +358,7 @@ impl Interval {
self.total_elapsed_epochs
}
pub const fn current_epoch_absolute_id(&self) -> EpochId {
pub const fn current_epoch_absolute_id(&self) -> u32 {
// since we count epochs starting from 0, if n epochs have elapsed, the current one has absolute id of n
self.total_elapsed_epochs
}
@@ -1,155 +0,0 @@
// Copyright 2025 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use crate::EpochId;
use cosmwasm_schema::cw_serde;
pub type KeyRotationId = u32;
#[cw_serde]
#[derive(Copy)]
#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
pub struct KeyRotationState {
/// Defines how long each key rotation is valid for (in terms of epochs)
pub validity_epochs: u32,
/// Records the initial epoch_id when the key rotation has been introduced (0 for fresh contracts).
/// It is used for determining when rotation is meant to advance.
#[cfg_attr(feature = "utoipa", schema(value_type = u32))]
pub initial_epoch_id: EpochId,
}
impl KeyRotationState {
pub fn key_rotation_id(&self, current_epoch_id: EpochId) -> KeyRotationId {
let diff = current_epoch_id.saturating_sub(self.initial_epoch_id);
diff / self.validity_epochs
}
pub fn next_rotation_starting_epoch_id(&self, current_epoch_id: EpochId) -> EpochId {
self.current_rotation_starting_epoch_id(current_epoch_id) + self.validity_epochs
}
pub fn current_rotation_starting_epoch_id(&self, current_epoch_id: EpochId) -> EpochId {
let current_rotation_id = self.key_rotation_id(current_epoch_id);
self.initial_epoch_id + self.validity_epochs * current_rotation_id
}
}
#[cw_serde]
pub struct KeyRotationIdResponse {
pub rotation_id: KeyRotationId,
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn key_rotation_id() {
let state = KeyRotationState {
validity_epochs: 24,
initial_epoch_id: 0,
};
assert_eq!(0, state.key_rotation_id(0));
assert_eq!(0, state.key_rotation_id(23));
assert_eq!(1, state.key_rotation_id(24));
assert_eq!(1, state.key_rotation_id(47));
assert_eq!(2, state.key_rotation_id(48));
let state = KeyRotationState {
validity_epochs: 12,
initial_epoch_id: 0,
};
assert_eq!(0, state.key_rotation_id(0));
assert_eq!(0, state.key_rotation_id(11));
assert_eq!(1, state.key_rotation_id(12));
assert_eq!(1, state.key_rotation_id(23));
assert_eq!(2, state.key_rotation_id(24));
let state = KeyRotationState {
validity_epochs: 24,
initial_epoch_id: 10000,
};
assert_eq!(0, state.key_rotation_id(123));
assert_eq!(0, state.key_rotation_id(10000));
assert_eq!(0, state.key_rotation_id(10001));
assert_eq!(0, state.key_rotation_id(10023));
assert_eq!(1, state.key_rotation_id(10024));
assert_eq!(1, state.key_rotation_id(10047));
assert_eq!(2, state.key_rotation_id(10048));
assert_eq!(2, state.key_rotation_id(10060));
}
#[test]
fn next_rotation_starting_epoch_id() {
let state = KeyRotationState {
validity_epochs: 24,
initial_epoch_id: 0,
};
assert_eq!(24, state.next_rotation_starting_epoch_id(0));
assert_eq!(24, state.next_rotation_starting_epoch_id(23));
assert_eq!(48, state.next_rotation_starting_epoch_id(24));
assert_eq!(48, state.next_rotation_starting_epoch_id(47));
assert_eq!(72, state.next_rotation_starting_epoch_id(48));
let state = KeyRotationState {
validity_epochs: 12,
initial_epoch_id: 0,
};
assert_eq!(12, state.next_rotation_starting_epoch_id(0));
assert_eq!(12, state.next_rotation_starting_epoch_id(11));
assert_eq!(24, state.next_rotation_starting_epoch_id(12));
assert_eq!(24, state.next_rotation_starting_epoch_id(23));
assert_eq!(36, state.next_rotation_starting_epoch_id(24));
let state = KeyRotationState {
validity_epochs: 24,
initial_epoch_id: 10000,
};
assert_eq!(10024, state.next_rotation_starting_epoch_id(123));
assert_eq!(10024, state.next_rotation_starting_epoch_id(10000));
assert_eq!(10024, state.next_rotation_starting_epoch_id(10001));
assert_eq!(10024, state.next_rotation_starting_epoch_id(10023));
assert_eq!(10048, state.next_rotation_starting_epoch_id(10024));
assert_eq!(10048, state.next_rotation_starting_epoch_id(10047));
assert_eq!(10072, state.next_rotation_starting_epoch_id(10048));
assert_eq!(10072, state.next_rotation_starting_epoch_id(10060));
}
#[test]
fn current_rotation_starting_epoch_id() {
let state = KeyRotationState {
validity_epochs: 24,
initial_epoch_id: 0,
};
assert_eq!(0, state.current_rotation_starting_epoch_id(0));
assert_eq!(0, state.current_rotation_starting_epoch_id(23));
assert_eq!(24, state.current_rotation_starting_epoch_id(24));
assert_eq!(24, state.current_rotation_starting_epoch_id(47));
assert_eq!(48, state.current_rotation_starting_epoch_id(48));
let state = KeyRotationState {
validity_epochs: 12,
initial_epoch_id: 0,
};
assert_eq!(0, state.current_rotation_starting_epoch_id(0));
assert_eq!(0, state.current_rotation_starting_epoch_id(11));
assert_eq!(12, state.current_rotation_starting_epoch_id(12));
assert_eq!(12, state.current_rotation_starting_epoch_id(23));
assert_eq!(24, state.current_rotation_starting_epoch_id(24));
let state = KeyRotationState {
validity_epochs: 24,
initial_epoch_id: 10000,
};
assert_eq!(10000, state.current_rotation_starting_epoch_id(123));
assert_eq!(10000, state.current_rotation_starting_epoch_id(10000));
assert_eq!(10000, state.current_rotation_starting_epoch_id(10001));
assert_eq!(10000, state.current_rotation_starting_epoch_id(10023));
assert_eq!(10024, state.current_rotation_starting_epoch_id(10024));
assert_eq!(10024, state.current_rotation_starting_epoch_id(10047));
assert_eq!(10048, state.current_rotation_starting_epoch_id(10048));
assert_eq!(10048, state.current_rotation_starting_epoch_id(10060));
}
}
@@ -1,6 +1,10 @@
// Copyright 2021-2023 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
#![warn(clippy::expect_used)]
#![warn(clippy::unwrap_used)]
#![warn(clippy::todo)]
mod config_score;
pub mod constants;
pub mod delegation;
@@ -9,7 +13,6 @@ pub mod events;
pub mod gateway;
pub mod helpers;
pub mod interval;
pub mod key_rotation;
pub mod mixnode;
pub mod msg;
pub mod nym_node;
@@ -34,7 +37,6 @@ pub use gateway::{
pub use interval::{
CurrentIntervalResponse, EpochId, EpochState, EpochStatus, Interval, IntervalId,
};
pub use key_rotation::*;
pub use mixnode::{
LegacyMixLayer, MixNode, MixNodeBond, MixNodeConfigUpdate, MixNodeDetails,
MixOwnershipResponse, MixnodeDetailsByIdentityResponse, MixnodeDetailsResponse, NodeCostParams,
@@ -170,11 +170,6 @@ impl NodeRewarding {
}
}
// we panic here as opposed to returning an error as this is undefined behaviour,
// because the pledge amount has decreased (i.e. slashing has occurred) which
// should not be possible under any situation. at this point we don't know how many other things
// might have failed so we have to bail
#[allow(clippy::panic)]
pub fn pending_detailed_operator_reward(&self, original_pledge: &Coin) -> StdResult<Decimal> {
let initial_dec = original_pledge.amount.into_base_decimal()?;
if initial_dec > self.operator {
@@ -194,11 +189,6 @@ impl NodeRewarding {
Ok(truncate_reward(delegator_reward, &delegation.amount.denom))
}
// we panic here as opposed to returning an error as this is undefined behaviour,
// because the pledge amount has decreased (i.e. slashing has occurred) which
// should not be possible under any situation. at this point we don't know how many other things
// might have failed so we have to bail
#[allow(clippy::panic)]
pub fn withdraw_operator_reward(
&mut self,
original_pledge: &Coin,
@@ -35,7 +35,6 @@ use crate::{
PreassignedGatewayIdsResponse,
},
interval::{CurrentIntervalResponse, EpochStatus},
key_rotation::{KeyRotationIdResponse, KeyRotationState},
mixnode::{
MixOwnershipResponse, MixStakeSaturationResponse, MixnodeDetailsByIdentityResponse,
MixnodeDetailsResponse, MixnodeRewardingDetailsResponse, PagedMixnodeBondsResponse,
@@ -82,18 +81,6 @@ pub struct InstantiateMsg {
#[serde(default)]
pub interval_operating_cost: OperatingCostRange,
#[serde(default)]
pub key_validity_in_epochs: Option<u32>,
}
impl InstantiateMsg {
// needs to give us enough time to pre-announce key for following epoch
// and have an overlap with the preceding epoch
pub const MIN_KEY_ROTATION_VALIDITY: u32 = 3;
pub fn key_validity_in_epochs(&self) -> u32 {
self.key_validity_in_epochs.unwrap_or(24)
}
}
#[cw_serde]
@@ -870,15 +857,6 @@ pub enum QueryMsg {
/// Cosmos address used for the query of the signing nonce.
address: String,
},
// sphinx key rotation-related
#[cfg_attr(feature = "schema", returns(KeyRotationState))]
/// Gets the current state config of the key rotation (i.e. starting epoch id and validity duration)
GetKeyRotationState {},
/// Gets the current key rotation id
#[cfg_attr(feature = "schema", returns(KeyRotationIdResponse))]
GetKeyRotationId {},
}
#[cw_serde]
@@ -151,9 +151,6 @@ impl Simulator {
}
}
// this code is not meant to be used in production systems, only in tests
// so a panic due to inconsistent arguments is fine
#[allow(clippy::panic)]
pub fn simulate_epoch(
&mut self,
node_params: &BTreeMap<NodeId, NodeRewardingParameters>,
@@ -1,27 +0,0 @@
[package]
name = "nym-pool-contract-common"
version = "0.1.0"
description = "Common library for the Nym Pool contract"
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]
thiserror = { workspace = true }
serde = { workspace = true }
schemars = { workspace = true }
cosmwasm-std = { workspace = true }
cosmwasm-schema = { workspace = true }
cw-controllers = { workspace = true }
[dev-dependencies]
time = { workspace = true, features = ["macros"] }
[features]
schema = []
@@ -1,11 +0,0 @@
// Copyright 2025 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
pub mod storage_keys {
pub const CONTRACT_ADMIN: &str = "contract-admin";
pub const POOL_DENOMINATION: &str = "pool_denom";
pub const GRANTERS: &str = "granters";
pub const GRANTS: &str = "grants";
pub const TOTAL_LOCKED: &str = "total_locked";
pub const LOCKED_GRANTEES: &str = "locked_grantees";
}
@@ -1,98 +0,0 @@
// Copyright 2025 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use cosmwasm_std::{Coin, Uint128};
use cw_controllers::AdminError;
use thiserror::Error;
#[derive(Error, Debug, PartialEq)]
pub enum NymPoolContractError {
#[error("could not perform contract migration: {comment}")]
FailedMigration { comment: String },
#[error(transparent)]
Admin(#[from] AdminError),
#[error(transparent)]
StdErr(#[from] cosmwasm_std::StdError),
#[error("this sender is not authorised to revoke this grant. its neither the admin or the original (and still whitelisted) granter")]
UnauthorizedGrantRevocation,
#[error("the specified address is already a whitelisted granter")]
AlreadyAGranter,
#[error("{addr} is not a permitted granter")]
InvalidGranter { addr: String },
#[error("invalid coin denomination. got {got}, but expected {expected}")]
InvalidDenom { expected: String, got: String },
#[error("there already exists an active grant for {grantee}. it was granted by {granter} at block height {created_at_height}")]
GrantAlreadyExist {
granter: String,
grantee: String,
created_at_height: u64,
},
#[error("could not find any active grants for {grantee}")]
GrantNotFound { grantee: String },
#[error("the provided timestamp value ({timestamp}) is set in the past. the current block timestamp is {current_block_timestamp}")]
TimestampInThePast {
timestamp: u64,
current_block_timestamp: u64,
},
#[error("there are not enough tokens to process this request. {available} are available, but {required} is needed.")]
InsufficientTokens { available: Coin, required: Coin },
#[error("the period length can't be zero")]
ZeroAllowancePeriod,
#[error("the provided coin value is zero")]
ZeroAmount,
#[error("the periodic spend limit of {periodic} was set to be higher than the total spend limit {total_limit}")]
PeriodicGrantOverSpendLimit { periodic: Coin, total_limit: Coin },
#[error("the accumulation spend limit of {accumulation} was set to be lower than the periodic grant amount of {periodic_grant}")]
AccumulationBelowGrantAmount {
accumulation: Coin,
periodic_grant: Coin,
},
#[error("the accumulation spend limit of {accumulation} was set to be higher than the total spend limit of {total_limit}")]
AccumulationOverSpendLimit {
accumulation: Coin,
total_limit: Coin,
},
#[error("the specified delayed allowance would never be available. it would become active at {available_timestamp} yet it expires at {expiration_timestamp}")]
UnattainableDelayedAllowance {
expiration_timestamp: u64,
available_timestamp: u64,
},
#[error("could not unlock {requested} tokens from {grantee}. it only has {locked} locked")]
InsufficientLockedTokens {
grantee: String,
locked: Uint128,
requested: Uint128,
},
#[error("attempted to spend more tokens than permitted by the current allowance")]
SpendingAboveAllowance,
#[error("attempted to send an empty allowance usage request")]
EmptyUsageRequest,
#[error("the associated grant has already expired")]
GrantExpired,
#[error("the associated grant hasn't expired yet")]
GrantNotExpired,
#[error("this grant is not available yet. it will become usable at {available_at_timestamp}")]
GrantNotYetAvailable { available_at_timestamp: u64 },
}
@@ -1,12 +0,0 @@
// Copyright 2025 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
pub mod constants;
pub mod error;
pub mod msg;
pub mod types;
mod utils;
pub use error::*;
pub use msg::{ExecuteMsg, InstantiateMsg, MigrateMsg, QueryMsg};
pub use types::*;
@@ -1,125 +0,0 @@
// Copyright 2025 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use crate::{Allowance, TransferRecipient};
use cosmwasm_schema::cw_serde;
use cosmwasm_std::Coin;
use std::collections::HashMap;
#[cfg(feature = "schema")]
use crate::types::{
AvailableTokensResponse, GrantResponse, GranterResponse, GrantersPagedResponse,
GrantsPagedResponse, LockedTokensPagedResponse, LockedTokensResponse,
TotalLockedTokensResponse,
};
#[cw_serde]
pub struct InstantiateMsg {
pub pool_denomination: String,
/// Initial map of grants to be created at instantiation
pub grants: HashMap<String, Allowance>,
}
#[cw_serde]
pub enum ExecuteMsg {
/// Change the admin
UpdateAdmin {
admin: String,
// flag to determine whether old admin should be removed from the granter set
// and new one should be included instead
// the reason it's provided as an option is to make it possible to skip this field
// when creating transaction directly with nyxd
update_granter_set: Option<bool>,
},
/// Attempt to grant new allowance to the specified grantee
GrantAllowance {
grantee: String,
allowance: Box<Allowance>,
},
/// Attempt to revoke previously granted allowance
RevokeAllowance { grantee: String },
/// Attempt to use allowance
UseAllowance { recipients: Vec<TransferRecipient> },
/// Attempt to withdraw the specified amount into the grantee's account
WithdrawAllowance { amount: Coin },
/// Attempt to lock part of existing allowance for future use
LockAllowance { amount: Coin },
/// Attempt to unlock previously locked allowance
UnlockAllowance { amount: Coin },
/// Attempt to use part of the locked allowance
UseLockedAllowance { recipients: Vec<TransferRecipient> },
/// Attempt to withdraw the specified amount of locked tokens into the grantee's account
WithdrawLockedAllowance { amount: Coin },
/// Attempt to add a new account to the permitted set of grant granters
AddNewGranter { granter: String },
/// Revoke the provided account from the permitted set of granters
RevokeGranter { granter: String },
/// Attempt to remove expired grant from the storage and unlock (if any) locked tokens
RemoveExpiredGrant { grantee: String },
}
#[cw_serde]
#[cfg_attr(feature = "schema", derive(cosmwasm_schema::QueryResponses))]
pub enum QueryMsg {
#[cfg_attr(feature = "schema", returns(cw_controllers::AdminResponse))]
Admin {},
#[cfg_attr(feature = "schema", returns(AvailableTokensResponse))]
GetAvailableTokens {},
#[cfg_attr(feature = "schema", returns(TotalLockedTokensResponse))]
GetTotalLockedTokens {},
#[cfg_attr(feature = "schema", returns(LockedTokensResponse))]
GetLockedTokens { grantee: String },
#[cfg_attr(feature = "schema", returns(GrantResponse))]
GetGrant { grantee: String },
#[cfg_attr(feature = "schema", returns(GranterResponse))]
GetGranter { granter: String },
#[cfg_attr(feature = "schema", returns(LockedTokensPagedResponse))]
GetLockedTokensPaged {
/// Controls the maximum number of entries returned by the query. Note that too large values will be overwritten by a saner default.
limit: Option<u32>,
/// Pagination control for the values returned by the query. Note that the provided value itself will **not** be used for the response.
start_after: Option<String>,
},
#[cfg_attr(feature = "schema", returns(GrantersPagedResponse))]
GetGrantersPaged {
/// Controls the maximum number of entries returned by the query. Note that too large values will be overwritten by a saner default.
limit: Option<u32>,
/// Pagination control for the values returned by the query. Note that the provided value itself will **not** be used for the response.
start_after: Option<String>,
},
#[cfg_attr(feature = "schema", returns(GrantsPagedResponse))]
GetGrantsPaged {
/// Controls the maximum number of entries returned by the query. Note that too large values will be overwritten by a saner default.
limit: Option<u32>,
/// Pagination control for the values returned by the query. Note that the provided value itself will **not** be used for the response.
start_after: Option<String>,
},
}
#[cw_serde]
pub struct MigrateMsg {
//
}
File diff suppressed because it is too large Load Diff
@@ -1,77 +0,0 @@
// Copyright 2025 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use crate::NymPoolContractError;
use cosmwasm_std::Env;
pub fn ensure_unix_timestamp_not_in_the_past(
unix_timestamp: u64,
env: &Env,
) -> Result<(), NymPoolContractError> {
if unix_timestamp < env.block.time.seconds() {
return Err(NymPoolContractError::TimestampInThePast {
timestamp: unix_timestamp,
current_block_timestamp: env.block.time.seconds(),
});
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
use cosmwasm_std::testing::mock_env;
use cosmwasm_std::Timestamp;
use time::macros::datetime;
#[test]
fn ensuring_unix_timestamp_not_in_the_past() {
let unix_epoch = 0;
let date_in_the_past = datetime!(1984-01-02 3:45 UTC);
let sane_block_time = datetime!(2025-01-28 12:15 UTC);
let before_block = datetime!(2025-01-28 12:00 UTC);
let after_block = datetime!(2025-01-28 12:30 UTC);
let mut env = mock_env();
env.block.time = Timestamp::from_seconds(sane_block_time.unix_timestamp() as u64);
let res = ensure_unix_timestamp_not_in_the_past(unix_epoch, &env).unwrap_err();
assert_eq!(
NymPoolContractError::TimestampInThePast {
timestamp: unix_epoch,
current_block_timestamp: env.block.time.seconds(),
},
res
);
let res =
ensure_unix_timestamp_not_in_the_past(date_in_the_past.unix_timestamp() as u64, &env)
.unwrap_err();
assert_eq!(
NymPoolContractError::TimestampInThePast {
timestamp: date_in_the_past.unix_timestamp() as u64,
current_block_timestamp: env.block.time.seconds(),
},
res
);
let res = ensure_unix_timestamp_not_in_the_past(before_block.unix_timestamp() as u64, &env)
.unwrap_err();
assert_eq!(
NymPoolContractError::TimestampInThePast {
timestamp: before_block.unix_timestamp() as u64,
current_block_timestamp: env.block.time.seconds(),
},
res
);
let res =
ensure_unix_timestamp_not_in_the_past(sane_block_time.unix_timestamp() as u64, &env);
assert!(res.is_ok());
let res = ensure_unix_timestamp_not_in_the_past(after_block.unix_timestamp() as u64, &env);
assert!(res.is_ok());
}
}
+9 -2
View File
@@ -20,6 +20,8 @@ nym-credentials = { path = "../credentials" }
nym-compact-ecash = { path = "../nym_offline_compact_ecash" }
nym-ecash-time = { path = "../ecash-time" }
[target."cfg(not(target_arch = \"wasm32\"))".dependencies.sqlx-pool-guard]
path = "../../sqlx-pool-guard"
[target."cfg(not(target_arch = \"wasm32\"))".dependencies.sqlx]
workspace = true
@@ -31,8 +33,13 @@ features = ["rt-multi-thread", "net", "signal", "fs"]
[build-dependencies]
sqlx = { workspace = true, features = ["runtime-tokio-rustls", "sqlite", "macros", "migrate"] }
sqlx = { workspace = true, features = [
"runtime-tokio-rustls",
"sqlite",
"macros",
"migrate",
] }
tokio = { workspace = true, features = ["rt-multi-thread", "macros"] }
[features]
persistent-storage = ["bincode", "serde"]
persistent-storage = ["bincode", "serde"]
@@ -7,10 +7,11 @@ use crate::models::{
};
use nym_ecash_time::Date;
use sqlx::{Executor, Sqlite, Transaction};
use sqlx_pool_guard::SqlitePoolGuard;
#[derive(Clone)]
pub struct SqliteEcashTicketbookManager {
connection_pool: sqlx::SqlitePool,
connection_pool: SqlitePoolGuard,
}
impl SqliteEcashTicketbookManager {
@@ -19,7 +20,7 @@ impl SqliteEcashTicketbookManager {
/// # Arguments
///
/// * `connection_pool`: database connection pool to use.
pub fn new(connection_pool: sqlx::SqlitePool) -> Self {
pub fn new(connection_pool: SqlitePoolGuard) -> Self {
SqliteEcashTicketbookManager { connection_pool }
}
@@ -33,7 +34,7 @@ impl SqliteEcashTicketbookManager {
"DELETE FROM ecash_ticketbook WHERE expiration_date <= ?",
deadline
)
.execute(&self.connection_pool)
.execute(&*self.connection_pool)
.await?;
Ok(())
}
@@ -60,7 +61,7 @@ impl SqliteEcashTicketbookManager {
data,
expiration_date,
)
.execute(&self.connection_pool)
.execute(&*self.connection_pool)
.await?;
Ok(())
@@ -90,7 +91,7 @@ impl SqliteEcashTicketbookManager {
epoch_id,
total_tickets,
used_tickets,
).execute(&self.connection_pool).await?;
).execute(&*self.connection_pool).await?;
Ok(())
}
@@ -105,7 +106,7 @@ impl SqliteEcashTicketbookManager {
"#,
)
.bind(data)
.fetch_optional(&self.connection_pool)
.fetch_optional(&*self.connection_pool)
.await?
.is_some();
@@ -121,7 +122,7 @@ impl SqliteEcashTicketbookManager {
FROM ecash_ticketbook
"#,
)
.fetch_all(&self.connection_pool)
.fetch_all(&*self.connection_pool)
.await
}
@@ -143,7 +144,7 @@ impl SqliteEcashTicketbookManager {
ticketbook_id,
expected_current_total_spent
)
.execute(&self.connection_pool)
.execute(&*self.connection_pool)
.await?
.rows_affected();
Ok(affected > 0)
@@ -153,7 +154,7 @@ impl SqliteEcashTicketbookManager {
&self,
) -> Result<Vec<StoredPendingTicketbook>, sqlx::Error> {
sqlx::query_as("SELECT * FROM pending_issuance")
.fetch_all(&self.connection_pool)
.fetch_all(&*self.connection_pool)
.await
}
@@ -165,7 +166,7 @@ impl SqliteEcashTicketbookManager {
"DELETE FROM pending_issuance WHERE deposit_id = ?",
pending_id
)
.execute(&self.connection_pool)
.execute(&*self.connection_pool)
.await?;
Ok(())
}
@@ -182,7 +183,7 @@ impl SqliteEcashTicketbookManager {
"#,
epoch_id
)
.fetch_optional(&self.connection_pool)
.fetch_optional(&*self.connection_pool)
.await
}
@@ -208,7 +209,7 @@ impl SqliteEcashTicketbookManager {
serialisation_revision,
epoch_id
)
.execute(&self.connection_pool)
.execute(&*self.connection_pool)
.await?;
Ok(())
}
@@ -225,7 +226,7 @@ impl SqliteEcashTicketbookManager {
"#,
epoch_id
)
.fetch_optional(&self.connection_pool)
.fetch_optional(&*self.connection_pool)
.await
}
@@ -251,7 +252,7 @@ impl SqliteEcashTicketbookManager {
serialisation_revision,
epoch_id,
)
.execute(&self.connection_pool)
.execute(&*self.connection_pool)
.await?;
Ok(())
}
@@ -269,7 +270,7 @@ impl SqliteEcashTicketbookManager {
"#,
expiration_date
)
.fetch_optional(&self.connection_pool)
.fetch_optional(&*self.connection_pool)
.await
}
@@ -298,7 +299,7 @@ impl SqliteEcashTicketbookManager {
serialisation_revision,
expiration_date
)
.execute(&self.connection_pool)
.execute(&*self.connection_pool)
.await?;
Ok(())
}
@@ -37,6 +37,7 @@ use sqlx::{
sqlite::{SqliteAutoVacuum, SqliteSynchronous},
ConnectOptions,
};
use sqlx_pool_guard::SqlitePoolGuard;
use std::path::Path;
use zeroize::Zeroizing;
@@ -62,7 +63,7 @@ impl PersistentStorage {
.journal_mode(sqlx::sqlite::SqliteJournalMode::Wal)
.synchronous(SqliteSynchronous::Normal)
.auto_vacuum(SqliteAutoVacuum::Incremental)
.filename(database_path)
.filename(&database_path)
.create_if_missing(true)
.disable_statement_logging();
@@ -74,13 +75,17 @@ impl PersistentStorage {
}
};
if let Err(err) = sqlx::migrate!("./migrations").run(&connection_pool).await {
let connection_pool =
SqlitePoolGuard::new(database_path.as_ref().to_path_buf(), connection_pool);
if let Err(err) = sqlx::migrate!("./migrations").run(&*connection_pool).await {
error!("Failed to perform migration on the SQLx database: {err}");
connection_pool.close().await;
return Err(err.into());
}
Ok(PersistentStorage {
storage_manager: SqliteEcashTicketbookManager::new(connection_pool.clone()),
storage_manager: SqliteEcashTicketbookManager::new(connection_pool),
})
}
}
+1 -9
View File
@@ -19,7 +19,7 @@ pub use shared_key::{
SharedGatewayKey, SharedKeyConversionError, SharedKeyUsageError, SharedSymmetricKey,
};
pub const CURRENT_PROTOCOL_VERSION: u8 = EMBEDDED_KEY_ROTATION_INFO_VERSION;
pub const CURRENT_PROTOCOL_VERSION: u8 = AUTHENTICATE_V2_PROTOCOL_VERSION;
/// Defines the current version of the communication protocol between gateway and clients.
/// It has to be incremented for any breaking change.
@@ -28,12 +28,10 @@ pub const CURRENT_PROTOCOL_VERSION: u8 = EMBEDDED_KEY_ROTATION_INFO_VERSION;
// 2 - changes to client credentials structure
// 3 - change to AES-GCM-SIV and non-zero IVs
// 4 - introduction of v2 authentication protocol to prevent reply attacks
// 5 - add key rotation information to the serialised mix packet
pub const INITIAL_PROTOCOL_VERSION: u8 = 1;
pub const CREDENTIAL_UPDATE_V2_PROTOCOL_VERSION: u8 = 2;
pub const AES_GCM_SIV_PROTOCOL_VERSION: u8 = 3;
pub const AUTHENTICATE_V2_PROTOCOL_VERSION: u8 = 4;
pub const EMBEDDED_KEY_ROTATION_INFO_VERSION: u8 = 5;
// TODO: could using `Mac` trait here for OutputSize backfire?
// Should hmac itself be exposed, imported and used instead?
@@ -42,7 +40,6 @@ pub type LegacyGatewayMacSize = <GatewayIntegrityHmacAlgorithm as OutputSizeUser
pub trait GatewayProtocolVersionExt {
fn supports_aes256_gcm_siv(&self) -> bool;
fn supports_authenticate_v2(&self) -> bool;
fn supports_key_rotation_packet(&self) -> bool;
}
impl GatewayProtocolVersionExt for Option<u8> {
@@ -55,9 +52,4 @@ impl GatewayProtocolVersionExt for Option<u8> {
let Some(protocol) = *self else { return false };
protocol >= AUTHENTICATE_V2_PROTOCOL_VERSION
}
fn supports_key_rotation_packet(&self) -> bool {
let Some(protocol) = *self else { return false };
protocol >= EMBEDDED_KEY_ROTATION_INFO_VERSION
}
}
@@ -11,9 +11,6 @@ use tungstenite::Message;
#[non_exhaustive]
pub enum BinaryRequest {
ForwardSphinx { packet: MixPacket },
// identical to `ForwardSphinx`, but also contains information about sphinx key rotation used
ForwardSphinxV2 { packet: MixPacket },
}
#[repr(u8)]
@@ -21,9 +18,6 @@ pub enum BinaryRequest {
#[non_exhaustive]
pub enum BinaryRequestKind {
ForwardSphinx = 1,
// identical to `ForwardSphinx`, but also contains information about sphinx key rotation used
ForwardSphinxV2 = 2,
}
// Right now the only valid `BinaryRequest` is a request to forward a sphinx packet.
@@ -35,7 +29,6 @@ impl BinaryRequest {
pub fn kind(&self) -> BinaryRequestKind {
match self {
BinaryRequest::ForwardSphinx { .. } => BinaryRequestKind::ForwardSphinx,
BinaryRequest::ForwardSphinxV2 { .. } => BinaryRequestKind::ForwardSphinxV2,
}
}
@@ -45,13 +38,9 @@ impl BinaryRequest {
) -> Result<Self, GatewayRequestsError> {
match kind {
BinaryRequestKind::ForwardSphinx => {
let packet = MixPacket::try_from_v1_bytes(plaintext)?;
let packet = MixPacket::try_from_bytes(plaintext)?;
Ok(BinaryRequest::ForwardSphinx { packet })
}
BinaryRequestKind::ForwardSphinxV2 => {
let packet = MixPacket::try_from_v2_bytes(plaintext)?;
Ok(BinaryRequest::ForwardSphinxV2 { packet })
}
}
}
@@ -69,8 +58,7 @@ impl BinaryRequest {
let kind = self.kind();
let plaintext = match self {
BinaryRequest::ForwardSphinx { packet } => packet.into_v1_bytes()?,
BinaryRequest::ForwardSphinxV2 { packet } => packet.into_v2_bytes()?,
BinaryRequest::ForwardSphinx { packet } => packet.into_bytes()?,
};
BinaryData::make_encrypted_blob(kind as u8, &plaintext, shared_key)
@@ -82,9 +70,7 @@ impl BinaryRequest {
) -> Result<Message, GatewayRequestsError> {
// all variants are currently encrypted
let blob = match self {
BinaryRequest::ForwardSphinx { .. } | BinaryRequest::ForwardSphinxV2 { .. } => {
self.into_encrypted_tagged_bytes(shared_key)?
}
BinaryRequest::ForwardSphinx { .. } => self.into_encrypted_tagged_bytes(shared_key)?,
};
Ok(Message::Binary(blob))
+1 -1
View File
@@ -193,7 +193,7 @@ impl PersistentStatsStorage {
pub async fn get_started_sessions_count(
&self,
start_date: Date,
) -> Result<i64, StatsStorageError> {
) -> Result<i32, StatsStorageError> {
Ok(self
.session_manager
.get_started_sessions_count(start_date)
+1 -1
View File
@@ -160,7 +160,7 @@ impl SessionManager {
.await
}
pub(crate) async fn get_started_sessions_count(&self, start_date: Date) -> Result<i64> {
pub(crate) async fn get_started_sessions_count(&self, start_date: Date) -> Result<i32> {
Ok(sqlx::query!(
"SELECT COUNT(*) as count FROM sessions_active WHERE date(start_time) = ?",
start_date
@@ -0,0 +1,32 @@
{
"db_name": "SQLite",
"query": "\n SELECT \n id as \"id!\",\n client_address_bs58 as \"client_address_bs58!\",\n content as \"content!\" \n FROM message_store \n WHERE client_address_bs58 = ? AND id > ?\n ORDER BY id ASC\n LIMIT ?;\n ",
"describe": {
"columns": [
{
"name": "id!",
"ordinal": 0,
"type_info": "Int64"
},
{
"name": "client_address_bs58!",
"ordinal": 1,
"type_info": "Text"
},
{
"name": "content!",
"ordinal": 2,
"type_info": "Blob"
}
],
"parameters": {
"Right": 3
},
"nullable": [
false,
false,
false
]
},
"hash": "03fe56298a6d60cdd5304a2953811a533d59b4f1f0e4efecd32c09256b657e24"
}
@@ -1,12 +0,0 @@
{
"db_name": "SQLite",
"query": "DELETE FROM message_store WHERE timestamp < ?",
"describe": {
"columns": [],
"parameters": {
"Right": 1
},
"nullable": []
},
"hash": "071fbde5277c0806ed47fa15a9c6288609379049828a94008f854b3daeed21d1"
}
@@ -16,7 +16,7 @@
{
"name": "protocol_version",
"ordinal": 2,
"type_info": "Integer"
"type_info": "Int64"
},
{
"name": "endpoint",
@@ -31,17 +31,17 @@
{
"name": "tx_bytes",
"ordinal": 5,
"type_info": "Integer"
"type_info": "Int64"
},
{
"name": "rx_bytes",
"ordinal": 6,
"type_info": "Integer"
"type_info": "Int64"
},
{
"name": "persistent_keepalive_interval",
"ordinal": 7,
"type_info": "Integer"
"type_info": "Int64"
},
{
"name": "allowed_ips",
@@ -51,7 +51,7 @@
{
"name": "client_id",
"ordinal": 9,
"type_info": "Integer"
"type_info": "Int64"
}
],
"parameters": {
@@ -6,7 +6,7 @@
{
"name": "signer_id",
"ordinal": 0,
"type_info": "Integer"
"type_info": "Int64"
}
],
"parameters": {
@@ -1,12 +0,0 @@
{
"db_name": "SQLite",
"query": "UPDATE shared_keys SET last_used_authentication = ? WHERE client_id = ?;",
"describe": {
"columns": [],
"parameters": {
"Right": 2
},
"nullable": []
},
"hash": "451158af8f5602e30445986a8de22d2e065c4e9dce4e7463a875ca8c21ac97c1"
}
@@ -0,0 +1,38 @@
{
"db_name": "SQLite",
"query": "SELECT * FROM shared_keys WHERE client_address_bs58 = ?",
"describe": {
"columns": [
{
"name": "client_id",
"ordinal": 0,
"type_info": "Int64"
},
{
"name": "client_address_bs58",
"ordinal": 1,
"type_info": "Text"
},
{
"name": "derived_aes128_ctr_blake3_hmac_keys_bs58",
"ordinal": 2,
"type_info": "Text"
},
{
"name": "derived_aes256_gcm_siv_key",
"ordinal": 3,
"type_info": "Blob"
}
],
"parameters": {
"Right": 1
},
"nullable": [
false,
false,
true,
true
]
},
"hash": "564c7da81081fab34754b76eeeedd48f3bc18842c03ef5a5c331bbee4c41c71c"
}
@@ -6,7 +6,7 @@
{
"name": "client_id",
"ordinal": 0,
"type_info": "Integer"
"type_info": "Int64"
}
],
"parameters": {
@@ -6,7 +6,7 @@
{
"name": "id",
"ordinal": 0,
"type_info": "Integer"
"type_info": "Int64"
},
{
"name": "client_type: ClientType",
@@ -6,7 +6,7 @@
{
"name": "ticket_id",
"ordinal": 0,
"type_info": "Integer"
"type_info": "Int64"
},
{
"name": "data!",
@@ -6,14 +6,14 @@
{
"name": "exists",
"ordinal": 0,
"type_info": "Integer"
"type_info": "Int"
}
],
"parameters": {
"Right": 1
},
"nullable": [
false
null
]
},
"hash": "7f8af0799d7ae5f751b9964e9566589bf768e7079079f584beb0c1ba16d43a5c"
@@ -6,7 +6,7 @@
{
"name": "signer_id",
"ordinal": 0,
"type_info": "Integer"
"type_info": "Int64"
}
],
"parameters": {
@@ -6,7 +6,7 @@
{
"name": "available",
"ordinal": 0,
"type_info": "Integer"
"type_info": "Int64"
}
],
"parameters": {
@@ -6,7 +6,7 @@
{
"name": "ticket_id",
"ordinal": 0,
"type_info": "Integer"
"type_info": "Int64"
},
{
"name": "serial_number",
@@ -6,7 +6,7 @@
{
"name": "ticket_id",
"ordinal": 0,
"type_info": "Integer"
"type_info": "Int64"
},
{
"name": "serial_number",
@@ -16,7 +16,7 @@
{
"name": "protocol_version",
"ordinal": 2,
"type_info": "Integer"
"type_info": "Int64"
},
{
"name": "endpoint",
@@ -31,17 +31,17 @@
{
"name": "tx_bytes",
"ordinal": 5,
"type_info": "Integer"
"type_info": "Int64"
},
{
"name": "rx_bytes",
"ordinal": 6,
"type_info": "Integer"
"type_info": "Int64"
},
{
"name": "persistent_keepalive_interval",
"ordinal": 7,
"type_info": "Integer"
"type_info": "Int64"
},
{
"name": "allowed_ips",
@@ -51,7 +51,7 @@
{
"name": "client_id",
"ordinal": 9,
"type_info": "Integer"
"type_info": "Int64"
}
],
"parameters": {
@@ -0,0 +1,32 @@
{
"db_name": "SQLite",
"query": "\n SELECT \n id as \"id!\",\n client_address_bs58 as \"client_address_bs58!\",\n content as \"content!\"\n FROM message_store\n WHERE client_address_bs58 = ?\n ORDER BY id ASC\n LIMIT ?;\n ",
"describe": {
"columns": [
{
"name": "id!",
"ordinal": 0,
"type_info": "Int64"
},
{
"name": "client_address_bs58!",
"ordinal": 1,
"type_info": "Text"
},
{
"name": "content!",
"ordinal": 2,
"type_info": "Blob"
}
],
"parameters": {
"Right": 2
},
"nullable": [
false,
false,
false
]
},
"hash": "e3860c0c31ca03cc0b22ca34cef5f535a94c78d3491d44d7c8bf1b34a840839d"
}
@@ -6,7 +6,7 @@
{
"name": "proposal_id",
"ordinal": 0,
"type_info": "Integer"
"type_info": "Int64"
}
],
"parameters": {
+22 -5
View File
@@ -1,8 +1,6 @@
// Copyright 2021-2024 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: GPL-3.0-only
use std::time::SystemTime;
use crate::error::GatewayStorageError;
use nym_credentials_interface::{AvailableBandwidth, ClientTicket, CredentialSpendingData};
use nym_gateway_requests::shared_key::{LegacySharedKeys, SharedGatewayKey, SharedSymmetricKey};
@@ -115,7 +113,7 @@ pub struct WireguardPeer {
pub preshared_key: Option<String>,
pub protocol_version: Option<i64>,
pub endpoint: Option<String>,
pub last_handshake: Option<OffsetDateTime>,
pub last_handshake: Option<sqlx::types::chrono::NaiveDateTime>,
pub tx_bytes: i64,
pub rx_bytes: i64,
pub persistent_keepalive_interval: Option<i64>,
@@ -130,7 +128,18 @@ impl From<defguard_wireguard_rs::host::Peer> for WireguardPeer {
preshared_key: value.preshared_key.as_ref().map(|k| k.to_string()),
protocol_version: value.protocol_version.map(|v| v as i64),
endpoint: value.endpoint.map(|e| e.to_string()),
last_handshake: value.last_handshake.map(OffsetDateTime::from),
last_handshake: value.last_handshake.and_then(|t| {
if let Ok(d) = t.duration_since(std::time::UNIX_EPOCH) {
if let Ok(millis) = d.as_millis().try_into() {
sqlx::types::chrono::DateTime::from_timestamp_millis(millis)
.map(|d| d.naive_utc())
} else {
None
}
} else {
None
}
}),
tx_bytes: value.tx_bytes as i64,
rx_bytes: value.rx_bytes as i64,
persistent_keepalive_interval: value.persistent_keepalive_interval.map(|v| v as i64),
@@ -171,7 +180,15 @@ impl TryFrom<WireguardPeer> for defguard_wireguard_rs::host::Peer {
.map(|e| e.parse())
.transpose()
.map_err(|e| Self::Error::TypeConversion(format!("endpoint {e}")))?,
last_handshake: value.last_handshake.map(SystemTime::from),
last_handshake: value.last_handshake.and_then(|t| {
let unix_time = std::time::UNIX_EPOCH;
if let Ok(millis) = t.and_utc().timestamp_millis().try_into() {
let duration = std::time::Duration::from_millis(millis);
unix_time.checked_add(duration)
} else {
None
}
}),
tx_bytes: value
.tx_bytes
.try_into()
+1 -1
View File
@@ -92,7 +92,7 @@ impl TicketStorageManager {
)
.fetch_one(&self.connection_pool)
.await
.map(|result| result.exists == 1)
.map(|result| result.exists == Some(1))
}
pub(crate) async fn remove_binary_ticket_data(
+1 -6
View File
@@ -10,14 +10,10 @@ license.workspace = true
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[features]
default=["tunneling"]
tunneling=[]
[dependencies]
async-trait = { workspace = true }
bincode = { workspace = true }
reqwest = { workspace = true, features = ["json", "gzip", "deflate", "brotli", "zstd", "rustls-tls"] }
reqwest = { workspace = true, features = ["json", "gzip", "deflate", "brotli", "zstd"] }
http.workspace = true
url = { workspace = true }
once_cell = { workspace = true }
@@ -25,7 +21,6 @@ serde = { workspace = true }
serde_json = { workspace = true }
thiserror = { workspace = true }
tracing = { workspace = true }
itertools = { workspace = true }
# used for decoding text responses (they were already implicitly included)
bytes = { workspace = true }
-118
View File
@@ -1,118 +0,0 @@
// Copyright 2023 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
//! Utilities for and implementation of request tunneling
use std::sync::atomic::{AtomicBool, Ordering};
use tracing::warn;
use crate::ClientBuilder;
// #[cfg(feature = "tunneling")]
#[derive(Debug)]
pub(crate) struct Front {
pub(crate) policy: FrontPolicy,
enabled: AtomicBool,
}
impl Clone for Front {
fn clone(&self) -> Self {
Self {
policy: self.policy.clone(),
enabled: AtomicBool::new(self.enabled.load(Ordering::Relaxed)),
}
}
}
impl Front {
pub(crate) fn new(policy: FrontPolicy) -> Self {
Self {
enabled: AtomicBool::new(policy == FrontPolicy::Always),
policy,
}
}
pub(crate) fn is_enabled(&self) -> bool {
match self.policy {
FrontPolicy::Off => false,
FrontPolicy::OnRetry => self.enabled.load(Ordering::Relaxed),
FrontPolicy::Always => true,
}
}
// Used to indicate that the client hit an error that should trigger the retry policy
// to enable fronting.
pub(crate) fn retry_enable(&self) {
if self.is_enabled() {
return;
}
if matches!(self.policy, FrontPolicy::OnRetry) {
self.enabled.store(true, Ordering::Relaxed);
}
}
}
#[derive(Debug, Default, PartialEq, Clone)]
#[cfg(feature = "tunneling")]
pub enum FrontPolicy {
Always,
OnRetry,
#[default]
Off,
}
impl ClientBuilder {
/// Enable and configure request tunneling for API requests.
#[cfg(feature = "tunneling")]
pub fn with_fronting(mut self, policy: FrontPolicy) -> Self {
let front = Front::new(policy);
// Check if any of the supplied urls even support fronting
if !self.urls.iter().any(|url| url.has_front()) {
warn!("fronting is enabled, but none of the supplied urls have configured fronting domains");
}
self.front = Some(front);
self
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::{ApiClientCore, Url, NO_PARAMS};
#[tokio::test]
async fn nym_api_works() {
let url1 = Url::new(
"https://validator.global.ssl.fastly.net",
Some(vec!["https://yelp.global.ssl.fastly.net"]),
)
.unwrap(); // fastly
// let url2 = Url::new(
// "https://validator.nymtech.net",
// Some(vec!["https://cdn77.com"]),
// ).unwrap(); // cdn77
let client = ClientBuilder::new::<_, &str>(url1)
.expect("bad url")
.with_fronting(FrontPolicy::Always)
.build::<&str>()
.expect("failed to build client");
let response = client
.send_request::<_, (), &str, &str, &str>(
reqwest::Method::GET,
&["api", "v1", "network", "details"],
NO_PARAMS,
None,
)
.await
.expect("failed get request");
// println!("{response:?}");
assert_eq!(response.status(), 200);
}
}
+304 -427
View File
@@ -136,33 +136,29 @@
//! ```
#![warn(missing_docs)]
pub use reqwest::StatusCode;
pub use reqwest::{IntoUrl, StatusCode};
use crate::path::RequestPath;
use async_trait::async_trait;
use bytes::Bytes;
use http::header::CONTENT_TYPE;
use http::HeaderMap;
use itertools::Itertools;
use mime::Mime;
use reqwest::header::HeaderValue;
use reqwest::{RequestBuilder, Response};
use serde::de::DeserializeOwned;
use serde::{Deserialize, Serialize};
use std::fmt::Display;
use std::sync::atomic::{AtomicUsize, Ordering};
use std::time::Duration;
use thiserror::Error;
use tracing::{debug, instrument, warn};
use url::Url;
#[cfg(not(target_arch = "wasm32"))]
use std::net::SocketAddr;
#[cfg(not(target_arch = "wasm32"))]
use std::sync::Arc;
#[cfg(feature = "tunneling")]
mod fronted;
mod url;
pub use url::{IntoUrl, Url};
mod user_agent;
pub use user_agent::UserAgent;
@@ -251,6 +247,207 @@ impl HttpClientError {
}
}
/// A `ClientBuilder` can be used to create a [`Client`] with custom configuration applied consistently
/// and state tracked across subsequent requests.
pub struct ClientBuilder {
url: Url,
timeout: Option<Duration>,
custom_user_agent: bool,
reqwest_client_builder: reqwest::ClientBuilder,
#[allow(dead_code)] // not dead code, just unused in wasm
use_secure_dns: bool,
}
impl ClientBuilder {
/// Constructs a new `ClientBuilder`.
///
/// This is the same as `Client::builder()`.
pub fn new<U, E>(url: U) -> Result<Self, HttpClientError<E>>
where
U: IntoUrl,
E: Display,
{
let str_url = url.as_str();
// a naive check: if the provided URL does not start with http(s), add that scheme
if !str_url.starts_with("http") {
let alt = format!("http://{str_url}");
warn!("the provided url ('{str_url}') does not contain scheme information. Changing it to '{alt}' ...");
// TODO: or should we maybe default to https?
Self::new(alt)
} else {
Ok(Self::new_with_url(url.into_url()?))
}
}
/// Constructs a new http `ClientBuilder` from a valid url.
pub fn new_with_url(url: Url) -> Self {
if !url.scheme().starts_with("http") {
warn!("the provided url ('{url}') does not use HTTP / HTTPS scheme");
}
#[cfg(target_arch = "wasm32")]
let reqwest_client_builder = reqwest::ClientBuilder::new();
#[cfg(not(target_arch = "wasm32"))]
let reqwest_client_builder = {
// Note: I believe the manual enable calls for the compression methods are extra
// as the various compression features for `reqwest` crate should be enabled
// just by including the feature which:
// `"Enable[s] auto decompression by checking the Content-Encoding response header."`
//
// I am going to leave these here anyways so that removing a decompression method
// from the features list will throw an error if it is not also removed here.
reqwest::ClientBuilder::new()
.gzip(true)
.deflate(true)
.brotli(true)
.zstd(true)
};
ClientBuilder {
url,
timeout: None,
custom_user_agent: false,
reqwest_client_builder,
use_secure_dns: true,
}
}
/// Enables a total request timeout other than the default.
///
/// The timeout is applied from when the request starts connecting until the response body has finished. Also considered a total deadline.
///
/// Default is [`DEFAULT_TIMEOUT`].
pub fn with_timeout(mut self, timeout: Duration) -> Self {
self.timeout = Some(timeout);
self
}
/// Provide a pre-configured [`reqwest::ClientBuilder`]
pub fn with_reqwest_builder(mut self, reqwest_builder: reqwest::ClientBuilder) -> Self {
self.reqwest_client_builder = reqwest_builder;
self
}
/// Sets the `User-Agent` header to be used by this client.
pub fn with_user_agent<V>(mut self, value: V) -> Self
where
V: TryInto<HeaderValue>,
V::Error: Into<http::Error>,
{
self.custom_user_agent = true;
self.reqwest_client_builder = self.reqwest_client_builder.user_agent(value);
self
}
/// Override DNS resolution for specific domains to particular IP addresses.
///
/// Set the port to `0` to use the conventional port for the given scheme (e.g. 80 for http).
/// Ports in the URL itself will always be used instead of the port in the overridden addr.
#[cfg(not(target_arch = "wasm32"))]
pub fn resolve_to_addrs(mut self, domain: &str, addrs: &[SocketAddr]) -> ClientBuilder {
self.reqwest_client_builder = self.reqwest_client_builder.resolve_to_addrs(domain, addrs);
self
}
/// Returns a Client that uses this ClientBuilder configuration.
pub fn build<E>(self) -> Result<Client, HttpClientError<E>>
where
E: Display,
{
#[cfg(target_arch = "wasm32")]
let reqwest_client = self.reqwest_client_builder.build()?;
// TODO: we should probably be propagating the error rather than panicking,
// but that'd break bunch of things due to type changes
#[cfg(not(target_arch = "wasm32"))]
let reqwest_client = {
let mut builder = self
.reqwest_client_builder
.timeout(self.timeout.unwrap_or(DEFAULT_TIMEOUT));
// if no custom user agent was set, use a default
if !self.custom_user_agent {
builder =
builder.user_agent(format!("nym-http-api-client/{}", env!("CARGO_PKG_VERSION")))
}
// unless explicitly disabled use the DoT/DoH enabled resolver
if self.use_secure_dns {
builder = builder.dns_resolver(Arc::new(HickoryDnsResolver::default()));
}
builder.build()?
};
Ok(Client {
base_url: self.url,
reqwest_client,
#[cfg(target_arch = "wasm32")]
request_timeout: self.timeout.unwrap_or(DEFAULT_TIMEOUT),
})
}
}
/// A simple extendable client wrapper for http request with extra url sanitization.
#[derive(Debug, Clone)]
pub struct Client {
base_url: Url,
reqwest_client: reqwest::Client,
#[cfg(target_arch = "wasm32")]
request_timeout: Duration,
}
impl Client {
/// Create a new http `Client`
// no timeout until https://github.com/seanmonstar/reqwest/issues/1135 is fixed
//
// In order to prevent interference in API requests at the DNS phase we default to a resolver
// that uses DoT and DoH.
pub fn new(base_url: Url, timeout: Option<Duration>) -> Self {
Self::new_url::<_, String>(base_url, timeout).expect(
"we provided valid url and we were unwrapping previous construction errors anyway",
)
}
/// Attempt to create a new http client from a something that can be converted to a URL
pub fn new_url<U, E>(url: U, timeout: Option<Duration>) -> Result<Self, HttpClientError<E>>
where
U: IntoUrl,
E: Display,
{
let builder = Self::builder(url)?;
match timeout {
Some(timeout) => builder.with_timeout(timeout).build(),
None => builder.build(),
}
}
/// Creates a [`ClientBuilder`] to configure a [`Client`].
///
/// This is the same as [`ClientBuilder::new()`].
pub fn builder<U, E>(url: U) -> Result<ClientBuilder, HttpClientError<E>>
where
U: IntoUrl,
E: Display,
{
ClientBuilder::new(url)
}
/// Update the host that this client uses when sending API requests.
pub fn change_base_url(&mut self, new_url: Url) {
self.base_url = new_url
}
/// Get the currently configured host that this client uses when sending API requests.
pub fn current_url(&self) -> &Url {
&self.base_url
}
}
/// Core functionality required for types acting as API clients.
///
/// This trait defines the "skinny waist" of behaviors that are required by an API client. More
@@ -351,366 +548,6 @@ pub trait ApiClientCore {
}
}
/// A `ClientBuilder` can be used to create a [`Client`] with custom configuration applied consistently
/// and state tracked across subsequent requests.
pub struct ClientBuilder {
urls: Vec<Url>,
timeout: Option<Duration>,
custom_user_agent: bool,
reqwest_client_builder: reqwest::ClientBuilder,
#[allow(dead_code)] // not dead code, just unused in wasm
use_secure_dns: bool,
#[cfg(feature = "tunneling")]
front: Option<fronted::Front>,
retry_limit: usize,
}
impl ClientBuilder {
/// Constructs a new `ClientBuilder`.
///
/// This is the same as `Client::builder()`.
pub fn new<U, E>(url: U) -> Result<Self, HttpClientError<E>>
where
U: IntoUrl,
E: Display,
{
let str_url = url.as_str();
// a naive check: if the provided URL does not start with http(s), add that scheme
if !str_url.starts_with("http") {
let alt = format!("http://{str_url}");
warn!("the provided url ('{str_url}') does not contain scheme information. Changing it to '{alt}' ...");
// TODO: or should we maybe default to https?
Self::new(alt)
} else {
let url = url.to_url()?;
Ok(Self::new_with_urls(vec![url]))
}
}
/// Constructs a new http `ClientBuilder` from a valid url.
pub fn new_with_urls(urls: Vec<Url>) -> Self {
let urls = Self::check_urls(urls);
#[cfg(target_arch = "wasm32")]
let reqwest_client_builder = reqwest::ClientBuilder::new();
#[cfg(not(target_arch = "wasm32"))]
let reqwest_client_builder = {
// Note: I believe the manual enable calls for the compression methods are extra
// as the various compression features for `reqwest` crate should be enabled
// just by including the feature which:
// `"Enable[s] auto decompression by checking the Content-Encoding response header."`
//
// I am going to leave these here anyways so that removing a decompression method
// from the features list will throw an error if it is not also removed here.
reqwest::ClientBuilder::new()
.gzip(true)
.deflate(true)
.brotli(true)
.zstd(true)
};
ClientBuilder {
urls,
timeout: None,
custom_user_agent: false,
reqwest_client_builder,
use_secure_dns: true,
#[cfg(feature = "tunneling")]
front: None,
retry_limit: 0,
}
}
/// Add an additional URL to the set usable by this constructed `Client`
pub fn add_url(mut self, url: Url) -> Self {
self.urls.push(url);
self
}
fn check_urls(mut urls: Vec<Url>) -> Vec<Url> {
// remove any duplicate URLs
urls = urls.into_iter().unique().collect();
// warn about any invalid URLs
urls.iter()
.filter(|url| !url.scheme().contains("http") && !url.scheme().contains("https"))
.for_each(|url| {
warn!("the provided url ('{url}') does not use HTTP / HTTPS scheme");
});
urls
}
/// Enables a total request timeout other than the default.
///
/// The timeout is applied from when the request starts connecting until the response body has finished. Also considered a total deadline.
///
/// Default is [`DEFAULT_TIMEOUT`].
pub fn with_timeout(mut self, timeout: Duration) -> Self {
self.timeout = Some(timeout);
self
}
/// Sets the maximum number of retries for a request. This defaults to 0, indicating no retries.
///
/// Note that setting a retry limit of 3 (for example) will result in 4 attempts to send the
/// request in the case that all are unsuccessful.
///
/// If multiple urls (or fronting configurations if enabled) are available, retried requests
/// will be sent to the next URL in the list.
pub fn with_retries(mut self, retry_limit: usize) -> Self {
self.retry_limit = retry_limit;
self
}
/// Provide a pre-configured [`reqwest::ClientBuilder`]
pub fn with_reqwest_builder(mut self, reqwest_builder: reqwest::ClientBuilder) -> Self {
self.reqwest_client_builder = reqwest_builder;
self
}
/// Sets the `User-Agent` header to be used by this client.
pub fn with_user_agent<V>(mut self, value: V) -> Self
where
V: TryInto<HeaderValue>,
V::Error: Into<http::Error>,
{
self.custom_user_agent = true;
self.reqwest_client_builder = self.reqwest_client_builder.user_agent(value);
self
}
/// Override DNS resolution for specific domains to particular IP addresses.
///
/// Set the port to `0` to use the conventional port for the given scheme (e.g. 80 for http).
/// Ports in the URL itself will always be used instead of the port in the overridden addr.
#[cfg(not(target_arch = "wasm32"))]
pub fn resolve_to_addrs(mut self, domain: &str, addrs: &[SocketAddr]) -> ClientBuilder {
self.reqwest_client_builder = self.reqwest_client_builder.resolve_to_addrs(domain, addrs);
self
}
/// Returns a Client that uses this ClientBuilder configuration.
pub fn build<E>(self) -> Result<Client, HttpClientError<E>>
where
E: Display,
{
#[cfg(target_arch = "wasm32")]
let reqwest_client = self.reqwest_client_builder.build()?;
// TODO: we should probably be propagating the error rather than panicking,
// but that'd break bunch of things due to type changes
#[cfg(not(target_arch = "wasm32"))]
let reqwest_client = {
let mut builder = self
.reqwest_client_builder
.timeout(self.timeout.unwrap_or(DEFAULT_TIMEOUT));
// if no custom user agent was set, use a default
if !self.custom_user_agent {
builder =
builder.user_agent(format!("nym-http-api-client/{}", env!("CARGO_PKG_VERSION")))
}
// unless explicitly disabled use the DoT/DoH enabled resolver
if self.use_secure_dns {
builder = builder.dns_resolver(Arc::new(HickoryDnsResolver::default()));
}
builder.build()?
};
let client = Client {
base_urls: self.urls,
current_idx: Arc::new(AtomicUsize::new(0)),
reqwest_client,
#[cfg(feature = "tunneling")]
front: self.front,
#[cfg(target_arch = "wasm32")]
request_timeout: self.timeout.unwrap_or(DEFAULT_TIMEOUT),
retry_limit: self.retry_limit,
};
Ok(client)
}
}
/// A simple extendable client wrapper for http request with extra url sanitization.
#[derive(Debug, Clone)]
pub struct Client {
base_urls: Vec<Url>,
current_idx: Arc<AtomicUsize>,
reqwest_client: reqwest::Client,
#[cfg(feature = "tunneling")]
front: Option<fronted::Front>,
#[cfg(target_arch = "wasm32")]
request_timeout: Duration,
retry_limit: usize,
}
impl Client {
/// Create a new http `Client`
// no timeout until https://github.com/seanmonstar/reqwest/issues/1135 is fixed
//
// In order to prevent interference in API requests at the DNS phase we default to a resolver
// that uses DoT and DoH.
pub fn new(base_url: ::url::Url, timeout: Option<Duration>) -> Self {
Self::new_url::<_, String>(base_url, timeout).expect(
"we provided valid url and we were unwrapping previous construction errors anyway",
)
}
/// Attempt to create a new http client from a something that can be converted to a URL
pub fn new_url<U, E>(url: U, timeout: Option<Duration>) -> Result<Self, HttpClientError<E>>
where
U: IntoUrl,
E: Display,
{
let builder = Self::builder(url)?;
match timeout {
Some(timeout) => builder.with_timeout(timeout).build(),
None => builder.build(),
}
}
/// Creates a [`ClientBuilder`] to configure a [`Client`].
///
/// This is the same as [`ClientBuilder::new()`].
pub fn builder<U, E>(url: U) -> Result<ClientBuilder, HttpClientError<E>>
where
U: IntoUrl,
E: Display,
{
ClientBuilder::new(url)
}
/// Update the set of hosts that this client uses when sending API requests.
pub fn change_base_urls(&mut self, new_urls: Vec<Url>) {
self.current_idx.store(0, Ordering::Relaxed);
self.base_urls = new_urls
}
/// Create new instance of `Client` using the provided base url and existing client config
pub fn clone_with_new_url(&self, new_url: Url) -> Self {
Client {
base_urls: vec![new_url],
current_idx: Arc::new(Default::default()),
reqwest_client: self.reqwest_client.clone(),
front: self.front.clone(),
retry_limit: self.retry_limit,
#[cfg(target_arch = "wasm32")]
request_timeout: self.request_timeout,
}
}
/// Get the currently configured host that this client uses when sending API requests.
pub fn current_url(&self) -> &Url {
&self.base_urls[self.current_idx.load(std::sync::atomic::Ordering::Relaxed)]
}
/// Get the currently configured host that this client uses when sending API requests.
pub fn base_urls(&self) -> &[Url] {
&self.base_urls
}
/// Get a mutable reference to the hosts that this client uses when sending API requests.
pub fn base_urls_mut(&mut self) -> &mut [Url] {
&mut self.base_urls
}
/// Change the currently configured limit on the number of retries for a request.
pub fn change_retry_limit(&mut self, limit: usize) {
self.retry_limit = limit;
}
/// If multiple base urls are available rotate to next (e.g. when the current one resulted in an error)
fn update_host(&self) {
#[cfg(feature = "tunneling")]
if let Some(ref front) = self.front {
if front.is_enabled() {
// if we are using fronting, try updating to the next front
let url = self.current_url();
// try to update the current host to use a next front, if one is available, otherwise
// we move on and try the next base url (if one is available)
if url.has_front() && !url.update() {
// we swapped to the next front for the current host
return;
}
}
}
if self.base_urls.len() > 1 {
let orig = self.current_idx.load(Ordering::Relaxed);
let mut next = (orig + 1) % self.base_urls.len();
// if fronting is enabled we want to update to a host that has fronts configured
#[cfg(feature = "tunneling")]
if let Some(ref front) = self.front {
if front.is_enabled() {
while next != orig {
if self.base_urls[next].has_front() {
// we have a front for the next host, so we can use it
break;
}
next = (next + 1) % self.base_urls.len();
}
}
}
self.current_idx.store(next, Ordering::Relaxed);
}
}
/// Make modifications to the request to apply the current state of this client i.e. the
/// currently configured host. This is required as a caller may use this client to create a
/// request, but then have the state of the client change before the caller uses the client to
/// send their request.
///
/// This enures that the outgoing requests benefit from the configured fallback mechanisms, even
/// for requests that were created before the state of the client changed.
///
/// This method assumes that any updates to the state of the client are made before the call to
/// this method. For example, if the client is configured to rotate hosts after each error, this
/// method should be called after the host has been updated -- i.e. as part of the subsequent
/// send.
fn apply_hosts_to_req(&self, r: &mut reqwest::Request) {
let url = self.current_url();
r.url_mut().set_host(url.host_str()).unwrap();
#[cfg(feature = "tunneling")]
if let Some(ref front) = self.front {
if front.is_enabled() {
// this should never fail as we are transplanting the host from one url to another
r.url_mut().set_host(url.front_str()).unwrap();
let actual_host: HeaderValue = url
.host_str()
.unwrap_or("")
.parse()
.unwrap_or(HeaderValue::from_static(""));
// If the map did have this key present, the new value is associated with the key
// and all previous values are removed. (reqwest HeaderMap docs)
_ = r.headers_mut().insert(reqwest::header::HOST, actual_host);
}
}
}
}
#[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
#[cfg_attr(not(target_arch = "wasm32"), async_trait)]
impl ApiClientCore for Client {
@@ -728,77 +565,33 @@ impl ApiClientCore for Client {
K: AsRef<str>,
V: AsRef<str>,
{
let url = self.current_url();
let url = sanitize_url(url, path, params);
let url = sanitize_url(&self.base_url, path, params);
let mut req = reqwest::Request::new(method, url.into());
self.apply_hosts_to_req(&mut req);
let mut rb = RequestBuilder::from_parts(self.reqwest_client.clone(), req);
let mut request = self.reqwest_client.request(method.clone(), url);
if let Some(body) = json_body {
rb = rb.json(body);
request = request.json(body);
}
rb
request
}
async fn send<E>(&self, request: RequestBuilder) -> Result<Response, HttpClientError<E>>
where
E: Display,
{
let mut attempts = 0;
loop {
// try_clone may fail if the body is a stream in which case using retries is not advised.
let r = request
.try_clone()
.ok_or(HttpClientError::GenericRequestFailure(
"failed to send request".to_string(),
))?;
#[cfg(target_arch = "wasm32")]
{
Ok(
wasmtimer::tokio::timeout(self.request_timeout, request.send())
.await
.map_err(|_timeout| HttpClientError::RequestTimeout)??,
)
}
// apply any changes based on the current state of the client wrt. hosts,
// fronting domains, etc.
let mut req = r.build()?;
self.apply_hosts_to_req(&mut req);
#[cfg(target_arch = "wasm32")]
let response: Result<Response, HttpClientError<E>> = {
Ok(wasmtimer::tokio::timeout(
self.request_timeout,
self.reqwest_client.execute(req),
)
.await
.map_err(|_timeout| HttpClientError::RequestTimeout)??)
};
#[cfg(not(target_arch = "wasm32"))]
let response = self.reqwest_client.execute(req).await;
match response {
Ok(resp) => return Ok(resp),
Err(e) => {
// if we have multiple urls, update to the next
self.update_host();
#[cfg(feature = "tunneling")]
if let Some(ref front) = self.front {
// If fronting is set to be enabled on error, enable domain fronting as we
// have encountered an error.
front.retry_enable();
}
if attempts < self.retry_limit {
warn!("Retrying request due to http error: {}", e);
attempts += 1;
continue;
}
// if we have exhausted our attempts, return the error
#[allow(clippy::useless_conversion)] // conversion considered useless in wasm
return Err(e.into());
}
}
#[cfg(not(target_arch = "wasm32"))]
{
Ok(request.send().await?)
}
}
}
@@ -1256,4 +1049,88 @@ fn try_get_mime_type(headers: &HeaderMap) -> Option<Mime> {
}
#[cfg(test)]
mod tests;
mod tests {
use super::*;
#[test]
fn sanitizing_urls() {
let base_url: Url = "http://foomp.com".parse().unwrap();
// works with a full string
assert_eq!(
"http://foomp.com/foo/bar",
sanitize_url(&base_url, "/foo//bar/", NO_PARAMS).as_str()
);
// (and leading slash doesn't matter)
assert_eq!(
"http://foomp.com/foo/bar",
sanitize_url(&base_url, "foo//bar/", NO_PARAMS).as_str()
);
// works with 1 segment
assert_eq!(
"http://foomp.com/foo",
sanitize_url(&base_url, &["foo"], NO_PARAMS).as_str()
);
// works with 2 segments
assert_eq!(
"http://foomp.com/foo/bar",
sanitize_url(&base_url, &["foo", "bar"], NO_PARAMS).as_str()
);
// works with leading slash
assert_eq!(
"http://foomp.com/foo",
sanitize_url(&base_url, &["/foo"], NO_PARAMS).as_str()
);
assert_eq!(
"http://foomp.com/foo/bar",
sanitize_url(&base_url, &["/foo", "bar"], NO_PARAMS).as_str()
);
assert_eq!(
"http://foomp.com/foo/bar",
sanitize_url(&base_url, &["foo", "/bar"], NO_PARAMS).as_str()
);
// works with trailing slash
assert_eq!(
"http://foomp.com/foo",
sanitize_url(&base_url, &["foo/"], NO_PARAMS).as_str()
);
assert_eq!(
"http://foomp.com/foo/bar",
sanitize_url(&base_url, &["foo/", "bar"], NO_PARAMS).as_str()
);
assert_eq!(
"http://foomp.com/foo/bar",
sanitize_url(&base_url, &["foo", "bar/"], NO_PARAMS).as_str()
);
// works with both leading and trailing slash
assert_eq!(
"http://foomp.com/foo",
sanitize_url(&base_url, &["/foo/"], NO_PARAMS).as_str()
);
assert_eq!(
"http://foomp.com/foo/bar",
sanitize_url(&base_url, &["/foo/", "/bar/"], NO_PARAMS).as_str()
);
// adds params
assert_eq!(
"http://foomp.com/foo/bar?foomp=baz",
sanitize_url(&base_url, &["foo", "bar"], &[("foomp", "baz")]).as_str()
);
assert_eq!(
"http://foomp.com/foo/bar?arg1=val1&arg2=val2",
sanitize_url(
&base_url,
&["/foo/", "/bar/"],
&[("arg1", "val1"), ("arg2", "val2")]
)
.as_str()
);
}
}
-224
View File
@@ -1,224 +0,0 @@
use super::*;
#[test]
fn sanitizing_urls() {
let base_url: Url = "http://foomp.com".parse().unwrap();
// works with a full string
assert_eq!(
"http://foomp.com/foo/bar",
sanitize_url(&base_url, "/foo//bar/", NO_PARAMS).as_str()
);
// (and leading slash doesn't matter)
assert_eq!(
"http://foomp.com/foo/bar",
sanitize_url(&base_url, "foo//bar/", NO_PARAMS).as_str()
);
// works with 1 segment
assert_eq!(
"http://foomp.com/foo",
sanitize_url(&base_url, &["foo"], NO_PARAMS).as_str()
);
// works with 2 segments
assert_eq!(
"http://foomp.com/foo/bar",
sanitize_url(&base_url, &["foo", "bar"], NO_PARAMS).as_str()
);
// works with leading slash
assert_eq!(
"http://foomp.com/foo",
sanitize_url(&base_url, &["/foo"], NO_PARAMS).as_str()
);
assert_eq!(
"http://foomp.com/foo/bar",
sanitize_url(&base_url, &["/foo", "bar"], NO_PARAMS).as_str()
);
assert_eq!(
"http://foomp.com/foo/bar",
sanitize_url(&base_url, &["foo", "/bar"], NO_PARAMS).as_str()
);
// works with trailing slash
assert_eq!(
"http://foomp.com/foo",
sanitize_url(&base_url, &["foo/"], NO_PARAMS).as_str()
);
assert_eq!(
"http://foomp.com/foo/bar",
sanitize_url(&base_url, &["foo/", "bar"], NO_PARAMS).as_str()
);
assert_eq!(
"http://foomp.com/foo/bar",
sanitize_url(&base_url, &["foo", "bar/"], NO_PARAMS).as_str()
);
// works with both leading and trailing slash
assert_eq!(
"http://foomp.com/foo",
sanitize_url(&base_url, &["/foo/"], NO_PARAMS).as_str()
);
assert_eq!(
"http://foomp.com/foo/bar",
sanitize_url(&base_url, &["/foo/", "/bar/"], NO_PARAMS).as_str()
);
// adds params
assert_eq!(
"http://foomp.com/foo/bar?foomp=baz",
sanitize_url(&base_url, &["foo", "bar"], &[("foomp", "baz")]).as_str()
);
assert_eq!(
"http://foomp.com/foo/bar?arg1=val1&arg2=val2",
sanitize_url(
&base_url,
&["/foo/", "/bar/"],
&[("arg1", "val1"), ("arg2", "val2")]
)
.as_str()
);
}
// - Do the retries work
// - Do we use fallback urls on retry if multiple are provided
// - Do we use the next front on retry if multiple are provided
// - If we have more retries than urls, do we wrap back to the first one again
// - on error without retries is where we have multiple urls, is the url updated?
#[tokio::test]
async fn api_client_retry() -> Result<(), Box<dyn std::error::Error>> {
let client = ClientBuilder::new_with_urls(vec![
"http://broken.nym.badurl".parse()?,
"http://example.com/".parse()?,
])
.with_retries(3)
.build::<HttpClientError>()?;
let req = client.create_get_request(&["/"], NO_PARAMS);
let resp = client.send::<HttpClientError>(req).await?;
assert_eq!(resp.status(), 200);
// check that the url was updated
assert_eq!(client.current_url().as_str(), "http://example.com/");
Ok(())
}
#[test]
fn host_updating() {
let url = Url::new("http://example.com", None).unwrap();
let mut client = ClientBuilder::new::<_, &str>(url)
.unwrap()
.build::<&str>()
.unwrap();
// check that the url is set correctly
let current_url = client.current_url();
assert_eq!(current_url.as_str(), "http://example.com/");
assert_eq!(current_url.front_str(), None);
// update the url
client.update_host();
// check that the url is still the same since there is one URL
assert_eq!(client.current_url().as_str(), "http://example.com/");
// =======================================
// we rotate through urls when available
let new_urls = vec![
Url::new("http://example.com", None).unwrap(),
Url::new("http://example.org", None).unwrap(),
];
client.change_base_urls(new_urls);
assert_eq!(client.current_url().as_str(), "http://example.com/");
client.update_host();
// check that the url got updated now that there are multiple URLs
assert_eq!(client.current_url().as_str(), "http://example.org/");
assert_eq!(client.current_url().front_str(), None);
client.update_host();
assert_eq!(client.current_url().as_str(), "http://example.com/");
// =======================================
// we rotate through urls when available if fronting is disabled
let new_urls = vec![
Url::new(
"http://example.com",
Some(vec!["http://front1.com", "http://front2.com"]),
)
.unwrap(),
Url::new("http://example.org", None).unwrap(),
];
client.change_base_urls(new_urls);
assert_eq!(client.current_url().as_str(), "http://example.com/");
client.update_host();
// check that the url got updated now that there are multiple URLs
assert_eq!(client.current_url().as_str(), "http://example.org/");
}
#[test]
#[cfg(feature = "tunneling")]
fn fronted_host_updating() {
let url = Url::new("http://example.com", Some(vec!["http://front1.com"])).unwrap();
let mut client = ClientBuilder::new::<_, &str>(url)
.unwrap()
.with_fronting(crate::fronted::FrontPolicy::Always)
.build::<&str>()
.unwrap();
// check that the url is set correctly
let current_url = client.current_url();
assert_eq!(current_url.as_str(), "http://example.com/");
assert_eq!(current_url.front_str(), Some("front1.com"));
// update the url
client.update_host();
// check that the url is still the same since there is one URL and one front
let current_url = client.current_url();
assert_eq!(current_url.as_str(), "http://example.com/");
assert_eq!(current_url.front_str(), Some("front1.com"));
// =======================================
// we rotate through front urls when available if fronting is enabled
let new_urls = vec![
Url::new(
"http://example.com",
Some(vec!["http://front1.com", "http://front2.com"]),
)
.unwrap(),
Url::new("http://example.org", None).unwrap(),
];
client.change_base_urls(new_urls);
let current_url = client.current_url();
assert_eq!(current_url.as_str(), "http://example.com/");
assert_eq!(current_url.front_str(), Some("front1.com"));
// update the url - this should keep the same host but change the front
client.update_host();
let current_url = client.current_url();
// check that the url is still the same since there is one URL
assert_eq!(current_url.as_str(), "http://example.com/");
assert_eq!(current_url.front_str(), Some("front2.com"));
// update the url - this should wrap around to the first front as the second url is not fronted
client.update_host();
let current_url = client.current_url();
assert_eq!(current_url.as_str(), "http://example.com/");
assert_eq!(current_url.front_str(), Some("front1.com"));
}
-291
View File
@@ -1,291 +0,0 @@
//! Url handling for the HTTP API client.
//!
//! This module provides a `Url` struct that wraps around the `url::Url` type and adds
//! functionality for handling front domains, which are used for reverse proxying.
use std::fmt::Display;
use std::sync::atomic::{AtomicUsize, Ordering};
use std::sync::Arc;
use itertools::Itertools;
use url::form_urlencoded;
pub use url::ParseError;
/// A trait to try to convert some type into a `Url`.
pub trait IntoUrl {
/// Parse as a valid `Url`
fn to_url(self) -> Result<Url, ParseError>;
/// Returns the string representation of the URL.
fn as_str(&self) -> &str;
}
impl IntoUrl for &str {
fn to_url(self) -> Result<Url, ParseError> {
let url = url::Url::parse(self)?;
Ok(url.into())
}
fn as_str(&self) -> &str {
self
}
}
impl IntoUrl for String {
fn to_url(self) -> Result<Url, ParseError> {
let url = url::Url::parse(&self)?;
Ok(url.into())
}
fn as_str(&self) -> &str {
self
}
}
impl IntoUrl for reqwest::Url {
fn to_url(self) -> Result<Url, ParseError> {
Ok(self.into())
}
fn as_str(&self) -> &str {
self.as_str()
}
}
/// When configuring fronting, some configurations will require a specific backend host
/// to be used for the request to be properly reverse proxied.
#[derive(Debug, Clone)]
pub struct Url {
url: url::Url,
fronts: Option<Vec<url::Url>>,
current_front: Arc<AtomicUsize>,
}
impl IntoUrl for Url {
fn to_url(self) -> Result<Url, ParseError> {
Ok(self)
}
fn as_str(&self) -> &str {
self.url.as_str()
}
}
impl PartialEq for Url {
fn eq(&self, other: &Self) -> bool {
let current = self.current_front.load(Ordering::Relaxed);
let other_current = other.current_front.load(Ordering::Relaxed);
self.fronts == other.fronts && self.url == other.url && current == other_current
}
}
impl Eq for Url {}
impl std::hash::Hash for Url {
fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
let current = self.current_front.load(Ordering::Relaxed);
self.fronts.hash(state);
self.url.hash(state);
current.hash(state);
}
}
impl Display for Url {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self.fronts {
Some(ref fronts) => {
let current = self.current_front.load(Ordering::Relaxed);
if let Some(front) = fronts.get(current) {
write!(f, "{front}=>{}", self.url)
} else {
write!(f, "{}", self.url)
}
}
None => write!(f, "{}", self.url),
}
}
}
impl From<Url> for url::Url {
fn from(val: Url) -> Self {
val.url
}
}
impl From<reqwest::Url> for Url {
fn from(url: url::Url) -> Self {
Self {
url,
fronts: None,
current_front: Arc::new(AtomicUsize::new(0)),
}
}
}
impl AsRef<url::Url> for Url {
fn as_ref(&self) -> &url::Url {
&self.url
}
}
impl AsMut<url::Url> for Url {
fn as_mut(&mut self) -> &mut url::Url {
&mut self.url
}
}
impl std::str::FromStr for Url {
type Err = url::ParseError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
let url = url::Url::parse(s)?;
Ok(Self {
url,
fronts: None,
current_front: Arc::new(AtomicUsize::new(0)),
})
}
}
impl Url {
/// Create a new `Url` instance with the given something that can be parsed as a URL and
/// optional tunneling domains
pub fn new<U: reqwest::IntoUrl>(
url: U,
fronts: Option<Vec<U>>,
) -> Result<Self, reqwest::Error> {
let mut url = Self {
url: url.into_url()?,
fronts: None,
current_front: Arc::new(AtomicUsize::new(0)),
};
// ensure that the provided URLs are valid
if let Some(front_domains) = fronts {
let f: Vec<reqwest::Url> = front_domains
.into_iter()
.map(|front| front.into_url())
.try_collect()?;
url.fronts = Some(f);
}
Ok(url)
}
/// Parse an absolute URL from a string.
pub fn parse(s: &str) -> Result<Self, ParseError> {
let url = url::Url::parse(s)?;
Ok(Self {
url,
fronts: None,
current_front: Arc::new(AtomicUsize::new(0)),
})
}
/// Returns true if the URL has a front domain set
pub fn has_front(&self) -> bool {
if let Some(fronts) = &self.fronts {
return !fronts.is_empty();
}
false
}
/// Return the string representation of the current front host (domain or IP address) for this
/// URL, if any.
pub fn front_str(&self) -> Option<&str> {
let current = self.current_front.load(Ordering::Relaxed);
self.fronts
.as_ref()
.and_then(|fronts| fronts.get(current))
.and_then(|url| url.host_str())
}
/// Return the string representation of the host (domain or IP address) for this URL, if any.
pub fn host_str(&self) -> Option<&str> {
self.url.host_str()
}
/// Return the serialization of this URL.
///
/// This is fast since that serialization is already stored in the inner url::Url struct.
pub fn as_str(&self) -> &str {
self.url.as_str()
}
/// Returns true if updating the front wraps back to the first front, or if no fronts are set
pub fn update(&self) -> bool {
if let Some(fronts) = &self.fronts {
if fronts.len() > 1 {
let current = self.current_front.load(Ordering::Relaxed);
let next = (current + 1) % fronts.len();
self.current_front.store(next, Ordering::Relaxed);
return next == 0;
}
}
true
}
/// Return the scheme of this URL, lower-cased, as an ASCII string without the : delimiter.
pub fn scheme(&self) -> &str {
self.url.scheme()
}
/// Parse the URLs query string, if any, as application/x-www-form-urlencoded and return an
/// iterator of (key, value) pairs.
pub fn query_pairs(&self) -> form_urlencoded::Parse<'_> {
self.url.query_pairs()
}
/// Manipulate this URLs query string, viewed as a sequence of name/value pairs in
/// application/x-www-form-urlencoded syntax.
pub fn query_pairs_mut(&mut self) -> form_urlencoded::Serializer<'_, ::url::UrlQuery<'_>> {
self.url.query_pairs_mut()
}
/// Change this URLs query string. If `query` is `None`, this URLs query string will be cleared.
pub fn set_query(&mut self, query: Option<&str>) {
self.url.set_query(query);
}
/// Change this URLs path.
pub fn set_path(&mut self, path: &str) {
self.url.set_path(path);
}
/// Change this URLs scheme.
pub fn set_scheme(&mut self, scheme: &str) {
self.url.set_scheme(scheme).unwrap();
}
/// Change this URLs host.
///
/// Removing the host (calling this with None) will also remove any username, password, and port number.
pub fn set_host(&mut self, host: &str) {
self.url.set_host(Some(host)).unwrap();
}
/// Change this URLs port number.
///
/// Note that default port numbers are not reflected in the serialization.
///
/// If this URL is cannot-be-a-base, does not have a host, or has the `file` scheme; do nothing and return `Err`.
pub fn set_port(&mut self, port: u16) {
self.url.set_port(Some(port)).unwrap();
}
/// Return an object with methods to manipulate this URLs path segments.
///
/// Return Err(()) if this URL is cannot-be-a-base.
pub fn path_segments(&self) -> Option<std::str::Split<'_, char>> {
self.url.path_segments()
}
/// Return an object with methods to manipulate this URLs path segments.
///
/// Return Err(()) if this URL is cannot-be-a-base.
#[allow(clippy::result_unit_err)]
pub fn path_segments_mut(&mut self) -> Result<::url::PathSegmentsMut<'_>, ()> {
self.url.path_segments_mut()
}
}
@@ -30,10 +30,6 @@ impl<T> Bincode<T> {
self.0.headers.insert(name, value.into());
self
}
pub(crate) fn map<U, F: FnOnce(T) -> U>(self, op: F) -> Bincode<U> {
Bincode(self.0.map(op))
}
}
impl<T> IntoResponse for Bincode<T>
@@ -32,10 +32,6 @@ impl<T> Json<T> {
self.0.headers.insert(name, value.into());
self
}
pub(crate) fn map<U, F: FnOnce(T) -> U>(self, op: F) -> Json<U> {
Json(self.0.map(op))
}
}
impl<T> IntoResponse for Json<T>
+2 -16
View File
@@ -14,10 +14,11 @@ pub mod bincode;
pub mod json;
pub mod yaml;
pub use bincode::Bincode;
pub use json::Json;
pub use yaml::Yaml;
pub use bincode::Bincode;
#[derive(Debug, Clone, Default)]
pub(crate) struct ResponseWrapper<T> {
data: T,
@@ -32,13 +33,6 @@ impl<T> ResponseWrapper<T> {
}
}
pub(crate) fn map<U, F: FnOnce(T) -> U>(self, op: F) -> ResponseWrapper<U> {
ResponseWrapper {
data: op(self.data),
headers: self.headers,
}
}
#[must_use]
pub(crate) fn with_header(
mut self,
@@ -66,14 +60,6 @@ impl<T> FormattedResponse<T> {
}
}
pub fn map<U, F: FnOnce(T) -> U>(self, op: F) -> FormattedResponse<U> {
match self {
FormattedResponse::Json(inner) => FormattedResponse::Json(inner.map(op)),
FormattedResponse::Yaml(inner) => FormattedResponse::Yaml(inner.map(op)),
FormattedResponse::Bincode(inner) => FormattedResponse::Bincode(inner.map(op)),
}
}
#[must_use]
pub fn with_header(
self,
@@ -30,10 +30,6 @@ impl<T> Yaml<T> {
self.0.headers.insert(name, value.into());
self
}
pub(crate) fn map<U, F: FnOnce(T) -> U>(self, op: F) -> Yaml<U> {
Yaml(self.0.map(op))
}
}
impl<T> IntoResponse for Yaml<T>
@@ -10,6 +10,7 @@ repository = { workspace = true }
[dependencies]
rand = { workspace = true }
bs58 = { workspace = true }
serde = { workspace = true }
thiserror = { workspace = true }
tracing = { workspace = true }
@@ -21,7 +22,7 @@ nym-sphinx-types = { path = "../types" }
nym-topology = { path = "../../topology" }
[target."cfg(target_arch = \"wasm32\")".dependencies.wasm-bindgen]
workspace = true
version = "0.2.95"
[dev-dependencies]
rand_chacha = { workspace = true }
@@ -6,4 +6,4 @@ pub mod reply_surb;
pub mod requests;
pub use encryption_key::{SurbEncryptionKey, SurbEncryptionKeySize};
pub use reply_surb::{ReplySurb, ReplySurbError, ReplySurbWithKeyRotation};
pub use reply_surb::{ReplySurb, ReplySurbError};
@@ -8,13 +8,16 @@ use nym_sphinx_addressing::nodes::{
NymNodeRoutingAddress, NymNodeRoutingAddressError, MAX_NODE_ADDRESS_UNPADDED_LEN,
};
use nym_sphinx_params::packet_sizes::PacketSize;
use nym_sphinx_params::{PacketType, ReplySurbKeyDigestAlgorithm, SphinxKeyRotation};
use nym_sphinx_params::{PacketType, ReplySurbKeyDigestAlgorithm};
use nym_sphinx_types::{
NymPacket, SURBMaterial, SphinxError, HEADER_SIZE, NODE_ADDRESS_LENGTH, SURB,
X25519_WITH_EXPLICIT_PAYLOAD_KEYS_VERSION,
};
use nym_topology::{NymRouteProvider, NymTopologyError};
use rand::{CryptoRng, RngCore};
use serde::de::{Error as SerdeError, Visitor};
use serde::{Deserialize, Deserializer, Serialize, Serializer};
use std::fmt::{self, Formatter};
use std::time::Duration;
use thiserror::Error;
@@ -45,6 +48,44 @@ pub struct ReplySurb {
pub(crate) encryption_key: SurbEncryptionKey,
}
// Serialize + Deserialize is not really used anymore (it was for a CBOR experiment)
// however, if we decided we needed it again, it's already here
impl Serialize for ReplySurb {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
serializer.serialize_bytes(&self.to_bytes())
}
}
impl<'de> Deserialize<'de> for ReplySurb {
fn deserialize<D>(deserializer: D) -> Result<Self, <D as Deserializer<'de>>::Error>
where
D: Deserializer<'de>,
{
struct ReplySurbVisitor;
impl Visitor<'_> for ReplySurbVisitor {
type Value = ReplySurb;
fn expecting(&self, formatter: &mut Formatter<'_>) -> fmt::Result {
write!(formatter, "A replySURB must contain a valid symmetric encryption key and a correctly formed sphinx header")
}
fn visit_bytes<E>(self, bytes: &[u8]) -> Result<Self::Value, E>
where
E: SerdeError,
{
ReplySurb::from_bytes(bytes)
.map_err(|_| SerdeError::invalid_length(bytes.len(), &self))
}
}
deserializer.deserialize_bytes(ReplySurbVisitor)
}
}
impl ReplySurb {
/// base overhead of a reply surb that exists regardless of type or number of key materials.
pub(crate) const BASE_OVERHEAD: usize =
@@ -82,7 +123,6 @@ impl ReplySurb {
Ok(ReplySurb {
surb: surb_material.construct_SURB().unwrap(),
encryption_key: SurbEncryptionKey::new(rng),
// used_key_rotation: SphinxKeyRotation::from(topology.current_key_rotation()),
})
}
@@ -158,75 +198,8 @@ impl ReplySurb {
.use_surb(message_bytes, packet_size.payload_size())
.expect("this error indicates inconsistent message length checking - it shouldn't have happened!");
let first_hop_address = NymNodeRoutingAddress::try_from(first_hop)?;
let first_hop_address = NymNodeRoutingAddress::try_from(first_hop).unwrap();
Ok((NymPacket::Sphinx(packet), first_hop_address))
}
pub fn to_legacy(self) -> ReplySurbWithKeyRotation {
self.with_key_rotation(SphinxKeyRotation::Unknown)
}
pub fn with_key_rotation(self, key_rotation: SphinxKeyRotation) -> ReplySurbWithKeyRotation {
ReplySurbWithKeyRotation {
inner: self,
key_rotation,
}
}
}
#[derive(Debug)]
pub struct ReplySurbWithKeyRotation {
pub(crate) inner: ReplySurb,
pub(crate) key_rotation: SphinxKeyRotation,
}
impl ReplySurbWithKeyRotation {
pub fn encryption_key(&self) -> &SurbEncryptionKey {
self.inner.encryption_key()
}
pub fn inner_reply_surb(&self) -> &ReplySurb {
&self.inner
}
pub fn key_rotation(&self) -> SphinxKeyRotation {
self.key_rotation
}
pub fn apply_surb<M: AsRef<[u8]>>(
self,
message: M,
packet_size: PacketSize,
_packet_type: PacketType,
) -> Result<AppliedReplySurb, ReplySurbError> {
let (packet, first_hop_address) =
self.inner.apply_surb(message, packet_size, _packet_type)?;
Ok(AppliedReplySurb {
packet,
first_hop_address,
key_rotation: self.key_rotation,
})
}
}
pub struct AppliedReplySurb {
pub(crate) packet: NymPacket,
pub(crate) first_hop_address: NymNodeRoutingAddress,
pub(crate) key_rotation: SphinxKeyRotation,
}
impl AppliedReplySurb {
pub fn first_hop_address(&self) -> NymNodeRoutingAddress {
self.first_hop_address
}
pub fn key_rotation(&self) -> SphinxKeyRotation {
self.key_rotation
}
pub fn into_packet(self) -> NymPacket {
self.packet
}
}
@@ -1,16 +1,16 @@
// Copyright 2022 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use crate::requests::v1::{AdditionalSurbsV1, DataV1, HeartbeatV1};
use crate::requests::v2::{AdditionalSurbsV2, DataV2, HeartbeatV2};
use crate::{ReplySurbError, ReplySurbWithKeyRotation};
use crate::{ReplySurb, ReplySurbError};
use nym_sphinx_addressing::clients::{Recipient, RecipientFormattingError};
use nym_sphinx_params::key_rotation::InvalidSphinxKeyRotation;
use rand::{CryptoRng, RngCore};
use std::fmt::{Display, Formatter};
use std::mem;
use thiserror::Error;
use crate::requests::v1::{AdditionalSurbsV1, DataV1, HeartbeatV1};
use crate::requests::v2::{AdditionalSurbsV2, DataV2, HeartbeatV2};
#[cfg(target_arch = "wasm32")]
use wasm_bindgen::prelude::*;
@@ -84,7 +84,7 @@ impl AnonymousSenderTag {
#[derive(Debug, Error)]
pub enum InvalidReplyRequestError {
#[error("Did not provide sufficient number of bytes to deserialise a valid request")]
#[error("Did not provide sufficient number of bytes to deserialize a valid request")]
RequestTooShortToDeserialize,
#[error("{received} is not a valid content tag for a repliable message")]
@@ -93,13 +93,10 @@ pub enum InvalidReplyRequestError {
#[error("{received} is not a valid content tag for a reply message")]
InvalidReplyContentTag { received: u8 },
#[error("failed to deserialise sphinx key rotation details: {0}")]
MalformedSphinxKeyRotation(#[from] InvalidSphinxKeyRotation),
#[error("failed to deserialise recipient information: {0}")]
#[error("failed to deserialize recipient information - {0}")]
MalformedRecipient(#[from] RecipientFormattingError),
#[error("failed to deserialise replySURB: {0}")]
#[error("failed to deserialize replySURB - {0}")]
MalformedReplySurb(#[from] ReplySurbError),
}
@@ -139,7 +136,7 @@ impl RepliableMessage {
use_legacy_surb_format: bool,
data: Vec<u8>,
sender_tag: AnonymousSenderTag,
reply_surbs: Vec<ReplySurbWithKeyRotation>,
reply_surbs: Vec<ReplySurb>,
) -> Self {
let content = if use_legacy_surb_format {
RepliableMessageContent::Data(DataV1 {
@@ -162,7 +159,7 @@ impl RepliableMessage {
pub fn new_additional_surbs(
use_legacy_surb_format: bool,
sender_tag: AnonymousSenderTag,
reply_surbs: Vec<ReplySurbWithKeyRotation>,
reply_surbs: Vec<ReplySurb>,
) -> Self {
let content = if use_legacy_surb_format {
RepliableMessageContent::AdditionalSurbs(AdditionalSurbsV1 { reply_surbs })
@@ -487,10 +484,9 @@ mod tests {
use crate::requests::v1::{AdditionalSurbsV1, DataV1, HeartbeatV1};
use crate::requests::v2::{AdditionalSurbsV2, DataV2, HeartbeatV2};
use crate::requests::{AnonymousSenderTag, RepliableMessageContent, ReplyMessageContent};
use crate::{ReplySurb, ReplySurbWithKeyRotation, SurbEncryptionKey};
use crate::{ReplySurb, SurbEncryptionKey};
use nym_crypto::asymmetric::{ed25519, x25519};
use nym_sphinx_addressing::clients::Recipient;
use nym_sphinx_params::SphinxKeyRotation;
use nym_sphinx_types::{
Delay, Destination, DestinationAddressBytes, Node, NodeAddressBytes, PrivateKey,
SURBMaterial, NODE_ADDRESS_LENGTH, X25519_WITH_EXPLICIT_PAYLOAD_KEYS_VERSION,
@@ -575,12 +571,10 @@ mod tests {
n: usize,
legacy: bool,
hops: u8,
) -> Vec<ReplySurbWithKeyRotation> {
) -> Vec<ReplySurb> {
let mut surbs = Vec::with_capacity(n);
for _ in 0..n {
surbs.push(
reply_surb(rng, legacy, hops).with_key_rotation(SphinxKeyRotation::Unknown),
)
surbs.push(reply_surb(rng, legacy, hops))
}
surbs
}
@@ -2,7 +2,7 @@
// SPDX-License-Identifier: Apache-2.0
use crate::requests::InvalidReplyRequestError;
use crate::{ReplySurb, ReplySurbWithKeyRotation};
use crate::ReplySurb;
use nym_sphinx_types::PAYLOAD_KEY_SIZE;
use std::fmt::Display;
use std::mem;
@@ -14,10 +14,10 @@ const fn v1_reply_surb_serialised_len() -> usize {
ReplySurb::BASE_OVERHEAD + 4 * PAYLOAD_KEY_SIZE
}
fn v1_reply_surbs_serialised_len(surbs: &[ReplySurbWithKeyRotation]) -> usize {
fn v1_reply_surbs_serialised_len(surbs: &[ReplySurb]) -> usize {
// sanity checks; this should probably be removed later on
if let Some(reply_surb) = surbs.first() {
if reply_surb.inner.surb.uses_key_seeds() {
if reply_surb.surb.uses_key_seeds() {
error!("using v1 surbs encoding with updated structure - the surbs will be unusable")
}
}
@@ -30,7 +30,7 @@ fn v1_reply_surbs_serialised_len(surbs: &[ReplySurbWithKeyRotation]) -> usize {
// NUM_SURBS (u32) || SURB_DATA
fn recover_reply_surbs_v1(
bytes: &[u8],
) -> Result<(Vec<ReplySurbWithKeyRotation>, usize), InvalidReplyRequestError> {
) -> Result<(Vec<ReplySurb>, usize), InvalidReplyRequestError> {
let mut consumed = mem::size_of::<u32>();
if bytes.len() < consumed {
return Err(InvalidReplyRequestError::RequestTooShortToDeserialize);
@@ -45,7 +45,7 @@ fn recover_reply_surbs_v1(
let mut reply_surbs = Vec::with_capacity(num_surbs as usize);
for _ in 0..num_surbs as usize {
let surb_bytes = &bytes[consumed..consumed + surb_size];
let reply_surb = ReplySurb::from_bytes(surb_bytes)?.to_legacy();
let reply_surb = ReplySurb::from_bytes(surb_bytes)?;
reply_surbs.push(reply_surb);
consumed += surb_size;
@@ -55,21 +55,19 @@ fn recover_reply_surbs_v1(
}
// length (u32) prefixed reply surbs with legacy serialisation of 4 hops and full payload keys attached
fn reply_surbs_bytes_v1(
reply_surbs: &[ReplySurbWithKeyRotation],
) -> impl Iterator<Item = u8> + use<'_> {
fn reply_surbs_bytes_v1(reply_surbs: &[ReplySurb]) -> impl Iterator<Item = u8> + use<'_> {
let num_surbs = reply_surbs.len() as u32;
num_surbs
.to_be_bytes()
.into_iter()
.chain(reply_surbs.iter().flat_map(|s| s.inner.to_bytes()))
.chain(reply_surbs.iter().flat_map(|s| s.to_bytes()))
}
#[derive(Debug)]
pub struct DataV1 {
pub message: Vec<u8>,
pub reply_surbs: Vec<ReplySurbWithKeyRotation>,
pub reply_surbs: Vec<ReplySurb>,
}
impl Display for DataV1 {
@@ -85,7 +83,7 @@ impl Display for DataV1 {
#[derive(Debug)]
pub struct AdditionalSurbsV1 {
pub reply_surbs: Vec<ReplySurbWithKeyRotation>,
pub reply_surbs: Vec<ReplySurb>,
}
impl Display for AdditionalSurbsV1 {
@@ -100,7 +98,7 @@ impl Display for AdditionalSurbsV1 {
#[derive(Debug)]
pub struct HeartbeatV1 {
pub additional_reply_surbs: Vec<ReplySurbWithKeyRotation>,
pub additional_reply_surbs: Vec<ReplySurb>,
}
impl Display for HeartbeatV1 {
@@ -2,8 +2,7 @@
// SPDX-License-Identifier: Apache-2.0
use crate::requests::InvalidReplyRequestError;
use crate::{ReplySurb, ReplySurbWithKeyRotation};
use nym_sphinx_params::SphinxKeyRotation;
use crate::ReplySurb;
use nym_sphinx_types::constants::PAYLOAD_KEY_SEED_SIZE;
use std::fmt::Display;
use std::iter::once;
@@ -14,29 +13,21 @@ const fn v2_reply_surb_serialised_len(num_hops: u8) -> usize {
}
// sphinx doesn't support more than 5 hops (so cast to u8 is safe)
// ASSUMPTION: all surbs are generated with the same parameters (if they're not, then the client is hurting itself),
// which includes the same number of hops and the same underlying sphinx key rotation
fn reply_surbs_hops(reply_surbs: &[ReplySurbWithKeyRotation]) -> u8 {
// ASSUMPTION: all surbs are generated with the same parameters (if they're not, then the client is hurting itself)
fn reply_surbs_hops(reply_surbs: &[ReplySurb]) -> u8 {
reply_surbs
.first()
.map(|reply_surb| reply_surb.inner.surb.materials_count() as u8)
.map(|reply_surb| reply_surb.surb.materials_count() as u8)
.unwrap_or_default()
}
fn key_rotation(reply_surbs: &[ReplySurbWithKeyRotation]) -> SphinxKeyRotation {
reply_surbs
.first()
.map(|reply_surb| reply_surb.key_rotation)
.unwrap_or_default()
}
fn v2_reply_surbs_serialised_len(surbs: &[ReplySurbWithKeyRotation]) -> usize {
fn v2_reply_surbs_serialised_len(surbs: &[ReplySurb]) -> usize {
let num_surbs = surbs.len();
let num_hops = reply_surbs_hops(surbs);
// sanity checks; this should probably be removed later on
if let Some(reply_surb) = surbs.first() {
if !reply_surb.inner.surb.uses_key_seeds() {
if !reply_surb.surb.uses_key_seeds() {
error!("using v2 surbs encoding with legacy structure - the surbs will be unusable")
}
}
@@ -44,14 +35,14 @@ fn v2_reply_surbs_serialised_len(surbs: &[ReplySurbWithKeyRotation]) -> usize {
// when serialising surbs are always prepended with:
// - u16-encoded count,
// - u8-encoded number of hops
// - u8-encoded sphinx key rotation (or unused for 'old' variant)
// - u8 reserved value
4 + num_surbs * v2_reply_surb_serialised_len(num_hops)
}
// NUM_SURBS (u16) || HOPS (u8) || KEY ROTATION (u8) || SURB_DATA
// NUM_SURBS (u16) || HOPS (u8) || RESERVED (u8) || SURB_DATA
fn recover_reply_surbs_v2(
bytes: &[u8],
) -> Result<(Vec<ReplySurbWithKeyRotation>, usize), InvalidReplyRequestError> {
) -> Result<(Vec<ReplySurb>, usize), InvalidReplyRequestError> {
if bytes.len() < 4 {
return Err(InvalidReplyRequestError::RequestTooShortToDeserialize);
}
@@ -59,7 +50,7 @@ fn recover_reply_surbs_v2(
// we're not attaching more than 65k surbs...
let num_surbs = u16::from_be_bytes([bytes[0], bytes[1]]);
let num_hops = bytes[2];
let key_rotation = SphinxKeyRotation::try_from(bytes[3])?;
let _reserved = bytes[3];
let mut consumed = 4;
let surb_size = v2_reply_surb_serialised_len(num_hops);
@@ -70,7 +61,7 @@ fn recover_reply_surbs_v2(
let mut reply_surbs = Vec::with_capacity(num_surbs as usize);
for _ in 0..num_surbs as usize {
let surb_bytes = &bytes[consumed..consumed + surb_size];
let reply_surb = ReplySurb::from_bytes(surb_bytes)?.with_key_rotation(key_rotation);
let reply_surb = ReplySurb::from_bytes(surb_bytes)?;
reply_surbs.push(reply_surb);
consumed += surb_size;
@@ -79,25 +70,23 @@ fn recover_reply_surbs_v2(
Ok((reply_surbs, consumed))
}
fn reply_surbs_bytes_v2(
reply_surbs: &[ReplySurbWithKeyRotation],
) -> impl Iterator<Item = u8> + use<'_> {
fn reply_surbs_bytes_v2(reply_surbs: &[ReplySurb]) -> impl Iterator<Item = u8> + use<'_> {
let num_surbs = reply_surbs.len() as u16;
let num_hops = reply_surbs_hops(reply_surbs);
let key_rotation = key_rotation(reply_surbs) as u8;
let reserved = 0;
num_surbs
.to_be_bytes()
.into_iter()
.chain(once(num_hops))
.chain(once(key_rotation))
.chain(reply_surbs.iter().flat_map(|surb| surb.inner.to_bytes()))
.chain(once(reserved))
.chain(reply_surbs.iter().flat_map(|surb| surb.to_bytes()))
}
#[derive(Debug)]
pub struct DataV2 {
pub message: Vec<u8>,
pub reply_surbs: Vec<ReplySurbWithKeyRotation>,
pub reply_surbs: Vec<ReplySurb>,
}
impl Display for DataV2 {
@@ -113,7 +102,7 @@ impl Display for DataV2 {
#[derive(Debug)]
pub struct AdditionalSurbsV2 {
pub reply_surbs: Vec<ReplySurbWithKeyRotation>,
pub reply_surbs: Vec<ReplySurb>,
}
impl Display for AdditionalSurbsV2 {
@@ -128,7 +117,7 @@ impl Display for AdditionalSurbsV2 {
#[derive(Debug)]
pub struct HeartbeatV2 {
pub additional_reply_surbs: Vec<ReplySurbWithKeyRotation>,
pub additional_reply_surbs: Vec<ReplySurb>,
}
impl Display for HeartbeatV2 {
+2 -12
View File
@@ -10,9 +10,7 @@ use nym_sphinx_addressing::nodes::NymNodeRoutingAddress;
use nym_sphinx_chunking::fragment::COVER_FRAG_ID;
use nym_sphinx_forwarding::packet::MixPacket;
use nym_sphinx_params::packet_sizes::PacketSize;
use nym_sphinx_params::{
PacketEncryptionAlgorithm, PacketHkdfAlgorithm, PacketType, SphinxKeyRotation,
};
use nym_sphinx_params::{PacketEncryptionAlgorithm, PacketHkdfAlgorithm, PacketType};
use nym_sphinx_types::NymPacket;
use nym_topology::{NymRouteProvider, NymTopologyError};
use rand::{CryptoRng, RngCore};
@@ -127,9 +125,6 @@ where
let delays = nym_sphinx_routing::generate_hop_delays(average_packet_delay, route.len());
let destination = full_address.as_sphinx_destination();
let rotation_id = topology.current_key_rotation();
let sphinx_key_rotation = SphinxKeyRotation::from(rotation_id);
let first_hop_address =
NymNodeRoutingAddress::try_from(route.first().unwrap().address).unwrap();
@@ -151,12 +146,7 @@ where
)?,
};
Ok(MixPacket::new(
first_hop_address,
packet,
packet_type,
sphinx_key_rotation,
))
Ok(MixPacket::new(first_hop_address, packet, packet_type))
}
/// Helper function used to determine if given message represents a loop cover message.
+1 -1
View File
@@ -11,5 +11,5 @@ repository = { workspace = true }
nym-sphinx-addressing = { path = "../addressing" }
nym-sphinx-params = { path = "../params" }
nym-sphinx-types = { path = "../types", features = ["sphinx", "outfox"] }
nym-sphinx-anonymous-replies = { path = "../anonymous-replies" }
nym-outfox = { path = "../../../nym-outfox" }
thiserror = { workspace = true }
+34 -105
View File
@@ -2,36 +2,24 @@
// SPDX-License-Identifier: Apache-2.0
use nym_sphinx_addressing::nodes::{NymNodeRoutingAddress, NymNodeRoutingAddressError};
use nym_sphinx_params::{PacketSize, PacketType, SphinxKeyRotation};
use nym_sphinx_params::{PacketSize, PacketType};
use nym_sphinx_types::{NymPacket, NymPacketError};
use nym_sphinx_anonymous_replies::reply_surb::AppliedReplySurb;
use nym_sphinx_params::key_rotation::InvalidSphinxKeyRotation;
use nym_sphinx_params::packet_sizes::InvalidPacketSize;
use nym_sphinx_params::packet_types::InvalidPacketType;
use std::net::SocketAddr;
use std::fmt::{self, Debug, Formatter};
use thiserror::Error;
#[derive(Debug, Error)]
pub enum MixPacketFormattingError {
#[error("too few bytes provided to recover from bytes")]
TooFewBytesProvided,
#[error("provided packet mode is invalid: {0}")]
InvalidPacketType(#[from] InvalidPacketType),
#[error("received request had an invalid packet size: {0}")]
InvalidPacketSize(#[from] InvalidPacketSize),
#[error("provided key rotation is invalid: {0}")]
InvalidKeyRotation(#[from] InvalidSphinxKeyRotation),
#[error("provided packet mode is invalid")]
InvalidPacketType,
#[error("received request had invalid size - received {0}")]
InvalidPacketSize(usize),
#[error("address field was incorrectly encoded")]
InvalidAddress,
#[error("received sphinx packet was malformed")]
MalformedSphinxPacket,
#[error("Packet: {0}")]
Packet(#[from] NymPacketError),
}
@@ -42,12 +30,20 @@ impl From<NymNodeRoutingAddressError> for MixPacketFormattingError {
}
}
#[derive(Debug)]
pub struct MixPacket {
next_hop: NymNodeRoutingAddress,
packet: NymPacket,
packet_type: PacketType,
key_rotation: SphinxKeyRotation,
}
impl Debug for MixPacket {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
write!(
f,
"MixPacket to {:?} with packet_type {:?}. Packet {:?}",
self.next_hop, self.packet_type, self.packet
)
}
}
impl MixPacket {
@@ -55,25 +51,11 @@ impl MixPacket {
next_hop: NymNodeRoutingAddress,
packet: NymPacket,
packet_type: PacketType,
key_rotation: SphinxKeyRotation,
) -> Self {
MixPacket {
next_hop,
packet,
packet_type,
key_rotation,
}
}
pub fn from_applied_surb(
applied_reply_surb: AppliedReplySurb,
packet_type: PacketType,
) -> Self {
MixPacket {
next_hop: applied_reply_surb.first_hop_address(),
key_rotation: applied_reply_surb.key_rotation(),
packet: applied_reply_surb.into_packet(),
packet_type,
}
}
@@ -81,10 +63,6 @@ impl MixPacket {
self.next_hop
}
pub fn next_hop_address(&self) -> SocketAddr {
self.next_hop.into()
}
pub fn packet(&self) -> &NymPacket {
&self.packet
}
@@ -93,90 +71,41 @@ impl MixPacket {
self.packet
}
pub fn key_rotation(&self) -> SphinxKeyRotation {
self.key_rotation
}
pub fn packet_type(&self) -> PacketType {
self.packet_type
}
// the message is formatted as follows:
// packet_type || FIRST_HOP || packet
pub fn try_from_v1_bytes(b: &[u8]) -> Result<Self, MixPacketFormattingError> {
// we need at least 1 byte to read packet type and another one to read type of the encoded first hop address
if b.len() < 2 {
return Err(MixPacketFormattingError::TooFewBytesProvided);
}
let packet_type = PacketType::try_from(b[0])?;
pub fn try_from_bytes(b: &[u8]) -> Result<Self, MixPacketFormattingError> {
let packet_type = match PacketType::try_from(b[0]) {
Ok(mode) => mode,
Err(_) => return Err(MixPacketFormattingError::InvalidPacketType),
};
let next_hop = NymNodeRoutingAddress::try_from_bytes(&b[1..])?;
let addr_offset = next_hop.bytes_min_len();
let packet_data = &b[addr_offset + 1..];
let packet_size = packet_data.len();
if PacketSize::get_type(packet_size).is_err() {
Err(MixPacketFormattingError::InvalidPacketSize(packet_size))
} else {
let packet = match packet_type {
PacketType::Outfox => NymPacket::outfox_from_bytes(packet_data)?,
_ => NymPacket::sphinx_from_bytes(packet_data)?,
};
// make sure the received data length corresponds to a valid packet
let _ = PacketSize::get_type(packet_size)?;
let packet = match packet_type {
PacketType::Mix => NymPacket::sphinx_from_bytes(packet_data)?,
PacketType::Outfox => NymPacket::outfox_from_bytes(packet_data)?,
};
Ok(MixPacket {
next_hop,
packet,
packet_type,
key_rotation: SphinxKeyRotation::Unknown,
})
}
pub fn into_v1_bytes(self) -> Result<Vec<u8>, MixPacketFormattingError> {
Ok(std::iter::once(self.packet_type as u8)
.chain(self.next_hop.as_bytes())
.chain(self.packet.to_bytes()?)
.collect())
}
// the message is formatted as follows:
// packet_type || KEY_ROTATION || FIRST_HOP || packet
pub fn try_from_v2_bytes(b: &[u8]) -> Result<Self, MixPacketFormattingError> {
// we need at least 1 byte to read packet type, 1 byte to read key rotation
// and finally another one to read type of the encoded first hop address
if b.len() < 3 {
return Err(MixPacketFormattingError::TooFewBytesProvided);
Ok(MixPacket {
next_hop,
packet,
packet_type,
})
}
let packet_type = PacketType::try_from(b[0])?;
let key_rotation = SphinxKeyRotation::try_from(b[1])?;
let next_hop = NymNodeRoutingAddress::try_from_bytes(&b[2..])?;
let addr_offset = next_hop.bytes_min_len();
let packet_data = &b[addr_offset + 2..];
let packet_size = packet_data.len();
// make sure the received data length corresponds to a valid packet
let _ = PacketSize::get_type(packet_size)?;
let packet = match packet_type {
PacketType::Mix => NymPacket::sphinx_from_bytes(packet_data)?,
PacketType::Outfox => NymPacket::outfox_from_bytes(packet_data)?,
};
Ok(MixPacket {
next_hop,
packet,
packet_type,
key_rotation,
})
}
pub fn into_v2_bytes(self) -> Result<Vec<u8>, MixPacketFormattingError> {
pub fn into_bytes(self) -> Result<Vec<u8>, MixPacketFormattingError> {
Ok(std::iter::once(self.packet_type as u8)
.chain(std::iter::once(self.key_rotation as u8))
.chain(self.next_hop.as_bytes())
.chain(self.packet.to_bytes()?)
.collect())
+24 -61
View File
@@ -3,7 +3,6 @@
use crate::packet::{FramedNymPacket, Header};
use bytes::{Buf, BufMut, BytesMut};
use nym_sphinx_params::key_rotation::InvalidSphinxKeyRotation;
use nym_sphinx_params::packet_sizes::{InvalidPacketSize, PacketSize};
use nym_sphinx_params::packet_types::InvalidPacketType;
use nym_sphinx_params::packet_version::{InvalidPacketVersion, PacketVersion};
@@ -24,9 +23,6 @@ pub enum NymCodecError {
#[error("the packet version information was malformed: {0}")]
InvalidPacketVersion(#[from] InvalidPacketVersion),
#[error("the sphinx key rotation information was malformed: {0}")]
InvalidSphinxKeyRotation(#[from] InvalidSphinxKeyRotation),
#[error("received unsupported packet version {received}. max supported is {max_supported}")]
UnsupportedPacketVersion {
received: PacketVersion,
@@ -69,8 +65,8 @@ impl Decoder for NymCodec {
fn decode(&mut self, src: &mut BytesMut) -> Result<Option<Self::Item>, Self::Error> {
if src.is_empty() {
// can't do anything if we have no bytes, but let's reserve enough for the most
// conservative case, i.e. receiving a legacy ack packet
src.reserve(Header::INITIAL_SIZE + PacketSize::AckPacket.size());
// conservative case, i.e. receiving an ack packet
src.reserve(Header::SIZE + PacketSize::AckPacket.size());
return Ok(None);
}
@@ -81,20 +77,17 @@ impl Decoder for NymCodec {
None => return Ok(None), // we have some data but not enough to get header back
};
let header_size = header.encoded_size();
let packet_size = header.packet_size.size();
let frame_len = Header::SIZE + packet_size;
let frame_size = header_size + packet_size;
if src.len() < frame_size {
if src.len() < frame_len {
// we don't have enough bytes to read the rest of frame
// (we have already read the full header)
src.reserve(packet_size);
return Ok(None);
}
// advance buffer past the header - at this point we have enough bytes
src.advance(header_size);
src.advance(Header::SIZE);
let packet_bytes = src.split_to(packet_size);
let packet = if let Some(slice) = packet_bytes.get(..) {
// here it could be debatable whether stream is corrupt or not,
@@ -107,7 +100,8 @@ impl Decoder for NymCodec {
return Ok(None);
};
let framed_packet = FramedNymPacket { header, packet };
// let packet = SphinxPacket::from_bytes(&sphinx_packet_bytes)?;
let nymsphinx_packet = FramedNymPacket { header, packet };
// As per docs:
// Before returning from the function, implementations should ensure that the buffer
@@ -120,11 +114,11 @@ impl Decoder for NymCodec {
// we also assume the next packet coming from the same client will use exactly the same versioning
// as the current packet
let mut allocate_for_next_packet = header.encoded_size() + PacketSize::AckPacket.size();
let mut allocate_for_next_packet = Header::SIZE + PacketSize::AckPacket.size();
if !src.is_empty() {
match Header::decode(src) {
Ok(Some(next_header)) => {
allocate_for_next_packet = next_header.frame_size();
allocate_for_next_packet = Header::SIZE + next_header.packet_size.size();
}
Ok(None) => {
// we don't have enough information to know how much to reserve, fallback to the ack case
@@ -132,52 +126,22 @@ impl Decoder for NymCodec {
// the next frame will be malformed but let's leave handling the error to the next
// call to 'decode', as presumably, the current sphinx packet is still valid
Err(_) => return Ok(Some(framed_packet)),
Err(_) => return Ok(Some(nymsphinx_packet)),
};
}
src.reserve(allocate_for_next_packet);
Ok(Some(framed_packet))
Ok(Some(nymsphinx_packet))
}
}
#[cfg(test)]
mod packet_encoding {
use super::*;
use nym_sphinx_params::packet_version::{
CURRENT_PACKET_VERSION, INITIAL_PACKET_VERSION_NUMBER,
};
use nym_sphinx_params::PacketType;
use nym_sphinx_types::{
Delay as SphinxDelay, Destination, DestinationAddressBytes, Node, NodeAddressBytes,
NymPacket, PrivateKey, DESTINATION_ADDRESS_LENGTH, IDENTIFIER_LENGTH, NODE_ADDRESS_LENGTH,
PrivateKey, DESTINATION_ADDRESS_LENGTH, IDENTIFIER_LENGTH, NODE_ADDRESS_LENGTH,
};
fn dummy_header() -> Header {
Header {
packet_version: CURRENT_PACKET_VERSION,
packet_size: Default::default(),
key_rotation: Default::default(),
packet_type: Default::default(),
}
}
fn dummy_outfox() -> Header {
Header {
packet_type: PacketType::Outfox,
packet_size: PacketSize::OutfoxRegularPacket,
..dummy_legacy_header()
}
}
fn dummy_legacy_header() -> Header {
Header {
packet_version: PacketVersion::try_from(INITIAL_PACKET_VERSION_NUMBER).unwrap(),
packet_size: Default::default(),
key_rotation: Default::default(),
packet_type: Default::default(),
}
}
fn random_pubkey() -> nym_sphinx_types::PublicKey {
let private_key = PrivateKey::random();
(&private_key).into()
@@ -258,7 +222,7 @@ mod packet_encoding {
#[test]
fn whole_packet_can_be_decoded_from_a_valid_encoded_instance() {
let header = dummy_header();
let header = Default::default();
let sphinx_packet = make_valid_sphinx_packet(Default::default());
let sphinx_bytes = sphinx_packet.to_bytes().unwrap();
@@ -277,7 +241,7 @@ mod packet_encoding {
#[test]
fn whole_outfox_can_be_decoded_from_a_valid_encoded_instance() {
let header = dummy_outfox();
let header = Header::outfox();
let packet = make_valid_outfox_packet(PacketSize::OutfoxRegularPacket);
let packet_bytes = packet.to_bytes().unwrap();
@@ -305,7 +269,7 @@ mod packet_encoding {
assert!(NymCodec.decode(&mut empty_bytes).unwrap().is_none());
assert_eq!(
empty_bytes.capacity(),
Header::INITIAL_SIZE + PacketSize::AckPacket.size()
Header::SIZE + PacketSize::AckPacket.size()
);
}
@@ -323,14 +287,13 @@ mod packet_encoding {
let header = Header {
packet_version: PacketVersion::new(),
packet_size,
key_rotation: Default::default(),
packet_type: Default::default(),
..Default::default()
};
let mut bytes = BytesMut::new();
header.encode(&mut bytes);
assert!(NymCodec.decode(&mut bytes).unwrap().is_none());
assert_eq!(bytes.capacity(), Header::V8_SIZE + packet_size.size())
assert_eq!(bytes.capacity(), Header::SIZE + packet_size.size())
}
}
@@ -338,7 +301,7 @@ mod packet_encoding {
fn for_full_frame_with_versioned_header() {
// if full frame is used exactly, there should be enough space for header + ack packet
let packet = FramedNymPacket {
header: dummy_header(),
header: Header::default(),
packet: make_valid_sphinx_packet(Default::default()),
};
@@ -347,7 +310,7 @@ mod packet_encoding {
assert!(NymCodec.decode(&mut bytes).unwrap().is_some());
assert_eq!(
bytes.capacity(),
Header::V8_SIZE + PacketSize::AckPacket.size()
Header::SIZE + PacketSize::AckPacket.size()
);
}
@@ -364,7 +327,7 @@ mod packet_encoding {
for packet_size in packet_sizes {
let first_packet = FramedNymPacket {
header: dummy_header(),
header: Header::default(),
packet: make_valid_sphinx_packet(Default::default()),
};
@@ -383,12 +346,12 @@ mod packet_encoding {
#[test]
fn can_decode_two_packets_immediately() {
let packet1 = FramedNymPacket {
header: dummy_header(),
header: Header::default(),
packet: make_valid_sphinx_packet(Default::default()),
};
let packet2 = FramedNymPacket {
header: dummy_header(),
header: Header::default(),
packet: make_valid_sphinx_packet(Default::default()),
};
@@ -405,12 +368,12 @@ mod packet_encoding {
#[test]
fn can_decode_two_packets_in_separate_calls() {
let packet1 = FramedNymPacket {
header: dummy_header(),
header: Header::default(),
packet: make_valid_sphinx_packet(Default::default()),
};
let packet2 = FramedNymPacket {
header: dummy_header(),
header: Header::default(),
packet: make_valid_sphinx_packet(Default::default()),
};
+22 -84
View File
@@ -3,8 +3,6 @@
use crate::codec::NymCodecError;
use bytes::{BufMut, BytesMut};
use nym_sphinx_forwarding::packet::MixPacket;
use nym_sphinx_params::key_rotation::SphinxKeyRotation;
use nym_sphinx_params::packet_sizes::PacketSize;
use nym_sphinx_params::packet_version::{PacketVersion, CURRENT_PACKET_VERSION};
use nym_sphinx_params::PacketType;
@@ -19,20 +17,8 @@ pub struct FramedNymPacket {
pub(crate) packet: NymPacket,
}
impl From<MixPacket> for FramedNymPacket {
fn from(packet: MixPacket) -> Self {
let typ = packet.packet_type();
let rot = packet.key_rotation();
FramedNymPacket::new(packet.into_packet(), typ, rot)
}
}
impl FramedNymPacket {
pub fn new(
packet: NymPacket,
packet_type: PacketType,
key_rotation: SphinxKeyRotation,
) -> Self {
pub fn new(packet: NymPacket, packet_type: PacketType) -> Self {
// If this fails somebody is using the library in a super incorrect way, because they
// already managed to somehow create a sphinx packet
let packet_size = PacketSize::get_type(packet.len()).unwrap();
@@ -40,7 +26,6 @@ impl FramedNymPacket {
let header = Header {
packet_version: PacketVersion::new(),
packet_size,
key_rotation,
packet_type,
};
@@ -67,10 +52,6 @@ impl FramedNymPacket {
&self.packet
}
pub fn key_rotation(&self) -> SphinxKeyRotation {
self.header.key_rotation
}
pub fn is_sphinx(&self) -> bool {
self.packet.is_sphinx()
}
@@ -79,16 +60,13 @@ impl FramedNymPacket {
// Contains any metadata that might be useful for sending between mix nodes.
// TODO: in theory all those data could be put in a single `u8` by setting appropriate bits,
// but would that really be worth it?
#[derive(Debug, PartialEq, Eq, Copy, Clone)]
#[derive(Debug, Default, PartialEq, Eq, Copy, Clone)]
pub struct Header {
/// Represents the wire format version used to construct this packet.
pub packet_version: PacketVersion,
pub(crate) packet_version: PacketVersion,
/// Represents type and consequently size of the included SphinxPacket.
pub packet_size: PacketSize,
/// Represents information regarding which key rotation has been used for constructing this packet.
pub key_rotation: SphinxKeyRotation,
pub(crate) packet_size: PacketSize,
/// Represents whether this packet is sent in a `vpn_mode` meaning it should not get delayed
/// and shared keys might get reused. Mixnodes are capable of inferring this mode from the
@@ -99,48 +77,35 @@ pub struct Header {
/// (note: this will be behind some encryption, either something implemented by us or some SSL action)
// Note: currently packet_type is deprecated but is still left as a concept behind to not break
// compatibility with existing network
pub packet_type: PacketType,
pub(crate) packet_type: PacketType,
}
impl Header {
pub(crate) const INITIAL_SIZE: usize = 3;
pub(crate) const V8_SIZE: usize = 4;
pub(crate) const SIZE: usize = 3;
pub fn outfox() -> Header {
Header {
packet_version: PacketVersion::default(),
packet_size: PacketSize::OutfoxRegularPacket,
packet_type: PacketType::Outfox,
}
}
pub(crate) fn encode(&self, dst: &mut BytesMut) {
let len = self.encoded_size();
if dst.len() < len {
dst.reserve(len);
}
dst.reserve(Self::SIZE);
dst.put_u8(self.packet_version.as_u8());
dst.put_u8(self.packet_size as u8);
dst.put_u8(self.packet_type as u8);
if !self.packet_version.is_initial() {
dst.put_u8(self.key_rotation as u8)
}
// reserve bytes for the actual packet
dst.reserve(self.packet_size.size());
}
pub(crate) fn frame_size(&self) -> usize {
self.encoded_size() + self.packet_size.size()
}
pub(crate) fn encoded_size(&self) -> usize {
if self.packet_version.is_initial() {
Self::INITIAL_SIZE
} else {
Self::V8_SIZE
}
}
pub(crate) fn decode(src: &mut BytesMut) -> Result<Option<Self>, NymCodecError> {
if src.len() < Self::INITIAL_SIZE {
if src.len() < Self::SIZE {
// can't do anything if we don't have enough bytes - but reserve enough for the next call
src.reserve(Self::INITIAL_SIZE);
src.reserve(Self::SIZE);
return Ok(None);
}
@@ -154,23 +119,10 @@ impl Header {
});
}
// we need to be able to decode the full header
if !packet_version.is_initial() && src.len() < Self::V8_SIZE {
src.reserve(1);
return Ok(None);
}
let key_rotation = if packet_version.is_initial() {
SphinxKeyRotation::Unknown
} else {
SphinxKeyRotation::try_from(src[3])?
};
Ok(Some(Header {
packet_version,
packet_size: PacketSize::try_from(src[1])?,
packet_type: PacketType::try_from(src[2])?,
key_rotation,
}))
}
}
@@ -178,20 +130,10 @@ impl Header {
#[cfg(test)]
mod header_encoding {
use super::*;
use nym_sphinx_params::packet_version::INITIAL_PACKET_VERSION_NUMBER;
fn dummy_header() -> Header {
Header {
packet_version: CURRENT_PACKET_VERSION,
packet_size: Default::default(),
key_rotation: Default::default(),
packet_type: Default::default(),
}
}
#[test]
fn header_can_be_decoded_from_a_valid_encoded_instance() {
let header = dummy_header();
let header = Header::default();
let mut bytes = BytesMut::new();
header.encode(&mut bytes);
let decoded = Header::decode(&mut bytes).unwrap().unwrap();
@@ -211,7 +153,6 @@ mod header_encoding {
PacketVersion::new().as_u8(),
unknown_packet_size,
PacketType::default() as u8,
SphinxKeyRotation::EvenRotation as u8,
]
.as_ref(),
);
@@ -226,9 +167,7 @@ mod header_encoding {
let mut bytes = BytesMut::from(
[
PacketVersion::try_from(INITIAL_PACKET_VERSION_NUMBER)
.unwrap()
.as_u8(),
PacketVersion::new().as_u8(),
PacketSize::default() as u8,
unknown_packet_type,
]
@@ -242,12 +181,12 @@ mod header_encoding {
let mut empty_bytes = BytesMut::new();
let decode_attempt_1 = Header::decode(&mut empty_bytes).unwrap();
assert!(decode_attempt_1.is_none());
assert!(empty_bytes.capacity() > Header::V8_SIZE);
assert!(empty_bytes.capacity() > Header::SIZE);
let mut empty_bytes = BytesMut::with_capacity(1);
let decode_attempt_2 = Header::decode(&mut empty_bytes).unwrap();
assert!(decode_attempt_2.is_none());
assert!(empty_bytes.capacity() > Header::V8_SIZE);
assert!(empty_bytes.capacity() > Header::SIZE);
}
#[test]
@@ -263,7 +202,7 @@ mod header_encoding {
let header = Header {
packet_version: PacketVersion::new(),
packet_size,
..dummy_header()
..Default::default()
};
let mut bytes = BytesMut::new();
header.encode(&mut bytes);
@@ -278,7 +217,6 @@ mod header_encoding {
let unchecked_header = Header {
packet_version: future_version,
packet_size: PacketSize::RegularPacket,
key_rotation: SphinxKeyRotation::EvenRotation,
packet_type: PacketType::Mix,
};
let mut bytes = BytesMut::new();
+14 -53
View File
@@ -5,7 +5,7 @@ use crate::packet::FramedNymPacket;
use nym_sphinx_acknowledgements::surb_ack::{SurbAck, SurbAckRecoveryError};
use nym_sphinx_addressing::nodes::{NymNodeRoutingAddress, NymNodeRoutingAddressError};
use nym_sphinx_forwarding::packet::MixPacket;
use nym_sphinx_params::{PacketSize, PacketType, SphinxKeyRotation};
use nym_sphinx_params::{PacketSize, PacketType};
use nym_sphinx_types::header::shared_secret::ExpandedSharedSecret;
use nym_sphinx_types::{
Delay as SphinxDelay, DestinationAddressBytes, NodeAddressBytes, NymPacket, NymPacketError,
@@ -103,18 +103,10 @@ pub enum PacketProcessingError {
#[error("attempted to partially process an outfox packet")]
PartialOutfoxProcessing,
#[error("the key needed for unwrapping this packet has already expired")]
ExpiredKey,
#[error("this packet has already been processed before")]
PacketReplay,
}
pub struct PartialyUnwrappedPacketWithKeyRotation {
pub packet: PartiallyUnwrappedPacket,
pub used_key_rotation: u32,
}
pub struct PartiallyUnwrappedPacket {
received_data: FramedNymPacket,
partial_result: PartialMixProcessingResult,
@@ -127,19 +119,16 @@ impl PartiallyUnwrappedPacket {
pub fn new(
received_data: FramedNymPacket,
sphinx_key: &PrivateKey,
) -> Result<Self, (FramedNymPacket, PacketProcessingError)> {
) -> Result<Self, PacketProcessingError> {
let partial_result = match received_data.packet() {
NymPacket::Sphinx(packet) => {
let expanded_shared_secret =
packet.header.compute_expanded_shared_secret(sphinx_key);
// don't continue if the header is malformed
if let Err(err) = packet
packet
.header
.ensure_header_integrity(&expanded_shared_secret)
{
return Err((received_data, err.into()));
}
.ensure_header_integrity(&expanded_shared_secret)?;
PartialMixProcessingResult::Sphinx {
expanded_shared_secret,
@@ -158,7 +147,6 @@ impl PartiallyUnwrappedPacket {
let packet_size = self.received_data.packet_size();
let packet_type = self.received_data.packet_type();
let key_rotation = self.received_data.header.key_rotation;
let packet = self.received_data.into_inner();
// currently partial unwrapping is only implemented for sphinx packets.
@@ -173,22 +161,12 @@ impl PartiallyUnwrappedPacket {
return Err(PacketProcessingError::PartialOutfoxProcessing);
};
let processed_packet = packet.process_with_expanded_secret(&expanded_shared_secret)?;
wrap_processed_sphinx_packet(processed_packet, packet_size, packet_type, key_rotation)
wrap_processed_sphinx_packet(processed_packet, packet_size, packet_type)
}
pub fn replay_tag(&self) -> Option<&[u8; REPLAY_TAG_SIZE]> {
self.partial_result.replay_tag()
}
pub fn with_key_rotation(
self,
used_key_rotation: u32,
) -> PartialyUnwrappedPacketWithKeyRotation {
PartialyUnwrappedPacketWithKeyRotation {
packet: self,
used_key_rotation,
}
}
}
impl From<(FramedNymPacket, PartialMixProcessingResult)> for PartiallyUnwrappedPacket {
@@ -208,14 +186,13 @@ pub fn process_framed_packet(
) -> Result<MixProcessingResult, PacketProcessingError> {
let packet_size = received.packet_size();
let packet_type = received.packet_type();
let key_rotation = received.key_rotation();
// unwrap the sphinx packet
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
perform_final_processing(processed_packet, packet_size, packet_type, key_rotation)
perform_final_processing(processed_packet, packet_size, packet_type)
}
fn perform_framed_unwrapping(
@@ -240,7 +217,6 @@ fn wrap_processed_sphinx_packet(
packet: nym_sphinx_types::ProcessedPacket,
packet_size: PacketSize,
packet_type: PacketType,
key_rotation: SphinxKeyRotation,
) -> Result<MixProcessingResult, PacketProcessingError> {
let processing_data = match packet.data {
ProcessedPacketData::ForwardHop {
@@ -252,7 +228,6 @@ fn wrap_processed_sphinx_packet(
next_hop_address,
delay,
packet_type,
key_rotation,
),
// right now there's no use for the surb_id included in the header - probably it should get removed from the
// sphinx all together?
@@ -265,7 +240,6 @@ fn wrap_processed_sphinx_packet(
payload.recover_plaintext()?,
packet_size,
packet_type,
key_rotation,
),
}?;
@@ -279,7 +253,6 @@ fn wrap_processed_outfox_packet(
packet: OutfoxProcessedPacket,
packet_size: PacketSize,
packet_type: PacketType,
key_rotation: SphinxKeyRotation,
) -> Result<MixProcessingResult, PacketProcessingError> {
let next_address = *packet.next_address();
let packet = packet.into_packet();
@@ -289,7 +262,6 @@ fn wrap_processed_outfox_packet(
packet.recover_plaintext()?.to_vec(),
packet_size,
packet_type,
key_rotation,
)?;
Ok(MixProcessingResult {
packet_version: MixPacketVersion::Outfox,
@@ -300,7 +272,6 @@ fn wrap_processed_outfox_packet(
NymNodeRoutingAddress::try_from_bytes(&next_address)?,
NymPacket::Outfox(packet),
PacketType::Outfox,
SphinxKeyRotation::Unknown,
);
Ok(MixProcessingResult {
packet_version: MixPacketVersion::Outfox,
@@ -316,14 +287,13 @@ fn perform_final_processing(
packet: NymProcessedPacket,
packet_size: PacketSize,
packet_type: PacketType,
key_rotation: SphinxKeyRotation,
) -> Result<MixProcessingResult, PacketProcessingError> {
match packet {
NymProcessedPacket::Sphinx(packet) => {
wrap_processed_sphinx_packet(packet, packet_size, packet_type, key_rotation)
wrap_processed_sphinx_packet(packet, packet_size, packet_type)
}
NymProcessedPacket::Outfox(packet) => {
wrap_processed_outfox_packet(packet, packet_size, packet_type, key_rotation)
wrap_processed_outfox_packet(packet, packet_size, packet_type)
}
}
}
@@ -333,10 +303,8 @@ fn process_final_hop(
payload: Vec<u8>,
packet_size: PacketSize,
packet_type: PacketType,
key_rotation: SphinxKeyRotation,
) -> Result<MixProcessingResultData, PacketProcessingError> {
let (forward_ack, message) =
split_into_ack_and_message(payload, packet_size, packet_type, key_rotation)?;
let (forward_ack, message) = split_into_ack_and_message(payload, packet_size, packet_type)?;
Ok(MixProcessingResultData::FinalHop {
final_hop_data: ProcessedFinalHop {
@@ -351,7 +319,6 @@ fn split_into_ack_and_message(
data: Vec<u8>,
packet_size: PacketSize,
packet_type: PacketType,
key_rotation: SphinxKeyRotation,
) -> Result<(Option<MixPacket>, Vec<u8>), PacketProcessingError> {
match packet_size {
PacketSize::AckPacket | PacketSize::OutfoxAckPacket => {
@@ -373,7 +340,7 @@ fn split_into_ack_and_message(
return Err(err.into());
}
};
let forward_ack = MixPacket::new(ack_first_hop, ack_packet, packet_type, key_rotation);
let forward_ack = MixPacket::new(ack_first_hop, ack_packet, packet_type);
Ok((Some(forward_ack), message))
}
}
@@ -401,11 +368,10 @@ fn process_forward_hop(
forward_address: NodeAddressBytes,
delay: SphinxDelay,
packet_type: PacketType,
key_rotation: SphinxKeyRotation,
) -> Result<MixProcessingResultData, PacketProcessingError> {
let next_hop_address = NymNodeRoutingAddress::try_from(forward_address)?;
let packet = MixPacket::new(next_hop_address, packet, packet_type, key_rotation);
let packet = MixPacket::new(next_hop_address, packet, packet_type);
Ok(MixProcessingResultData::ForwardHop {
packet,
delay: Some(delay),
@@ -456,13 +422,9 @@ mod tests {
#[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,
SphinxKeyRotation::EvenRotation,
)
.unwrap();
let (ack, message) =
split_into_ack_and_message(data.clone(), PacketSize::AckPacket, PacketType::Mix)
.unwrap();
assert!(ack.is_none());
assert_eq!(data, message)
}
@@ -474,7 +436,6 @@ mod tests {
data.clone(),
PacketSize::OutfoxAckPacket,
PacketType::Outfox,
SphinxKeyRotation::EvenRotation,
)
.unwrap();
assert!(ack.is_none());
@@ -1,50 +0,0 @@
// Copyright 2025 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use thiserror::Error;
#[derive(Default, Clone, Copy, Debug, PartialEq, Eq)]
#[repr(u8)]
pub enum SphinxKeyRotation {
// for legacy packets, where there's no explicit information which key has been used
#[default]
Unknown = 0,
OddRotation = 1,
EvenRotation = 2,
}
#[derive(Debug, Error)]
#[error("{received} is not a valid encoding of a sphinx key rotation")]
pub struct InvalidSphinxKeyRotation {
received: u8,
}
// convert from particular rotation id into SphinxKeyRotation variant
impl From<u32> for SphinxKeyRotation {
fn from(value: u32) -> Self {
if value == 0 || value == u32::MAX {
SphinxKeyRotation::Unknown
} else if value % 2 == 0 {
SphinxKeyRotation::EvenRotation
} else {
SphinxKeyRotation::OddRotation
}
}
}
// convert from an encoded SphinxKeyRotation into particular variant
// if value is actually provided, it MUST be one of the two. otherwise is invalid
impl TryFrom<u8> for SphinxKeyRotation {
type Error = InvalidSphinxKeyRotation;
fn try_from(value: u8) -> Result<Self, Self::Error> {
match value {
_ if value == (Self::Unknown as u8) => Ok(Self::Unknown),
_ if value == (Self::OddRotation as u8) => Ok(Self::OddRotation),
_ if value == (Self::EvenRotation as u8) => Ok(Self::EvenRotation),
received => Err(InvalidSphinxKeyRotation { received }),
}
}
}
-3
View File
@@ -9,12 +9,9 @@ use nym_crypto::Aes256GcmSiv;
type Aes128Ctr = ctr::Ctr64BE<Aes128>;
// Re-export for ease of use
pub use key_rotation::SphinxKeyRotation;
pub use packet_sizes::PacketSize;
pub use packet_types::PacketType;
pub use packet_version::PacketVersion;
pub mod key_rotation;
pub mod packet_sizes;
pub mod packet_types;
pub mod packet_version;
+1 -1
View File
@@ -11,7 +11,7 @@ use std::fmt;
use thiserror::Error;
#[derive(Error, Debug)]
#[error("{received} is not a valid packet type tag")]
#[error("{received} is not a valid packet mode tag")]
pub struct InvalidPacketType {
received: u8,
}
@@ -10,15 +10,13 @@ use thiserror::Error;
// - packet_version (starting with v1.1.0)
// - packet_size indicator
// - packet_type
// - sphinx key rotation (starting with v1.12.0/v1.13.0 - either Cheddar or Dolcelatte release)
// it also just so happens that the only valid values for packet_size indicator include values 1-6
// therefore if we receive byte `7` (or larger than that) we'll know we received a versioned packet,
// otherwise we should treat it as legacy
/// Increment it whenever we perform any breaking change in the wire format!
pub const INITIAL_PACKET_VERSION_NUMBER: u8 = 7;
pub const KEY_ROTATION_VERSION_NUMBER: u8 = 8;
pub const CURRENT_PACKET_VERSION_NUMBER: u8 = KEY_ROTATION_VERSION_NUMBER;
pub const CURRENT_PACKET_VERSION_NUMBER: u8 = INITIAL_PACKET_VERSION_NUMBER;
pub const CURRENT_PACKET_VERSION: PacketVersion =
PacketVersion::unchecked(CURRENT_PACKET_VERSION_NUMBER);
@@ -40,10 +38,6 @@ impl PacketVersion {
PacketVersion(CURRENT_PACKET_VERSION_NUMBER)
}
pub fn is_initial(&self) -> bool {
self.0 == INITIAL_PACKET_VERSION_NUMBER
}
const fn unchecked(version: u8) -> PacketVersion {
PacketVersion(version)
}

Some files were not shown because too many files have changed in this diff Show More