Compare commits
53 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| bda262810b | |||
| 1596277c85 | |||
| 1898853263 | |||
| 4e5ccf7926 | |||
| 0a8eb940bb | |||
| 34fb67602c | |||
| 766ae8dd8e | |||
| bd1fd73ba0 | |||
| b2266d04ef | |||
| 911b365609 | |||
| e9acc014ed | |||
| 0f66e5a154 | |||
| f8337d9b38 | |||
| 4fb252c44b | |||
| 17708cdf92 | |||
| a9c56ef9ac | |||
| 724420f97c | |||
| 0a48fa6172 | |||
| 5c8749a2e1 | |||
| 18d9d807f2 | |||
| 3a7393d316 | |||
| 6ce5f707c6 | |||
| 766a1d4497 | |||
| 35c83f0a31 | |||
| 01dd4a7972 | |||
| c2e335557e | |||
| 40e1cbc7a9 | |||
| c133e0e88b | |||
| 5b716633de | |||
| 834538300d | |||
| bd0d70f7cd | |||
| 979485c582 | |||
| d95f66bd90 | |||
| 906dfb2fb0 | |||
| 7daa726626 | |||
| 067f492ad6 | |||
| ed73ec9ce6 | |||
| 61606630bd | |||
| 2d3deeb424 | |||
| 3827dc357d | |||
| a70e9e23d3 | |||
| dc59149a5d | |||
| e418c7587a | |||
| 33339c085d | |||
| 863f329106 | |||
| 314a37cabe | |||
| 917f391948 | |||
| 0b4deda621 | |||
| d01867ca8d | |||
| 502c63b291 | |||
| a4e674c98b | |||
| 7f97f13799 | |||
| 85604e8305 |
@@ -38,15 +38,14 @@ jobs:
|
|||||||
rm -rf ci-builds || true
|
rm -rf ci-builds || true
|
||||||
mkdir -p $OUTPUT_DIR
|
mkdir -p $OUTPUT_DIR
|
||||||
echo $OUTPUT_DIR
|
echo $OUTPUT_DIR
|
||||||
|
|
||||||
- name: Install Dependencies (Linux)
|
- name: Install Dependencies (Linux)
|
||||||
run: sudo apt-get update && sudo apt-get -y install libudev-dev
|
run: sudo apt-get update && sudo apt-get -y install libudev-dev
|
||||||
|
|
||||||
- name: Sets env vars for tokio if set in manual dispatch inputs
|
- name: Sets env vars for tokio if set in manual dispatch inputs
|
||||||
run: |
|
|
||||||
echo 'RUSTFLAGS="--cfg tokio_unstable"' >> $GITHUB_ENV
|
|
||||||
if: github.event_name == 'workflow_dispatch' && inputs.add_tokio_unstable == true
|
if: github.event_name == 'workflow_dispatch' && inputs.add_tokio_unstable == true
|
||||||
|
run: |
|
||||||
|
echo "RUSTFLAGS=--cfg tokio_unstable" >> $GITHUB_ENV
|
||||||
|
echo "CARGO_FEATURES=--features tokio-console" >> $GITHUB_ENV
|
||||||
- name: Install Rust stable
|
- name: Install Rust stable
|
||||||
uses: actions-rs/toolchain@v1
|
uses: actions-rs/toolchain@v1
|
||||||
with:
|
with:
|
||||||
@@ -103,7 +102,6 @@ jobs:
|
|||||||
if [ ${{ github.event_name == 'workflow_dispatch' && inputs.enable_deb == true }} = true ]; then
|
if [ ${{ github.event_name == 'workflow_dispatch' && inputs.enable_deb == true }} = true ]; then
|
||||||
cp target/debian/*.deb $OUTPUT_DIR
|
cp target/debian/*.deb $OUTPUT_DIR
|
||||||
fi
|
fi
|
||||||
|
|
||||||
- name: Deploy branch to CI www
|
- name: Deploy branch to CI www
|
||||||
continue-on-error: true
|
continue-on-error: true
|
||||||
uses: easingthemes/ssh-deploy@main
|
uses: easingthemes/ssh-deploy@main
|
||||||
|
|||||||
@@ -38,7 +38,7 @@ jobs:
|
|||||||
strategy:
|
strategy:
|
||||||
fail-fast: false
|
fail-fast: false
|
||||||
matrix:
|
matrix:
|
||||||
os: [ arc-ubuntu-22.04, custom-windows-11, custom-macos-15 ]
|
os: [ arc-linux-latest, custom-windows-11, custom-macos-15 ]
|
||||||
runs-on: ${{ matrix.os }}
|
runs-on: ${{ matrix.os }}
|
||||||
env:
|
env:
|
||||||
CARGO_TERM_COLOR: always
|
CARGO_TERM_COLOR: always
|
||||||
@@ -46,9 +46,9 @@ jobs:
|
|||||||
RUSTUP_PERMIT_COPY_RENAME: 1
|
RUSTUP_PERMIT_COPY_RENAME: 1
|
||||||
steps:
|
steps:
|
||||||
- name: Install Dependencies (Linux)
|
- name: Install Dependencies (Linux)
|
||||||
run: sudo apt-get update && sudo apt-get -y install libwebkit2gtk-4.0-dev build-essential curl wget libssl-dev libgtk-3-dev libudev-dev squashfs-tools protobuf-compiler
|
run: sudo apt-get update && sudo apt-get -y install libwebkit2gtk-4.0-dev build-essential curl wget libssl-dev libgtk-3-dev libudev-dev squashfs-tools protobuf-compiler cmake
|
||||||
continue-on-error: true
|
continue-on-error: true
|
||||||
if: contains(matrix.os, 'ubuntu')
|
if: contains(matrix.os, 'linux')
|
||||||
|
|
||||||
- name: Check out repository code
|
- name: Check out repository code
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v4
|
||||||
@@ -63,7 +63,7 @@ jobs:
|
|||||||
|
|
||||||
# To avoid running out of disk space, skip generating debug symbols
|
# To avoid running out of disk space, skip generating debug symbols
|
||||||
- name: Set debug to false (unix)
|
- name: Set debug to false (unix)
|
||||||
if: contains(matrix.os, 'ubuntu') || contains(matrix.os, 'mac')
|
if: contains(matrix.os, 'linux') || contains(matrix.os, 'mac')
|
||||||
run: |
|
run: |
|
||||||
sed -i.bak 's/\[profile.dev\]/\[profile.dev\]\ndebug = false/' Cargo.toml
|
sed -i.bak 's/\[profile.dev\]/\[profile.dev\]\ndebug = false/' Cargo.toml
|
||||||
git diff
|
git diff
|
||||||
@@ -93,14 +93,14 @@ jobs:
|
|||||||
command: build
|
command: build
|
||||||
|
|
||||||
- name: Build all examples
|
- name: Build all examples
|
||||||
if: contains(matrix.os, 'ubuntu')
|
if: contains(matrix.os, 'linux')
|
||||||
uses: actions-rs/cargo@v1
|
uses: actions-rs/cargo@v1
|
||||||
with:
|
with:
|
||||||
command: build
|
command: build
|
||||||
args: --workspace --examples
|
args: --workspace --examples
|
||||||
|
|
||||||
- name: Run all tests
|
- name: Run all tests
|
||||||
if: contains(matrix.os, 'ubuntu')
|
if: contains(matrix.os, 'linux')
|
||||||
uses: actions-rs/cargo@v1
|
uses: actions-rs/cargo@v1
|
||||||
env:
|
env:
|
||||||
NYM_API: https://sandbox-nym-api1.nymtech.net/api
|
NYM_API: https://sandbox-nym-api1.nymtech.net/api
|
||||||
@@ -109,7 +109,7 @@ jobs:
|
|||||||
args: --workspace
|
args: --workspace
|
||||||
|
|
||||||
- name: Run expensive tests
|
- name: Run expensive tests
|
||||||
if: (github.ref == 'refs/heads/develop' || github.event.pull_request.base.ref == 'develop' || github.event.pull_request.base.ref == 'master') && contains(matrix.os, 'ubuntu')
|
if: (github.ref == 'refs/heads/develop' || github.event.pull_request.base.ref == 'develop' || github.event.pull_request.base.ref == 'master') && contains(matrix.os, 'linux')
|
||||||
uses: actions-rs/cargo@v1
|
uses: actions-rs/cargo@v1
|
||||||
with:
|
with:
|
||||||
command: test
|
command: test
|
||||||
|
|||||||
@@ -40,7 +40,8 @@ jobs:
|
|||||||
- name: Get version from cargo.toml
|
- name: Get version from cargo.toml
|
||||||
id: get_version
|
id: get_version
|
||||||
run: |
|
run: |
|
||||||
yq -oy '.package.version' ${{ env.WORKING_DIRECTORY }}/Cargo.toml
|
VERSION=$(yq -oy '.package.version' ${{ env.WORKING_DIRECTORY }}/Cargo.toml)
|
||||||
|
echo "result=$VERSION" >> $GITHUB_OUTPUT
|
||||||
|
|
||||||
- name: cleanup-gateway-probe-ref
|
- name: cleanup-gateway-probe-ref
|
||||||
id: cleanup_gateway_probe_ref
|
id: cleanup_gateway_probe_ref
|
||||||
@@ -52,13 +53,16 @@ jobs:
|
|||||||
- name: Set GIT_TAG variable
|
- name: Set GIT_TAG variable
|
||||||
run: echo "GIT_TAG=${{ env.WORKING_DIRECTORY }}-${{ steps.get_version.outputs.result }}-${{ steps.cleanup_gateway_probe_ref.outputs.git_ref }}" >> $GITHUB_ENV
|
run: echo "GIT_TAG=${{ env.WORKING_DIRECTORY }}-${{ steps.get_version.outputs.result }}-${{ steps.cleanup_gateway_probe_ref.outputs.git_ref }}" >> $GITHUB_ENV
|
||||||
|
|
||||||
- name: Set RELEASE_TAG variable
|
- name: Initialize RELEASE_TAG
|
||||||
|
run: echo "RELEASE_TAG=" >> $GITHUB_ENV
|
||||||
|
|
||||||
|
- name: Set RELEASE_TAG for release
|
||||||
if: github.event.inputs.release_image == 'true'
|
if: github.event.inputs.release_image == 'true'
|
||||||
run: echo "RELEASE_TAG=golden-" >> $GITHUB_ENV
|
run: echo "RELEASE_TAG=golden-" >> $GITHUB_ENV
|
||||||
|
|
||||||
- name: Set IMAGE_NAME_AND_TAGS variable
|
- name: Set IMAGE_NAME_AND_TAGS variable
|
||||||
run: echo "IMAGE_NAME_AND_TAGS=${{ env.CONTAINER_NAME }}:${{ env.RELEASE_TAG }}${{ steps.get_version.outputs.result }}-${{ steps.cleanup_gateway_probe_ref.outputs.git_ref }}" >> $GITHUB_ENV
|
run: echo "IMAGE_NAME_AND_TAGS=${{ env.CONTAINER_NAME }}:${{ env.RELEASE_TAG }}${{ steps.get_version.outputs.result }}-${{ steps.cleanup_gateway_probe_ref.outputs.git_ref }}" >> $GITHUB_ENV
|
||||||
|
|
||||||
- name: New env vars
|
- name: New env vars
|
||||||
run: echo "RELEASE_TAG='$RELEASE_TAG' GIT_TAG='$GIT_TAG' IMAGE_NAME_AND_TAGS='$IMAGE_NAME_AND_TAGS'"
|
run: echo "RELEASE_TAG='$RELEASE_TAG' GIT_TAG='$GIT_TAG' IMAGE_NAME_AND_TAGS='$IMAGE_NAME_AND_TAGS'"
|
||||||
|
|
||||||
|
|||||||
@@ -4,6 +4,42 @@ Post 1.0.0 release, the changelog format is based on [Keep a Changelog](https://
|
|||||||
|
|
||||||
## [Unreleased]
|
## [Unreleased]
|
||||||
|
|
||||||
|
## [2025.15-gruyere] (2025-08-20)
|
||||||
|
|
||||||
|
- Migrate strum to 0.27.2 ([#5960])
|
||||||
|
- WG exit policy scripts update ([#5921])
|
||||||
|
- Make DNS Resolver fallback optional ([#5920])
|
||||||
|
- nym-node debug command to reset providers db ([#5914])
|
||||||
|
- basic zulip client for sending messages ([#5913])
|
||||||
|
- chore: allow compatibility with 'CDLA-Permissive-2.0' ([#5910])
|
||||||
|
- feat: ecash liveness check ([#5890])
|
||||||
|
- Remove old free credential handle ([#5864])
|
||||||
|
|
||||||
|
[#5960]: https://github.com/nymtech/nym/pull/5960
|
||||||
|
[#5921]: https://github.com/nymtech/nym/pull/5921
|
||||||
|
[#5920]: https://github.com/nymtech/nym/pull/5920
|
||||||
|
[#5914]: https://github.com/nymtech/nym/pull/5914
|
||||||
|
[#5913]: https://github.com/nymtech/nym/pull/5913
|
||||||
|
[#5910]: https://github.com/nymtech/nym/pull/5910
|
||||||
|
[#5890]: https://github.com/nymtech/nym/pull/5890
|
||||||
|
[#5864]: https://github.com/nymtech/nym/pull/5864
|
||||||
|
|
||||||
|
## [2025.14-feta] (2025-08-05)
|
||||||
|
|
||||||
|
- chore: nym node tokio console ([#5909])
|
||||||
|
- Feature/dkg snapshot epoch ([#5900])
|
||||||
|
- Feature/dkg epoch dealers query ([#5899])
|
||||||
|
- sqlx-pool-guard: allocate more memory on windows ([#5896])
|
||||||
|
- Support mnemonic in the NS agent ([#5883])
|
||||||
|
- Allow PG database backend ([#5880])
|
||||||
|
|
||||||
|
[#5909]: https://github.com/nymtech/nym/pull/5909
|
||||||
|
[#5900]: https://github.com/nymtech/nym/pull/5900
|
||||||
|
[#5899]: https://github.com/nymtech/nym/pull/5899
|
||||||
|
[#5896]: https://github.com/nymtech/nym/pull/5896
|
||||||
|
[#5883]: https://github.com/nymtech/nym/pull/5883
|
||||||
|
[#5880]: https://github.com/nymtech/nym/pull/5880
|
||||||
|
|
||||||
## [2025.13-emmental] (2025-07-22)
|
## [2025.13-emmental] (2025-07-22)
|
||||||
|
|
||||||
- fix: don't allow mixnode running in exit mode ([#5898])
|
- fix: don't allow mixnode running in exit mode ([#5898])
|
||||||
|
|||||||
Generated
+776
-1491
File diff suppressed because it is too large
Load Diff
+21
-9
@@ -39,7 +39,8 @@ members = [
|
|||||||
"common/cosmwasm-smart-contracts/ecash-contract",
|
"common/cosmwasm-smart-contracts/ecash-contract",
|
||||||
"common/cosmwasm-smart-contracts/group-contract",
|
"common/cosmwasm-smart-contracts/group-contract",
|
||||||
"common/cosmwasm-smart-contracts/mixnet-contract",
|
"common/cosmwasm-smart-contracts/mixnet-contract",
|
||||||
"common/cosmwasm-smart-contracts/multisig-contract", "common/cosmwasm-smart-contracts/nym-performance-contract",
|
"common/cosmwasm-smart-contracts/multisig-contract",
|
||||||
|
"common/cosmwasm-smart-contracts/nym-performance-contract",
|
||||||
"common/cosmwasm-smart-contracts/nym-pool-contract",
|
"common/cosmwasm-smart-contracts/nym-pool-contract",
|
||||||
"common/cosmwasm-smart-contracts/vesting-contract",
|
"common/cosmwasm-smart-contracts/vesting-contract",
|
||||||
"common/credential-storage",
|
"common/credential-storage",
|
||||||
@@ -49,6 +50,8 @@ members = [
|
|||||||
"common/credentials-interface",
|
"common/credentials-interface",
|
||||||
"common/crypto",
|
"common/crypto",
|
||||||
"common/dkg",
|
"common/dkg",
|
||||||
|
"common/ecash-signer-check",
|
||||||
|
"common/ecash-signer-check-types",
|
||||||
"common/ecash-time",
|
"common/ecash-time",
|
||||||
"common/execute",
|
"common/execute",
|
||||||
"common/exit-policy",
|
"common/exit-policy",
|
||||||
@@ -89,7 +92,7 @@ members = [
|
|||||||
"common/socks5/requests",
|
"common/socks5/requests",
|
||||||
"common/statistics",
|
"common/statistics",
|
||||||
"common/store-cipher",
|
"common/store-cipher",
|
||||||
"common/task",
|
"common/task", "common/test-utils",
|
||||||
"common/ticketbooks-merkle",
|
"common/ticketbooks-merkle",
|
||||||
"common/topology",
|
"common/topology",
|
||||||
"common/tun",
|
"common/tun",
|
||||||
@@ -99,15 +102,22 @@ members = [
|
|||||||
"common/wasm/storage",
|
"common/wasm/storage",
|
||||||
"common/wasm/utils",
|
"common/wasm/utils",
|
||||||
"common/wireguard",
|
"common/wireguard",
|
||||||
|
"common/wireguard-private-metadata/client",
|
||||||
|
"common/wireguard-private-metadata/server",
|
||||||
|
"common/wireguard-private-metadata/shared",
|
||||||
|
"common/wireguard-private-metadata/tests",
|
||||||
"common/wireguard-types",
|
"common/wireguard-types",
|
||||||
|
"common/zulip-client",
|
||||||
"documentation/autodoc",
|
"documentation/autodoc",
|
||||||
"gateway",
|
"gateway",
|
||||||
"nym-api",
|
"nym-api",
|
||||||
"nym-api/nym-api-requests",
|
"nym-api/nym-api-requests",
|
||||||
|
"nym-authenticator-client",
|
||||||
"nym-browser-extension/storage",
|
"nym-browser-extension/storage",
|
||||||
"nym-credential-proxy/nym-credential-proxy",
|
"nym-credential-proxy/nym-credential-proxy",
|
||||||
"nym-credential-proxy/nym-credential-proxy-requests",
|
"nym-credential-proxy/nym-credential-proxy-requests",
|
||||||
"nym-credential-proxy/vpn-api-lib-wasm",
|
"nym-credential-proxy/vpn-api-lib-wasm",
|
||||||
|
"nym-ip-packet-client",
|
||||||
"nym-network-monitor",
|
"nym-network-monitor",
|
||||||
"nym-node",
|
"nym-node",
|
||||||
"nym-node-status-api/nym-node-status-agent",
|
"nym-node-status-api/nym-node-status-agent",
|
||||||
@@ -118,12 +128,12 @@ members = [
|
|||||||
"nym-outfox",
|
"nym-outfox",
|
||||||
"nym-statistics-api",
|
"nym-statistics-api",
|
||||||
"nym-validator-rewarder",
|
"nym-validator-rewarder",
|
||||||
|
"nym-wg-gateway-client",
|
||||||
"nyx-chain-watcher",
|
"nyx-chain-watcher",
|
||||||
"sdk/ffi/cpp",
|
"sdk/ffi/cpp",
|
||||||
"sdk/ffi/go",
|
"sdk/ffi/go",
|
||||||
"sdk/ffi/shared",
|
"sdk/ffi/shared",
|
||||||
"sdk/rust/nym-sdk",
|
"sdk/rust/nym-sdk",
|
||||||
"service-providers/authenticator",
|
|
||||||
"service-providers/common",
|
"service-providers/common",
|
||||||
"service-providers/ip-packet-router",
|
"service-providers/ip-packet-router",
|
||||||
"service-providers/network-requester",
|
"service-providers/network-requester",
|
||||||
@@ -161,7 +171,6 @@ default-members = [
|
|||||||
"nym-statistics-api",
|
"nym-statistics-api",
|
||||||
"nym-validator-rewarder",
|
"nym-validator-rewarder",
|
||||||
"nyx-chain-watcher",
|
"nyx-chain-watcher",
|
||||||
"service-providers/authenticator",
|
|
||||||
"service-providers/ip-packet-router",
|
"service-providers/ip-packet-router",
|
||||||
"service-providers/network-requester",
|
"service-providers/network-requester",
|
||||||
"tools/nymvisor",
|
"tools/nymvisor",
|
||||||
@@ -176,7 +185,7 @@ homepage = "https://nymtech.net"
|
|||||||
documentation = "https://nymtech.net"
|
documentation = "https://nymtech.net"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
license = "Apache-2.0"
|
license = "Apache-2.0"
|
||||||
rust-version = "1.80"
|
rust-version = "1.81"
|
||||||
readme = "README.md"
|
readme = "README.md"
|
||||||
|
|
||||||
[workspace.dependencies]
|
[workspace.dependencies]
|
||||||
@@ -218,7 +227,7 @@ clap_complete_fig = "4.5"
|
|||||||
colored = "2.2"
|
colored = "2.2"
|
||||||
comfy-table = "7.1.4"
|
comfy-table = "7.1.4"
|
||||||
console = "0.15.11"
|
console = "0.15.11"
|
||||||
console-subscriber = "0.1.1"
|
console-subscriber = "0.4.1"
|
||||||
console_error_panic_hook = "0.1"
|
console_error_panic_hook = "0.1"
|
||||||
const-str = "0.5.6"
|
const-str = "0.5.6"
|
||||||
const_format = "0.2.34"
|
const_format = "0.2.34"
|
||||||
@@ -317,11 +326,11 @@ si-scale = "0.2.3"
|
|||||||
snow = "0.9.6"
|
snow = "0.9.6"
|
||||||
sphinx-packet = "=0.6.0"
|
sphinx-packet = "=0.6.0"
|
||||||
sqlx = "0.8.6"
|
sqlx = "0.8.6"
|
||||||
strum = "0.26"
|
strum = "0.27.2"
|
||||||
strum_macros = "0.26"
|
strum_macros = "0.27.2"
|
||||||
subtle-encoding = "0.5"
|
subtle-encoding = "0.5"
|
||||||
syn = "1"
|
syn = "1"
|
||||||
sysinfo = "0.33.0"
|
sysinfo = "0.37.0"
|
||||||
tap = "1.0.1"
|
tap = "1.0.1"
|
||||||
tar = "0.4.44"
|
tar = "0.4.44"
|
||||||
tempfile = "3.20"
|
tempfile = "3.20"
|
||||||
@@ -435,6 +444,9 @@ opt-level = 'z'
|
|||||||
# lto = true
|
# lto = true
|
||||||
opt-level = 'z'
|
opt-level = 'z'
|
||||||
|
|
||||||
|
[workspace.lints.rust]
|
||||||
|
unexpected_cfgs = { level = "warn", check-cfg = ['cfg(tokio_unstable)'] }
|
||||||
|
|
||||||
[workspace.lints.clippy]
|
[workspace.lints.clippy]
|
||||||
unwrap_used = "deny"
|
unwrap_used = "deny"
|
||||||
expect_used = "deny"
|
expect_used = "deny"
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "nym-client"
|
name = "nym-client"
|
||||||
version = "1.1.59"
|
version = "1.1.61"
|
||||||
authors = ["Dave Hrycyszyn <futurechimp@users.noreply.github.com>", "Jędrzej Stuczyński <andrew@nymtech.net>"]
|
authors = ["Dave Hrycyszyn <futurechimp@users.noreply.github.com>", "Jędrzej Stuczyński <andrew@nymtech.net>"]
|
||||||
description = "Implementation of the Nym Client"
|
description = "Implementation of the Nym Client"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
|
|||||||
@@ -111,7 +111,7 @@ impl SocketClient {
|
|||||||
let dkg_query_client = if self.config.base.client.disabled_credentials_mode {
|
let dkg_query_client = if self.config.base.client.disabled_credentials_mode {
|
||||||
None
|
None
|
||||||
} else {
|
} else {
|
||||||
Some(default_query_dkg_client_from_config(&self.config.base))
|
Some(default_query_dkg_client_from_config(&self.config.base)?)
|
||||||
};
|
};
|
||||||
|
|
||||||
let storage = self.initialise_storage().await?;
|
let storage = self.initialise_storage().await?;
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "nym-socks5-client"
|
name = "nym-socks5-client"
|
||||||
version = "1.1.59"
|
version = "1.1.61"
|
||||||
authors = ["Dave Hrycyszyn <futurechimp@users.noreply.github.com>"]
|
authors = ["Dave Hrycyszyn <futurechimp@users.noreply.github.com>"]
|
||||||
description = "A SOCKS5 localhost proxy that converts incoming messages to Sphinx and sends them to a Nym address"
|
description = "A SOCKS5 localhost proxy that converts incoming messages to Sphinx and sends them to a Nym address"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ use nym_credentials_interface::{
|
|||||||
};
|
};
|
||||||
use nym_ecash_time::Date;
|
use nym_ecash_time::Date;
|
||||||
use nym_validator_client::coconut::all_ecash_api_clients;
|
use nym_validator_client::coconut::all_ecash_api_clients;
|
||||||
use nym_validator_client::nym_api::EpochId;
|
use nym_validator_client::nym_api::{EpochId, NymApiClientExt};
|
||||||
use nym_validator_client::nyxd::contract_traits::DkgQueryClient;
|
use nym_validator_client::nyxd::contract_traits::DkgQueryClient;
|
||||||
use nym_validator_client::EcashApiClient;
|
use nym_validator_client::EcashApiClient;
|
||||||
use rand::prelude::SliceRandom;
|
use rand::prelude::SliceRandom;
|
||||||
@@ -207,7 +207,7 @@ where
|
|||||||
<St as Storage>::StorageError: Send + Sync + 'static,
|
<St as Storage>::StorageError: Send + Sync + 'static,
|
||||||
{
|
{
|
||||||
if let Some(stored) = storage
|
if let Some(stored) = storage
|
||||||
.get_expiration_date_signatures(expiration_date)
|
.get_expiration_date_signatures(expiration_date, epoch_id)
|
||||||
.await
|
.await
|
||||||
.map_err(BandwidthControllerError::credential_storage_error)?
|
.map_err(BandwidthControllerError::credential_storage_error)?
|
||||||
{
|
{
|
||||||
@@ -220,7 +220,7 @@ where
|
|||||||
ecash_apis,
|
ecash_apis,
|
||||||
|api| async move {
|
|api| async move {
|
||||||
api.api_client
|
api.api_client
|
||||||
.global_expiration_date_signatures(Some(expiration_date))
|
.global_expiration_date_signatures(Some(expiration_date), Some(epoch_id))
|
||||||
.await
|
.await
|
||||||
},
|
},
|
||||||
format!("aggregated coin index signatures for date {expiration_date}"),
|
format!("aggregated coin index signatures for date {expiration_date}"),
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ async-trait = { workspace = true }
|
|||||||
base64 = { workspace = true }
|
base64 = { workspace = true }
|
||||||
bs58 = { workspace = true }
|
bs58 = { workspace = true }
|
||||||
clap = { workspace = true, optional = true }
|
clap = { workspace = true, optional = true }
|
||||||
|
cfg-if = { workspace = true }
|
||||||
comfy-table = { workspace = true, optional = true }
|
comfy-table = { workspace = true, optional = true }
|
||||||
futures = { workspace = true }
|
futures = { workspace = true }
|
||||||
humantime = { workspace = true }
|
humantime = { workspace = true }
|
||||||
@@ -52,6 +53,7 @@ nym-client-core-config-types = { path = "./config-types", features = [
|
|||||||
nym-client-core-surb-storage = { path = "./surb-storage" }
|
nym-client-core-surb-storage = { path = "./surb-storage" }
|
||||||
nym-client-core-gateways-storage = { path = "./gateways-storage" }
|
nym-client-core-gateways-storage = { path = "./gateways-storage" }
|
||||||
nym-ecash-time = { path = "../ecash-time" }
|
nym-ecash-time = { path = "../ecash-time" }
|
||||||
|
nym-mixnet-contract-common = { path = "../cosmwasm-smart-contracts/mixnet-contract" }
|
||||||
|
|
||||||
[target."cfg(not(target_arch = \"wasm32\"))".dependencies]
|
[target."cfg(not(target_arch = \"wasm32\"))".dependencies]
|
||||||
nym-mixnet-client = { path = "../client-libs/mixnet-client", default-features = false }
|
nym-mixnet-client = { path = "../client-libs/mixnet-client", default-features = false }
|
||||||
@@ -123,3 +125,6 @@ fs-surb-storage = ["nym-client-core-surb-storage/fs-surb-storage"]
|
|||||||
fs-gateways-storage = ["nym-client-core-gateways-storage/fs-gateways-storage"]
|
fs-gateways-storage = ["nym-client-core-gateways-storage/fs-gateways-storage"]
|
||||||
wasm = ["nym-gateway-client/wasm"]
|
wasm = ["nym-gateway-client/wasm"]
|
||||||
metrics-server = []
|
metrics-server = []
|
||||||
|
|
||||||
|
[lints]
|
||||||
|
workspace = true
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ name = "nym-client-core-gateways-storage"
|
|||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
license.workspace = true
|
license.workspace = true
|
||||||
|
rust-version.workspace = true
|
||||||
|
|
||||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||||
|
|
||||||
@@ -26,6 +27,7 @@ features = ["runtime-tokio-rustls", "sqlite", "macros", "migrate", "time"]
|
|||||||
optional = true
|
optional = true
|
||||||
|
|
||||||
[build-dependencies]
|
[build-dependencies]
|
||||||
|
anyhow = { workspace = true }
|
||||||
tokio = { workspace = true, features = ["rt-multi-thread", "macros"] }
|
tokio = { workspace = true, features = ["rt-multi-thread", "macros"] }
|
||||||
sqlx = { workspace = true, features = [
|
sqlx = { workspace = true, features = [
|
||||||
"runtime-tokio-rustls",
|
"runtime-tokio-rustls",
|
||||||
|
|||||||
@@ -2,23 +2,30 @@
|
|||||||
// SPDX-License-Identifier: Apache-2.0
|
// SPDX-License-Identifier: Apache-2.0
|
||||||
|
|
||||||
#[tokio::main]
|
#[tokio::main]
|
||||||
async fn main() {
|
async fn main() -> anyhow::Result<()> {
|
||||||
#[cfg(feature = "fs-gateways-storage")]
|
#[cfg(feature = "fs-gateways-storage")]
|
||||||
{
|
{
|
||||||
|
use anyhow::Context;
|
||||||
use sqlx::{Connection, SqliteConnection};
|
use sqlx::{Connection, SqliteConnection};
|
||||||
use std::env;
|
use std::env;
|
||||||
|
|
||||||
let out_dir = env::var("OUT_DIR").unwrap();
|
let out_dir = env::var("OUT_DIR")?;
|
||||||
let database_path = format!("{out_dir}/gateways-storage-example.sqlite");
|
let database_path = format!("{out_dir}/gateways-storage-example.sqlite");
|
||||||
|
|
||||||
|
// remove the db file if it already existed from previous build
|
||||||
|
// in case it was from a different branch
|
||||||
|
if std::fs::exists(&database_path)? {
|
||||||
|
std::fs::remove_file(&database_path)?;
|
||||||
|
}
|
||||||
|
|
||||||
let mut conn = SqliteConnection::connect(&format!("sqlite://{database_path}?mode=rwc"))
|
let mut conn = SqliteConnection::connect(&format!("sqlite://{database_path}?mode=rwc"))
|
||||||
.await
|
.await
|
||||||
.expect("Failed to create SQLx database connection");
|
.context("Failed to create SQLx database connection")?;
|
||||||
|
|
||||||
sqlx::migrate!("./fs_gateways_migrations")
|
sqlx::migrate!("./fs_gateways_migrations")
|
||||||
.run(&mut conn)
|
.run(&mut conn)
|
||||||
.await
|
.await
|
||||||
.expect("Failed to perform SQLx migrations");
|
.context("Failed to perform SQLx migrations")?;
|
||||||
|
|
||||||
#[cfg(target_family = "unix")]
|
#[cfg(target_family = "unix")]
|
||||||
println!("cargo:rustc-env=DATABASE_URL=sqlite://{}", &database_path);
|
println!("cargo:rustc-env=DATABASE_URL=sqlite://{}", &database_path);
|
||||||
@@ -28,4 +35,6 @@ async fn main() {
|
|||||||
// not a valid windows path... but hey, it works...
|
// not a valid windows path... but hey, it works...
|
||||||
println!("cargo:rustc-env=DATABASE_URL=sqlite:///{}", &database_path);
|
println!("cargo:rustc-env=DATABASE_URL=sqlite:///{}", &database_path);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -58,6 +58,7 @@ where
|
|||||||
Some(data) => data,
|
Some(data) => data,
|
||||||
None => {
|
None => {
|
||||||
// SAFETY: one of those arguments must have been set
|
// SAFETY: one of those arguments must have been set
|
||||||
|
#[allow(clippy::unwrap_used)]
|
||||||
fs::read(common_args.signatures_path.unwrap())?
|
fs::read(common_args.signatures_path.unwrap())?
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -64,6 +64,7 @@ where
|
|||||||
Some(data) => data,
|
Some(data) => data,
|
||||||
None => {
|
None => {
|
||||||
// SAFETY: one of those arguments must have been set
|
// SAFETY: one of those arguments must have been set
|
||||||
|
#[allow(clippy::unwrap_used)]
|
||||||
fs::read(common_args.credential_path.unwrap())?
|
fs::read(common_args.credential_path.unwrap())?
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -58,6 +58,7 @@ where
|
|||||||
Some(data) => data,
|
Some(data) => data,
|
||||||
None => {
|
None => {
|
||||||
// SAFETY: one of those arguments must have been set
|
// SAFETY: one of those arguments must have been set
|
||||||
|
#[allow(clippy::unwrap_used)]
|
||||||
fs::read(common_args.signatures_path.unwrap())?
|
fs::read(common_args.signatures_path.unwrap())?
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -58,6 +58,7 @@ where
|
|||||||
Some(data) => data,
|
Some(data) => data,
|
||||||
None => {
|
None => {
|
||||||
// SAFETY: one of those arguments must have been set
|
// SAFETY: one of those arguments must have been set
|
||||||
|
#[allow(clippy::unwrap_used)]
|
||||||
fs::read(common_args.key_path.unwrap())?
|
fs::read(common_args.key_path.unwrap())?
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -57,7 +57,7 @@ use nym_task::{TaskClient, TaskHandle};
|
|||||||
use nym_topology::provider_trait::TopologyProvider;
|
use nym_topology::provider_trait::TopologyProvider;
|
||||||
use nym_topology::HardcodedTopologyProvider;
|
use nym_topology::HardcodedTopologyProvider;
|
||||||
use nym_validator_client::nym_api::NymApiClientExt;
|
use nym_validator_client::nym_api::NymApiClientExt;
|
||||||
use nym_validator_client::{nyxd::contract_traits::DkgQueryClient, NymApiClient, UserAgent};
|
use nym_validator_client::{nyxd::contract_traits::DkgQueryClient, UserAgent};
|
||||||
use rand::prelude::SliceRandom;
|
use rand::prelude::SliceRandom;
|
||||||
use rand::rngs::OsRng;
|
use rand::rngs::OsRng;
|
||||||
use rand::thread_rng;
|
use rand::thread_rng;
|
||||||
@@ -135,9 +135,11 @@ pub enum ClientInputStatus {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl ClientInputStatus {
|
impl ClientInputStatus {
|
||||||
|
#[allow(clippy::panic)]
|
||||||
pub fn register_producer(&mut self) -> ClientInput {
|
pub fn register_producer(&mut self) -> ClientInput {
|
||||||
match std::mem::replace(self, ClientInputStatus::Connected) {
|
match std::mem::replace(self, ClientInputStatus::Connected) {
|
||||||
ClientInputStatus::AwaitingProducer { client_input } => client_input,
|
ClientInputStatus::AwaitingProducer { client_input } => client_input,
|
||||||
|
// critical failure implying misuse of software
|
||||||
ClientInputStatus::Connected => panic!("producer was already registered before"),
|
ClientInputStatus::Connected => panic!("producer was already registered before"),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -149,9 +151,11 @@ pub enum ClientOutputStatus {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl ClientOutputStatus {
|
impl ClientOutputStatus {
|
||||||
|
#[allow(clippy::panic)]
|
||||||
pub fn register_consumer(&mut self) -> ClientOutput {
|
pub fn register_consumer(&mut self) -> ClientOutput {
|
||||||
match std::mem::replace(self, ClientOutputStatus::Connected) {
|
match std::mem::replace(self, ClientOutputStatus::Connected) {
|
||||||
ClientOutputStatus::AwaitingConsumer { client_output } => client_output,
|
ClientOutputStatus::AwaitingConsumer { client_output } => client_output,
|
||||||
|
// critical failure implying misuse of software
|
||||||
ClientOutputStatus::Connected => panic!("consumer was already registered before"),
|
ClientOutputStatus::Connected => panic!("consumer was already registered before"),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -562,7 +566,7 @@ where
|
|||||||
custom_provider: Option<Box<dyn TopologyProvider + Send + Sync>>,
|
custom_provider: Option<Box<dyn TopologyProvider + Send + Sync>>,
|
||||||
config_topology: config::Topology,
|
config_topology: config::Topology,
|
||||||
nym_api_urls: Vec<Url>,
|
nym_api_urls: Vec<Url>,
|
||||||
nym_api_client: NymApiClient,
|
nym_api_client: nym_http_api_client::Client,
|
||||||
) -> Box<dyn TopologyProvider + Send + Sync> {
|
) -> Box<dyn TopologyProvider + Send + Sync> {
|
||||||
// if no custom provider was ... provided ..., create one using nym-api
|
// if no custom provider was ... provided ..., create one using nym-api
|
||||||
custom_provider.unwrap_or_else(|| {
|
custom_provider.unwrap_or_else(|| {
|
||||||
@@ -707,11 +711,14 @@ where
|
|||||||
})?;
|
})?;
|
||||||
|
|
||||||
let store_clone = mem_store.clone();
|
let store_clone = mem_store.clone();
|
||||||
spawn_future(async move {
|
spawn_future!(
|
||||||
persistent_storage
|
async move {
|
||||||
.flush_on_shutdown(store_clone, shutdown)
|
persistent_storage
|
||||||
.await
|
.flush_on_shutdown(store_clone, shutdown)
|
||||||
});
|
.await
|
||||||
|
},
|
||||||
|
"PersistentReplyStorage::flush_on_shutdown"
|
||||||
|
);
|
||||||
|
|
||||||
Ok(mem_store)
|
Ok(mem_store)
|
||||||
}
|
}
|
||||||
@@ -732,7 +739,7 @@ where
|
|||||||
let mut rng = OsRng;
|
let mut rng = OsRng;
|
||||||
let keys = if let Some(derivation_material) = derivation_material {
|
let keys = if let Some(derivation_material) = derivation_material {
|
||||||
ClientKeys::from_master_key(&mut rng, &derivation_material)
|
ClientKeys::from_master_key(&mut rng, &derivation_material)
|
||||||
.map_err(|_| ClientCoreError::HkdfDerivationError {})?
|
.map_err(|_| ClientCoreError::HkdfDerivationError)?
|
||||||
} else {
|
} else {
|
||||||
ClientKeys::generate_new(&mut rng)
|
ClientKeys::generate_new(&mut rng)
|
||||||
};
|
};
|
||||||
@@ -742,21 +749,42 @@ where
|
|||||||
setup_gateway(setup_method, key_store, details_store).await
|
setup_gateway(setup_method, key_store, details_store).await
|
||||||
}
|
}
|
||||||
|
|
||||||
fn construct_nym_api_client(config: &Config, user_agent: Option<UserAgent>) -> NymApiClient {
|
fn construct_nym_api_client(
|
||||||
|
config: &Config,
|
||||||
|
user_agent: Option<UserAgent>,
|
||||||
|
) -> Result<nym_http_api_client::Client, ClientCoreError> {
|
||||||
let mut nym_api_urls = config.get_nym_api_endpoints();
|
let mut nym_api_urls = config.get_nym_api_endpoints();
|
||||||
nym_api_urls.shuffle(&mut thread_rng());
|
nym_api_urls.shuffle(&mut thread_rng());
|
||||||
|
|
||||||
|
let mut builder = nym_http_api_client::Client::builder::<
|
||||||
|
_,
|
||||||
|
nym_validator_client::models::RequestError,
|
||||||
|
>(nym_api_urls[0].clone())
|
||||||
|
.map_err(|e| ClientCoreError::NymApiQueryFailure {
|
||||||
|
source: nym_validator_client::nym_api::error::NymAPIError::GenericRequestFailure(
|
||||||
|
e.to_string(),
|
||||||
|
),
|
||||||
|
})?;
|
||||||
|
|
||||||
if let Some(user_agent) = user_agent {
|
if let Some(user_agent) = user_agent {
|
||||||
NymApiClient::new_with_user_agent(nym_api_urls[0].clone(), user_agent)
|
builder = builder.with_user_agent(user_agent);
|
||||||
} else {
|
|
||||||
NymApiClient::new(nym_api_urls[0].clone())
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
builder = builder.with_bincode();
|
||||||
|
|
||||||
|
builder
|
||||||
|
.build::<nym_validator_client::models::RequestError>()
|
||||||
|
.map_err(|e| ClientCoreError::NymApiQueryFailure {
|
||||||
|
source: nym_validator_client::nym_api::error::NymAPIError::GenericRequestFailure(
|
||||||
|
e.to_string(),
|
||||||
|
),
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn determine_key_rotation_state(
|
async fn determine_key_rotation_state(
|
||||||
client: &NymApiClient,
|
client: &nym_http_api_client::Client,
|
||||||
) -> Result<KeyRotationConfig, ClientCoreError> {
|
) -> Result<KeyRotationConfig, ClientCoreError> {
|
||||||
Ok(client.nym_api.get_key_rotation_info().await?.into())
|
Ok(client.get_key_rotation_info().await?.into())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn start_base(mut self) -> Result<BaseClient, ClientCoreError>
|
pub async fn start_base(mut self) -> Result<BaseClient, ClientCoreError>
|
||||||
@@ -823,7 +851,7 @@ where
|
|||||||
.dkg_query_client
|
.dkg_query_client
|
||||||
.map(|client| BandwidthController::new(credential_store, client));
|
.map(|client| BandwidthController::new(credential_store, client));
|
||||||
|
|
||||||
let nym_api_client = Self::construct_nym_api_client(&self.config, self.user_agent.clone());
|
let nym_api_client = Self::construct_nym_api_client(&self.config, self.user_agent.clone())?;
|
||||||
let key_rotation_config = Self::determine_key_rotation_state(&nym_api_client).await?;
|
let key_rotation_config = Self::determine_key_rotation_state(&nym_api_client).await?;
|
||||||
|
|
||||||
let topology_provider = Self::setup_topology_provider(
|
let topology_provider = Self::setup_topology_provider(
|
||||||
|
|||||||
@@ -114,41 +114,32 @@ pub async fn setup_fs_gateways_storage<P: AsRef<Path>>(
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn create_bandwidth_controller<St: CredentialStorage>(
|
|
||||||
config: &Config,
|
|
||||||
storage: St,
|
|
||||||
) -> BandwidthController<QueryHttpRpcNyxdClient, St> {
|
|
||||||
let nyxd_url = config
|
|
||||||
.get_validator_endpoints()
|
|
||||||
.pop()
|
|
||||||
.expect("No nyxd validator endpoint provided");
|
|
||||||
|
|
||||||
create_bandwidth_controller_with_urls(nyxd_url, storage)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn create_bandwidth_controller_with_urls<St: CredentialStorage>(
|
pub fn create_bandwidth_controller_with_urls<St: CredentialStorage>(
|
||||||
nyxd_url: Url,
|
nyxd_url: Url,
|
||||||
storage: St,
|
storage: St,
|
||||||
) -> BandwidthController<QueryHttpRpcNyxdClient, St> {
|
) -> Result<BandwidthController<QueryHttpRpcNyxdClient, St>, ClientCoreError> {
|
||||||
let client = default_query_dkg_client(nyxd_url);
|
let client = default_query_dkg_client(nyxd_url)?;
|
||||||
|
|
||||||
BandwidthController::new(storage, client)
|
Ok(BandwidthController::new(storage, client))
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn default_query_dkg_client_from_config(config: &Config) -> QueryHttpRpcNyxdClient {
|
pub fn default_query_dkg_client_from_config(
|
||||||
|
config: &Config,
|
||||||
|
) -> Result<QueryHttpRpcNyxdClient, ClientCoreError> {
|
||||||
let nyxd_url = config
|
let nyxd_url = config
|
||||||
.get_validator_endpoints()
|
.get_validator_endpoints()
|
||||||
.pop()
|
.pop()
|
||||||
.expect("No nyxd validator endpoint provided");
|
.ok_or(ClientCoreError::RpcClientMissingUrl)?;
|
||||||
|
|
||||||
default_query_dkg_client(nyxd_url)
|
default_query_dkg_client(nyxd_url)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn default_query_dkg_client(nyxd_url: Url) -> QueryHttpRpcNyxdClient {
|
pub fn default_query_dkg_client(nyxd_url: Url) -> Result<QueryHttpRpcNyxdClient, ClientCoreError> {
|
||||||
let details = nym_network_defaults::NymNetworkDetails::new_from_env();
|
let details = nym_network_defaults::NymNetworkDetails::new_from_env();
|
||||||
let client_config = nyxd::Config::try_from_nym_network_details(&details)
|
let client_config = nyxd::Config::try_from_nym_network_details(&details)
|
||||||
.expect("failed to construct validator client config");
|
.map_err(|source| ClientCoreError::InvalidNetworkDetails { source })?;
|
||||||
// overwrite env configuration with config URLs
|
// overwrite env configuration with config URLs
|
||||||
|
|
||||||
QueryHttpRpcNyxdClient::connect(client_config, nyxd_url.as_str())
|
QueryHttpRpcNyxdClient::connect(client_config, nyxd_url.as_str())
|
||||||
.expect("Could not construct query client")
|
.map_err(|source| ClientCoreError::RpcClientCreationFailure { source })
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -235,6 +235,7 @@ impl LoopCoverTrafficStream<OsRng> {
|
|||||||
tokio::task::yield_now().await;
|
tokio::task::yield_now().await;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[allow(clippy::panic)]
|
||||||
pub fn start(mut self) {
|
pub fn start(mut self) {
|
||||||
if self.cover_traffic.disable_loop_cover_traffic_stream {
|
if self.cover_traffic.disable_loop_cover_traffic_stream {
|
||||||
// we should have never got here in the first place - the task should have never been created to begin with
|
// we should have never got here in the first place - the task should have never been created to begin with
|
||||||
@@ -251,27 +252,30 @@ impl LoopCoverTrafficStream<OsRng> {
|
|||||||
|
|
||||||
let mut shutdown = self.task_client.fork("select");
|
let mut shutdown = self.task_client.fork("select");
|
||||||
|
|
||||||
spawn_future(async move {
|
spawn_future!(
|
||||||
debug!("Started LoopCoverTrafficStream with graceful shutdown support");
|
async move {
|
||||||
|
debug!("Started LoopCoverTrafficStream with graceful shutdown support");
|
||||||
|
|
||||||
while !shutdown.is_shutdown() {
|
while !shutdown.is_shutdown() {
|
||||||
tokio::select! {
|
tokio::select! {
|
||||||
biased;
|
biased;
|
||||||
_ = shutdown.recv() => {
|
_ = shutdown.recv() => {
|
||||||
tracing::trace!("LoopCoverTrafficStream: Received shutdown");
|
tracing::trace!("LoopCoverTrafficStream: Received shutdown");
|
||||||
}
|
}
|
||||||
next = self.next() => {
|
next = self.next() => {
|
||||||
if next.is_some() {
|
if next.is_some() {
|
||||||
self.on_new_message().await;
|
self.on_new_message().await;
|
||||||
} else {
|
} else {
|
||||||
tracing::trace!("LoopCoverTrafficStream: Stopping since channel closed");
|
tracing::trace!("LoopCoverTrafficStream: Stopping since channel closed");
|
||||||
break;
|
break;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
shutdown.recv_timeout().await;
|
||||||
shutdown.recv_timeout().await;
|
tracing::debug!("LoopCoverTrafficStream: Exiting");
|
||||||
tracing::debug!("LoopCoverTrafficStream: Exiting");
|
},
|
||||||
})
|
"LoopCoverTrafficStream"
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -96,72 +96,93 @@ impl MixTrafficController {
|
|||||||
mut mix_packets: Vec<MixPacket>,
|
mut mix_packets: Vec<MixPacket>,
|
||||||
) -> Result<(), ErasedGatewayError> {
|
) -> Result<(), ErasedGatewayError> {
|
||||||
debug_assert!(!mix_packets.is_empty());
|
debug_assert!(!mix_packets.is_empty());
|
||||||
|
let send_future = if mix_packets.len() == 1 {
|
||||||
let result = if mix_packets.len() == 1 {
|
// SAFETY: we just checked we have one packet
|
||||||
|
#[allow(clippy::unwrap_used)]
|
||||||
let mix_packet = mix_packets.pop().unwrap();
|
let mix_packet = mix_packets.pop().unwrap();
|
||||||
self.gateway_transceiver.send_mix_packet(mix_packet).await
|
self.gateway_transceiver.send_mix_packet(mix_packet)
|
||||||
} else {
|
} else {
|
||||||
self.gateway_transceiver
|
self.gateway_transceiver.batch_send_mix_packets(mix_packets)
|
||||||
.batch_send_mix_packets(mix_packets)
|
|
||||||
.await
|
|
||||||
};
|
};
|
||||||
|
|
||||||
if result.is_err() {
|
tokio::select! {
|
||||||
self.consecutive_gateway_failure_count += 1;
|
biased;
|
||||||
} else {
|
_ = self.task_client.recv() => {
|
||||||
trace!("We *might* have managed to forward sphinx packet(s) to the gateway!");
|
trace!("received shutdown while handling messages");
|
||||||
self.consecutive_gateway_failure_count = 0;
|
Ok(())
|
||||||
}
|
}
|
||||||
|
result = send_future => {
|
||||||
|
if result.is_err() {
|
||||||
|
self.consecutive_gateway_failure_count += 1;
|
||||||
|
} else {
|
||||||
|
trace!("We *might* have managed to forward sphinx packet(s) to the gateway!");
|
||||||
|
self.consecutive_gateway_failure_count = 0;
|
||||||
|
}
|
||||||
|
|
||||||
result
|
result
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn on_client_request(&mut self, client_request: ClientRequest) {
|
||||||
|
tokio::select! {
|
||||||
|
biased;
|
||||||
|
_ = self.task_client.recv() => {
|
||||||
|
trace!("received shutdown while handling client request");
|
||||||
|
}
|
||||||
|
result = self.gateway_transceiver.send_client_request(client_request) => {
|
||||||
|
if let Err(err) = result {
|
||||||
|
error!("Failed to send client request: {err}")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn start(mut self) {
|
pub fn start(mut self) {
|
||||||
spawn_future(async move {
|
spawn_future!(
|
||||||
debug!("Started MixTrafficController with graceful shutdown support");
|
async move {
|
||||||
|
debug!("Started MixTrafficController with graceful shutdown support");
|
||||||
while !self.task_client.is_shutdown() {
|
while !self.task_client.is_shutdown() {
|
||||||
tokio::select! {
|
tokio::select! {
|
||||||
mix_packets = self.mix_rx.recv() => match mix_packets {
|
biased;
|
||||||
Some(mix_packets) => {
|
_ = self.task_client.recv() => {
|
||||||
if let Err(err) = self.on_messages(mix_packets).await {
|
tracing::trace!("MixTrafficController: Received shutdown");
|
||||||
error!("Failed to send sphinx packet(s) to the gateway: {err}");
|
|
||||||
if self.consecutive_gateway_failure_count == MAX_FAILURE_COUNT {
|
|
||||||
// Disconnect from the gateway. If we should try to re-connect
|
|
||||||
// is handled at a higher layer.
|
|
||||||
error!("Failed to send sphinx packet to the gateway {MAX_FAILURE_COUNT} times in a row - assuming the gateway is dead");
|
|
||||||
// Do we need to handle the embedded mixnet client case
|
|
||||||
// separately?
|
|
||||||
self.task_client.send_we_stopped(Box::new(ClientCoreError::GatewayFailedToForwardMessages));
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
None => {
|
|
||||||
tracing::trace!("MixTrafficController: Stopping since channel closed");
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
},
|
mix_packets = self.mix_rx.recv() => match mix_packets {
|
||||||
client_request = self.client_rx.recv() => match client_request {
|
Some(mix_packets) => {
|
||||||
Some(client_request) => {
|
if let Err(err) = self.on_messages(mix_packets).await {
|
||||||
match self.gateway_transceiver.send_client_request(client_request).await {
|
error!("Failed to send sphinx packet(s) to the gateway: {err}");
|
||||||
Ok(_) => (),
|
if self.consecutive_gateway_failure_count == MAX_FAILURE_COUNT {
|
||||||
Err(e) => error!("Failed to send client request: {e}"),
|
// Disconnect from the gateway. If we should try to re-connect
|
||||||
};
|
// is handled at a higher layer.
|
||||||
|
error!("Failed to send sphinx packet to the gateway {MAX_FAILURE_COUNT} times in a row - assuming the gateway is dead");
|
||||||
|
// Do we need to handle the embedded mixnet client case
|
||||||
|
// separately?
|
||||||
|
self.task_client.send_we_stopped(Box::new(ClientCoreError::GatewayFailedToForwardMessages));
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
None => {
|
||||||
|
tracing::trace!("MixTrafficController: Stopping since channel closed");
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
client_request = self.client_rx.recv() => match client_request {
|
||||||
|
Some(client_request) => {
|
||||||
|
self.on_client_request(client_request).await;
|
||||||
|
},
|
||||||
|
None => {
|
||||||
|
tracing::trace!("MixTrafficController, client request channel closed");
|
||||||
|
}
|
||||||
},
|
},
|
||||||
None => {
|
|
||||||
tracing::trace!("MixTrafficController, client request channel closed");
|
|
||||||
}
|
|
||||||
},
|
|
||||||
_ = self.task_client.recv() => {
|
|
||||||
tracing::trace!("MixTrafficController: Received shutdown");
|
|
||||||
break;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
self.task_client.recv_timeout().await;
|
||||||
self.task_client.recv_timeout().await;
|
tracing::debug!("MixTrafficController: Exiting");
|
||||||
|
},
|
||||||
tracing::debug!("MixTrafficController: Exiting");
|
"MixTrafficController"
|
||||||
});
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -269,6 +269,8 @@ pub struct MockGateway {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl Default for MockGateway {
|
impl Default for MockGateway {
|
||||||
|
// test code
|
||||||
|
#[allow(clippy::unwrap_used)]
|
||||||
fn default() -> Self {
|
fn default() -> Self {
|
||||||
MockGateway {
|
MockGateway {
|
||||||
dummy_identity: "3ebjp1Fb9hdcS1AR6AZihgeJiMHkB5jjJUsvqNnfQwU7"
|
dummy_identity: "3ebjp1Fb9hdcS1AR6AZihgeJiMHkB5jjJUsvqNnfQwU7"
|
||||||
|
|||||||
+3
-1
@@ -194,10 +194,11 @@ impl ActionController {
|
|||||||
trace!("{frag_id} is updating its delay");
|
trace!("{frag_id} is updating its delay");
|
||||||
// TODO: is it possible to solve this without either locking or temporarily removing the value?
|
// TODO: is it possible to solve this without either locking or temporarily removing the value?
|
||||||
if let Some((pending_ack_data, queue_key)) = self.pending_acks_data.remove(&frag_id) {
|
if let Some((pending_ack_data, queue_key)) = self.pending_acks_data.remove(&frag_id) {
|
||||||
// this Action is triggered by `RetransmissionRequestListener` (for 'normal' packets)
|
// SAFETY: this Action is triggered by `RetransmissionRequestListener` (for 'normal' packets)
|
||||||
// or `ReplyController` (for 'reply' packets) which held the other potential
|
// or `ReplyController` (for 'reply' packets) which held the other potential
|
||||||
// reference to this Arc. HOWEVER, before the Action was pushed onto the queue, the reference
|
// reference to this Arc. HOWEVER, before the Action was pushed onto the queue, the reference
|
||||||
// was dropped hence this unwrap is safe.
|
// was dropped hence this unwrap is safe.
|
||||||
|
#[allow(clippy::unwrap_used)]
|
||||||
let mut inner_data = Arc::try_unwrap(pending_ack_data).unwrap();
|
let mut inner_data = Arc::try_unwrap(pending_ack_data).unwrap();
|
||||||
inner_data.update_retransmitted(delay);
|
inner_data.update_retransmitted(delay);
|
||||||
|
|
||||||
@@ -209,6 +210,7 @@ impl ActionController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// note: when the entry expires it's automatically removed from pending_acks_timers
|
// note: when the entry expires it's automatically removed from pending_acks_timers
|
||||||
|
#[allow(clippy::panic)]
|
||||||
fn handle_expired_ack_timer(&mut self, expired_ack: Expired<FragmentIdentifier>) {
|
fn handle_expired_ack_timer(&mut self, expired_ack: Expired<FragmentIdentifier>) {
|
||||||
let frag_id = expired_ack.into_inner();
|
let frag_id = expired_ack.into_inner();
|
||||||
|
|
||||||
|
|||||||
+10
-4
@@ -120,6 +120,7 @@ where
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[allow(clippy::panic)]
|
||||||
async fn on_input_message(&mut self, msg: InputMessage) {
|
async fn on_input_message(&mut self, msg: InputMessage) {
|
||||||
match msg {
|
match msg {
|
||||||
InputMessage::Regular {
|
InputMessage::Regular {
|
||||||
@@ -213,7 +214,9 @@ where
|
|||||||
self.handle_premade_packets(msgs, lane).await
|
self.handle_premade_packets(msgs, lane).await
|
||||||
}
|
}
|
||||||
// MessageWrappers can't be nested
|
// MessageWrappers can't be nested
|
||||||
InputMessage::MessageWrapper { .. } => unimplemented!(),
|
InputMessage::MessageWrapper { .. } => {
|
||||||
|
panic!("attempted to use nested MessageWrapper")
|
||||||
|
}
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -223,6 +226,11 @@ where
|
|||||||
|
|
||||||
while !self.task_client.is_shutdown() {
|
while !self.task_client.is_shutdown() {
|
||||||
tokio::select! {
|
tokio::select! {
|
||||||
|
biased;
|
||||||
|
_ = self.task_client.recv() => {
|
||||||
|
tracing::trace!("InputMessageListener: Received shutdown");
|
||||||
|
break;
|
||||||
|
}
|
||||||
input_msg = self.input_receiver.recv() => match input_msg {
|
input_msg = self.input_receiver.recv() => match input_msg {
|
||||||
Some(input_msg) => {
|
Some(input_msg) => {
|
||||||
self.on_input_message(input_msg).await;
|
self.on_input_message(input_msg).await;
|
||||||
@@ -232,9 +240,7 @@ where
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
_ = self.task_client.recv() => {
|
|
||||||
tracing::trace!("InputMessageListener: Received shutdown");
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
self.task_client.recv_timeout().await;
|
self.task_client.recv_timeout().await;
|
||||||
|
|||||||
+35
-20
@@ -298,29 +298,44 @@ where
|
|||||||
let mut sent_notification_listener = self.sent_notification_listener;
|
let mut sent_notification_listener = self.sent_notification_listener;
|
||||||
let mut action_controller = self.action_controller;
|
let mut action_controller = self.action_controller;
|
||||||
|
|
||||||
spawn_future(async move {
|
spawn_future!(
|
||||||
acknowledgement_listener.run().await;
|
async move {
|
||||||
debug!("The acknowledgement listener has finished execution!");
|
acknowledgement_listener.run().await;
|
||||||
});
|
debug!("The acknowledgement listener has finished execution!");
|
||||||
|
},
|
||||||
|
"AcknowledgementController::AcknowledgementListener"
|
||||||
|
);
|
||||||
|
|
||||||
spawn_future(async move {
|
spawn_future!(
|
||||||
input_message_listener.run().await;
|
async move {
|
||||||
debug!("The input listener has finished execution!");
|
input_message_listener.run().await;
|
||||||
});
|
debug!("The input listener has finished execution!");
|
||||||
|
},
|
||||||
|
"AcknowledgementController::InputMessageListener"
|
||||||
|
);
|
||||||
|
|
||||||
spawn_future(async move {
|
spawn_future!(
|
||||||
retransmission_request_listener.run(packet_type).await;
|
async move {
|
||||||
debug!("The retransmission request listener has finished execution!");
|
retransmission_request_listener.run(packet_type).await;
|
||||||
});
|
debug!("The retransmission request listener has finished execution!");
|
||||||
|
},
|
||||||
|
"AcknowledgementController::RetransmissionRequestListener"
|
||||||
|
);
|
||||||
|
|
||||||
spawn_future(async move {
|
spawn_future!(
|
||||||
sent_notification_listener.run().await;
|
async move {
|
||||||
debug!("The sent notification listener has finished execution!");
|
sent_notification_listener.run().await;
|
||||||
});
|
debug!("The sent notification listener has finished execution!");
|
||||||
|
},
|
||||||
|
"AcknowledgementController::SentNotificationListener"
|
||||||
|
);
|
||||||
|
|
||||||
spawn_future(async move {
|
spawn_future!(
|
||||||
action_controller.run().await;
|
async move {
|
||||||
debug!("The controller has finished execution!");
|
action_controller.run().await;
|
||||||
});
|
debug!("The controller has finished execution!");
|
||||||
|
},
|
||||||
|
"AcknowledgementController::ActionController"
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+6
-3
@@ -179,6 +179,11 @@ where
|
|||||||
|
|
||||||
while !self.task_client.is_shutdown() {
|
while !self.task_client.is_shutdown() {
|
||||||
tokio::select! {
|
tokio::select! {
|
||||||
|
biased;
|
||||||
|
_ = self.task_client.recv() => {
|
||||||
|
tracing::trace!("RetransmissionRequestListener: Received shutdown");
|
||||||
|
break;
|
||||||
|
}
|
||||||
timed_out_ack = self.request_receiver.next() => match timed_out_ack {
|
timed_out_ack = self.request_receiver.next() => match timed_out_ack {
|
||||||
Some(timed_out_ack) => self.on_retransmission_request(timed_out_ack, packet_type).await,
|
Some(timed_out_ack) => self.on_retransmission_request(timed_out_ack, packet_type).await,
|
||||||
None => {
|
None => {
|
||||||
@@ -186,9 +191,7 @@ where
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
_ = self.task_client.recv() => {
|
|
||||||
tracing::trace!("RetransmissionRequestListener: Received shutdown");
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
self.task_client.recv_timeout().await;
|
self.task_client.recv_timeout().await;
|
||||||
|
|||||||
@@ -35,6 +35,9 @@ pub enum PreparationError {
|
|||||||
#[error(transparent)]
|
#[error(transparent)]
|
||||||
NymTopologyError(#[from] NymTopologyError),
|
NymTopologyError(#[from] NymTopologyError),
|
||||||
|
|
||||||
|
#[error("message wasn't split into any fragments!")]
|
||||||
|
EmptyFragments,
|
||||||
|
|
||||||
#[error("message too long for a single SURB, splitting into {fragments} fragments.")]
|
#[error("message too long for a single SURB, splitting into {fragments} fragments.")]
|
||||||
MessageTooLongForSingleSurb { fragments: usize },
|
MessageTooLongForSingleSurb { fragments: usize },
|
||||||
|
|
||||||
@@ -320,6 +323,16 @@ where
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if fragment.is_empty() {
|
||||||
|
error!("CRITICAL FAILURE: our split message didn't result in any sendable fragments");
|
||||||
|
return Err(SurbWrappedPreparationError {
|
||||||
|
source: PreparationError::EmptyFragments,
|
||||||
|
returned_surbs: Some(vec![reply_surb]),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// SAFETY: we just checked we have one fragment
|
||||||
|
#[allow(clippy::unwrap_used)]
|
||||||
let chunk = fragment.pop().unwrap();
|
let chunk = fragment.pop().unwrap();
|
||||||
let chunk_clone = chunk.clone();
|
let chunk_clone = chunk.clone();
|
||||||
let prepared_fragment = self
|
let prepared_fragment = self
|
||||||
@@ -535,6 +548,7 @@ where
|
|||||||
pending_acks.push(pending_ack);
|
pending_acks.push(pending_ack);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
drop(topology_permit);
|
||||||
self.insert_pending_acks(pending_acks);
|
self.insert_pending_acks(pending_acks);
|
||||||
self.forward_messages(real_messages, lane).await;
|
self.forward_messages(real_messages, lane).await;
|
||||||
|
|
||||||
@@ -657,6 +671,7 @@ where
|
|||||||
.zip(reply_surbs.into_iter())
|
.zip(reply_surbs.into_iter())
|
||||||
.map(|(fragment, reply_surb)| {
|
.map(|(fragment, reply_surb)| {
|
||||||
// unwrap here is fine as we know we have a valid topology
|
// unwrap here is fine as we know we have a valid topology
|
||||||
|
#[allow(clippy::unwrap_used)]
|
||||||
self.message_preparer
|
self.message_preparer
|
||||||
.prepare_reply_chunk_for_sending(
|
.prepare_reply_chunk_for_sending(
|
||||||
fragment,
|
fragment,
|
||||||
@@ -716,17 +731,21 @@ where
|
|||||||
|
|
||||||
// tells real message sender (with the poisson timer) to send this to the mix network
|
// tells real message sender (with the poisson timer) to send this to the mix network
|
||||||
pub(crate) async fn forward_messages(
|
pub(crate) async fn forward_messages(
|
||||||
&self,
|
&mut self,
|
||||||
messages: Vec<RealMessage>,
|
messages: Vec<RealMessage>,
|
||||||
transmission_lane: TransmissionLane,
|
transmission_lane: TransmissionLane,
|
||||||
) {
|
) {
|
||||||
if let Err(err) = self
|
tokio::select! {
|
||||||
.real_message_sender
|
biased;
|
||||||
.send((messages, transmission_lane))
|
_ = self.task_client.recv() => {
|
||||||
.await
|
trace!("received shutdown while attempting to forward mixnet messages");
|
||||||
{
|
}
|
||||||
if !self.task_client.is_shutdown_poll() {
|
sending_res = self.real_message_sender.send((messages, transmission_lane)) => {
|
||||||
error!("Failed to forward messages to the real message sender: {err}");
|
if sending_res.is_err() {
|
||||||
|
error!(
|
||||||
|
"failed to forward mixnet messages due to closed channel (outside of shutdown!)"
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -224,14 +224,20 @@ impl RealMessagesController<OsRng> {
|
|||||||
let ack_control = self.ack_control;
|
let ack_control = self.ack_control;
|
||||||
let mut reply_control = self.reply_control;
|
let mut reply_control = self.reply_control;
|
||||||
|
|
||||||
spawn_future(async move {
|
spawn_future!(
|
||||||
out_queue_control.run().await;
|
async move {
|
||||||
debug!("The out queue controller has finished execution!");
|
out_queue_control.run().await;
|
||||||
});
|
debug!("The out queue controller has finished execution!");
|
||||||
spawn_future(async move {
|
},
|
||||||
reply_control.run().await;
|
"RealMessagesController::OutQueueControl)"
|
||||||
debug!("The reply controller has finished execution!");
|
);
|
||||||
});
|
spawn_future!(
|
||||||
|
async move {
|
||||||
|
reply_control.run().await;
|
||||||
|
debug!("The reply controller has finished execution!");
|
||||||
|
},
|
||||||
|
"RealMessagesController::ReplyController"
|
||||||
|
);
|
||||||
|
|
||||||
ack_control.start(packet_type);
|
ack_control.start(packet_type);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -249,6 +249,8 @@ where
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// SAFETY: our topology must be valid at this point
|
||||||
|
#[allow(clippy::expect_used)]
|
||||||
(
|
(
|
||||||
generate_loop_cover_packet(
|
generate_loop_cover_packet(
|
||||||
&mut self.rng,
|
&mut self.rng,
|
||||||
@@ -278,17 +280,33 @@ where
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
if let Err(err) = self.mix_tx.send(vec![next_message]).await {
|
let sending_res = tokio::select! {
|
||||||
if !self.task_client.is_shutdown_poll() {
|
biased;
|
||||||
tracing::error!("Failed to send: {err}");
|
_ = self.task_client.recv() => {
|
||||||
|
trace!("received shutdown signal while attempting to send mix message");
|
||||||
|
return
|
||||||
|
}
|
||||||
|
sending_res = self.mix_tx.send(vec![next_message]) => {
|
||||||
|
sending_res
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
match sending_res {
|
||||||
|
Err(_) => {
|
||||||
|
if !self.task_client.is_shutdown_poll() {
|
||||||
|
tracing::error!(
|
||||||
|
"failed to send mixnet packet due to closed channel (outside of shutdown!)"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(_) => {
|
||||||
|
let event = if fragment_id.is_some() {
|
||||||
|
PacketStatisticsEvent::RealPacketSent(packet_size)
|
||||||
|
} else {
|
||||||
|
PacketStatisticsEvent::CoverPacketSent(packet_size)
|
||||||
|
};
|
||||||
|
self.stats_tx.report(event.into());
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
let event = if fragment_id.is_some() {
|
|
||||||
PacketStatisticsEvent::RealPacketSent(packet_size)
|
|
||||||
} else {
|
|
||||||
PacketStatisticsEvent::CoverPacketSent(packet_size)
|
|
||||||
};
|
|
||||||
self.stats_tx.report(event.into());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// notify ack controller about sending our message only after we actually managed to push it
|
// notify ack controller about sending our message only after we actually managed to push it
|
||||||
@@ -439,6 +457,8 @@ where
|
|||||||
tracing::trace!("handling real_messages: size: {}", real_messages.len());
|
tracing::trace!("handling real_messages: size: {}", real_messages.len());
|
||||||
|
|
||||||
self.transmission_buffer.store(&conn_id, real_messages);
|
self.transmission_buffer.store(&conn_id, real_messages);
|
||||||
|
// SAFETY: we just stored the message
|
||||||
|
#[allow(clippy::expect_used)]
|
||||||
let real_next = self.pop_next_message().expect("Just stored one");
|
let real_next = self.pop_next_message().expect("Just stored one");
|
||||||
|
|
||||||
Poll::Ready(Some(StreamMessage::Real(Box::new(real_next))))
|
Poll::Ready(Some(StreamMessage::Real(Box::new(real_next))))
|
||||||
@@ -487,6 +507,8 @@ where
|
|||||||
|
|
||||||
// First store what we got for the given connection id
|
// First store what we got for the given connection id
|
||||||
self.transmission_buffer.store(&conn_id, real_messages);
|
self.transmission_buffer.store(&conn_id, real_messages);
|
||||||
|
// SAFETY: we just stored the message
|
||||||
|
#[allow(clippy::expect_used)]
|
||||||
let real_next = self.pop_next_message().expect("we just added one");
|
let real_next = self.pop_next_message().expect("we just added one");
|
||||||
|
|
||||||
Poll::Ready(Some(StreamMessage::Real(Box::new(real_next))))
|
Poll::Ready(Some(StreamMessage::Real(Box::new(real_next))))
|
||||||
|
|||||||
@@ -198,6 +198,7 @@ impl<R: MessageReceiver> ReceivedMessagesBuffer<R> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[allow(clippy::panic)]
|
||||||
async fn disconnect_sender(&mut self) {
|
async fn disconnect_sender(&mut self) {
|
||||||
let mut guard = self.inner.lock().await;
|
let mut guard = self.inner.lock().await;
|
||||||
if guard.message_sender.is_none() {
|
if guard.message_sender.is_none() {
|
||||||
@@ -208,6 +209,7 @@ impl<R: MessageReceiver> ReceivedMessagesBuffer<R> {
|
|||||||
guard.message_sender = None;
|
guard.message_sender = None;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[allow(clippy::panic)]
|
||||||
async fn connect_sender(&mut self, sender: ReconstructedMessagesSender) {
|
async fn connect_sender(&mut self, sender: ReconstructedMessagesSender) {
|
||||||
let mut guard = self.inner.lock().await;
|
let mut guard = self.inner.lock().await;
|
||||||
if guard.message_sender.is_some() {
|
if guard.message_sender.is_some() {
|
||||||
@@ -599,14 +601,20 @@ impl<R: MessageReceiver + Clone + Send + 'static> ReceivedMessagesBufferControll
|
|||||||
let mut fragmented_message_receiver = self.fragmented_message_receiver;
|
let mut fragmented_message_receiver = self.fragmented_message_receiver;
|
||||||
let mut request_receiver = self.request_receiver;
|
let mut request_receiver = self.request_receiver;
|
||||||
|
|
||||||
spawn_future(async move {
|
spawn_future!(
|
||||||
match fragmented_message_receiver.run().await {
|
async move {
|
||||||
Ok(_) => {}
|
match fragmented_message_receiver.run().await {
|
||||||
Err(e) => error!("{e}"),
|
Ok(_) => {}
|
||||||
}
|
Err(e) => error!("{e}"),
|
||||||
});
|
}
|
||||||
spawn_future(async move {
|
},
|
||||||
request_receiver.run().await;
|
"ReceivedMessagesBufferController::FragmentedMessageReceiver"
|
||||||
});
|
);
|
||||||
|
spawn_future!(
|
||||||
|
async move {
|
||||||
|
request_receiver.run().await;
|
||||||
|
},
|
||||||
|
"ReceivedMessagesBufferController::RequestReceiver"
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -155,8 +155,9 @@ where
|
|||||||
data: Vec<Arc<PendingAcknowledgement>>,
|
data: Vec<Arc<PendingAcknowledgement>>,
|
||||||
) {
|
) {
|
||||||
trace!("re-inserting pending retransmissions for {recipient}");
|
trace!("re-inserting pending retransmissions for {recipient}");
|
||||||
// the underlying entry MUST exist as we've just got data from there
|
// SAFETY: the underlying entry MUST exist as we've just got data from there
|
||||||
// and we hold a mut reference
|
// and we hold a mut reference
|
||||||
|
#[allow(clippy::expect_used)]
|
||||||
let map_entry = &mut self
|
let map_entry = &mut self
|
||||||
.surb_senders
|
.surb_senders
|
||||||
.get_mut(recipient)
|
.get_mut(recipient)
|
||||||
@@ -429,6 +430,7 @@ where
|
|||||||
.pop_at_most_n_next_messages_at_random(amount)
|
.pop_at_most_n_next_messages_at_random(amount)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[allow(clippy::panic)]
|
||||||
async fn try_clear_pending_queue(&mut self, target: AnonymousSenderTag) {
|
async fn try_clear_pending_queue(&mut self, target: AnonymousSenderTag) {
|
||||||
trace!("trying to clear pending queue");
|
trace!("trying to clear pending queue");
|
||||||
let available_surbs = self.surbs_storage.available_surbs(&target);
|
let available_surbs = self.surbs_storage.available_surbs(&target);
|
||||||
|
|||||||
@@ -165,9 +165,12 @@ impl StatisticsControl {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn start(mut self) {
|
pub(crate) fn start(mut self) {
|
||||||
spawn_future(async move {
|
spawn_future!(
|
||||||
self.run().await;
|
async move {
|
||||||
})
|
self.run().await;
|
||||||
|
},
|
||||||
|
"StatisticsControl"
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn create_and_start(
|
pub(crate) fn create_and_start(
|
||||||
|
|||||||
@@ -126,7 +126,7 @@ impl TopologyAccessor {
|
|||||||
.map(|p| p.topology.clone())
|
.map(|p| p.topology.clone())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn current_route_provider(&self) -> Option<RwLockReadGuard<NymRouteProvider>> {
|
pub async fn current_route_provider(&self) -> Option<RwLockReadGuard<'_, NymRouteProvider>> {
|
||||||
let provider = self.inner.topology.read().await;
|
let provider = self.inner.topology.read().await;
|
||||||
if provider.topology.is_empty() {
|
if provider.topology.is_empty() {
|
||||||
None
|
None
|
||||||
|
|||||||
@@ -145,36 +145,39 @@ impl TopologyRefresher {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn start(mut self) {
|
pub fn start(mut self) {
|
||||||
spawn_future(async move {
|
spawn_future!(
|
||||||
debug!("Started TopologyRefresher with graceful shutdown support");
|
async move {
|
||||||
|
debug!("Started TopologyRefresher with graceful shutdown support");
|
||||||
|
|
||||||
#[cfg(not(target_arch = "wasm32"))]
|
#[cfg(not(target_arch = "wasm32"))]
|
||||||
let mut interval = tokio_stream::wrappers::IntervalStream::new(tokio::time::interval(
|
let mut interval = tokio_stream::wrappers::IntervalStream::new(
|
||||||
self.refresh_rate,
|
tokio::time::interval(self.refresh_rate),
|
||||||
));
|
);
|
||||||
|
|
||||||
#[cfg(target_arch = "wasm32")]
|
#[cfg(target_arch = "wasm32")]
|
||||||
let mut interval =
|
let mut interval =
|
||||||
gloo_timers::future::IntervalStream::new(self.refresh_rate.as_millis() as u32);
|
gloo_timers::future::IntervalStream::new(self.refresh_rate.as_millis() as u32);
|
||||||
|
|
||||||
// We already have an initial topology, so no need to refresh it immediately.
|
// We already have an initial topology, so no need to refresh it immediately.
|
||||||
// My understanding is that js setInterval does not fire immediately, so it's not
|
// My understanding is that js setInterval does not fire immediately, so it's not
|
||||||
// needed there.
|
// needed there.
|
||||||
#[cfg(not(target_arch = "wasm32"))]
|
#[cfg(not(target_arch = "wasm32"))]
|
||||||
interval.next().await;
|
interval.next().await;
|
||||||
|
|
||||||
while !self.task_client.is_shutdown() {
|
while !self.task_client.is_shutdown() {
|
||||||
tokio::select! {
|
tokio::select! {
|
||||||
_ = interval.next() => {
|
_ = interval.next() => {
|
||||||
self.try_refresh().await;
|
self.try_refresh().await;
|
||||||
},
|
},
|
||||||
_ = self.task_client.recv() => {
|
_ = self.task_client.recv() => {
|
||||||
tracing::trace!("TopologyRefresher: Received shutdown");
|
tracing::trace!("TopologyRefresher: Received shutdown");
|
||||||
},
|
},
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
self.task_client.recv_timeout().await;
|
||||||
self.task_client.recv_timeout().await;
|
tracing::debug!("TopologyRefresher: Exiting");
|
||||||
tracing::debug!("TopologyRefresher: Exiting");
|
},
|
||||||
})
|
"TopologyRefresher"
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,8 +2,10 @@
|
|||||||
// SPDX-License-Identifier: Apache-2.0
|
// SPDX-License-Identifier: Apache-2.0
|
||||||
|
|
||||||
use async_trait::async_trait;
|
use async_trait::async_trait;
|
||||||
|
use nym_mixnet_contract_common::EpochRewardedSet;
|
||||||
use nym_topology::provider_trait::{ToTopologyMetadata, TopologyProvider};
|
use nym_topology::provider_trait::{ToTopologyMetadata, TopologyProvider};
|
||||||
use nym_topology::NymTopology;
|
use nym_topology::NymTopology;
|
||||||
|
use nym_validator_client::nym_api::NymApiClientExt;
|
||||||
use rand::prelude::SliceRandom;
|
use rand::prelude::SliceRandom;
|
||||||
use rand::thread_rng;
|
use rand::thread_rng;
|
||||||
use std::cmp::min;
|
use std::cmp::min;
|
||||||
@@ -39,30 +41,43 @@ impl Config {
|
|||||||
pub struct NymApiTopologyProvider {
|
pub struct NymApiTopologyProvider {
|
||||||
config: Config,
|
config: Config,
|
||||||
|
|
||||||
validator_client: nym_validator_client::client::NymApiClient,
|
validator_client: nym_http_api_client::Client,
|
||||||
nym_api_urls: Vec<Url>,
|
nym_api_urls: Vec<Url>,
|
||||||
currently_used_api: usize,
|
currently_used_api: usize,
|
||||||
|
use_bincode: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl NymApiTopologyProvider {
|
impl NymApiTopologyProvider {
|
||||||
pub fn new(
|
pub fn new(
|
||||||
config: impl Into<Config>,
|
config: impl Into<Config>,
|
||||||
mut nym_api_urls: Vec<Url>,
|
mut nym_api_urls: Vec<Url>,
|
||||||
mut validator_client: nym_validator_client::client::NymApiClient,
|
validator_client: nym_http_api_client::Client,
|
||||||
) -> Self {
|
) -> Self {
|
||||||
nym_api_urls.shuffle(&mut thread_rng());
|
nym_api_urls.shuffle(&mut thread_rng());
|
||||||
validator_client.change_nym_api(nym_api_urls[0].clone());
|
let mut provider = NymApiTopologyProvider {
|
||||||
|
|
||||||
NymApiTopologyProvider {
|
|
||||||
config: config.into(),
|
config: config.into(),
|
||||||
validator_client,
|
validator_client,
|
||||||
nym_api_urls,
|
nym_api_urls,
|
||||||
currently_used_api: 0,
|
currently_used_api: 0,
|
||||||
}
|
use_bincode: true,
|
||||||
|
};
|
||||||
|
// Set all API URLs - the client will try them in order with automatic failover
|
||||||
|
provider.validator_client.change_base_urls(
|
||||||
|
provider
|
||||||
|
.nym_api_urls
|
||||||
|
.iter()
|
||||||
|
.map(|u| u.clone().into())
|
||||||
|
.collect(),
|
||||||
|
);
|
||||||
|
provider
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn disable_bincode(&mut self) {
|
pub fn disable_bincode(&mut self) {
|
||||||
self.validator_client.use_bincode = false;
|
self.use_bincode = false;
|
||||||
|
// Note: The unified client doesn't support toggling bincode after creation.
|
||||||
|
// This would require recreating the client without bincode.
|
||||||
|
// For now, we'll track the preference but it won't take effect.
|
||||||
|
warn!("Disabling bincode on existing client is not currently supported");
|
||||||
}
|
}
|
||||||
|
|
||||||
fn use_next_nym_api(&mut self) {
|
fn use_next_nym_api(&mut self) {
|
||||||
@@ -72,8 +87,19 @@ impl NymApiTopologyProvider {
|
|||||||
}
|
}
|
||||||
|
|
||||||
self.currently_used_api = (self.currently_used_api + 1) % self.nym_api_urls.len();
|
self.currently_used_api = (self.currently_used_api + 1) % self.nym_api_urls.len();
|
||||||
self.validator_client
|
|
||||||
.change_nym_api(self.nym_api_urls[self.currently_used_api].clone())
|
// Provide all URLs starting from the next one in rotation order
|
||||||
|
// This enables automatic failover to other endpoints
|
||||||
|
let rotated_urls: Vec<_> = self
|
||||||
|
.nym_api_urls
|
||||||
|
.iter()
|
||||||
|
.cycle()
|
||||||
|
.skip(self.currently_used_api)
|
||||||
|
.take(self.nym_api_urls.len())
|
||||||
|
.map(|u| u.clone().into())
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
self.validator_client.change_base_urls(rotated_urls)
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn get_current_compatible_topology(&mut self) -> Option<NymTopology> {
|
async fn get_current_compatible_topology(&mut self) -> Option<NymTopology> {
|
||||||
@@ -99,8 +125,13 @@ impl NymApiTopologyProvider {
|
|||||||
.filter(|n| n.performance.round_to_integer() >= self.config.min_node_performance())
|
.filter(|n| n.performance.round_to_integer() >= self.config.min_node_performance())
|
||||||
.collect::<Vec<_>>();
|
.collect::<Vec<_>>();
|
||||||
|
|
||||||
NymTopology::new(metadata.to_topology_metadata(), rewarded_set, Vec::new())
|
let epoch_rewarded_set: EpochRewardedSet = rewarded_set.into();
|
||||||
.with_skimmed_nodes(&nodes_filtered)
|
NymTopology::new(
|
||||||
|
metadata.to_topology_metadata(),
|
||||||
|
epoch_rewarded_set,
|
||||||
|
Vec::new(),
|
||||||
|
)
|
||||||
|
.with_skimmed_nodes(&nodes_filtered)
|
||||||
} else {
|
} else {
|
||||||
// if we're not using extended topology, we're only getting active set mixnodes and gateways
|
// if we're not using extended topology, we're only getting active set mixnodes and gateways
|
||||||
|
|
||||||
@@ -148,8 +179,13 @@ impl NymApiTopologyProvider {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
NymTopology::new(metadata.to_topology_metadata(), rewarded_set, Vec::new())
|
let epoch_rewarded_set: EpochRewardedSet = rewarded_set.into();
|
||||||
.with_skimmed_nodes(&nodes)
|
NymTopology::new(
|
||||||
|
metadata.to_topology_metadata(),
|
||||||
|
epoch_rewarded_set,
|
||||||
|
Vec::new(),
|
||||||
|
)
|
||||||
|
.with_skimmed_nodes(&nodes)
|
||||||
};
|
};
|
||||||
|
|
||||||
if !topology.is_minimally_routable() {
|
if !topology.is_minimally_routable() {
|
||||||
|
|||||||
@@ -7,7 +7,9 @@ use nym_gateway_client::error::GatewayClientError;
|
|||||||
use nym_topology::node::RoutingNodeError;
|
use nym_topology::node::RoutingNodeError;
|
||||||
use nym_topology::{NodeId, NymTopologyError};
|
use nym_topology::{NodeId, NymTopologyError};
|
||||||
use nym_validator_client::nym_api::error::NymAPIError;
|
use nym_validator_client::nym_api::error::NymAPIError;
|
||||||
|
use nym_validator_client::nyxd::error::NyxdError;
|
||||||
use nym_validator_client::ValidatorClientError;
|
use nym_validator_client::ValidatorClientError;
|
||||||
|
use rand::distributions::WeightedError;
|
||||||
use std::error::Error;
|
use std::error::Error;
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
|
|
||||||
@@ -230,7 +232,19 @@ pub enum ClientCoreError {
|
|||||||
UnexpectedKeyUpgrade { gateway_id: String },
|
UnexpectedKeyUpgrade { gateway_id: String },
|
||||||
|
|
||||||
#[error("failed to derive keys from master key")]
|
#[error("failed to derive keys from master key")]
|
||||||
HkdfDerivationError {},
|
HkdfDerivationError,
|
||||||
|
|
||||||
|
#[error("missing url for constructing RPC client")]
|
||||||
|
RpcClientMissingUrl,
|
||||||
|
|
||||||
|
#[error("provided nym network details were malformed: {source}")]
|
||||||
|
InvalidNetworkDetails { source: NyxdError },
|
||||||
|
|
||||||
|
#[error("failed to construct RPC client: {source}")]
|
||||||
|
RpcClientCreationFailure { source: NyxdError },
|
||||||
|
|
||||||
|
#[error("failed to select valid gateway due to incomputable latency")]
|
||||||
|
GatewaySelectionFailure { source: WeightedError },
|
||||||
}
|
}
|
||||||
|
|
||||||
impl From<tungstenite::Error> for ClientCoreError {
|
impl From<tungstenite::Error> for ClientCoreError {
|
||||||
|
|||||||
@@ -7,7 +7,8 @@ use futures::{SinkExt, StreamExt};
|
|||||||
use nym_crypto::asymmetric::ed25519;
|
use nym_crypto::asymmetric::ed25519;
|
||||||
use nym_gateway_client::GatewayClient;
|
use nym_gateway_client::GatewayClient;
|
||||||
use nym_topology::node::RoutingNode;
|
use nym_topology::node::RoutingNode;
|
||||||
use nym_validator_client::client::IdentityKeyRef;
|
use nym_validator_client::client::{IdentityKeyRef, NymApiClientExt};
|
||||||
|
use nym_validator_client::nym_nodes::SkimmedNodesWithMetadata;
|
||||||
use nym_validator_client::UserAgent;
|
use nym_validator_client::UserAgent;
|
||||||
use rand::{seq::SliceRandom, Rng};
|
use rand::{seq::SliceRandom, Rng};
|
||||||
#[cfg(unix)]
|
#[cfg(unix)]
|
||||||
@@ -83,6 +84,48 @@ struct GatewayWithLatency<'a, G: ConnectableGateway> {
|
|||||||
latency: Duration,
|
latency: Duration,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Helper to collect all pages of entry nodes - replicates NymApiClient's convenience method
|
||||||
|
async fn get_all_basic_entry_nodes_with_metadata(
|
||||||
|
client: &nym_http_api_client::Client,
|
||||||
|
use_bincode: bool,
|
||||||
|
) -> Result<SkimmedNodesWithMetadata, ClientCoreError> {
|
||||||
|
// Get first page to obtain metadata
|
||||||
|
let mut page = 0;
|
||||||
|
let res = client
|
||||||
|
.get_basic_entry_assigned_nodes_v2(false, Some(page), None, 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;
|
||||||
|
|
||||||
|
// Collect remaining pages
|
||||||
|
loop {
|
||||||
|
let mut res = client
|
||||||
|
.get_basic_entry_assigned_nodes_v2(false, Some(page), None, use_bincode)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
if !metadata.consistency_check(&res.metadata) {
|
||||||
|
return Err(ClientCoreError::ValidatorClientError(
|
||||||
|
nym_validator_client::ValidatorClientError::InconsistentPagedMetadata,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
nodes.append(&mut res.nodes.data);
|
||||||
|
if nodes.len() < res.nodes.pagination.total {
|
||||||
|
page += 1
|
||||||
|
} else {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(SkimmedNodesWithMetadata::new(nodes, metadata))
|
||||||
|
}
|
||||||
|
|
||||||
impl<'a, G: ConnectableGateway> GatewayWithLatency<'a, G> {
|
impl<'a, G: ConnectableGateway> GatewayWithLatency<'a, G> {
|
||||||
fn new(gateway: &'a G, latency: Duration) -> Self {
|
fn new(gateway: &'a G, latency: Duration) -> Self {
|
||||||
GatewayWithLatency { gateway, latency }
|
GatewayWithLatency { gateway, latency }
|
||||||
@@ -99,16 +142,32 @@ pub async fn gateways_for_init<R: Rng>(
|
|||||||
let nym_api = nym_apis
|
let nym_api = nym_apis
|
||||||
.choose(rng)
|
.choose(rng)
|
||||||
.ok_or(ClientCoreError::ListOfNymApisIsEmpty)?;
|
.ok_or(ClientCoreError::ListOfNymApisIsEmpty)?;
|
||||||
let client = if let Some(user_agent) = user_agent {
|
|
||||||
nym_validator_client::client::NymApiClient::new_with_user_agent(nym_api.clone(), user_agent)
|
// Use the unified HTTP client directly with optional user agent
|
||||||
} else {
|
let mut builder = nym_http_api_client::Client::builder(nym_api.clone())
|
||||||
nym_validator_client::client::NymApiClient::new(nym_api.clone())
|
.map_err(|e| {
|
||||||
};
|
ClientCoreError::ValidatorClientError(
|
||||||
|
nym_validator_client::ValidatorClientError::NymAPIError { source: e },
|
||||||
|
)
|
||||||
|
})?
|
||||||
|
.with_bincode(); // Use bincode for better performance
|
||||||
|
|
||||||
|
if let Some(user_agent) = user_agent {
|
||||||
|
builder = builder.with_user_agent(user_agent);
|
||||||
|
}
|
||||||
|
|
||||||
|
let client = builder
|
||||||
|
.build::<nym_validator_client::models::RequestError>()
|
||||||
|
.map_err(|e| {
|
||||||
|
ClientCoreError::ValidatorClientError(
|
||||||
|
nym_validator_client::ValidatorClientError::NymAPIError { source: e },
|
||||||
|
)
|
||||||
|
})?;
|
||||||
|
|
||||||
tracing::debug!("Fetching list of gateways from: {nym_api}");
|
tracing::debug!("Fetching list of gateways from: {nym_api}");
|
||||||
|
|
||||||
let gateways = client
|
// Use our helper to handle pagination
|
||||||
.get_all_basic_entry_assigned_nodes_with_metadata()
|
let gateways = get_all_basic_entry_nodes_with_metadata(&client, true)
|
||||||
.await?
|
.await?
|
||||||
.nodes;
|
.nodes;
|
||||||
info!("nym api reports {} gateways", gateways.len());
|
info!("nym api reports {} gateways", gateways.len());
|
||||||
@@ -148,7 +207,7 @@ async fn connect(endpoint: &str) -> Result<WsConn, ClientCoreError> {
|
|||||||
JSWebsocket::new(endpoint).map_err(|_| ClientCoreError::GatewayJsConnectionFailure)
|
JSWebsocket::new(endpoint).map_err(|_| ClientCoreError::GatewayJsConnectionFailure)
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn measure_latency<G>(gateway: &G) -> Result<GatewayWithLatency<G>, ClientCoreError>
|
async fn measure_latency<G>(gateway: &G) -> Result<GatewayWithLatency<'_, G>, ClientCoreError>
|
||||||
where
|
where
|
||||||
G: ConnectableGateway,
|
G: ConnectableGateway,
|
||||||
{
|
{
|
||||||
@@ -245,7 +304,7 @@ pub async fn choose_gateway_by_latency<R: Rng, G: ConnectableGateway + Clone>(
|
|||||||
let gateways_with_latency = gateways_with_latency.lock().await;
|
let gateways_with_latency = gateways_with_latency.lock().await;
|
||||||
let chosen = gateways_with_latency
|
let chosen = gateways_with_latency
|
||||||
.choose_weighted(rng, |item| 1. / item.latency.as_secs_f32())
|
.choose_weighted(rng, |item| 1. / item.latency.as_secs_f32())
|
||||||
.expect("invalid selection weight!");
|
.map_err(|source| ClientCoreError::GatewaySelectionFailure { source })?;
|
||||||
|
|
||||||
info!(
|
info!(
|
||||||
"chose gateway {} with average latency of {:?}",
|
"chose gateway {} with average latency of {:?}",
|
||||||
|
|||||||
@@ -18,18 +18,54 @@ pub use nym_topology::{
|
|||||||
};
|
};
|
||||||
|
|
||||||
#[cfg(target_arch = "wasm32")]
|
#[cfg(target_arch = "wasm32")]
|
||||||
pub(crate) fn spawn_future<F>(future: F)
|
pub fn spawn_future<F>(future: F)
|
||||||
where
|
where
|
||||||
F: Future<Output = ()> + 'static,
|
F: Future<Output = ()> + 'static,
|
||||||
{
|
{
|
||||||
wasm_bindgen_futures::spawn_local(future);
|
wasm_bindgen_futures::spawn_local(future);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TODO: expose similar API to the rest of the codebase,
|
||||||
|
// perhaps with some simple trait for a task to define its name
|
||||||
|
|
||||||
#[cfg(not(target_arch = "wasm32"))]
|
#[cfg(not(target_arch = "wasm32"))]
|
||||||
pub(crate) fn spawn_future<F>(future: F)
|
#[track_caller]
|
||||||
|
pub fn spawn_future<F>(future: F)
|
||||||
where
|
where
|
||||||
F: Future + Send + 'static,
|
F: Future + Send + 'static,
|
||||||
F::Output: Send + 'static,
|
F::Output: Send + 'static,
|
||||||
{
|
{
|
||||||
tokio::spawn(future);
|
tokio::spawn(future);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(not(target_arch = "wasm32"))]
|
||||||
|
#[track_caller]
|
||||||
|
pub fn spawn_named_future<F>(future: F, name: &str)
|
||||||
|
where
|
||||||
|
F: Future + Send + 'static,
|
||||||
|
F::Output: Send + 'static,
|
||||||
|
{
|
||||||
|
cfg_if::cfg_if! {if #[cfg(tokio_unstable)] {
|
||||||
|
#[allow(clippy::expect_used)]
|
||||||
|
tokio::task::Builder::new().name(name).spawn(future).expect("failed to spawn future");
|
||||||
|
} else {
|
||||||
|
let _ = name;
|
||||||
|
tracing::debug!(r#"the underlying binary hasn't been built with `RUSTFLAGS="--cfg tokio_unstable"` - the future naming won't do anything"#);
|
||||||
|
spawn_future(future);
|
||||||
|
}}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[macro_export]
|
||||||
|
macro_rules! spawn_future {
|
||||||
|
($future:expr) => {{
|
||||||
|
$crate::spawn_future($future)
|
||||||
|
}};
|
||||||
|
($future:expr, $name:expr) => {{
|
||||||
|
cfg_if::cfg_if! {if #[cfg(not(target_arch = "wasm32"))] {
|
||||||
|
$crate::spawn_named_future($future, $name)
|
||||||
|
} else {
|
||||||
|
let _ = $name;
|
||||||
|
$crate::spawn_future($future)
|
||||||
|
}}
|
||||||
|
}};
|
||||||
|
}
|
||||||
|
|||||||
@@ -30,6 +30,7 @@ optional = true
|
|||||||
path = "../../../sqlx-pool-guard"
|
path = "../../../sqlx-pool-guard"
|
||||||
|
|
||||||
[build-dependencies]
|
[build-dependencies]
|
||||||
|
anyhow = { workspace = true }
|
||||||
tokio = { workspace = true, features = ["rt-multi-thread", "macros"] }
|
tokio = { workspace = true, features = ["rt-multi-thread", "macros"] }
|
||||||
sqlx = { workspace = true, features = [
|
sqlx = { workspace = true, features = [
|
||||||
"runtime-tokio-rustls",
|
"runtime-tokio-rustls",
|
||||||
|
|||||||
@@ -2,23 +2,24 @@
|
|||||||
// SPDX-License-Identifier: Apache-2.0
|
// SPDX-License-Identifier: Apache-2.0
|
||||||
|
|
||||||
#[tokio::main]
|
#[tokio::main]
|
||||||
async fn main() {
|
async fn main() -> anyhow::Result<()> {
|
||||||
#[cfg(feature = "fs-surb-storage")]
|
#[cfg(feature = "fs-surb-storage")]
|
||||||
{
|
{
|
||||||
|
use anyhow::Context;
|
||||||
use sqlx::{Connection, SqliteConnection};
|
use sqlx::{Connection, SqliteConnection};
|
||||||
use std::env;
|
use std::env;
|
||||||
|
|
||||||
let out_dir = env::var("OUT_DIR").unwrap();
|
let out_dir = env::var("OUT_DIR")?;
|
||||||
let database_path = format!("{out_dir}/fs-surbs-example.sqlite");
|
let database_path = format!("{out_dir}/fs-surbs-example.sqlite");
|
||||||
|
|
||||||
let mut conn = SqliteConnection::connect(&format!("sqlite://{database_path}?mode=rwc"))
|
let mut conn = SqliteConnection::connect(&format!("sqlite://{database_path}?mode=rwc"))
|
||||||
.await
|
.await
|
||||||
.expect("Failed to create SQLx database connection");
|
.context("Failed to create SQLx database connection")?;
|
||||||
|
|
||||||
sqlx::migrate!("./fs_surbs_migrations")
|
sqlx::migrate!("./fs_surbs_migrations")
|
||||||
.run(&mut conn)
|
.run(&mut conn)
|
||||||
.await
|
.await
|
||||||
.expect("Failed to perform SQLx migrations");
|
.context("Failed to perform SQLx migrations")?;
|
||||||
|
|
||||||
#[cfg(target_family = "unix")]
|
#[cfg(target_family = "unix")]
|
||||||
println!("cargo:rustc-env=DATABASE_URL=sqlite://{}", &database_path);
|
println!("cargo:rustc-env=DATABASE_URL=sqlite://{}", &database_path);
|
||||||
@@ -28,4 +29,6 @@ async fn main() {
|
|||||||
// not a valid windows path... but hey, it works...
|
// not a valid windows path... but hey, it works...
|
||||||
println!("cargo:rustc-env=DATABASE_URL=sqlite:///{}", &database_path);
|
println!("cargo:rustc-env=DATABASE_URL=sqlite:///{}", &database_path);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -201,7 +201,7 @@ impl<C, St> GatewayClient<C, St> {
|
|||||||
#[cfg(not(target_arch = "wasm32"))]
|
#[cfg(not(target_arch = "wasm32"))]
|
||||||
pub async fn establish_connection(&mut self) -> Result<(), GatewayClientError> {
|
pub async fn establish_connection(&mut self) -> Result<(), GatewayClientError> {
|
||||||
debug!(
|
debug!(
|
||||||
"Attemting to establish connection to gateway at: {}",
|
"Attempting to establish connection to gateway at: {}",
|
||||||
self.gateway_address
|
self.gateway_address
|
||||||
);
|
);
|
||||||
let (ws_stream, _) = connect_async(
|
let (ws_stream, _) = connect_async(
|
||||||
|
|||||||
@@ -337,7 +337,7 @@ impl PartiallyDelegatedHandle {
|
|||||||
// check if the split stream didn't error out
|
// check if the split stream didn't error out
|
||||||
let receive_res = stream_receiver
|
let receive_res = stream_receiver
|
||||||
.try_recv()
|
.try_recv()
|
||||||
.expect("stream sender was somehow dropped without sending anything!");
|
.map_err(|_| GatewayClientError::ConnectionAbruptlyClosed)?;
|
||||||
|
|
||||||
if let Some(res) = receive_res {
|
if let Some(res) = receive_res {
|
||||||
let _res = res?;
|
let _res = res?;
|
||||||
|
|||||||
@@ -5,8 +5,8 @@ use crate::nyxd::{self, NyxdClient};
|
|||||||
use crate::signing::direct_wallet::DirectSecp256k1HdWallet;
|
use crate::signing::direct_wallet::DirectSecp256k1HdWallet;
|
||||||
use crate::signing::signer::{NoSigner, OfflineSigner};
|
use crate::signing::signer::{NoSigner, OfflineSigner};
|
||||||
use crate::{
|
use crate::{
|
||||||
nym_api, DirectSigningReqwestRpcValidatorClient, QueryReqwestRpcValidatorClient,
|
DirectSigningReqwestRpcValidatorClient, QueryReqwestRpcValidatorClient, ReqwestRpcClient,
|
||||||
ReqwestRpcClient, ValidatorClientError,
|
ValidatorClientError,
|
||||||
};
|
};
|
||||||
use nym_api_requests::ecash::models::{
|
use nym_api_requests::ecash::models::{
|
||||||
AggregatedCoinIndicesSignatureResponse, AggregatedExpirationDateSignatureResponse,
|
AggregatedCoinIndicesSignatureResponse, AggregatedExpirationDateSignatureResponse,
|
||||||
@@ -153,7 +153,7 @@ impl Config {
|
|||||||
pub struct Client<C, S = NoSigner> {
|
pub struct Client<C, S = NoSigner> {
|
||||||
// ideally they would have been read-only, but unfortunately rust doesn't have such features
|
// ideally they would have been read-only, but unfortunately rust doesn't have such features
|
||||||
// #[deprecated(note = "please use `nym_api_client` instead")]
|
// #[deprecated(note = "please use `nym_api_client` instead")]
|
||||||
pub nym_api: nym_api::Client,
|
pub nym_api: nym_http_api_client::Client,
|
||||||
// pub nym_api_client: NymApiClient,
|
// pub nym_api_client: NymApiClient,
|
||||||
pub nyxd: NyxdClient<C, S>,
|
pub nyxd: NyxdClient<C, S>,
|
||||||
}
|
}
|
||||||
@@ -214,7 +214,7 @@ impl Client<ReqwestRpcClient> {
|
|||||||
|
|
||||||
impl<C> Client<C> {
|
impl<C> Client<C> {
|
||||||
pub fn new_with_rpc_client(config: Config, rpc_client: C) -> Self {
|
pub fn new_with_rpc_client(config: Config, rpc_client: C) -> Self {
|
||||||
let nym_api_client = nym_api::Client::new(config.api_url.clone(), None);
|
let nym_api_client = nym_http_api_client::Client::new(config.api_url.clone(), None);
|
||||||
|
|
||||||
Client {
|
Client {
|
||||||
nym_api: nym_api_client,
|
nym_api: nym_api_client,
|
||||||
@@ -228,7 +228,7 @@ impl<C, S> Client<C, S> {
|
|||||||
where
|
where
|
||||||
S: OfflineSigner,
|
S: OfflineSigner,
|
||||||
{
|
{
|
||||||
let nym_api_client = nym_api::Client::new(config.api_url.clone(), None);
|
let nym_api_client = nym_http_api_client::Client::new(config.api_url.clone(), None);
|
||||||
|
|
||||||
Client {
|
Client {
|
||||||
nym_api: nym_api_client,
|
nym_api: nym_api_client,
|
||||||
@@ -385,38 +385,25 @@ impl<C, S> Client<C, S> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// DEPRECATED: Use nym_http_api_client::Client with from_network() or with_bincode() instead
|
||||||
|
#[deprecated(
|
||||||
|
since = "1.2.0",
|
||||||
|
note = "Use nym_http_api_client::Client::from_network() or ClientBuilder::with_bincode() instead"
|
||||||
|
)]
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
pub struct NymApiClient {
|
pub struct NymApiClient {
|
||||||
pub use_bincode: bool,
|
pub use_bincode: bool,
|
||||||
pub nym_api: nym_api::Client,
|
pub nym_api: nym_http_api_client::Client,
|
||||||
// TODO: perhaps if we really need it at some (currently I don't see any reasons for it)
|
// TODO: perhaps if we really need it at some (currently I don't see any reasons for it)
|
||||||
// we could re-implement the communication with the REST API on port 1317
|
// we could re-implement the communication with the REST API on port 1317
|
||||||
}
|
}
|
||||||
|
|
||||||
impl From<nym_api::Client> for NymApiClient {
|
|
||||||
fn from(nym_api: nym_api::Client) -> Self {
|
|
||||||
NymApiClient {
|
|
||||||
use_bincode: false,
|
|
||||||
nym_api,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// we have to allow the use of deprecated method here as they're calling the deprecated trait methods
|
// we have to allow the use of deprecated method here as they're calling the deprecated trait methods
|
||||||
#[allow(deprecated)]
|
#[allow(deprecated)]
|
||||||
impl NymApiClient {
|
impl NymApiClient {
|
||||||
pub fn new(api_url: Url) -> Self {
|
|
||||||
let nym_api = nym_api::Client::new(api_url, None);
|
|
||||||
|
|
||||||
NymApiClient {
|
|
||||||
use_bincode: true,
|
|
||||||
nym_api,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(not(target_arch = "wasm32"))]
|
#[cfg(not(target_arch = "wasm32"))]
|
||||||
pub fn new_with_timeout(api_url: Url, timeout: std::time::Duration) -> Self {
|
pub fn new_with_timeout(api_url: Url, timeout: std::time::Duration) -> Self {
|
||||||
let nym_api = nym_api::Client::new(api_url, Some(timeout));
|
let nym_api = nym_http_api_client::Client::new(api_url, Some(timeout));
|
||||||
|
|
||||||
NymApiClient {
|
NymApiClient {
|
||||||
use_bincode: true,
|
use_bincode: true,
|
||||||
@@ -431,7 +418,7 @@ impl NymApiClient {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn new_with_user_agent(api_url: Url, user_agent: impl Into<UserAgent>) -> Self {
|
pub fn new_with_user_agent(api_url: Url, user_agent: impl Into<UserAgent>) -> Self {
|
||||||
let nym_api = nym_api::Client::builder::<_, ValidatorClientError>(api_url)
|
let nym_api = nym_http_api_client::Client::builder::<_, ValidatorClientError>(api_url)
|
||||||
.expect("invalid api url")
|
.expect("invalid api url")
|
||||||
.with_user_agent(user_agent.into())
|
.with_user_agent(user_agent.into())
|
||||||
.build::<ValidatorClientError>()
|
.build::<ValidatorClientError>()
|
||||||
@@ -719,10 +706,11 @@ impl NymApiClient {
|
|||||||
pub async fn partial_expiration_date_signatures(
|
pub async fn partial_expiration_date_signatures(
|
||||||
&self,
|
&self,
|
||||||
expiration_date: Option<Date>,
|
expiration_date: Option<Date>,
|
||||||
|
epoch_id: Option<EpochId>,
|
||||||
) -> Result<PartialExpirationDateSignatureResponse, ValidatorClientError> {
|
) -> Result<PartialExpirationDateSignatureResponse, ValidatorClientError> {
|
||||||
Ok(self
|
Ok(self
|
||||||
.nym_api
|
.nym_api
|
||||||
.partial_expiration_date_signatures(expiration_date)
|
.partial_expiration_date_signatures(expiration_date, epoch_id)
|
||||||
.await?)
|
.await?)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -739,10 +727,11 @@ impl NymApiClient {
|
|||||||
pub async fn global_expiration_date_signatures(
|
pub async fn global_expiration_date_signatures(
|
||||||
&self,
|
&self,
|
||||||
expiration_date: Option<Date>,
|
expiration_date: Option<Date>,
|
||||||
|
epoch_id: Option<EpochId>,
|
||||||
) -> Result<AggregatedExpirationDateSignatureResponse, ValidatorClientError> {
|
) -> Result<AggregatedExpirationDateSignatureResponse, ValidatorClientError> {
|
||||||
Ok(self
|
Ok(self
|
||||||
.nym_api
|
.nym_api
|
||||||
.global_expiration_date_signatures(expiration_date)
|
.global_expiration_date_signatures(expiration_date, epoch_id)
|
||||||
.await?)
|
.await?)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -3,7 +3,6 @@
|
|||||||
|
|
||||||
use crate::nyxd::contract_traits::{DkgQueryClient, PagedDkgQueryClient};
|
use crate::nyxd::contract_traits::{DkgQueryClient, PagedDkgQueryClient};
|
||||||
use crate::nyxd::error::NyxdError;
|
use crate::nyxd::error::NyxdError;
|
||||||
use crate::NymApiClient;
|
|
||||||
use nym_coconut_dkg_common::types::{EpochId, NodeIndex};
|
use nym_coconut_dkg_common::types::{EpochId, NodeIndex};
|
||||||
use nym_coconut_dkg_common::verification_key::ContractVKShare;
|
use nym_coconut_dkg_common::verification_key::ContractVKShare;
|
||||||
use nym_compact_ecash::error::CompactEcashError;
|
use nym_compact_ecash::error::CompactEcashError;
|
||||||
@@ -15,7 +14,7 @@ use url::Url;
|
|||||||
// TODO: it really doesn't feel like this should live in this crate.
|
// TODO: it really doesn't feel like this should live in this crate.
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
pub struct EcashApiClient {
|
pub struct EcashApiClient {
|
||||||
pub api_client: NymApiClient,
|
pub api_client: nym_http_api_client::Client,
|
||||||
pub verification_key: VerificationKeyAuth,
|
pub verification_key: VerificationKeyAuth,
|
||||||
pub node_id: NodeIndex,
|
pub node_id: NodeIndex,
|
||||||
pub cosmos_address: cosmrs::AccountId,
|
pub cosmos_address: cosmrs::AccountId,
|
||||||
@@ -25,10 +24,10 @@ impl Display for EcashApiClient {
|
|||||||
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
|
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
|
||||||
write!(
|
write!(
|
||||||
f,
|
f,
|
||||||
"[id: {}] {} @ {}",
|
"[id: {}] {} @ {:?}",
|
||||||
self.node_id,
|
self.node_id,
|
||||||
self.cosmos_address,
|
self.cosmos_address,
|
||||||
self.api_client.api_url()
|
self.api_client.base_urls()
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -60,6 +59,9 @@ pub enum EcashApiError {
|
|||||||
source: CompactEcashError,
|
source: CompactEcashError,
|
||||||
},
|
},
|
||||||
|
|
||||||
|
#[error("failed to create API client: {0}")]
|
||||||
|
ClientError(String),
|
||||||
|
|
||||||
#[error("the provided account address is malformed: {source}")]
|
#[error("the provided account address is malformed: {source}")]
|
||||||
MalformedAccountAddress {
|
MalformedAccountAddress {
|
||||||
#[from]
|
#[from]
|
||||||
@@ -89,8 +91,16 @@ impl TryFrom<ContractVKShare> for EcashApiClient {
|
|||||||
// In non-client applications this resolver can cause warning logs about H2 connection
|
// In non-client applications this resolver can cause warning logs about H2 connection
|
||||||
// failure. This indicates that the long lived https connection was closed by the remote
|
// failure. This indicates that the long lived https connection was closed by the remote
|
||||||
// peer and the resolver will have to reconnect. It should not impact actual functionality
|
// peer and the resolver will have to reconnect. It should not impact actual functionality
|
||||||
|
let api_client = nym_http_api_client::Client::builder::<
|
||||||
|
_,
|
||||||
|
nym_api_requests::models::RequestError,
|
||||||
|
>(url_address)
|
||||||
|
.map_err(|e| EcashApiError::ClientError(e.to_string()))?
|
||||||
|
.build::<nym_api_requests::models::RequestError>()
|
||||||
|
.map_err(|e| EcashApiError::ClientError(e.to_string()))?;
|
||||||
|
|
||||||
Ok(EcashApiClient {
|
Ok(EcashApiClient {
|
||||||
api_client: NymApiClient::new(url_address),
|
api_client,
|
||||||
verification_key: VerificationKeyAuth::try_from_bs58(&share.share)?,
|
verification_key: VerificationKeyAuth::try_from_bs58(&share.share)?,
|
||||||
node_id: share.node_index,
|
node_id: share.node_index,
|
||||||
cosmos_address: share.owner.as_str().parse()?,
|
cosmos_address: share.owner.as_str().parse()?,
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
|
use crate::nym_api::NymApiClientExt;
|
||||||
use crate::nyxd::contract_traits::MixnetQueryClient;
|
use crate::nyxd::contract_traits::MixnetQueryClient;
|
||||||
use crate::nyxd::error::NyxdError;
|
use crate::nyxd::error::NyxdError;
|
||||||
use crate::nyxd::Config as ClientConfig;
|
use crate::nyxd::Config as ClientConfig;
|
||||||
use crate::{NymApiClient, QueryHttpRpcNyxdClient, ValidatorClientError};
|
use crate::{QueryHttpRpcNyxdClient, ValidatorClientError};
|
||||||
use colored::Colorize;
|
use colored::Colorize;
|
||||||
use core::fmt;
|
use core::fmt;
|
||||||
use itertools::Itertools;
|
use itertools::Itertools;
|
||||||
@@ -87,8 +88,19 @@ fn setup_connection_tests<H: BuildHasher + 'static>(
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
let api_connection_test_clients = api_urls.map(|(network, url)| {
|
let api_connection_test_clients = api_urls.filter_map(|(network, url)| {
|
||||||
ClientForConnectionTest::Api(network, url.clone(), NymApiClient::new(url))
|
match nym_http_api_client::Client::builder(url.clone())
|
||||||
|
.and_then(|b| b.build::<nym_api_requests::models::RequestError>())
|
||||||
|
{
|
||||||
|
Ok(client) => Some(ClientForConnectionTest::Api(network, url, client)),
|
||||||
|
Err(err) => {
|
||||||
|
eprintln!(
|
||||||
|
"Failed to create API client for {}: {err}",
|
||||||
|
network.network_name
|
||||||
|
);
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
nyxd_connection_test_clients.chain(api_connection_test_clients)
|
nyxd_connection_test_clients.chain(api_connection_test_clients)
|
||||||
@@ -160,7 +172,7 @@ async fn test_nyxd_connection(
|
|||||||
async fn test_nym_api_connection(
|
async fn test_nym_api_connection(
|
||||||
network: NymNetworkDetails,
|
network: NymNetworkDetails,
|
||||||
url: &Url,
|
url: &Url,
|
||||||
client: &NymApiClient,
|
client: &nym_http_api_client::Client,
|
||||||
) -> ConnectionResult {
|
) -> ConnectionResult {
|
||||||
let result = match timeout(
|
let result = match timeout(
|
||||||
Duration::from_secs(CONNECTION_TEST_TIMEOUT_SEC),
|
Duration::from_secs(CONNECTION_TEST_TIMEOUT_SEC),
|
||||||
@@ -186,7 +198,7 @@ async fn test_nym_api_connection(
|
|||||||
|
|
||||||
enum ClientForConnectionTest {
|
enum ClientForConnectionTest {
|
||||||
Nyxd(NymNetworkDetails, Url, Box<QueryHttpRpcNyxdClient>),
|
Nyxd(NymNetworkDetails, Url, Box<QueryHttpRpcNyxdClient>),
|
||||||
Api(NymNetworkDetails, Url, NymApiClient),
|
Api(NymNetworkDetails, Url, nym_http_api_client::Client),
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ClientForConnectionTest {
|
impl ClientForConnectionTest {
|
||||||
|
|||||||
@@ -14,7 +14,6 @@ pub mod signing;
|
|||||||
pub use crate::error::ValidatorClientError;
|
pub use crate::error::ValidatorClientError;
|
||||||
pub use crate::rpc::reqwest::ReqwestRpcClient;
|
pub use crate::rpc::reqwest::ReqwestRpcClient;
|
||||||
pub use crate::signing::direct_wallet::DirectSecp256k1HdWallet;
|
pub use crate::signing::direct_wallet::DirectSecp256k1HdWallet;
|
||||||
pub use client::NymApiClient;
|
|
||||||
pub use client::{Client, Config, EcashApiClient};
|
pub use client::{Client, Config, EcashApiClient};
|
||||||
pub use nym_api_requests::*;
|
pub use nym_api_requests::*;
|
||||||
pub use nym_http_api_client::UserAgent;
|
pub use nym_http_api_client::UserAgent;
|
||||||
|
|||||||
@@ -3,19 +3,22 @@
|
|||||||
|
|
||||||
use crate::nym_api::error::NymAPIError;
|
use crate::nym_api::error::NymAPIError;
|
||||||
use crate::nym_api::routes::{ecash, CORE_STATUS_COUNT, SINCE_ARG};
|
use crate::nym_api::routes::{ecash, CORE_STATUS_COUNT, SINCE_ARG};
|
||||||
|
use crate::nym_nodes::SkimmedNodesWithMetadata;
|
||||||
use async_trait::async_trait;
|
use async_trait::async_trait;
|
||||||
use nym_api_requests::ecash::models::{
|
use nym_api_requests::ecash::models::{
|
||||||
AggregatedCoinIndicesSignatureResponse, AggregatedExpirationDateSignatureResponse,
|
AggregatedCoinIndicesSignatureResponse, AggregatedExpirationDateSignatureResponse,
|
||||||
BatchRedeemTicketsBody, EcashBatchTicketRedemptionResponse, EcashTicketVerificationResponse,
|
BatchRedeemTicketsBody, EcashBatchTicketRedemptionResponse, EcashSignerStatusResponse,
|
||||||
IssuedTicketbooksChallengeCommitmentRequest, IssuedTicketbooksChallengeCommitmentResponse,
|
EcashTicketVerificationResponse, IssuedTicketbooksChallengeCommitmentRequest,
|
||||||
IssuedTicketbooksDataRequest, IssuedTicketbooksDataResponse, IssuedTicketbooksForCountResponse,
|
IssuedTicketbooksChallengeCommitmentResponse, IssuedTicketbooksDataRequest,
|
||||||
IssuedTicketbooksForResponse, VerifyEcashTicketBody,
|
IssuedTicketbooksDataResponse, IssuedTicketbooksForCountResponse, IssuedTicketbooksForResponse,
|
||||||
|
VerifyEcashTicketBody,
|
||||||
};
|
};
|
||||||
use nym_api_requests::ecash::VerificationKeyResponse;
|
use nym_api_requests::ecash::VerificationKeyResponse;
|
||||||
use nym_api_requests::models::{
|
use nym_api_requests::models::{
|
||||||
AnnotationResponse, ApiHealthResponse, BinaryBuildInformationOwned, ChainStatusResponse,
|
AnnotationResponse, ApiHealthResponse, BinaryBuildInformationOwned, ChainBlocksStatusResponse,
|
||||||
KeyRotationInfoResponse, LegacyDescribedMixNode, NodePerformanceResponse, NodeRefreshBody,
|
ChainStatusResponse, KeyRotationInfoResponse, LegacyDescribedMixNode, NodePerformanceResponse,
|
||||||
NymNodeDescription, PerformanceHistoryResponse, RewardedSetResponse,
|
NodeRefreshBody, NymNodeDescription, PerformanceHistoryResponse, RewardedSetResponse,
|
||||||
|
SignerInformationResponse,
|
||||||
};
|
};
|
||||||
use nym_api_requests::nym_nodes::{
|
use nym_api_requests::nym_nodes::{
|
||||||
NodesByAddressesRequestBody, NodesByAddressesResponse, PaginatedCachedNodesResponseV1,
|
NodesByAddressesRequestBody, NodesByAddressesResponse, PaginatedCachedNodesResponseV1,
|
||||||
@@ -35,7 +38,7 @@ pub use nym_api_requests::{
|
|||||||
MixnodeStatusResponse, MixnodeUptimeHistoryResponse, RewardEstimationResponse,
|
MixnodeStatusResponse, MixnodeUptimeHistoryResponse, RewardEstimationResponse,
|
||||||
StakeSaturationResponse, UptimeResponse,
|
StakeSaturationResponse, UptimeResponse,
|
||||||
},
|
},
|
||||||
nym_nodes::{CachedNodesResponse, SemiSkimmedNode, SkimmedNode},
|
nym_nodes::{CachedNodesResponse, SemiSkimmedNode, SemiSkimmedNodesWithMetadata, SkimmedNode},
|
||||||
NymNetworkDetailsResponse,
|
NymNetworkDetailsResponse,
|
||||||
};
|
};
|
||||||
use nym_contracts_common::IdentityKey;
|
use nym_contracts_common::IdentityKey;
|
||||||
@@ -47,8 +50,8 @@ use time::format_description::BorrowedFormatItem;
|
|||||||
use time::Date;
|
use time::Date;
|
||||||
use tracing::instrument;
|
use tracing::instrument;
|
||||||
|
|
||||||
|
use crate::ValidatorClientError;
|
||||||
pub use nym_coconut_dkg_common::types::EpochId;
|
pub use nym_coconut_dkg_common::types::EpochId;
|
||||||
pub use nym_http_api_client::Client;
|
|
||||||
|
|
||||||
pub mod error;
|
pub mod error;
|
||||||
pub mod routes;
|
pub mod routes;
|
||||||
@@ -60,6 +63,9 @@ pub fn rfc_3339_date() -> Vec<BorrowedFormatItem<'static>> {
|
|||||||
#[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
|
#[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
|
||||||
#[cfg_attr(not(target_arch = "wasm32"), async_trait)]
|
#[cfg_attr(not(target_arch = "wasm32"), async_trait)]
|
||||||
pub trait NymApiClientExt: ApiClient {
|
pub trait NymApiClientExt: ApiClient {
|
||||||
|
/// Get the current API URL being used by the client
|
||||||
|
fn api_url(&self) -> &url::Url;
|
||||||
|
|
||||||
async fn health(&self) -> Result<ApiHealthResponse, NymAPIError> {
|
async fn health(&self) -> Result<ApiHealthResponse, NymAPIError> {
|
||||||
self.get_json(
|
self.get_json(
|
||||||
&[
|
&[
|
||||||
@@ -239,6 +245,162 @@ pub trait NymApiClientExt: ApiClient {
|
|||||||
.await
|
.await
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async fn get_current_rewarded_set(&self) -> Result<RewardedSetResponse, NymAPIError> {
|
||||||
|
self.get_rewarded_set().await
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn get_all_basic_nodes_with_metadata(
|
||||||
|
&self,
|
||||||
|
) -> Result<SkimmedNodesWithMetadata, NymAPIError> {
|
||||||
|
// unroll first loop iteration in order to obtain the metadata
|
||||||
|
let mut page = 0;
|
||||||
|
let res = self
|
||||||
|
.get_basic_nodes_v2(false, Some(page), None, true)
|
||||||
|
.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
|
||||||
|
.get_basic_nodes_v2(false, Some(page), None, true)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
if !metadata.consistency_check(&res.metadata) {
|
||||||
|
// Create a custom error for inconsistent metadata
|
||||||
|
return Err(NymAPIError::EndpointFailure {
|
||||||
|
status: reqwest::StatusCode::INTERNAL_SERVER_ERROR,
|
||||||
|
error: nym_api_requests::models::RequestError::new(
|
||||||
|
"Inconsistent paged metadata",
|
||||||
|
),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
nodes.append(&mut res.nodes.data);
|
||||||
|
if nodes.len() >= res.nodes.pagination.total {
|
||||||
|
break;
|
||||||
|
} else {
|
||||||
|
page += 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(SkimmedNodesWithMetadata::new(nodes, metadata))
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn get_all_basic_active_mixing_assigned_nodes_with_metadata(
|
||||||
|
&self,
|
||||||
|
) -> Result<SkimmedNodesWithMetadata, NymAPIError> {
|
||||||
|
// Get all mixing nodes that are in the active/rewarded set
|
||||||
|
let mut page = 0;
|
||||||
|
let res = self
|
||||||
|
.get_basic_active_mixing_assigned_nodes_v2(false, Some(page), None, false)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
let metadata = res.metadata;
|
||||||
|
let mut nodes = res.nodes.data;
|
||||||
|
|
||||||
|
if res.nodes.pagination.total == nodes.len() {
|
||||||
|
return Ok(SkimmedNodesWithMetadata::new(nodes, metadata));
|
||||||
|
}
|
||||||
|
|
||||||
|
page += 1;
|
||||||
|
|
||||||
|
loop {
|
||||||
|
let res = self
|
||||||
|
.get_basic_active_mixing_assigned_nodes_v2(false, Some(page), None, false)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
if !metadata.consistency_check(&res.metadata) {
|
||||||
|
return Err(NymAPIError::EndpointFailure {
|
||||||
|
status: reqwest::StatusCode::INTERNAL_SERVER_ERROR,
|
||||||
|
error: nym_api_requests::models::RequestError::new(
|
||||||
|
"Inconsistent paged metadata",
|
||||||
|
),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
nodes.append(&mut res.nodes.data.clone());
|
||||||
|
|
||||||
|
// Check if we've got all nodes
|
||||||
|
if nodes.len() >= res.nodes.pagination.total {
|
||||||
|
break;
|
||||||
|
} else {
|
||||||
|
page += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(SkimmedNodesWithMetadata::new(nodes, metadata))
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn get_all_basic_entry_assigned_nodes_with_metadata(
|
||||||
|
&self,
|
||||||
|
) -> Result<SkimmedNodesWithMetadata, NymAPIError> {
|
||||||
|
// Get all nodes that can act as entry gateways
|
||||||
|
let mut page = 0;
|
||||||
|
let res = self
|
||||||
|
.get_basic_entry_assigned_nodes_v2(false, Some(page), None, false)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
let metadata = res.metadata;
|
||||||
|
let mut nodes = res.nodes.data;
|
||||||
|
|
||||||
|
if res.nodes.pagination.total == nodes.len() {
|
||||||
|
return Ok(SkimmedNodesWithMetadata::new(nodes, metadata));
|
||||||
|
}
|
||||||
|
|
||||||
|
page += 1;
|
||||||
|
|
||||||
|
loop {
|
||||||
|
let res = self
|
||||||
|
.get_basic_entry_assigned_nodes_v2(false, Some(page), None, false)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
if !metadata.consistency_check(&res.metadata) {
|
||||||
|
return Err(NymAPIError::EndpointFailure {
|
||||||
|
status: reqwest::StatusCode::INTERNAL_SERVER_ERROR,
|
||||||
|
error: nym_api_requests::models::RequestError::new(
|
||||||
|
"Inconsistent paged metadata",
|
||||||
|
),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
nodes.append(&mut res.nodes.data.clone());
|
||||||
|
|
||||||
|
// Check if we've got all nodes
|
||||||
|
if nodes.len() >= res.nodes.pagination.total {
|
||||||
|
break;
|
||||||
|
} else {
|
||||||
|
page += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(SkimmedNodesWithMetadata::new(nodes, metadata))
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn get_all_described_nodes(&self) -> Result<Vec<NymNodeDescription>, NymAPIError> {
|
||||||
|
// 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 descriptions = Vec::new();
|
||||||
|
|
||||||
|
loop {
|
||||||
|
let mut res = self.get_nodes_described(Some(page), None).await?;
|
||||||
|
|
||||||
|
descriptions.append(&mut res.data);
|
||||||
|
if descriptions.len() < res.pagination.total {
|
||||||
|
page += 1
|
||||||
|
} else {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(descriptions)
|
||||||
|
}
|
||||||
|
|
||||||
#[tracing::instrument(level = "debug", skip_all)]
|
#[tracing::instrument(level = "debug", skip_all)]
|
||||||
async fn get_nym_nodes(
|
async fn get_nym_nodes(
|
||||||
&self,
|
&self,
|
||||||
@@ -266,6 +428,25 @@ pub trait NymApiClientExt: ApiClient {
|
|||||||
.await
|
.await
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async fn get_all_bonded_nym_nodes(&self) -> Result<Vec<NymNodeDetails>, ValidatorClientError> {
|
||||||
|
// 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 bonds = Vec::new();
|
||||||
|
|
||||||
|
loop {
|
||||||
|
let mut res = self.get_nym_nodes(Some(page), None).await?;
|
||||||
|
|
||||||
|
bonds.append(&mut res.data);
|
||||||
|
if bonds.len() < res.pagination.total {
|
||||||
|
page += 1
|
||||||
|
} else {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(bonds)
|
||||||
|
}
|
||||||
|
|
||||||
#[deprecated]
|
#[deprecated]
|
||||||
#[tracing::instrument(level = "debug", skip_all)]
|
#[tracing::instrument(level = "debug", skip_all)]
|
||||||
async fn get_basic_mixnodes(&self) -> Result<CachedNodesResponse<SkimmedNode>, NymAPIError> {
|
async fn get_basic_mixnodes(&self) -> Result<CachedNodesResponse<SkimmedNode>, NymAPIError> {
|
||||||
@@ -1101,8 +1282,9 @@ pub trait NymApiClientExt: ApiClient {
|
|||||||
async fn partial_expiration_date_signatures(
|
async fn partial_expiration_date_signatures(
|
||||||
&self,
|
&self,
|
||||||
expiration_date: Option<Date>,
|
expiration_date: Option<Date>,
|
||||||
|
epoch_id: Option<EpochId>,
|
||||||
) -> Result<PartialExpirationDateSignatureResponse, NymAPIError> {
|
) -> Result<PartialExpirationDateSignatureResponse, NymAPIError> {
|
||||||
let params = match expiration_date {
|
let mut params = match expiration_date {
|
||||||
None => Vec::new(),
|
None => Vec::new(),
|
||||||
Some(exp) => vec![(
|
Some(exp) => vec![(
|
||||||
ecash::EXPIRATION_DATE_PARAM,
|
ecash::EXPIRATION_DATE_PARAM,
|
||||||
@@ -1110,6 +1292,10 @@ pub trait NymApiClientExt: ApiClient {
|
|||||||
)],
|
)],
|
||||||
};
|
};
|
||||||
|
|
||||||
|
if let Some(epoch_id) = epoch_id {
|
||||||
|
params.push((ecash::EPOCH_ID_PARAM, epoch_id.to_string()));
|
||||||
|
}
|
||||||
|
|
||||||
self.get_json(
|
self.get_json(
|
||||||
&[
|
&[
|
||||||
routes::V1_API_VERSION,
|
routes::V1_API_VERSION,
|
||||||
@@ -1146,8 +1332,9 @@ pub trait NymApiClientExt: ApiClient {
|
|||||||
async fn global_expiration_date_signatures(
|
async fn global_expiration_date_signatures(
|
||||||
&self,
|
&self,
|
||||||
expiration_date: Option<Date>,
|
expiration_date: Option<Date>,
|
||||||
|
epoch_id: Option<EpochId>,
|
||||||
) -> Result<AggregatedExpirationDateSignatureResponse, NymAPIError> {
|
) -> Result<AggregatedExpirationDateSignatureResponse, NymAPIError> {
|
||||||
let params = match expiration_date {
|
let mut params = match expiration_date {
|
||||||
None => Vec::new(),
|
None => Vec::new(),
|
||||||
Some(exp) => vec![(
|
Some(exp) => vec![(
|
||||||
ecash::EXPIRATION_DATE_PARAM,
|
ecash::EXPIRATION_DATE_PARAM,
|
||||||
@@ -1155,6 +1342,10 @@ pub trait NymApiClientExt: ApiClient {
|
|||||||
)],
|
)],
|
||||||
};
|
};
|
||||||
|
|
||||||
|
if let Some(epoch_id) = epoch_id {
|
||||||
|
params.push((ecash::EPOCH_ID_PARAM, epoch_id.to_string()));
|
||||||
|
}
|
||||||
|
|
||||||
self.get_json(
|
self.get_json(
|
||||||
&[
|
&[
|
||||||
routes::V1_API_VERSION,
|
routes::V1_API_VERSION,
|
||||||
@@ -1331,6 +1522,22 @@ pub trait NymApiClientExt: ApiClient {
|
|||||||
.await
|
.await
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async fn get_chain_blocks_status(&self) -> Result<ChainBlocksStatusResponse, NymAPIError> {
|
||||||
|
self.get_json("/v1/network/chain-blocks-status", NO_PARAMS)
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
|
||||||
|
#[instrument(level = "debug", skip(self))]
|
||||||
|
async fn get_signer_status(&self) -> Result<EcashSignerStatusResponse, NymAPIError> {
|
||||||
|
self.get_json("/v1/ecash/signer-status", NO_PARAMS).await
|
||||||
|
}
|
||||||
|
|
||||||
|
#[instrument(level = "debug", skip(self))]
|
||||||
|
async fn get_signer_information(&self) -> Result<SignerInformationResponse, NymAPIError> {
|
||||||
|
self.get_json("/v1/api-status/signer-information", NO_PARAMS)
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
|
||||||
#[instrument(level = "debug", skip(self))]
|
#[instrument(level = "debug", skip(self))]
|
||||||
async fn get_key_rotation_info(&self) -> Result<KeyRotationInfoResponse, NymAPIError> {
|
async fn get_key_rotation_info(&self) -> Result<KeyRotationInfoResponse, NymAPIError> {
|
||||||
self.get_json(
|
self.get_json(
|
||||||
@@ -1343,8 +1550,49 @@ pub trait NymApiClientExt: ApiClient {
|
|||||||
)
|
)
|
||||||
.await
|
.await
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Method to change the base API URLs being used by the client
|
||||||
|
fn change_base_urls(&mut self, urls: Vec<url::Url>);
|
||||||
|
|
||||||
|
/// Retrieve expanded information for all bonded nodes on the network
|
||||||
|
async fn get_all_expanded_nodes(&self) -> Result<SemiSkimmedNodesWithMetadata, NymAPIError> {
|
||||||
|
// Unroll the first iteration to get the metadata
|
||||||
|
let mut page = 0;
|
||||||
|
|
||||||
|
let res = self.get_expanded_nodes(false, Some(page), None).await?;
|
||||||
|
let mut nodes = res.nodes.data;
|
||||||
|
let metadata = res.metadata;
|
||||||
|
|
||||||
|
if res.nodes.pagination.total == nodes.len() {
|
||||||
|
return Ok(SemiSkimmedNodesWithMetadata::new(nodes, metadata));
|
||||||
|
}
|
||||||
|
|
||||||
|
page += 1;
|
||||||
|
|
||||||
|
loop {
|
||||||
|
let mut res = self.get_expanded_nodes(false, Some(page), None).await?;
|
||||||
|
|
||||||
|
nodes.append(&mut res.nodes.data);
|
||||||
|
if nodes.len() < res.nodes.pagination.total {
|
||||||
|
page += 1
|
||||||
|
} else {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(SemiSkimmedNodesWithMetadata::new(nodes, metadata))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Client is already nym_http_api_client::Client (re-exported above), so just one impl needed
|
||||||
#[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
|
#[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
|
||||||
#[cfg_attr(not(target_arch = "wasm32"), async_trait)]
|
#[cfg_attr(not(target_arch = "wasm32"), async_trait)]
|
||||||
impl NymApiClientExt for Client {}
|
impl NymApiClientExt for nym_http_api_client::Client {
|
||||||
|
fn api_url(&self) -> &url::Url {
|
||||||
|
self.current_url().as_ref()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn change_base_urls(&mut self, urls: Vec<url::Url>) {
|
||||||
|
self.change_base_urls(urls.into_iter().map(|u| u.into()).collect());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -8,11 +8,11 @@ use crate::nyxd::CosmWasmClient;
|
|||||||
use async_trait::async_trait;
|
use async_trait::async_trait;
|
||||||
use cosmrs::AccountId;
|
use cosmrs::AccountId;
|
||||||
use cosmwasm_std::Addr;
|
use cosmwasm_std::Addr;
|
||||||
|
use nym_coconut_dkg_common::dealer::RegisteredDealerDetails;
|
||||||
use nym_coconut_dkg_common::types::{ChunkIndex, NodeIndex, StateAdvanceResponse};
|
use nym_coconut_dkg_common::types::{ChunkIndex, NodeIndex, StateAdvanceResponse};
|
||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
use tracing::trace;
|
use tracing::trace;
|
||||||
|
|
||||||
use nym_coconut_dkg_common::dealer::RegisteredDealerDetails;
|
|
||||||
pub use nym_coconut_dkg_common::{
|
pub use nym_coconut_dkg_common::{
|
||||||
dealer::{DealerDetailsResponse, PagedDealerIndexResponse, PagedDealerResponse},
|
dealer::{DealerDetailsResponse, PagedDealerIndexResponse, PagedDealerResponse},
|
||||||
dealing::{
|
dealing::{
|
||||||
@@ -21,7 +21,9 @@ pub use nym_coconut_dkg_common::{
|
|||||||
},
|
},
|
||||||
msg::QueryMsg as DkgQueryMsg,
|
msg::QueryMsg as DkgQueryMsg,
|
||||||
types::{DealerDetails, DealingIndex, Epoch, EpochId, EpochState, State},
|
types::{DealerDetails, DealingIndex, Epoch, EpochId, EpochState, State},
|
||||||
verification_key::{ContractVKShare, PagedVKSharesResponse, VkShareResponse},
|
verification_key::{
|
||||||
|
ContractVKShare, PagedVKSharesResponse, VerificationKeyShare, VkShareResponse,
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
#[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
|
#[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
|
||||||
|
|||||||
@@ -139,12 +139,22 @@ impl NyxdClient<HttpClient> {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn connect_with_network_details<U>(
|
||||||
|
endpoint: U,
|
||||||
|
network_details: NymNetworkDetails,
|
||||||
|
) -> Result<QueryHttpRpcNyxdClient, NyxdError>
|
||||||
|
where
|
||||||
|
U: TryInto<HttpClientUrl, Error = TendermintRpcError>,
|
||||||
|
{
|
||||||
|
let config = Config::try_from_nym_network_details(&network_details)?;
|
||||||
|
Self::connect(config, endpoint)
|
||||||
|
}
|
||||||
|
|
||||||
pub fn connect_to_default_env<U>(endpoint: U) -> Result<QueryHttpRpcNyxdClient, NyxdError>
|
pub fn connect_to_default_env<U>(endpoint: U) -> Result<QueryHttpRpcNyxdClient, NyxdError>
|
||||||
where
|
where
|
||||||
U: TryInto<HttpClientUrl, Error = TendermintRpcError>,
|
U: TryInto<HttpClientUrl, Error = TendermintRpcError>,
|
||||||
{
|
{
|
||||||
let config = Config::try_from_nym_network_details(&NymNetworkDetails::new_from_env())?;
|
Self::connect_with_network_details(endpoint, NymNetworkDetails::new_from_env())
|
||||||
Self::connect(config, endpoint)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -38,6 +38,7 @@ cosmrs = { workspace = true }
|
|||||||
cosmwasm-std = { workspace = true }
|
cosmwasm-std = { workspace = true }
|
||||||
|
|
||||||
nym-validator-client = { path = "../client-libs/validator-client" }
|
nym-validator-client = { path = "../client-libs/validator-client" }
|
||||||
|
nym-http-api-client = { path = "../http-api-client" }
|
||||||
nym-bin-common = { path = "../../common/bin-common", features = ["output_format"] }
|
nym-bin-common = { path = "../../common/bin-common", features = ["output_format"] }
|
||||||
nym-crypto = { path = "../../common/crypto", features = ["asymmetric"] }
|
nym-crypto = { path = "../../common/crypto", features = ["asymmetric"] }
|
||||||
nym-network-defaults = { path = "../network-defaults" }
|
nym-network-defaults = { path = "../network-defaults" }
|
||||||
|
|||||||
@@ -2,12 +2,12 @@
|
|||||||
// SPDX-License-Identifier: Apache-2.0
|
// SPDX-License-Identifier: Apache-2.0
|
||||||
|
|
||||||
use crate::context::errors::ContextError;
|
use crate::context::errors::ContextError;
|
||||||
|
pub use nym_http_api_client::Client as NymApiClient;
|
||||||
use nym_network_defaults::{
|
use nym_network_defaults::{
|
||||||
setup_env,
|
setup_env,
|
||||||
var_names::{MIXNET_CONTRACT_ADDRESS, NYM_API, NYXD, VESTING_CONTRACT_ADDRESS},
|
var_names::{MIXNET_CONTRACT_ADDRESS, NYM_API, NYXD, VESTING_CONTRACT_ADDRESS},
|
||||||
NymNetworkDetails,
|
NymNetworkDetails,
|
||||||
};
|
};
|
||||||
pub use nym_validator_client::nym_api::Client as NymApiClient;
|
|
||||||
use nym_validator_client::nyxd::{self, AccountId, NyxdClient};
|
use nym_validator_client::nyxd::{self, AccountId, NyxdClient};
|
||||||
use nym_validator_client::{
|
use nym_validator_client::{
|
||||||
DirectSigningHttpRpcNyxdClient, DirectSigningHttpRpcValidatorClient, QueryHttpRpcNyxdClient,
|
DirectSigningHttpRpcNyxdClient, DirectSigningHttpRpcValidatorClient, QueryHttpRpcNyxdClient,
|
||||||
|
|||||||
@@ -86,7 +86,7 @@ pub async fn execute(args: Args) -> anyhow::Result<()> {
|
|||||||
anyhow!("ticketbook got incorrectly imported - the master verification key is missing")
|
anyhow!("ticketbook got incorrectly imported - the master verification key is missing")
|
||||||
})?;
|
})?;
|
||||||
let expiration_signatures = persistent_storage
|
let expiration_signatures = persistent_storage
|
||||||
.get_expiration_date_signatures(expiration_date)
|
.get_expiration_date_signatures(expiration_date, epoch_id)
|
||||||
.await?
|
.await?
|
||||||
.ok_or_else(|| {
|
.ok_or_else(|| {
|
||||||
anyhow!(
|
anyhow!(
|
||||||
|
|||||||
@@ -120,7 +120,7 @@ async fn issue_to_file(args: Args, client: SigningClient) -> anyhow::Result<()>
|
|||||||
|
|
||||||
if args.include_expiration_date_signatures {
|
if args.include_expiration_date_signatures {
|
||||||
let signatures = credentials_store
|
let signatures = credentials_store
|
||||||
.get_expiration_date_signatures(expiration_date)
|
.get_expiration_date_signatures(expiration_date, epoch_id)
|
||||||
.await?
|
.await?
|
||||||
.ok_or(anyhow!("missing expiration date signatures!"))?;
|
.ok_or(anyhow!("missing expiration date signatures!"))?;
|
||||||
|
|
||||||
|
|||||||
@@ -4,6 +4,7 @@
|
|||||||
use clap::{Args, Subcommand};
|
use clap::{Args, Subcommand};
|
||||||
|
|
||||||
pub mod ecash;
|
pub mod ecash;
|
||||||
|
pub mod nyx;
|
||||||
|
|
||||||
#[derive(Debug, Args)]
|
#[derive(Debug, Args)]
|
||||||
#[clap(args_conflicts_with_subcommands = true, subcommand_required = true)]
|
#[clap(args_conflicts_with_subcommands = true, subcommand_required = true)]
|
||||||
@@ -16,4 +17,6 @@ pub struct Internal {
|
|||||||
pub enum InternalCommands {
|
pub enum InternalCommands {
|
||||||
/// Ecash related internal commands
|
/// Ecash related internal commands
|
||||||
Ecash(ecash::InternalEcash),
|
Ecash(ecash::InternalEcash),
|
||||||
|
|
||||||
|
Nyx(nyx::InternalNyx),
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,116 @@
|
|||||||
|
// Copyright 2025 - Nym Technologies SA <contact@nymtech.net>
|
||||||
|
// SPDX-License-Identifier: Apache-2.0
|
||||||
|
|
||||||
|
use crate::context::SigningClient;
|
||||||
|
use anyhow::bail;
|
||||||
|
use clap::Parser;
|
||||||
|
use nym_mixnet_contract_common::nym_node::Role;
|
||||||
|
use nym_mixnet_contract_common::reward_params::NodeRewardingParameters;
|
||||||
|
use nym_mixnet_contract_common::{
|
||||||
|
EpochRewardedSet, EpochState, NodeId, RewardingParams, RoleAssignment,
|
||||||
|
};
|
||||||
|
use nym_validator_client::nyxd::contract_traits::mixnet_query_client::MixnetQueryClientExt;
|
||||||
|
use nym_validator_client::nyxd::contract_traits::{MixnetQueryClient, MixnetSigningClient};
|
||||||
|
use rand::prelude::*;
|
||||||
|
use rand::thread_rng;
|
||||||
|
|
||||||
|
#[derive(Debug, Parser)]
|
||||||
|
pub struct Args {}
|
||||||
|
|
||||||
|
fn choose_new_nodes(
|
||||||
|
params: &RewardingParams,
|
||||||
|
rewarded_set: &EpochRewardedSet,
|
||||||
|
role: Role,
|
||||||
|
) -> Vec<NodeId> {
|
||||||
|
let mut rng = thread_rng();
|
||||||
|
|
||||||
|
match role {
|
||||||
|
Role::EntryGateway => rewarded_set
|
||||||
|
.assignment
|
||||||
|
.entry_gateways
|
||||||
|
.choose_multiple(&mut rng, params.rewarded_set.entry_gateways as usize)
|
||||||
|
.copied()
|
||||||
|
.collect(),
|
||||||
|
Role::Layer1 => rewarded_set
|
||||||
|
.assignment
|
||||||
|
.layer1
|
||||||
|
.choose_multiple(&mut rng, params.rewarded_set.mixnodes as usize / 3)
|
||||||
|
.copied()
|
||||||
|
.collect(),
|
||||||
|
Role::Layer2 => rewarded_set
|
||||||
|
.assignment
|
||||||
|
.layer2
|
||||||
|
.choose_multiple(&mut rng, params.rewarded_set.mixnodes as usize / 3)
|
||||||
|
.copied()
|
||||||
|
.collect(),
|
||||||
|
Role::Layer3 => rewarded_set
|
||||||
|
.assignment
|
||||||
|
.layer3
|
||||||
|
.choose_multiple(&mut rng, params.rewarded_set.mixnodes as usize / 3)
|
||||||
|
.copied()
|
||||||
|
.collect(),
|
||||||
|
Role::ExitGateway => rewarded_set
|
||||||
|
.assignment
|
||||||
|
.exit_gateways
|
||||||
|
.choose_multiple(&mut rng, params.rewarded_set.exit_gateways as usize)
|
||||||
|
.copied()
|
||||||
|
.collect(),
|
||||||
|
Role::Standby => rewarded_set
|
||||||
|
.assignment
|
||||||
|
.standby
|
||||||
|
.choose_multiple(&mut rng, params.rewarded_set.standby as usize)
|
||||||
|
.copied()
|
||||||
|
.collect(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn force_advance_epoch(_: Args, client: SigningClient) -> anyhow::Result<()> {
|
||||||
|
let current_epoch = client.get_current_interval_details().await?;
|
||||||
|
let epoch_status = client.get_current_epoch_status().await?;
|
||||||
|
if epoch_status.being_advanced_by.as_str() != client.address().to_string() {
|
||||||
|
bail!(
|
||||||
|
"this client is not authorised to perform any epoch operations. we need {}",
|
||||||
|
client.address()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
let rewarding_params = client.get_rewarding_parameters().await?;
|
||||||
|
let current_rewarded_set = client.get_rewarded_set().await?;
|
||||||
|
|
||||||
|
if !current_epoch.is_current_epoch_over {
|
||||||
|
println!("the current epoch is not over yet - there's nothing to do")
|
||||||
|
}
|
||||||
|
|
||||||
|
// is this most efficient? no. but it's simple
|
||||||
|
loop {
|
||||||
|
let epoch_status = client.get_current_epoch_status().await?;
|
||||||
|
|
||||||
|
match epoch_status.state {
|
||||||
|
EpochState::InProgress => break,
|
||||||
|
EpochState::Rewarding { final_node_id, .. } => {
|
||||||
|
println!("rewarding {final_node_id} with big fat 0...");
|
||||||
|
client
|
||||||
|
.reward_node(
|
||||||
|
final_node_id,
|
||||||
|
NodeRewardingParameters::new(Default::default(), Default::default()),
|
||||||
|
None,
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
}
|
||||||
|
EpochState::ReconcilingEvents => {
|
||||||
|
println!("trying to reconcile events...");
|
||||||
|
client.reconcile_epoch_events(None, None).await?;
|
||||||
|
}
|
||||||
|
EpochState::RoleAssignment { next } => {
|
||||||
|
let nodes = choose_new_nodes(&rewarding_params, ¤t_rewarded_set, next);
|
||||||
|
println!("assigning {nodes:?} as {next}");
|
||||||
|
|
||||||
|
client
|
||||||
|
.assign_roles(RoleAssignment { role: next, nodes }, None)
|
||||||
|
.await?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
// Copyright 2025 - Nym Technologies SA <contact@nymtech.net>
|
||||||
|
// SPDX-License-Identifier: Apache-2.0
|
||||||
|
|
||||||
|
use clap::{Args, Subcommand};
|
||||||
|
|
||||||
|
pub mod force_advance_epoch;
|
||||||
|
|
||||||
|
#[derive(Debug, Args)]
|
||||||
|
#[clap(args_conflicts_with_subcommands = true, subcommand_required = true)]
|
||||||
|
pub struct InternalNyx {
|
||||||
|
#[clap(subcommand)]
|
||||||
|
pub command: InternalNyxCommands,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Subcommand)]
|
||||||
|
pub enum InternalNyxCommands {
|
||||||
|
/// Attempt to force advance the current epoch
|
||||||
|
ForceAdvanceEpoch(force_advance_epoch::Args),
|
||||||
|
}
|
||||||
@@ -241,7 +241,7 @@ pub async fn delegate_to_multiple_mixnodes(args: Args, client: SigningClient) {
|
|||||||
let node_id = row.node_id.clone().parse::<u32>().unwrap();
|
let node_id = row.node_id.clone().parse::<u32>().unwrap();
|
||||||
let coins: Vec<Coin> = vec![];
|
let coins: Vec<Coin> = vec![];
|
||||||
undelegation_msgs.push((ExecuteMsg::Undelegate { node_id }, coins));
|
undelegation_msgs.push((ExecuteMsg::Undelegate { node_id }, coins));
|
||||||
undelegation_table.add_row(&[row.node_id.clone()]);
|
undelegation_table.add_row(std::slice::from_ref(&row.node_id));
|
||||||
|
|
||||||
if row.amount.amount > 0 {
|
if row.amount.amount > 0 {
|
||||||
delegation_msgs
|
delegation_msgs
|
||||||
|
|||||||
@@ -188,7 +188,7 @@ impl<C> ContractTesterBuilder<C> {
|
|||||||
*self.app.api()
|
*self.app.api()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn querier(&self) -> QuerierWrapper {
|
pub fn querier(&self) -> QuerierWrapper<'_> {
|
||||||
self.app.wrap()
|
self.app.wrap()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -57,7 +57,7 @@ pub trait NodeBond {
|
|||||||
|
|
||||||
fn is_unbonding(&self) -> bool;
|
fn is_unbonding(&self) -> bool;
|
||||||
|
|
||||||
fn identity(&self) -> IdentityKeyRef;
|
fn identity(&self) -> IdentityKeyRef<'_>;
|
||||||
|
|
||||||
fn original_pledge(&self) -> &Coin;
|
fn original_pledge(&self) -> &Coin;
|
||||||
|
|
||||||
@@ -125,7 +125,7 @@ impl NodeBond for MixNodeBond {
|
|||||||
self.is_unbonding
|
self.is_unbonding
|
||||||
}
|
}
|
||||||
|
|
||||||
fn identity(&self) -> IdentityKeyRef {
|
fn identity(&self) -> IdentityKeyRef<'_> {
|
||||||
self.identity()
|
self.identity()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -178,7 +178,7 @@ impl NodeBond for NymNodeBond {
|
|||||||
self.is_unbonding
|
self.is_unbonding
|
||||||
}
|
}
|
||||||
|
|
||||||
fn identity(&self) -> IdentityKeyRef {
|
fn identity(&self) -> IdentityKeyRef<'_> {
|
||||||
self.identity()
|
self.identity()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -58,7 +58,7 @@ impl<'a> PrimaryKey<'a> for Role {
|
|||||||
type Suffix = <u8 as PrimaryKey<'a>>::Suffix;
|
type Suffix = <u8 as PrimaryKey<'a>>::Suffix;
|
||||||
type SuperSuffix = <u8 as PrimaryKey<'a>>::SuperSuffix;
|
type SuperSuffix = <u8 as PrimaryKey<'a>>::SuperSuffix;
|
||||||
|
|
||||||
fn key(&self) -> Vec<Key> {
|
fn key(&self) -> Vec<Key<'_>> {
|
||||||
// I'm not sure why it wasn't possible to delegate the call to
|
// I'm not sure why it wasn't possible to delegate the call to
|
||||||
// `(*self as u8).key()` directly...
|
// `(*self as u8).key()` directly...
|
||||||
// I guess because of the `Key::Ref(&'a [u8])` variant?
|
// I guess because of the `Key::Ref(&'a [u8])` variant?
|
||||||
|
|||||||
@@ -86,6 +86,25 @@ impl IntervalRewardParams {
|
|||||||
pub fn to_inline_json(&self) -> String {
|
pub fn to_inline_json(&self) -> String {
|
||||||
to_json_string(self).unwrap_or_else(|_| "serialisation failure".into())
|
to_json_string(self).unwrap_or_else(|_| "serialisation failure".into())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn active_node_work(&self, standby_node_work: Decimal) -> WorkFactor {
|
||||||
|
self.active_set_work_factor * standby_node_work
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn standby_node_work(
|
||||||
|
&self,
|
||||||
|
rewarded_set_size: Decimal,
|
||||||
|
standby_set_size: Decimal,
|
||||||
|
) -> WorkFactor {
|
||||||
|
let f = self.active_set_work_factor;
|
||||||
|
let k = rewarded_set_size;
|
||||||
|
let one = Decimal::one();
|
||||||
|
|
||||||
|
// nodes in reserve
|
||||||
|
let k_r = standby_set_size;
|
||||||
|
|
||||||
|
one / (f * k - (f - one) * k_r)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Parameters used for reward calculation.
|
/// Parameters used for reward calculation.
|
||||||
@@ -109,18 +128,15 @@ pub struct RewardingParams {
|
|||||||
|
|
||||||
impl RewardingParams {
|
impl RewardingParams {
|
||||||
pub fn active_node_work(&self) -> WorkFactor {
|
pub fn active_node_work(&self) -> WorkFactor {
|
||||||
self.interval.active_set_work_factor * self.standby_node_work()
|
let standby_work = self.standby_node_work();
|
||||||
|
self.interval.active_node_work(standby_work)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn standby_node_work(&self) -> WorkFactor {
|
pub fn standby_node_work(&self) -> WorkFactor {
|
||||||
let f = self.interval.active_set_work_factor;
|
let rewarded_set_size = self.dec_rewarded_set_size();
|
||||||
let k = self.dec_rewarded_set_size();
|
let standby_set_size = self.dec_standby_set_size();
|
||||||
let one = Decimal::one();
|
self.interval
|
||||||
|
.standby_node_work(rewarded_set_size, standby_set_size)
|
||||||
// nodes in reserve
|
|
||||||
let k_r = self.dec_standby_set_size();
|
|
||||||
|
|
||||||
one / (f * k - (f - one) * k_r)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn rewarded_set_size(&self) -> u32 {
|
pub fn rewarded_set_size(&self) -> u32 {
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
|
|
||||||
use crate::config_score::{ConfigScoreParams, OutdatedVersionWeights, VersionScoreFormulaParams};
|
use crate::config_score::{ConfigScoreParams, OutdatedVersionWeights, VersionScoreFormulaParams};
|
||||||
use crate::nym_node::Role;
|
use crate::nym_node::Role;
|
||||||
|
use crate::reward_params::RewardedSetParams;
|
||||||
use crate::EpochId;
|
use crate::EpochId;
|
||||||
use contracts_common::Percent;
|
use contracts_common::Percent;
|
||||||
use cosmwasm_schema::cw_serde;
|
use cosmwasm_schema::cw_serde;
|
||||||
@@ -85,7 +86,11 @@ impl RewardedSet {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn rewarded_set_size(&self) -> usize {
|
pub fn rewarded_set_size(&self) -> usize {
|
||||||
self.active_set_size() + self.standby.len()
|
self.active_set_size() + self.standby_set_size()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn standby_set_size(&self) -> usize {
|
||||||
|
self.standby.len()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_role(&self, node_id: NodeId) -> Option<Role> {
|
pub fn get_role(&self, node_id: NodeId) -> Option<Role> {
|
||||||
@@ -110,6 +115,13 @@ impl RewardedSet {
|
|||||||
}
|
}
|
||||||
None
|
None
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn matches_parameters(&self, params: RewardedSetParams) -> bool {
|
||||||
|
self.entry_gateways.len() <= params.entry_gateways as usize
|
||||||
|
&& self.exit_gateways.len() <= params.exit_gateways as usize
|
||||||
|
&& self.layer1.len() + self.layer2.len() + self.layer3.len() <= params.mixnodes as usize
|
||||||
|
&& self.standby.len() <= params.standby as usize
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cw_serde]
|
#[cw_serde]
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ name = "nym-credential-storage"
|
|||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
license.workspace = true
|
license.workspace = true
|
||||||
|
rust-version.workspace = true
|
||||||
|
|
||||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||||
|
|
||||||
@@ -33,6 +34,7 @@ features = ["rt-multi-thread", "net", "signal", "fs"]
|
|||||||
|
|
||||||
|
|
||||||
[build-dependencies]
|
[build-dependencies]
|
||||||
|
anyhow = { workspace = true }
|
||||||
sqlx = { workspace = true, features = [
|
sqlx = { workspace = true, features = [
|
||||||
"runtime-tokio-rustls",
|
"runtime-tokio-rustls",
|
||||||
"sqlite",
|
"sqlite",
|
||||||
|
|||||||
@@ -3,22 +3,29 @@
|
|||||||
* SPDX-License-Identifier: Apache-2.0
|
* SPDX-License-Identifier: Apache-2.0
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
use anyhow::Context;
|
||||||
use sqlx::{Connection, SqliteConnection};
|
use sqlx::{Connection, SqliteConnection};
|
||||||
use std::env;
|
use std::env;
|
||||||
|
|
||||||
#[tokio::main]
|
#[tokio::main]
|
||||||
async fn main() {
|
async fn main() -> anyhow::Result<()> {
|
||||||
let out_dir = env::var("OUT_DIR").unwrap();
|
let out_dir = env::var("OUT_DIR")?;
|
||||||
let database_path = format!("{out_dir}/coconut-credential-example.sqlite");
|
let database_path = format!("{out_dir}/coconut-credential-example.sqlite");
|
||||||
|
|
||||||
|
// remove the db file if it already existed from previous build
|
||||||
|
// in case it was from a different branch
|
||||||
|
if std::fs::exists(&database_path)? {
|
||||||
|
std::fs::remove_file(&database_path)?;
|
||||||
|
}
|
||||||
|
|
||||||
let mut conn = SqliteConnection::connect(&format!("sqlite://{database_path}?mode=rwc"))
|
let mut conn = SqliteConnection::connect(&format!("sqlite://{database_path}?mode=rwc"))
|
||||||
.await
|
.await
|
||||||
.expect("Failed to create SQLx database connection");
|
.context("Failed to create SQLx database connection")?;
|
||||||
|
|
||||||
sqlx::migrate!("./migrations")
|
sqlx::migrate!("./migrations")
|
||||||
.run(&mut conn)
|
.run(&mut conn)
|
||||||
.await
|
.await
|
||||||
.expect("Failed to perform SQLx migrations");
|
.context("Failed to perform SQLx migrations")?;
|
||||||
|
|
||||||
#[cfg(target_family = "unix")]
|
#[cfg(target_family = "unix")]
|
||||||
println!("cargo:rustc-env=DATABASE_URL=sqlite://{}", &database_path);
|
println!("cargo:rustc-env=DATABASE_URL=sqlite://{}", &database_path);
|
||||||
@@ -27,4 +34,6 @@ async fn main() {
|
|||||||
// for some strange reason we need to add a leading `/` to the windows path even though it's
|
// for some strange reason we need to add a leading `/` to the windows path even though it's
|
||||||
// not a valid windows path... but hey, it works...
|
// not a valid windows path... but hey, it works...
|
||||||
println!("cargo:rustc-env=DATABASE_URL=sqlite:///{}", &database_path);
|
println!("cargo:rustc-env=DATABASE_URL=sqlite:///{}", &database_path);
|
||||||
|
|
||||||
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|||||||
+123
@@ -0,0 +1,123 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2025 - Nym Technologies SA <contact@nymtech.net>
|
||||||
|
* SPDX-License-Identifier: Apache-2.0
|
||||||
|
*/
|
||||||
|
|
||||||
|
-- 1. add temporary `epoch_id` column
|
||||||
|
ALTER TABLE pending_issuance
|
||||||
|
ADD COLUMN epoch_id INTEGER;
|
||||||
|
|
||||||
|
-- 2. populate the value
|
||||||
|
UPDATE pending_issuance
|
||||||
|
SET epoch_id = (SELECT epoch_id
|
||||||
|
FROM expiration_date_signatures
|
||||||
|
WHERE expiration_date_signatures.expiration_date = pending_issuance.expiration_date);
|
||||||
|
|
||||||
|
-- 3. create new expiration_date_signatures table (with changed constraints)
|
||||||
|
CREATE TABLE expiration_date_signatures_new
|
||||||
|
(
|
||||||
|
expiration_date DATE NOT NULL,
|
||||||
|
|
||||||
|
epoch_id INTEGER NOT NULL,
|
||||||
|
|
||||||
|
serialization_revision INTEGER NOT NULL,
|
||||||
|
|
||||||
|
-- combined signatures for all tuples issued for given day
|
||||||
|
serialised_signatures BLOB NOT NULL,
|
||||||
|
|
||||||
|
PRIMARY KEY (epoch_id, expiration_date)
|
||||||
|
);
|
||||||
|
|
||||||
|
-- 4. migrate the data
|
||||||
|
INSERT INTO expiration_date_signatures_new (expiration_date, epoch_id, serialization_revision, serialised_signatures)
|
||||||
|
SELECT expiration_date, epoch_id, serialization_revision, serialised_signatures
|
||||||
|
FROM expiration_date_signatures;
|
||||||
|
|
||||||
|
-- 5. drop and recreate the table references (due to new FK)
|
||||||
|
|
||||||
|
-- 5.1.
|
||||||
|
-- (data for ticketbooks that have an associated deposit, but failed to get issued)
|
||||||
|
CREATE TABLE pending_issuance_new
|
||||||
|
(
|
||||||
|
deposit_id INTEGER NOT NULL PRIMARY KEY,
|
||||||
|
|
||||||
|
-- introduce a way for us to introduce breaking changes in serialization of data
|
||||||
|
serialization_revision INTEGER NOT NULL,
|
||||||
|
|
||||||
|
pending_ticketbook_data BLOB NOT NULL UNIQUE,
|
||||||
|
|
||||||
|
-- for each ticketbook we MUST have corresponding expiration date signatures
|
||||||
|
expiration_date DATE NOT NULL,
|
||||||
|
epoch_id INTEGER NOT NULL,
|
||||||
|
|
||||||
|
-- for each ticketbook we MUST have corresponding expiration date signatures
|
||||||
|
FOREIGN KEY (epoch_id, expiration_date) REFERENCES expiration_date_signatures_new (epoch_id, expiration_date)
|
||||||
|
);
|
||||||
|
|
||||||
|
INSERT INTO pending_issuance_new (deposit_id, serialization_revision, pending_ticketbook_data, expiration_date,
|
||||||
|
epoch_id)
|
||||||
|
SELECT deposit_id, serialization_revision, pending_ticketbook_data, expiration_date, epoch_id
|
||||||
|
FROM pending_issuance;
|
||||||
|
|
||||||
|
|
||||||
|
-- 5.2.
|
||||||
|
CREATE TABLE ecash_ticketbook_new
|
||||||
|
(
|
||||||
|
id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
|
||||||
|
|
||||||
|
-- introduce a way for us to introduce breaking changes in serialization of data
|
||||||
|
serialization_revision INTEGER NOT NULL,
|
||||||
|
|
||||||
|
-- the type of the associated ticketbook
|
||||||
|
ticketbook_type TEXT NOT NULL,
|
||||||
|
|
||||||
|
-- the actual crypto data of the ticketbook (wallet, keys, etc.)
|
||||||
|
ticketbook_data BLOB NOT NULL UNIQUE,
|
||||||
|
|
||||||
|
-- for each ticketbook we MUST have corresponding expiration date signatures
|
||||||
|
expiration_date DATE NOT NULL,
|
||||||
|
|
||||||
|
-- for each ticketbook we MUST have corresponding coin index signatures
|
||||||
|
epoch_id INTEGER NOT NULL,
|
||||||
|
|
||||||
|
-- the initial number of tickets the wallet has been created for
|
||||||
|
total_tickets INTEGER NOT NULL,
|
||||||
|
|
||||||
|
-- how many tickets have been used so far (the `l` value of the wallet)
|
||||||
|
used_tickets INTEGER NOT NULL,
|
||||||
|
|
||||||
|
|
||||||
|
-- FOREIGN KEYS:
|
||||||
|
|
||||||
|
-- for each ticketbook we MUST have corresponding coin index signatures
|
||||||
|
FOREIGN KEY (epoch_id) REFERENCES coin_indices_signatures (epoch_id),
|
||||||
|
|
||||||
|
-- for each ticketbook we MUST have corresponding expiration date signatures
|
||||||
|
FOREIGN KEY (expiration_date, epoch_id) REFERENCES expiration_date_signatures_new (expiration_date, epoch_id)
|
||||||
|
);
|
||||||
|
|
||||||
|
INSERT INTO ecash_ticketbook_new (id, serialization_revision, ticketbook_type, ticketbook_data, expiration_date,
|
||||||
|
epoch_id, total_tickets, used_tickets)
|
||||||
|
SELECT id,
|
||||||
|
serialization_revision,
|
||||||
|
ticketbook_type,
|
||||||
|
ticketbook_data,
|
||||||
|
expiration_date,
|
||||||
|
epoch_id,
|
||||||
|
total_tickets,
|
||||||
|
used_tickets
|
||||||
|
FROM ecash_ticketbook;
|
||||||
|
|
||||||
|
-- 6. finally swap out the old tables
|
||||||
|
-- drop old tables
|
||||||
|
DROP TABLE pending_issuance;
|
||||||
|
DROP TABLE ecash_ticketbook;
|
||||||
|
DROP TABLE expiration_date_signatures;
|
||||||
|
|
||||||
|
-- rename new tables
|
||||||
|
ALTER TABLE pending_issuance_new
|
||||||
|
RENAME TO pending_issuance;
|
||||||
|
ALTER TABLE ecash_ticketbook_new
|
||||||
|
RENAME TO ecash_ticketbook;
|
||||||
|
ALTER TABLE expiration_date_signatures_new
|
||||||
|
RENAME TO expiration_date_signatures;
|
||||||
@@ -28,7 +28,7 @@ struct EcashCredentialManagerInner {
|
|||||||
pending: HashMap<i64, RetrievedPendingTicketbook>,
|
pending: HashMap<i64, RetrievedPendingTicketbook>,
|
||||||
master_vk: HashMap<u64, VerificationKeyAuth>,
|
master_vk: HashMap<u64, VerificationKeyAuth>,
|
||||||
coin_indices_sigs: HashMap<u64, Vec<AnnotatedCoinIndexSignature>>,
|
coin_indices_sigs: HashMap<u64, Vec<AnnotatedCoinIndexSignature>>,
|
||||||
expiration_date_sigs: HashMap<Date, Vec<AnnotatedExpirationDateSignature>>,
|
expiration_date_sigs: HashMap<(u64, Date), Vec<AnnotatedExpirationDateSignature>>,
|
||||||
_next_id: i64,
|
_next_id: i64,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -242,10 +242,14 @@ impl MemoryEcachTicketbookManager {
|
|||||||
pub(crate) async fn get_expiration_date_signatures(
|
pub(crate) async fn get_expiration_date_signatures(
|
||||||
&self,
|
&self,
|
||||||
expiration_date: Date,
|
expiration_date: Date,
|
||||||
|
epoch_id: u64,
|
||||||
) -> Option<Vec<AnnotatedExpirationDateSignature>> {
|
) -> Option<Vec<AnnotatedExpirationDateSignature>> {
|
||||||
let guard = self.inner.read().await;
|
let guard = self.inner.read().await;
|
||||||
|
|
||||||
guard.expiration_date_sigs.get(&expiration_date).cloned()
|
guard
|
||||||
|
.expiration_date_sigs
|
||||||
|
.get(&(epoch_id, expiration_date))
|
||||||
|
.cloned()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) async fn insert_expiration_date_signatures(
|
pub(crate) async fn insert_expiration_date_signatures(
|
||||||
@@ -254,8 +258,9 @@ impl MemoryEcachTicketbookManager {
|
|||||||
) {
|
) {
|
||||||
let mut guard = self.inner.write().await;
|
let mut guard = self.inner.write().await;
|
||||||
|
|
||||||
guard
|
guard.expiration_date_sigs.insert(
|
||||||
.expiration_date_sigs
|
(sigs.epoch_id, sigs.expiration_date),
|
||||||
.insert(sigs.expiration_date, sigs.signatures.clone());
|
sigs.signatures.clone(),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -39,7 +39,7 @@ impl SqliteEcashTicketbookManager {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) async fn begin_storage_tx(&self) -> Result<Transaction<Sqlite>, sqlx::Error> {
|
pub(crate) async fn begin_storage_tx(&self) -> Result<Transaction<'_, Sqlite>, sqlx::Error> {
|
||||||
self.connection_pool.begin().await
|
self.connection_pool.begin().await
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -260,15 +260,17 @@ impl SqliteEcashTicketbookManager {
|
|||||||
pub(crate) async fn get_expiration_date_signatures(
|
pub(crate) async fn get_expiration_date_signatures(
|
||||||
&self,
|
&self,
|
||||||
expiration_date: Date,
|
expiration_date: Date,
|
||||||
|
epoch_id: i64,
|
||||||
) -> Result<Option<RawExpirationDateSignatures>, sqlx::Error> {
|
) -> Result<Option<RawExpirationDateSignatures>, sqlx::Error> {
|
||||||
sqlx::query_as!(
|
sqlx::query_as!(
|
||||||
RawExpirationDateSignatures,
|
RawExpirationDateSignatures,
|
||||||
r#"
|
r#"
|
||||||
SELECT epoch_id as "epoch_id: u32", serialised_signatures, serialization_revision as "serialization_revision: u8"
|
SELECT serialised_signatures, serialization_revision as "serialization_revision: u8"
|
||||||
FROM expiration_date_signatures
|
FROM expiration_date_signatures
|
||||||
WHERE expiration_date = ?
|
WHERE expiration_date = ? AND epoch_id = ?
|
||||||
"#,
|
"#,
|
||||||
expiration_date
|
expiration_date,
|
||||||
|
epoch_id
|
||||||
)
|
)
|
||||||
.fetch_optional(&*self.connection_pool)
|
.fetch_optional(&*self.connection_pool)
|
||||||
.await
|
.await
|
||||||
|
|||||||
@@ -166,10 +166,11 @@ impl Storage for EphemeralStorage {
|
|||||||
async fn get_expiration_date_signatures(
|
async fn get_expiration_date_signatures(
|
||||||
&self,
|
&self,
|
||||||
expiration_date: Date,
|
expiration_date: Date,
|
||||||
|
epoch_id: u64,
|
||||||
) -> Result<Option<Vec<AnnotatedExpirationDateSignature>>, Self::StorageError> {
|
) -> Result<Option<Vec<AnnotatedExpirationDateSignature>>, Self::StorageError> {
|
||||||
Ok(self
|
Ok(self
|
||||||
.storage_manager
|
.storage_manager
|
||||||
.get_expiration_date_signatures(expiration_date)
|
.get_expiration_date_signatures(expiration_date, epoch_id)
|
||||||
.await)
|
.await)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -60,7 +60,6 @@ pub struct StoredPendingTicketbook {
|
|||||||
|
|
||||||
#[cfg_attr(not(target_arch = "wasm32"), derive(sqlx::FromRow))]
|
#[cfg_attr(not(target_arch = "wasm32"), derive(sqlx::FromRow))]
|
||||||
pub struct RawExpirationDateSignatures {
|
pub struct RawExpirationDateSignatures {
|
||||||
pub epoch_id: u32,
|
|
||||||
pub serialised_signatures: Vec<u8>,
|
pub serialised_signatures: Vec<u8>,
|
||||||
pub serialization_revision: u8,
|
pub serialization_revision: u8,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -325,10 +325,11 @@ impl Storage for PersistentStorage {
|
|||||||
async fn get_expiration_date_signatures(
|
async fn get_expiration_date_signatures(
|
||||||
&self,
|
&self,
|
||||||
expiration_date: Date,
|
expiration_date: Date,
|
||||||
|
epoch_id: u64,
|
||||||
) -> Result<Option<Vec<AnnotatedExpirationDateSignature>>, Self::StorageError> {
|
) -> Result<Option<Vec<AnnotatedExpirationDateSignature>>, Self::StorageError> {
|
||||||
let Some(raw) = self
|
let Some(raw) = self
|
||||||
.storage_manager
|
.storage_manager
|
||||||
.get_expiration_date_signatures(expiration_date)
|
.get_expiration_date_signatures(expiration_date, epoch_id as i64)
|
||||||
.await?
|
.await?
|
||||||
else {
|
else {
|
||||||
return Ok(None);
|
return Ok(None);
|
||||||
|
|||||||
@@ -92,6 +92,7 @@ pub trait Storage: Clone + Send + Sync {
|
|||||||
async fn get_expiration_date_signatures(
|
async fn get_expiration_date_signatures(
|
||||||
&self,
|
&self,
|
||||||
expiration_date: Date,
|
expiration_date: Date,
|
||||||
|
epoch_id: u64,
|
||||||
) -> Result<Option<Vec<AnnotatedExpirationDateSignature>>, Self::StorageError>;
|
) -> Result<Option<Vec<AnnotatedExpirationDateSignature>>, Self::StorageError>;
|
||||||
|
|
||||||
async fn insert_expiration_date_signatures(
|
async fn insert_expiration_date_signatures(
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ use nym_api_requests::ecash::models::{BatchRedeemTicketsBody, VerifyEcashTicketB
|
|||||||
use nym_credentials_interface::Bandwidth;
|
use nym_credentials_interface::Bandwidth;
|
||||||
use nym_credentials_interface::{ClientTicket, TicketType};
|
use nym_credentials_interface::{ClientTicket, TicketType};
|
||||||
use nym_validator_client::coconut::EcashApiError;
|
use nym_validator_client::coconut::EcashApiError;
|
||||||
use nym_validator_client::nym_api::EpochId;
|
use nym_validator_client::nym_api::{EpochId, NymApiClientExt};
|
||||||
use nym_validator_client::nyxd::contract_traits::{
|
use nym_validator_client::nyxd::contract_traits::{
|
||||||
EcashSigningClient, MultisigQueryClient, MultisigSigningClient, PagedMultisigQueryClient,
|
EcashSigningClient, MultisigQueryClient, MultisigSigningClient, PagedMultisigQueryClient,
|
||||||
};
|
};
|
||||||
@@ -354,7 +354,7 @@ impl CredentialHandler {
|
|||||||
Err(err) => {
|
Err(err) => {
|
||||||
error!("failed to send ticket {ticket_id} for verification to ecash signer '{client}': {err}. if we don't reach quorum, we'll retry later");
|
error!("failed to send ticket {ticket_id} for verification to ecash signer '{client}': {err}. if we don't reach quorum, we'll retry later");
|
||||||
Err(EcashTicketError::ApiFailure(EcashApiError::NymApi {
|
Err(EcashTicketError::ApiFailure(EcashApiError::NymApi {
|
||||||
source: err,
|
source: nym_validator_client::ValidatorClientError::NymAPIError { source: err },
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -39,7 +39,7 @@ impl traits::EcashManager for EcashManager {
|
|||||||
async fn verification_key(
|
async fn verification_key(
|
||||||
&self,
|
&self,
|
||||||
epoch_id: EpochId,
|
epoch_id: EpochId,
|
||||||
) -> Result<RwLockReadGuard<VerificationKeyAuth>, EcashTicketError> {
|
) -> Result<RwLockReadGuard<'_, VerificationKeyAuth>, EcashTicketError> {
|
||||||
self.shared_state.verification_key(epoch_id).await
|
self.shared_state.verification_key(epoch_id).await
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -231,7 +231,7 @@ impl traits::EcashManager for MockEcashManager {
|
|||||||
async fn verification_key(
|
async fn verification_key(
|
||||||
&self,
|
&self,
|
||||||
_epoch_id: EpochId,
|
_epoch_id: EpochId,
|
||||||
) -> Result<RwLockReadGuard<VerificationKeyAuth>, EcashTicketError> {
|
) -> Result<RwLockReadGuard<'_, VerificationKeyAuth>, EcashTicketError> {
|
||||||
Ok(self.verfication_key.read().await)
|
Ok(self.verfication_key.read().await)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -125,7 +125,7 @@ impl SharedState {
|
|||||||
async fn set_epoch_data(
|
async fn set_epoch_data(
|
||||||
&self,
|
&self,
|
||||||
epoch_id: EpochId,
|
epoch_id: EpochId,
|
||||||
) -> Result<RwLockWriteGuard<BTreeMap<EpochId, EpochState>>, EcashTicketError> {
|
) -> Result<RwLockWriteGuard<'_, BTreeMap<EpochId, EpochState>>, EcashTicketError> {
|
||||||
let Some(threshold) = self.threshold(epoch_id).await? else {
|
let Some(threshold) = self.threshold(epoch_id).await? else {
|
||||||
return Err(EcashTicketError::DKGThresholdUnavailable { epoch_id });
|
return Err(EcashTicketError::DKGThresholdUnavailable { epoch_id });
|
||||||
};
|
};
|
||||||
@@ -186,7 +186,7 @@ impl SharedState {
|
|||||||
pub(crate) async fn api_clients(
|
pub(crate) async fn api_clients(
|
||||||
&self,
|
&self,
|
||||||
epoch_id: EpochId,
|
epoch_id: EpochId,
|
||||||
) -> Result<RwLockReadGuard<Vec<EcashApiClient>>, EcashTicketError> {
|
) -> Result<RwLockReadGuard<'_, Vec<EcashApiClient>>, EcashTicketError> {
|
||||||
let guard = self.epoch_data.read().await;
|
let guard = self.epoch_data.read().await;
|
||||||
|
|
||||||
// the key was already in the map
|
// the key was already in the map
|
||||||
@@ -212,7 +212,7 @@ impl SharedState {
|
|||||||
pub(crate) async fn verification_key(
|
pub(crate) async fn verification_key(
|
||||||
&self,
|
&self,
|
||||||
epoch_id: EpochId,
|
epoch_id: EpochId,
|
||||||
) -> Result<RwLockReadGuard<VerificationKeyAuth>, EcashTicketError> {
|
) -> Result<RwLockReadGuard<'_, VerificationKeyAuth>, EcashTicketError> {
|
||||||
let guard = self.epoch_data.read().await;
|
let guard = self.epoch_data.read().await;
|
||||||
|
|
||||||
// the key was already in the map
|
// the key was already in the map
|
||||||
@@ -235,11 +235,11 @@ impl SharedState {
|
|||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) async fn start_tx(&self) -> RwLockWriteGuard<DirectSigningHttpRpcNyxdClient> {
|
pub(crate) async fn start_tx(&self) -> RwLockWriteGuard<'_, DirectSigningHttpRpcNyxdClient> {
|
||||||
self.nyxd_client.write().await
|
self.nyxd_client.write().await
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) async fn start_query(&self) -> RwLockReadGuard<DirectSigningHttpRpcNyxdClient> {
|
pub(crate) async fn start_query(&self) -> RwLockReadGuard<'_, DirectSigningHttpRpcNyxdClient> {
|
||||||
self.nyxd_client.read().await
|
self.nyxd_client.read().await
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ pub trait EcashManager {
|
|||||||
async fn verification_key(
|
async fn verification_key(
|
||||||
&self,
|
&self,
|
||||||
epoch_id: EpochId,
|
epoch_id: EpochId,
|
||||||
) -> Result<RwLockReadGuard<VerificationKeyAuth>, EcashTicketError>;
|
) -> Result<RwLockReadGuard<'_, VerificationKeyAuth>, EcashTicketError>;
|
||||||
fn storage(&self) -> Box<dyn BandwidthGatewayStorage + Send + Sync>;
|
fn storage(&self) -> Box<dyn BandwidthGatewayStorage + Send + Sync>;
|
||||||
async fn check_payment(
|
async fn check_payment(
|
||||||
&self,
|
&self,
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
// SPDX-License-Identifier: Apache-2.0
|
// SPDX-License-Identifier: Apache-2.0
|
||||||
|
|
||||||
use crate::ecash::traits::EcashManager;
|
use crate::ecash::traits::EcashManager;
|
||||||
|
use async_trait::async_trait;
|
||||||
use bandwidth_storage_manager::BandwidthStorageManager;
|
use bandwidth_storage_manager::BandwidthStorageManager;
|
||||||
use nym_credentials::ecash::utils::{cred_exp_date, ecash_today, EcashTime};
|
use nym_credentials::ecash::utils::{cred_exp_date, ecash_today, EcashTime};
|
||||||
use nym_credentials_interface::{Bandwidth, ClientTicket, TicketType};
|
use nym_credentials_interface::{Bandwidth, ClientTicket, TicketType};
|
||||||
@@ -139,3 +140,18 @@ impl CredentialVerifier {
|
|||||||
.await)
|
.await)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[async_trait]
|
||||||
|
pub trait TicketVerifier {
|
||||||
|
/// Verify that the ticket is valid and cryptographically correct.
|
||||||
|
/// If the verification succeeds, also increase the bandwidth with the ticket's
|
||||||
|
/// amount and return the latest available bandwidth
|
||||||
|
async fn verify(&mut self) -> Result<i64>;
|
||||||
|
}
|
||||||
|
|
||||||
|
#[async_trait]
|
||||||
|
impl TicketVerifier for CredentialVerifier {
|
||||||
|
async fn verify(&mut self) -> Result<i64> {
|
||||||
|
self.verify().await
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ bls12_381 = { workspace = true, default-features = false }
|
|||||||
serde = { workspace = true, features = ["derive"] }
|
serde = { workspace = true, features = ["derive"] }
|
||||||
thiserror = { workspace = true }
|
thiserror = { workspace = true }
|
||||||
strum = { workspace = true, features = ["derive"] }
|
strum = { workspace = true, features = ["derive"] }
|
||||||
|
strum_macros = { workspace = true }
|
||||||
time = { workspace = true, features = ["serde"] }
|
time = { workspace = true, features = ["serde"] }
|
||||||
utoipa = { workspace = true }
|
utoipa = { workspace = true }
|
||||||
rand = { workspace = true }
|
rand = { workspace = true }
|
||||||
|
|||||||
@@ -229,9 +229,9 @@ impl From<PayInfo> for NymPayInfo {
|
|||||||
Serialize,
|
Serialize,
|
||||||
Deserialize,
|
Deserialize,
|
||||||
Hash,
|
Hash,
|
||||||
strum::Display,
|
strum_macros::Display,
|
||||||
strum::EnumString,
|
strum_macros::EnumString,
|
||||||
strum::EnumIter,
|
strum_macros::EnumIter,
|
||||||
)]
|
)]
|
||||||
#[serde(rename_all = "kebab-case")]
|
#[serde(rename_all = "kebab-case")]
|
||||||
#[strum(serialize_all = "kebab-case")]
|
#[strum(serialize_all = "kebab-case")]
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ nym-ecash-time = { path = "../ecash-time", features = ["expiration"] }
|
|||||||
nym-credentials-interface = { path = "../credentials-interface" }
|
nym-credentials-interface = { path = "../credentials-interface" }
|
||||||
nym-crypto = { path = "../crypto" }
|
nym-crypto = { path = "../crypto" }
|
||||||
nym-api-requests = { path = "../../nym-api/nym-api-requests" }
|
nym-api-requests = { path = "../../nym-api/nym-api-requests" }
|
||||||
|
nym-http-api-client = { path = "../http-api-client" }
|
||||||
nym-validator-client = { path = "../client-libs/validator-client", default-features = false }
|
nym-validator-client = { path = "../client-libs/validator-client", default-features = false }
|
||||||
nym-ecash-contract-common = { path = "../cosmwasm-smart-contracts/ecash-contract" }
|
nym-ecash-contract-common = { path = "../cosmwasm-smart-contracts/ecash-contract" }
|
||||||
nym-network-defaults = { path = "../network-defaults" }
|
nym-network-defaults = { path = "../network-defaults" }
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ use nym_credentials_interface::{
|
|||||||
use nym_crypto::asymmetric::ed25519;
|
use nym_crypto::asymmetric::ed25519;
|
||||||
use nym_ecash_contract_common::deposit::DepositId;
|
use nym_ecash_contract_common::deposit::DepositId;
|
||||||
use nym_ecash_time::{ecash_default_expiration_date, ecash_today, EcashTime};
|
use nym_ecash_time::{ecash_default_expiration_date, ecash_today, EcashTime};
|
||||||
use nym_validator_client::nym_api::EpochId;
|
use nym_validator_client::nym_api::{EpochId, NymApiClientExt};
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use time::Date;
|
use time::Date;
|
||||||
|
|
||||||
@@ -108,7 +108,7 @@ impl IssuanceTicketBook {
|
|||||||
signing_request.withdrawal_request.clone(),
|
signing_request.withdrawal_request.clone(),
|
||||||
self.deposit_id,
|
self.deposit_id,
|
||||||
request_signature,
|
request_signature,
|
||||||
signing_request.ecash_pub_key.clone(),
|
signing_request.ecash_pub_key,
|
||||||
signing_request.expiration_date,
|
signing_request.expiration_date,
|
||||||
signing_request.ticketbook_type,
|
signing_request.ticketbook_type,
|
||||||
)
|
)
|
||||||
@@ -116,7 +116,7 @@ impl IssuanceTicketBook {
|
|||||||
|
|
||||||
pub async fn obtain_blinded_credential(
|
pub async fn obtain_blinded_credential(
|
||||||
&self,
|
&self,
|
||||||
client: &nym_validator_client::client::NymApiClient,
|
client: &nym_http_api_client::Client,
|
||||||
request_body: &BlindSignRequestBody,
|
request_body: &BlindSignRequestBody,
|
||||||
) -> Result<BlindedSignature, Error> {
|
) -> Result<BlindedSignature, Error> {
|
||||||
let server_response = client.blind_sign(request_body).await?;
|
let server_response = client.blind_sign(request_body).await?;
|
||||||
@@ -179,7 +179,7 @@ impl IssuanceTicketBook {
|
|||||||
// ideally this would have been generic over credential type, but we really don't need secp256k1 keys for bandwidth vouchers
|
// ideally this would have been generic over credential type, but we really don't need secp256k1 keys for bandwidth vouchers
|
||||||
pub async fn obtain_partial_ticketbook_credential(
|
pub async fn obtain_partial_ticketbook_credential(
|
||||||
&self,
|
&self,
|
||||||
client: &nym_validator_client::client::NymApiClient,
|
client: &nym_http_api_client::Client,
|
||||||
signer_index: u64,
|
signer_index: u64,
|
||||||
validator_vk: &VerificationKeyAuth,
|
validator_vk: &VerificationKeyAuth,
|
||||||
signing_data: CredentialSigningData,
|
signing_data: CredentialSigningData,
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ use nym_credentials_interface::{
|
|||||||
VerificationKeyAuth, WalletSignatures,
|
VerificationKeyAuth, WalletSignatures,
|
||||||
};
|
};
|
||||||
use nym_validator_client::client::EcashApiClient;
|
use nym_validator_client::client::EcashApiClient;
|
||||||
|
use nym_validator_client::nym_api::NymApiClientExt;
|
||||||
|
|
||||||
// so we wouldn't break all the existing imports
|
// so we wouldn't break all the existing imports
|
||||||
pub use nym_ecash_time::{cred_exp_date, ecash_date_offset, ecash_today, EcashTime};
|
pub use nym_ecash_time::{cred_exp_date, ecash_date_offset, ecash_today, EcashTime};
|
||||||
@@ -51,7 +52,7 @@ pub async fn obtain_expiration_date_signatures(
|
|||||||
for ecash_api_client in ecash_api_clients.iter() {
|
for ecash_api_client in ecash_api_clients.iter() {
|
||||||
match ecash_api_client
|
match ecash_api_client
|
||||||
.api_client
|
.api_client
|
||||||
.partial_expiration_date_signatures(None)
|
.partial_expiration_date_signatures(None, None)
|
||||||
.await
|
.await
|
||||||
{
|
{
|
||||||
Ok(signature) => {
|
Ok(signature) => {
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
use crate::ecash::bandwidth::issued::CURRENT_SERIALIZATION_REVISION;
|
use crate::ecash::bandwidth::issued::CURRENT_SERIALIZATION_REVISION;
|
||||||
use nym_credentials_interface::CompactEcashError;
|
use nym_credentials_interface::CompactEcashError;
|
||||||
use nym_crypto::asymmetric::x25519::KeyRecoveryError;
|
use nym_crypto::asymmetric::x25519::KeyRecoveryError;
|
||||||
use nym_validator_client::ValidatorClientError;
|
use nym_validator_client::{nym_api::error::NymAPIError, ValidatorClientError};
|
||||||
use thiserror::Error;
|
use thiserror::Error;
|
||||||
|
|
||||||
#[derive(Debug, Error)]
|
#[derive(Debug, Error)]
|
||||||
@@ -37,6 +37,9 @@ pub enum Error {
|
|||||||
#[error("Ran into a validator client error - {0}")]
|
#[error("Ran into a validator client error - {0}")]
|
||||||
ValidatorClientError(#[from] ValidatorClientError),
|
ValidatorClientError(#[from] ValidatorClientError),
|
||||||
|
|
||||||
|
#[error("Nym API request failed - {0}")]
|
||||||
|
NymAPIError(#[from] NymAPIError),
|
||||||
|
|
||||||
#[error("Bandwidth operation overflowed. {0}")]
|
#[error("Bandwidth operation overflowed. {0}")]
|
||||||
BandwidthOverflow(String),
|
BandwidthOverflow(String),
|
||||||
|
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ repository = { workspace = true }
|
|||||||
aes-gcm-siv = { workspace = true, optional = true }
|
aes-gcm-siv = { workspace = true, optional = true }
|
||||||
aes = { workspace = true, optional = true }
|
aes = { workspace = true, optional = true }
|
||||||
aead = { workspace = true, optional = true }
|
aead = { workspace = true, optional = true }
|
||||||
|
base64.workspace = true
|
||||||
bs58 = { workspace = true }
|
bs58 = { workspace = true }
|
||||||
blake3 = { workspace = true, features = ["traits-preview"], optional = true }
|
blake3 = { workspace = true, features = ["traits-preview"], optional = true }
|
||||||
ctr = { workspace = true, optional = true }
|
ctr = { workspace = true, optional = true }
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
// Copyright 2021-2023 - Nym Technologies SA <contact@nymtech.net>
|
// Copyright 2021-2023 - Nym Technologies SA <contact@nymtech.net>
|
||||||
// SPDX-License-Identifier: Apache-2.0
|
// SPDX-License-Identifier: Apache-2.0
|
||||||
|
|
||||||
|
use base64::Engine;
|
||||||
use nym_pemstore::traits::{PemStorableKey, PemStorableKeyPair};
|
use nym_pemstore::traits::{PemStorableKey, PemStorableKeyPair};
|
||||||
use std::fmt::{self, Debug, Display, Formatter};
|
use std::fmt::{self, Debug, Display, Formatter};
|
||||||
use std::str::FromStr;
|
use std::str::FromStr;
|
||||||
@@ -158,6 +159,15 @@ impl PublicKey {
|
|||||||
.map_err(|source| KeyRecoveryError::MalformedPublicKeyString { source })?;
|
.map_err(|source| KeyRecoveryError::MalformedPublicKeyString { source })?;
|
||||||
Self::from_bytes(&bytes)
|
Self::from_bytes(&bytes)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn from_base64(s: &str) -> Option<Self> {
|
||||||
|
let bytes = base64::engine::general_purpose::STANDARD.decode(s).ok()?;
|
||||||
|
Self::from_bytes(&bytes).ok()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn to_base64(&self) -> String {
|
||||||
|
base64::engine::general_purpose::STANDARD.encode(self.as_bytes())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl FromStr for PublicKey {
|
impl FromStr for PublicKey {
|
||||||
|
|||||||
@@ -0,0 +1,27 @@
|
|||||||
|
[package]
|
||||||
|
name = "nym-ecash-signer-check-types"
|
||||||
|
version = "0.1.0"
|
||||||
|
authors.workspace = true
|
||||||
|
repository.workspace = true
|
||||||
|
homepage.workspace = true
|
||||||
|
documentation.workspace = true
|
||||||
|
edition.workspace = true
|
||||||
|
license.workspace = true
|
||||||
|
rust-version.workspace = true
|
||||||
|
readme.workspace = true
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
semver = { workspace = true }
|
||||||
|
serde = { workspace = true, features = ["derive"] }
|
||||||
|
url = { workspace = true }
|
||||||
|
thiserror = { workspace = true }
|
||||||
|
time = { workspace = true }
|
||||||
|
tracing = { workspace = true }
|
||||||
|
utoipa = { workspace = true }
|
||||||
|
|
||||||
|
nym-coconut-dkg-common = { path = "../cosmwasm-smart-contracts/coconut-dkg" }
|
||||||
|
nym-crypto = { path = "../crypto", features = ["asymmetric"] }
|
||||||
|
|
||||||
|
|
||||||
|
[lints]
|
||||||
|
workspace = true
|
||||||
@@ -0,0 +1,97 @@
|
|||||||
|
// Copyright 2025 - Nym Technologies SA <contact@nymtech.net>
|
||||||
|
// SPDX-License-Identifier: Apache-2.0
|
||||||
|
|
||||||
|
use nym_coconut_dkg_common::dealer::DealerDetails;
|
||||||
|
use nym_coconut_dkg_common::verification_key::{ContractVKShare, VerificationKeyShare};
|
||||||
|
use nym_crypto::asymmetric::ed25519;
|
||||||
|
use nym_crypto::asymmetric::ed25519::Ed25519RecoveryError;
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use thiserror::Error;
|
||||||
|
use url::Url;
|
||||||
|
use utoipa::ToSchema;
|
||||||
|
|
||||||
|
#[derive(Debug, Error)]
|
||||||
|
pub enum MalformedDealer {
|
||||||
|
#[error("dealer at {dealer_url} has provided invalid ed25519 pubkey: {source}")]
|
||||||
|
InvalidDealerPubkey {
|
||||||
|
dealer_url: String,
|
||||||
|
source: Ed25519RecoveryError,
|
||||||
|
},
|
||||||
|
|
||||||
|
#[error("dealer at {dealer_url} has provided invalid announce url: {source}")]
|
||||||
|
InvalidDealerAddress {
|
||||||
|
dealer_url: String,
|
||||||
|
source: url::ParseError,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Serialize, Deserialize, Clone, ToSchema)]
|
||||||
|
pub struct RawDealerInformation {
|
||||||
|
pub announce_address: String,
|
||||||
|
pub owner_address: String,
|
||||||
|
pub node_index: u64,
|
||||||
|
pub public_key: String,
|
||||||
|
pub verification_key_share: Option<VerificationKeyShare>,
|
||||||
|
pub share_verified: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl RawDealerInformation {
|
||||||
|
pub fn new(
|
||||||
|
dealer_details: &DealerDetails,
|
||||||
|
contract_share: Option<&ContractVKShare>,
|
||||||
|
) -> RawDealerInformation {
|
||||||
|
RawDealerInformation {
|
||||||
|
announce_address: dealer_details.announce_address.clone(),
|
||||||
|
owner_address: dealer_details.address.to_string(),
|
||||||
|
node_index: dealer_details.assigned_index,
|
||||||
|
public_key: dealer_details.ed25519_identity.clone(),
|
||||||
|
verification_key_share: contract_share.map(|s| s.share.clone()),
|
||||||
|
share_verified: contract_share.map(|s| s.verified).unwrap_or(false),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn parse(&self) -> Result<DealerInformation, MalformedDealer> {
|
||||||
|
Ok(DealerInformation {
|
||||||
|
announce_address: self.announce_address.parse().map_err(|source| {
|
||||||
|
MalformedDealer::InvalidDealerAddress {
|
||||||
|
dealer_url: self.announce_address.clone(),
|
||||||
|
source,
|
||||||
|
}
|
||||||
|
})?,
|
||||||
|
owner_address: self.owner_address.clone(),
|
||||||
|
node_index: self.node_index,
|
||||||
|
public_key: self.public_key.parse().map_err(|source| {
|
||||||
|
MalformedDealer::InvalidDealerPubkey {
|
||||||
|
dealer_url: self.announce_address.clone(),
|
||||||
|
source,
|
||||||
|
}
|
||||||
|
})?,
|
||||||
|
verification_key_share: self.verification_key_share.clone(),
|
||||||
|
share_verified: self.share_verified,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct DealerInformation {
|
||||||
|
pub announce_address: Url,
|
||||||
|
pub owner_address: String,
|
||||||
|
pub node_index: u64,
|
||||||
|
pub public_key: ed25519::PublicKey,
|
||||||
|
// no need to parse it into the full type as it doesn't get us anything
|
||||||
|
pub verification_key_share: Option<VerificationKeyShare>,
|
||||||
|
pub share_verified: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<DealerInformation> for RawDealerInformation {
|
||||||
|
fn from(d: DealerInformation) -> Self {
|
||||||
|
RawDealerInformation {
|
||||||
|
announce_address: d.announce_address.to_string(),
|
||||||
|
owner_address: d.owner_address,
|
||||||
|
node_index: d.node_index,
|
||||||
|
public_key: d.public_key.to_base58_string(),
|
||||||
|
verification_key_share: d.verification_key_share,
|
||||||
|
share_verified: d.share_verified,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,127 @@
|
|||||||
|
// Copyright 2025 - Nym Technologies SA <contact@nymtech.net>
|
||||||
|
// SPDX-License-Identifier: Apache-2.0
|
||||||
|
|
||||||
|
use nym_coconut_dkg_common::types::EpochId;
|
||||||
|
use nym_coconut_dkg_common::verification_key::VerificationKeyShare;
|
||||||
|
use nym_crypto::asymmetric::ed25519;
|
||||||
|
use std::time::Duration;
|
||||||
|
use time::OffsetDateTime;
|
||||||
|
use tracing::{debug, warn};
|
||||||
|
|
||||||
|
pub trait Verifiable {
|
||||||
|
fn verify_signature(&self, pub_key: &ed25519::PublicKey) -> bool;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub trait TimestampedResponse {
|
||||||
|
fn timestamp(&self) -> OffsetDateTime;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub trait LegacyChainResponse {
|
||||||
|
fn chain_synced(&self, now: OffsetDateTime, stall_threshold: Duration) -> bool;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub trait ChainResponse: Verifiable + TimestampedResponse {
|
||||||
|
fn chain_synced(&self) -> bool;
|
||||||
|
|
||||||
|
fn chain_available(
|
||||||
|
&self,
|
||||||
|
pub_key: &ed25519::PublicKey,
|
||||||
|
now: OffsetDateTime,
|
||||||
|
stale_response_threshold: Duration,
|
||||||
|
) -> bool {
|
||||||
|
if !self.verify_signature(pub_key) {
|
||||||
|
warn!("failed signature verification on chain status response");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// we rely on information provided from the api itself AS LONG AS it's not too outdated
|
||||||
|
if self.timestamp() + stale_response_threshold < now {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
self.chain_synced()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub trait LegacySignerResponse {
|
||||||
|
fn signer_identity(&self) -> &str;
|
||||||
|
|
||||||
|
fn signer_verification_key(&self) -> &Option<String>;
|
||||||
|
|
||||||
|
fn unprovable_signing_available(
|
||||||
|
&self,
|
||||||
|
pub_key: &ed25519::PublicKey,
|
||||||
|
expected_verification_key: Option<VerificationKeyShare>,
|
||||||
|
share_verified: bool,
|
||||||
|
) -> bool {
|
||||||
|
if self.signer_identity() != pub_key.to_base58_string() {
|
||||||
|
warn!("mismatched identity key on the legacy response");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// the contract share hasn't been verified yet, so we're probably in the middle of DKG
|
||||||
|
// thus if there's a bit of desync in the state, it's fine
|
||||||
|
if !share_verified {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if self.signer_verification_key() != &expected_verification_key {
|
||||||
|
warn!("mismatched [ecash] verification key on the legacy response");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub trait SignerResponse: Verifiable + TimestampedResponse {
|
||||||
|
fn has_signing_keys(&self) -> bool;
|
||||||
|
|
||||||
|
fn signer_disabled(&self) -> bool;
|
||||||
|
|
||||||
|
fn is_ecash_signer(&self) -> bool;
|
||||||
|
|
||||||
|
fn dkg_ecash_epoch_id(&self) -> EpochId;
|
||||||
|
|
||||||
|
fn provable_signing_available(
|
||||||
|
&self,
|
||||||
|
pub_key: &ed25519::PublicKey,
|
||||||
|
dkg_epoch_id: EpochId,
|
||||||
|
now: OffsetDateTime,
|
||||||
|
stale_response_threshold: Duration,
|
||||||
|
) -> bool {
|
||||||
|
if !self.verify_signature(pub_key) {
|
||||||
|
warn!("failed signature verification on chain status response");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// we rely on information provided from the api itself AS LONG AS it's not too outdated
|
||||||
|
if self.timestamp() + stale_response_threshold < now {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if !self.has_signing_keys() {
|
||||||
|
debug!("missing signing keys");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if self.signer_disabled() {
|
||||||
|
debug!("signer functionalities explicitly disabled");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if !self.is_ecash_signer() {
|
||||||
|
debug!("signer doesn't recognise it's a signer for this epoch");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if dkg_epoch_id != self.dkg_ecash_epoch_id() {
|
||||||
|
debug!(
|
||||||
|
"mismatched dkg epoch id. current: {dkg_epoch_id}, signer's: {}",
|
||||||
|
self.dkg_ecash_epoch_id()
|
||||||
|
);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
true
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
// Copyright 2025 - Nym Technologies SA <contact@nymtech.net>
|
||||||
|
// SPDX-License-Identifier: Apache-2.0
|
||||||
|
|
||||||
|
pub mod dealer_information;
|
||||||
|
pub mod helper_traits;
|
||||||
|
pub mod status;
|
||||||
@@ -0,0 +1,303 @@
|
|||||||
|
// Copyright 2025 - Nym Technologies SA <contact@nymtech.net>
|
||||||
|
// SPDX-License-Identifier: Apache-2.0
|
||||||
|
|
||||||
|
use crate::dealer_information::RawDealerInformation;
|
||||||
|
use crate::helper_traits::{
|
||||||
|
ChainResponse, LegacyChainResponse, LegacySignerResponse, SignerResponse,
|
||||||
|
};
|
||||||
|
use nym_coconut_dkg_common::types::EpochId;
|
||||||
|
use nym_coconut_dkg_common::verification_key::VerificationKeyShare;
|
||||||
|
use nym_crypto::asymmetric::ed25519;
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use std::time::Duration;
|
||||||
|
use time::OffsetDateTime;
|
||||||
|
use utoipa::ToSchema;
|
||||||
|
|
||||||
|
pub(crate) const CHAIN_STALL_THRESHOLD: Duration = Duration::from_secs(5 * 60);
|
||||||
|
pub(crate) const STALE_RESPONSE_THRESHOLD: Duration = Duration::from_secs(5 * 60);
|
||||||
|
|
||||||
|
// the reason for generics is not to remove duplication of code,
|
||||||
|
// but because without them, we'd be having problems with circular dependencies,
|
||||||
|
// i.e. nym-api-requests depending on ecash-signer-check-types and
|
||||||
|
// ecash-signer-check-types needing nym-api-requests
|
||||||
|
#[derive(Debug, Serialize, Deserialize, Clone, ToSchema)]
|
||||||
|
pub enum Status<L, T> {
|
||||||
|
/// The API, even though it reports correct version, did not response to the status query
|
||||||
|
Unreachable,
|
||||||
|
|
||||||
|
/// The API is running an outdated version that does not expose the required endpoint
|
||||||
|
Outdated,
|
||||||
|
|
||||||
|
/// Response to the legacy (unsigned) status query
|
||||||
|
ReachableLegacy { response: Box<L> },
|
||||||
|
|
||||||
|
/// Response to the current (signed) status query
|
||||||
|
Reachable { response: Box<T> },
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<L, T> Status<L, T>
|
||||||
|
where
|
||||||
|
L: LegacyChainResponse,
|
||||||
|
T: ChainResponse,
|
||||||
|
{
|
||||||
|
pub fn chain_available(&self, pub_key: ed25519::PublicKey) -> bool {
|
||||||
|
let now = OffsetDateTime::now_utc();
|
||||||
|
|
||||||
|
match self {
|
||||||
|
Status::Unreachable | Status::Outdated => false,
|
||||||
|
Status::ReachableLegacy { response } => {
|
||||||
|
response.chain_synced(now, CHAIN_STALL_THRESHOLD)
|
||||||
|
}
|
||||||
|
Status::Reachable { response } => {
|
||||||
|
response.chain_available(&pub_key, now, STALE_RESPONSE_THRESHOLD)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn chain_provably_stalled(&self, pub_key: ed25519::PublicKey) -> bool {
|
||||||
|
let now = OffsetDateTime::now_utc();
|
||||||
|
|
||||||
|
match self {
|
||||||
|
Status::Unreachable | Status::Outdated | Status::ReachableLegacy { .. } => false,
|
||||||
|
Status::Reachable { response } => {
|
||||||
|
!response.chain_available(&pub_key, now, STALE_RESPONSE_THRESHOLD)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn chain_unprovably_stalled(&self) -> bool {
|
||||||
|
let now = OffsetDateTime::now_utc();
|
||||||
|
|
||||||
|
match self {
|
||||||
|
Status::Unreachable | Status::Outdated | Status::Reachable { .. } => false,
|
||||||
|
Status::ReachableLegacy { response } => {
|
||||||
|
!response.chain_synced(now, CHAIN_STALL_THRESHOLD)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<L, T> Status<L, T>
|
||||||
|
where
|
||||||
|
L: LegacySignerResponse,
|
||||||
|
T: SignerResponse,
|
||||||
|
{
|
||||||
|
pub fn signing_available(
|
||||||
|
&self,
|
||||||
|
pub_key: ed25519::PublicKey,
|
||||||
|
dkg_epoch_id: u64,
|
||||||
|
expected_verification_key: Option<VerificationKeyShare>,
|
||||||
|
share_verified: bool,
|
||||||
|
) -> bool {
|
||||||
|
let now = OffsetDateTime::now_utc();
|
||||||
|
|
||||||
|
match self {
|
||||||
|
Status::Unreachable | Status::Outdated => false,
|
||||||
|
Status::ReachableLegacy { response } => response.unprovable_signing_available(
|
||||||
|
&pub_key,
|
||||||
|
expected_verification_key,
|
||||||
|
share_verified,
|
||||||
|
),
|
||||||
|
Status::Reachable { response } => response.provable_signing_available(
|
||||||
|
&pub_key,
|
||||||
|
dkg_epoch_id,
|
||||||
|
now,
|
||||||
|
STALE_RESPONSE_THRESHOLD,
|
||||||
|
),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn signing_provably_unavailable(
|
||||||
|
&self,
|
||||||
|
pub_key: ed25519::PublicKey,
|
||||||
|
dkg_epoch_id: EpochId,
|
||||||
|
) -> bool {
|
||||||
|
let now = OffsetDateTime::now_utc();
|
||||||
|
|
||||||
|
match self {
|
||||||
|
Status::Unreachable | Status::Outdated | Status::ReachableLegacy { .. } => false,
|
||||||
|
Status::Reachable { response } => !response.provable_signing_available(
|
||||||
|
&pub_key,
|
||||||
|
dkg_epoch_id,
|
||||||
|
now,
|
||||||
|
STALE_RESPONSE_THRESHOLD,
|
||||||
|
),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn signing_unprovably_unavailable(
|
||||||
|
&self,
|
||||||
|
pub_key: ed25519::PublicKey,
|
||||||
|
expected_verification_key: Option<VerificationKeyShare>,
|
||||||
|
share_verified: bool,
|
||||||
|
) -> bool {
|
||||||
|
match self {
|
||||||
|
Status::Unreachable | Status::Outdated | Status::Reachable { .. } => false,
|
||||||
|
Status::ReachableLegacy { response } => !response.unprovable_signing_available(
|
||||||
|
&pub_key,
|
||||||
|
expected_verification_key,
|
||||||
|
share_verified,
|
||||||
|
),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Serialize, Deserialize, Clone, ToSchema)]
|
||||||
|
pub struct SignerResult<LS, TS, LC, TC> {
|
||||||
|
pub dkg_epoch_id: u64,
|
||||||
|
pub information: RawDealerInformation,
|
||||||
|
pub status: SignerStatus<LS, TS, LC, TC>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<LS, TS, LC, TC> SignerResult<LS, TS, LC, TC> {
|
||||||
|
pub fn signer_unreachable(&self) -> bool {
|
||||||
|
matches!(self.status, SignerStatus::Unreachable)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn malformed_details(&self) -> bool {
|
||||||
|
self.information.parse().is_err()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<LS, TS, LC, TC> SignerResult<LS, TS, LC, TC>
|
||||||
|
where
|
||||||
|
LC: LegacyChainResponse,
|
||||||
|
TC: ChainResponse,
|
||||||
|
{
|
||||||
|
pub fn unknown_chain_status(&self) -> bool {
|
||||||
|
let Ok(_) = self.information.parse() else {
|
||||||
|
return true;
|
||||||
|
};
|
||||||
|
if let SignerStatus::Tested { .. } = &self.status {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
true
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn chain_available(&self) -> bool {
|
||||||
|
let Ok(parsed_info) = self.information.parse() else {
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
|
||||||
|
let SignerStatus::Tested { result } = &self.status else {
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
result
|
||||||
|
.local_chain_status
|
||||||
|
.chain_available(parsed_info.public_key)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn chain_provably_stalled(&self) -> bool {
|
||||||
|
let Ok(parsed_info) = self.information.parse() else {
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
|
||||||
|
let SignerStatus::Tested { result } = &self.status else {
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
|
||||||
|
result
|
||||||
|
.local_chain_status
|
||||||
|
.chain_provably_stalled(parsed_info.public_key)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn chain_unprovably_stalled(&self) -> bool {
|
||||||
|
let SignerStatus::Tested { result } = &self.status else {
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
|
||||||
|
result.local_chain_status.chain_unprovably_stalled()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<LS, TS, LC, TC> SignerResult<LS, TS, LC, TC>
|
||||||
|
where
|
||||||
|
LS: LegacySignerResponse,
|
||||||
|
TS: SignerResponse,
|
||||||
|
{
|
||||||
|
pub fn unknown_signing_status(&self) -> bool {
|
||||||
|
let Ok(_) = self.information.parse() else {
|
||||||
|
return true;
|
||||||
|
};
|
||||||
|
if let SignerStatus::Tested { .. } = &self.status {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
true
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn signing_available(&self) -> bool {
|
||||||
|
let Ok(parsed_info) = self.information.parse() else {
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
|
||||||
|
let SignerStatus::Tested { result } = &self.status else {
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
result.signing_status.signing_available(
|
||||||
|
parsed_info.public_key,
|
||||||
|
self.dkg_epoch_id,
|
||||||
|
parsed_info.verification_key_share,
|
||||||
|
parsed_info.share_verified,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn signing_provably_unavailable(&self) -> bool {
|
||||||
|
let Ok(parsed_info) = self.information.parse() else {
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
|
||||||
|
let SignerStatus::Tested { result } = &self.status else {
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
|
||||||
|
result
|
||||||
|
.signing_status
|
||||||
|
.signing_provably_unavailable(parsed_info.public_key, self.dkg_epoch_id)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn signing_unprovably_unavailable(&self) -> bool {
|
||||||
|
let Ok(parsed_info) = self.information.parse() else {
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
|
||||||
|
let SignerStatus::Tested { result } = &self.status else {
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
|
||||||
|
result.signing_status.signing_unprovably_unavailable(
|
||||||
|
parsed_info.public_key,
|
||||||
|
parsed_info.verification_key_share,
|
||||||
|
parsed_info.share_verified,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Serialize, Deserialize, Clone, ToSchema)]
|
||||||
|
pub enum SignerStatus<LS, TS, LC, TC> {
|
||||||
|
Unreachable,
|
||||||
|
ProvidedInvalidDetails,
|
||||||
|
Tested {
|
||||||
|
result: SignerTestResult<LS, TS, LC, TC>,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<LS, TS, LC, TC> SignerStatus<LS, TS, LC, TC> {
|
||||||
|
pub fn with_details(
|
||||||
|
self,
|
||||||
|
information: impl Into<RawDealerInformation>,
|
||||||
|
dkg_epoch_id: u64,
|
||||||
|
) -> SignerResult<LS, TS, LC, TC> {
|
||||||
|
SignerResult {
|
||||||
|
dkg_epoch_id,
|
||||||
|
status: self,
|
||||||
|
information: information.into(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Serialize, Deserialize, Clone, ToSchema)]
|
||||||
|
pub struct SignerTestResult<LS, TS, LC, TC> {
|
||||||
|
pub reported_version: String,
|
||||||
|
pub signing_status: Status<LS, TS>,
|
||||||
|
pub local_chain_status: Status<LC, TC>,
|
||||||
|
}
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
[package]
|
||||||
|
name = "nym-ecash-signer-check"
|
||||||
|
version = "0.1.0"
|
||||||
|
authors.workspace = true
|
||||||
|
repository.workspace = true
|
||||||
|
homepage.workspace = true
|
||||||
|
documentation.workspace = true
|
||||||
|
edition.workspace = true
|
||||||
|
license.workspace = true
|
||||||
|
rust-version.workspace = true
|
||||||
|
readme.workspace = true
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
futures = { workspace = true }
|
||||||
|
thiserror = { workspace = true }
|
||||||
|
semver = { workspace = true }
|
||||||
|
tokio = { workspace = true, features = ["time"] }
|
||||||
|
tracing = { workspace = true }
|
||||||
|
url = { workspace = true }
|
||||||
|
|
||||||
|
|
||||||
|
nym-validator-client = { path = "../client-libs/validator-client" }
|
||||||
|
nym-network-defaults = { path = "../network-defaults" }
|
||||||
|
nym-ecash-signer-check-types = { path = "../ecash-signer-check-types" }
|
||||||
|
nym-http-api-client = { path = "../http-api-client" }
|
||||||
|
|
||||||
|
[lints]
|
||||||
|
workspace = true
|
||||||
@@ -0,0 +1,231 @@
|
|||||||
|
// Copyright 2025 - Nym Technologies SA <contact@nymtech.net>
|
||||||
|
// SPDX-License-Identifier: Apache-2.0
|
||||||
|
|
||||||
|
use crate::{LocalChainStatus, SignerCheckError, SigningStatus, TypedSignerResult};
|
||||||
|
use nym_ecash_signer_check_types::dealer_information::RawDealerInformation;
|
||||||
|
use nym_ecash_signer_check_types::status::{SignerStatus, SignerTestResult};
|
||||||
|
use nym_validator_client::models::BinaryBuildInformationOwned;
|
||||||
|
use nym_validator_client::nym_api::NymApiClientExt;
|
||||||
|
use nym_validator_client::nyxd::contract_traits::dkg_query_client::{
|
||||||
|
ContractVKShare, DealerDetails,
|
||||||
|
};
|
||||||
|
use std::time::Duration;
|
||||||
|
use tracing::{error, warn};
|
||||||
|
use url::Url;
|
||||||
|
|
||||||
|
pub(crate) mod chain_status {
|
||||||
|
|
||||||
|
// Dorina
|
||||||
|
pub(crate) const MINIMUM_VERSION_LEGACY: semver::Version = semver::Version::new(1, 1, 51);
|
||||||
|
|
||||||
|
// Gruyere
|
||||||
|
pub(crate) const MINIMUM_VERSION: semver::Version = semver::Version::new(1, 1, 64);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) mod signing_status {
|
||||||
|
// Magura (possibly earlier)
|
||||||
|
pub(crate) const MINIMUM_LEGACY_VERSION: semver::Version = semver::Version::new(1, 1, 46);
|
||||||
|
|
||||||
|
// Gruyere
|
||||||
|
pub(crate) const MINIMUM_VERSION: semver::Version = semver::Version::new(1, 1, 64);
|
||||||
|
}
|
||||||
|
|
||||||
|
struct ClientUnderTest {
|
||||||
|
api_client: nym_http_api_client::Client,
|
||||||
|
build_info: Option<BinaryBuildInformationOwned>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ClientUnderTest {
|
||||||
|
pub(crate) fn new(api_url: &Url) -> Result<Self, SignerCheckError> {
|
||||||
|
// The builder should not fail with a valid URL that's already parsed
|
||||||
|
// If it does fail, it's an internal error that we can't recover from
|
||||||
|
let api_client = nym_http_api_client::Client::builder(api_url.clone())?.build()?;
|
||||||
|
|
||||||
|
Ok(ClientUnderTest {
|
||||||
|
api_client,
|
||||||
|
build_info: None,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) async fn try_retrieve_build_information(&mut self) -> bool {
|
||||||
|
match tokio::time::timeout(Duration::from_secs(5), self.api_client.build_information())
|
||||||
|
.await
|
||||||
|
{
|
||||||
|
Ok(Ok(build_information)) => {
|
||||||
|
self.build_info = Some(build_information);
|
||||||
|
true
|
||||||
|
}
|
||||||
|
Ok(Err(err)) => {
|
||||||
|
warn!("{}: failed to retrieve build information: {err}. the signer is most likely down", self.api_client.current_url());
|
||||||
|
false
|
||||||
|
}
|
||||||
|
Err(_timeout) => {
|
||||||
|
warn!(
|
||||||
|
"{}: timed out while attempting to retrieve build information",
|
||||||
|
self.api_client.current_url()
|
||||||
|
);
|
||||||
|
false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn version(&self) -> Option<semver::Version> {
|
||||||
|
self.build_info.as_ref().and_then(|build_info| {
|
||||||
|
build_info
|
||||||
|
.build_version
|
||||||
|
.parse()
|
||||||
|
.inspect_err(|err| {
|
||||||
|
error!(
|
||||||
|
"ecash signer '{}' reports invalid version {}: {err}",
|
||||||
|
self.api_client.current_url(),
|
||||||
|
build_info.build_version
|
||||||
|
)
|
||||||
|
})
|
||||||
|
.ok()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn supports_legacy_signing_status_query(&self) -> bool {
|
||||||
|
let Some(version) = self.version() else {
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
version >= signing_status::MINIMUM_LEGACY_VERSION
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn supports_signing_status_query(&self) -> bool {
|
||||||
|
let Some(version) = self.version() else {
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
version >= signing_status::MINIMUM_VERSION
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn supports_chain_status_query(&self) -> bool {
|
||||||
|
let Some(version) = self.version() else {
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
version >= chain_status::MINIMUM_VERSION
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn supports_legacy_chain_status_query(&self) -> bool {
|
||||||
|
let Some(version) = self.version() else {
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
version >= chain_status::MINIMUM_VERSION_LEGACY
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) async fn check_local_chain(&self) -> LocalChainStatus {
|
||||||
|
// check if it at least supports legacy query
|
||||||
|
if !self.supports_legacy_chain_status_query() {
|
||||||
|
return LocalChainStatus::Outdated;
|
||||||
|
}
|
||||||
|
|
||||||
|
// check if it supports the current query
|
||||||
|
if self.supports_chain_status_query() {
|
||||||
|
return match self.api_client.get_chain_blocks_status().await {
|
||||||
|
Ok(status) => LocalChainStatus::Reachable {
|
||||||
|
response: Box::new(status),
|
||||||
|
},
|
||||||
|
Err(err) => {
|
||||||
|
warn!(
|
||||||
|
"{}: failed to retrieve local chain status: {err}",
|
||||||
|
self.api_client.current_url()
|
||||||
|
);
|
||||||
|
LocalChainStatus::Unreachable
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// fallback to the legacy query
|
||||||
|
match self.api_client.get_chain_status().await {
|
||||||
|
Ok(status) => LocalChainStatus::ReachableLegacy {
|
||||||
|
response: Box::new(status),
|
||||||
|
},
|
||||||
|
Err(err) => {
|
||||||
|
warn!(
|
||||||
|
"{}: failed to retrieve [legacy] local chain status: {err}",
|
||||||
|
self.api_client.current_url()
|
||||||
|
);
|
||||||
|
LocalChainStatus::Unreachable
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) async fn check_signing_status(&self) -> SigningStatus {
|
||||||
|
// check if it at least supports legacy query
|
||||||
|
if !self.supports_legacy_signing_status_query() {
|
||||||
|
return SigningStatus::Outdated;
|
||||||
|
}
|
||||||
|
|
||||||
|
// check if it supports the current query
|
||||||
|
if self.supports_signing_status_query() {
|
||||||
|
return match self.api_client.get_signer_status().await {
|
||||||
|
Ok(response) => SigningStatus::Reachable {
|
||||||
|
response: Box::new(response),
|
||||||
|
},
|
||||||
|
Err(err) => {
|
||||||
|
warn!(
|
||||||
|
"{}: failed to retrieve signer chain status: {err}",
|
||||||
|
self.api_client.current_url()
|
||||||
|
);
|
||||||
|
SigningStatus::Unreachable
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// fallback to the legacy query
|
||||||
|
match self.api_client.get_signer_information().await {
|
||||||
|
Ok(status) => SigningStatus::ReachableLegacy {
|
||||||
|
response: Box::new(status),
|
||||||
|
},
|
||||||
|
Err(err) => {
|
||||||
|
warn!(
|
||||||
|
"{}: failed to retrieve [legacy] signer chain status: {err}",
|
||||||
|
self.api_client.current_url()
|
||||||
|
);
|
||||||
|
// NOTE: this might equally mean the signing is disabled
|
||||||
|
SigningStatus::Unreachable
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) async fn check_client(
|
||||||
|
dealer_details: DealerDetails,
|
||||||
|
dkg_epoch: u64,
|
||||||
|
contract_share: Option<&ContractVKShare>,
|
||||||
|
) -> TypedSignerResult {
|
||||||
|
let dealer_information = RawDealerInformation::new(&dealer_details, contract_share);
|
||||||
|
|
||||||
|
// 7. attempt to construct client instances out of them
|
||||||
|
let Ok(parsed_information) = dealer_information.parse() else {
|
||||||
|
return SignerStatus::ProvidedInvalidDetails.with_details(dealer_information, dkg_epoch);
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut client = match ClientUnderTest::new(&parsed_information.announce_address) {
|
||||||
|
Ok(client) => client,
|
||||||
|
Err(err) => {
|
||||||
|
error!("failed to create client instance: {err}");
|
||||||
|
return SignerStatus::Unreachable.with_details(dealer_information, dkg_epoch);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 8. check basic connection status - can you retrieve build information?
|
||||||
|
if !client.try_retrieve_build_information().await {
|
||||||
|
return SignerStatus::Unreachable.with_details(dealer_information, dkg_epoch);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 9. check perceived chain status
|
||||||
|
let local_chain_status = client.check_local_chain().await;
|
||||||
|
|
||||||
|
// 10. check signer status
|
||||||
|
let signing_status = client.check_signing_status().await;
|
||||||
|
|
||||||
|
SignerStatus::Tested {
|
||||||
|
result: SignerTestResult {
|
||||||
|
reported_version: client.version().map(|v| v.to_string()).unwrap_or_default(),
|
||||||
|
signing_status,
|
||||||
|
local_chain_status,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
.with_details(dealer_information, dkg_epoch)
|
||||||
|
}
|
||||||
@@ -0,0 +1,80 @@
|
|||||||
|
// Copyright 2025 - Nym Technologies SA <contact@nymtech.net>
|
||||||
|
// SPDX-License-Identifier: Apache-2.0
|
||||||
|
|
||||||
|
use crate::SignerCheckError;
|
||||||
|
use nym_crypto::asymmetric::ed25519;
|
||||||
|
use nym_validator_client::nyxd::contract_traits::dkg_query_client::{
|
||||||
|
ContractVKShare, DealerDetails, VerificationKeyShare,
|
||||||
|
};
|
||||||
|
use url::Url;
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct RawDealerInformation {
|
||||||
|
pub announce_address: String,
|
||||||
|
pub owner_address: String,
|
||||||
|
pub node_index: u64,
|
||||||
|
pub public_key: String,
|
||||||
|
pub verification_key_share: Option<VerificationKeyShare>,
|
||||||
|
pub share_verified: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl RawDealerInformation {
|
||||||
|
pub fn new(
|
||||||
|
dealer_details: &DealerDetails,
|
||||||
|
contract_share: Option<&ContractVKShare>,
|
||||||
|
) -> RawDealerInformation {
|
||||||
|
RawDealerInformation {
|
||||||
|
announce_address: dealer_details.announce_address.clone(),
|
||||||
|
owner_address: dealer_details.address.to_string(),
|
||||||
|
node_index: dealer_details.assigned_index,
|
||||||
|
public_key: dealer_details.ed25519_identity.clone(),
|
||||||
|
verification_key_share: contract_share.map(|s| s.share.clone()),
|
||||||
|
share_verified: contract_share.map(|s| s.verified).unwrap_or(false),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn parse(&self) -> Result<DealerInformation, SignerCheckError> {
|
||||||
|
Ok(DealerInformation {
|
||||||
|
announce_address: self.announce_address.parse().map_err(|source| {
|
||||||
|
SignerCheckError::InvalidDealerAddress {
|
||||||
|
dealer_url: self.announce_address.clone(),
|
||||||
|
source,
|
||||||
|
}
|
||||||
|
})?,
|
||||||
|
owner_address: self.owner_address.clone(),
|
||||||
|
node_index: self.node_index,
|
||||||
|
public_key: self.announce_address.parse().map_err(|source| {
|
||||||
|
SignerCheckError::InvalidDealerPubkey {
|
||||||
|
dealer_url: self.announce_address.clone(),
|
||||||
|
source,
|
||||||
|
}
|
||||||
|
})?,
|
||||||
|
verification_key_share: self.verification_key_share.clone(),
|
||||||
|
share_verified: self.share_verified,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct DealerInformation {
|
||||||
|
pub announce_address: Url,
|
||||||
|
pub owner_address: String,
|
||||||
|
pub node_index: u64,
|
||||||
|
pub public_key: ed25519::PublicKey,
|
||||||
|
// no need to parse it into the full type as it doesn't get us anything
|
||||||
|
pub verification_key_share: Option<VerificationKeyShare>,
|
||||||
|
pub share_verified: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<DealerInformation> for RawDealerInformation {
|
||||||
|
fn from(d: DealerInformation) -> Self {
|
||||||
|
RawDealerInformation {
|
||||||
|
announce_address: d.announce_address.to_string(),
|
||||||
|
owner_address: d.owner_address,
|
||||||
|
node_index: d.node_index,
|
||||||
|
public_key: d.public_key.to_base58_string(),
|
||||||
|
verification_key_share: d.verification_key_share,
|
||||||
|
share_verified: d.share_verified,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
// Copyright 2025 - Nym Technologies SA <contact@nymtech.net>
|
||||||
|
// SPDX-License-Identifier: Apache-2.0
|
||||||
|
|
||||||
|
use nym_http_api_client::HttpClientError;
|
||||||
|
use nym_validator_client::nyxd::error::NyxdError;
|
||||||
|
use thiserror::Error;
|
||||||
|
|
||||||
|
#[derive(Debug, Error)]
|
||||||
|
pub enum SignerCheckError {
|
||||||
|
#[error("failed to connect to nyxd chain due to invalid connection details: {source}")]
|
||||||
|
InvalidNyxdConnectionDetails { source: NyxdError },
|
||||||
|
|
||||||
|
#[error("failed to query the DKG contract: {source}")]
|
||||||
|
DKGContractQueryFailure { source: NyxdError },
|
||||||
|
|
||||||
|
#[error("failed to build client: {source}")]
|
||||||
|
HttpClient {
|
||||||
|
#[from]
|
||||||
|
source: HttpClientError,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
impl SignerCheckError {
|
||||||
|
pub fn invalid_nyxd_connection_details(source: NyxdError) -> Self {
|
||||||
|
Self::InvalidNyxdConnectionDetails { source }
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn dkg_contract_query_failure(source: NyxdError) -> Self {
|
||||||
|
Self::DKGContractQueryFailure { source }
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,127 @@
|
|||||||
|
// Copyright 2025 - Nym Technologies SA <contact@nymtech.net>
|
||||||
|
// SPDX-License-Identifier: Apache-2.0
|
||||||
|
|
||||||
|
use crate::client_check::check_client;
|
||||||
|
use futures::stream::{FuturesUnordered, StreamExt};
|
||||||
|
use nym_network_defaults::NymNetworkDetails;
|
||||||
|
use nym_validator_client::nyxd::contract_traits::{DkgQueryClient, PagedDkgQueryClient};
|
||||||
|
use nym_validator_client::QueryHttpRpcNyxdClient;
|
||||||
|
use std::collections::HashMap;
|
||||||
|
use url::Url;
|
||||||
|
|
||||||
|
pub use error::SignerCheckError;
|
||||||
|
use nym_ecash_signer_check_types::status::{SignerResult, Status};
|
||||||
|
use nym_validator_client::ecash::models::EcashSignerStatusResponse;
|
||||||
|
use nym_validator_client::models::{
|
||||||
|
ChainBlocksStatusResponse, ChainStatusResponse, SignerInformationResponse,
|
||||||
|
};
|
||||||
|
use nym_validator_client::nyxd::contract_traits::dkg_query_client::{
|
||||||
|
ContractVKShare, DealerDetails, Epoch,
|
||||||
|
};
|
||||||
|
|
||||||
|
mod client_check;
|
||||||
|
pub mod error;
|
||||||
|
|
||||||
|
pub type TypedSignerResult = SignerResult<
|
||||||
|
SignerInformationResponse,
|
||||||
|
EcashSignerStatusResponse,
|
||||||
|
ChainStatusResponse,
|
||||||
|
ChainBlocksStatusResponse,
|
||||||
|
>;
|
||||||
|
pub type LocalChainStatus = Status<ChainStatusResponse, ChainBlocksStatusResponse>;
|
||||||
|
pub type SigningStatus = Status<SignerInformationResponse, EcashSignerStatusResponse>;
|
||||||
|
|
||||||
|
pub struct SignersTestResult {
|
||||||
|
pub threshold: Option<u64>,
|
||||||
|
pub results: Vec<TypedSignerResult>,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn check_signers(
|
||||||
|
rpc_endpoint: Url,
|
||||||
|
// details such as denoms, prefixes, etc.
|
||||||
|
network_details: NymNetworkDetails,
|
||||||
|
) -> Result<SignersTestResult, SignerCheckError> {
|
||||||
|
// 1. create nyx client instance
|
||||||
|
let client = QueryHttpRpcNyxdClient::connect_with_network_details(
|
||||||
|
rpc_endpoint.as_str(),
|
||||||
|
network_details,
|
||||||
|
)
|
||||||
|
.map_err(SignerCheckError::invalid_nyxd_connection_details)?;
|
||||||
|
|
||||||
|
check_signers_with_client(&client).await
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct DkgDetails {
|
||||||
|
pub dkg_epoch: Epoch,
|
||||||
|
pub threshold: Option<u64>,
|
||||||
|
pub network_dealers: Vec<DealerDetails>,
|
||||||
|
pub submitted_shared: HashMap<u64, ContractVKShare>,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn check_signers_with_client<C>(client: &C) -> Result<SignersTestResult, SignerCheckError>
|
||||||
|
where
|
||||||
|
C: DkgQueryClient + Sync,
|
||||||
|
{
|
||||||
|
let dkg_details = dkg_details_with_client(client).await?;
|
||||||
|
check_known_dealers(dkg_details).await
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn dkg_details_with_client<C>(client: &C) -> Result<DkgDetails, SignerCheckError>
|
||||||
|
where
|
||||||
|
C: DkgQueryClient + Sync,
|
||||||
|
{
|
||||||
|
// 2. retrieve current dkg epoch
|
||||||
|
let dkg_epoch = client
|
||||||
|
.get_current_epoch()
|
||||||
|
.await
|
||||||
|
.map_err(SignerCheckError::dkg_contract_query_failure)?;
|
||||||
|
|
||||||
|
// 3. retrieve the dkg threshold as reference point
|
||||||
|
let threshold = client
|
||||||
|
.get_epoch_threshold(dkg_epoch.epoch_id)
|
||||||
|
.await
|
||||||
|
.map_err(SignerCheckError::dkg_contract_query_failure)?;
|
||||||
|
|
||||||
|
// 4. retrieve information on current DKG dealers (i.e. eligible signers)
|
||||||
|
let dealers = client
|
||||||
|
.get_all_current_dealers()
|
||||||
|
.await
|
||||||
|
.map_err(SignerCheckError::dkg_contract_query_failure)?;
|
||||||
|
|
||||||
|
// 5. retrieve their published keys (if available)
|
||||||
|
let shares: HashMap<_, _> = client
|
||||||
|
.get_all_verification_key_shares(dkg_epoch.epoch_id)
|
||||||
|
.await
|
||||||
|
.map_err(SignerCheckError::dkg_contract_query_failure)?
|
||||||
|
.into_iter()
|
||||||
|
.map(|share| (share.node_index, share))
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
Ok(DkgDetails {
|
||||||
|
dkg_epoch,
|
||||||
|
threshold,
|
||||||
|
network_dealers: dealers,
|
||||||
|
submitted_shared: shares,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn check_known_dealers(
|
||||||
|
dkg_details: DkgDetails,
|
||||||
|
) -> Result<SignersTestResult, SignerCheckError> {
|
||||||
|
// 6. for each dealer attempt to perform the checks
|
||||||
|
let results = dkg_details
|
||||||
|
.network_dealers
|
||||||
|
.into_iter()
|
||||||
|
.map(|d| {
|
||||||
|
let share = dkg_details.submitted_shared.get(&d.assigned_index);
|
||||||
|
check_client(d, dkg_details.dkg_epoch.epoch_id, share)
|
||||||
|
})
|
||||||
|
.collect::<FuturesUnordered<_>>()
|
||||||
|
.collect::<Vec<_>>()
|
||||||
|
.await;
|
||||||
|
|
||||||
|
Ok(SignersTestResult {
|
||||||
|
threshold: dkg_details.threshold,
|
||||||
|
results,
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -0,0 +1,73 @@
|
|||||||
|
// Copyright 2025 - Nym Technologies SA <contact@nymtech.net>
|
||||||
|
// SPDX-License-Identifier: Apache-2.0
|
||||||
|
|
||||||
|
use crate::chain_status::LocalChainStatus;
|
||||||
|
use crate::dealer_information::RawDealerInformation;
|
||||||
|
use crate::signing_status::SigningStatus;
|
||||||
|
use std::time::Duration;
|
||||||
|
|
||||||
|
pub(crate) const STALE_RESPONSE_THRESHOLD: Duration = Duration::from_secs(5 * 60);
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct SignerResult {
|
||||||
|
pub dkg_epoch_id: u64,
|
||||||
|
pub information: RawDealerInformation,
|
||||||
|
pub status: SignerStatus,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl SignerResult {
|
||||||
|
pub fn chain_available(&self) -> bool {
|
||||||
|
let Ok(parsed_info) = self.information.parse() else {
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
|
||||||
|
let SignerStatus::Tested { result } = &self.status else {
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
result.local_chain_status.available(parsed_info.public_key)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn signer_available(&self) -> bool {
|
||||||
|
let Ok(parsed_info) = self.information.parse() else {
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
let SignerStatus::Tested { result } = &self.status else {
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
|
||||||
|
result.signing_status.available(
|
||||||
|
parsed_info.public_key,
|
||||||
|
self.dkg_epoch_id,
|
||||||
|
parsed_info.verification_key_share,
|
||||||
|
parsed_info.share_verified,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub enum SignerStatus {
|
||||||
|
Unreachable,
|
||||||
|
ProvidedInvalidDetails,
|
||||||
|
Tested { result: SignerTestResult },
|
||||||
|
}
|
||||||
|
|
||||||
|
impl SignerStatus {
|
||||||
|
pub fn with_details(
|
||||||
|
self,
|
||||||
|
information: impl Into<RawDealerInformation>,
|
||||||
|
dkg_epoch_id: u64,
|
||||||
|
) -> SignerResult {
|
||||||
|
SignerResult {
|
||||||
|
dkg_epoch_id,
|
||||||
|
status: self,
|
||||||
|
information: information.into(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct SignerTestResult {
|
||||||
|
pub reported_version: String,
|
||||||
|
pub signing_status: SigningStatus,
|
||||||
|
pub local_chain_status: LocalChainStatus,
|
||||||
|
}
|
||||||
@@ -47,4 +47,7 @@ workspace = true
|
|||||||
default-features = false
|
default-features = false
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
|
anyhow = { workspace = true }
|
||||||
nym-compact-ecash = { path = "../nym_offline_compact_ecash" } # we need specific imports in tests
|
nym-compact-ecash = { path = "../nym_offline_compact_ecash" } # we need specific imports in tests
|
||||||
|
nym-test-utils = { path = "../test-utils" }
|
||||||
|
tokio = { workspace = true, features = ["full"] }
|
||||||
|
|||||||
@@ -89,7 +89,7 @@ mod tests {
|
|||||||
.unwrap();
|
.unwrap();
|
||||||
let blind_sig = issue(
|
let blind_sig = issue(
|
||||||
keypair.secret_key(),
|
keypair.secret_key(),
|
||||||
sig_req.ecash_pub_key.clone(),
|
sig_req.ecash_pub_key,
|
||||||
&sig_req.withdrawal_request,
|
&sig_req.withdrawal_request,
|
||||||
expiration_date.ecash_unix_timestamp(),
|
expiration_date.ecash_unix_timestamp(),
|
||||||
issuance.ticketbook_type().encode(),
|
issuance.ticketbook_type().encode(),
|
||||||
|
|||||||
@@ -109,3 +109,85 @@ GATEWAY -> CLIENT
|
|||||||
DONE(status)
|
DONE(status)
|
||||||
|
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
use crate::ClientControlRequest;
|
||||||
|
use futures::StreamExt;
|
||||||
|
use nym_test_utils::helpers::u64_seeded_rng;
|
||||||
|
use nym_test_utils::mocks::stream_sink::mock_streams;
|
||||||
|
use nym_test_utils::traits::{Leak, Timeboxed, TimeboxedSpawnable};
|
||||||
|
use tokio::join;
|
||||||
|
use tungstenite::Message;
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn basic_handshake() -> anyhow::Result<()> {
|
||||||
|
use anyhow::Context as _;
|
||||||
|
|
||||||
|
// solve the lifetime issue by just leaking the contents of the boxes
|
||||||
|
// which is perfectly fine in test
|
||||||
|
let client_rng = u64_seeded_rng(42).leak();
|
||||||
|
let gateway_rng = u64_seeded_rng(69).leak();
|
||||||
|
|
||||||
|
let client_keys = ed25519::KeyPair::new(client_rng).leak();
|
||||||
|
let gateway_keys = ed25519::KeyPair::new(gateway_rng).leak();
|
||||||
|
|
||||||
|
let (client_ws, gateway_ws) = mock_streams::<Message>();
|
||||||
|
|
||||||
|
// we need streams that return Result<Message, WsError>
|
||||||
|
let client_ws = client_ws.map(Ok);
|
||||||
|
let gateway_ws = gateway_ws.map(Ok);
|
||||||
|
|
||||||
|
let client_ws = client_ws.leak();
|
||||||
|
let gateway_ws = gateway_ws.leak();
|
||||||
|
|
||||||
|
let handshake_client = client_handshake(
|
||||||
|
client_rng,
|
||||||
|
client_ws,
|
||||||
|
client_keys,
|
||||||
|
*gateway_keys.public_key(),
|
||||||
|
false,
|
||||||
|
true,
|
||||||
|
TaskClient::dummy(),
|
||||||
|
);
|
||||||
|
|
||||||
|
let client_fut = handshake_client.spawn_timeboxed();
|
||||||
|
|
||||||
|
// we need to receive the first message so that it could be propagated to the gateway side of the handshake
|
||||||
|
let ClientControlRequest::RegisterHandshakeInitRequest {
|
||||||
|
protocol_version: _,
|
||||||
|
data,
|
||||||
|
} = (gateway_ws.next())
|
||||||
|
.timeboxed()
|
||||||
|
.await
|
||||||
|
.context("timeout")?
|
||||||
|
.context("no message!")??
|
||||||
|
.into_text()?
|
||||||
|
.parse::<ClientControlRequest>()?
|
||||||
|
else {
|
||||||
|
panic!("bad message")
|
||||||
|
};
|
||||||
|
|
||||||
|
let init_msg = data;
|
||||||
|
|
||||||
|
let handshake_gateway = gateway_handshake(
|
||||||
|
gateway_rng,
|
||||||
|
gateway_ws,
|
||||||
|
gateway_keys,
|
||||||
|
init_msg,
|
||||||
|
TaskClient::dummy(),
|
||||||
|
);
|
||||||
|
|
||||||
|
let gateway_fut = handshake_gateway.spawn_timeboxed();
|
||||||
|
let (client, gateway) = join!(client_fut, gateway_fut);
|
||||||
|
|
||||||
|
let client_key = client???;
|
||||||
|
let gateway_key = gateway???;
|
||||||
|
|
||||||
|
// ensure the created keys are the same
|
||||||
|
assert_eq!(client_key, gateway_key);
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ homepage.workspace = true
|
|||||||
documentation.workspace = true
|
documentation.workspace = true
|
||||||
edition.workspace = true
|
edition.workspace = true
|
||||||
license.workspace = true
|
license.workspace = true
|
||||||
|
rust-version.workspace = true
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
sqlx = { workspace = true, features = [
|
sqlx = { workspace = true, features = [
|
||||||
@@ -27,6 +28,7 @@ nym-statistics-common = { path = "../statistics" }
|
|||||||
|
|
||||||
|
|
||||||
[build-dependencies]
|
[build-dependencies]
|
||||||
|
anyhow = { workspace = true }
|
||||||
tokio = { workspace = true, features = ["rt-multi-thread", "macros"] }
|
tokio = { workspace = true, features = ["rt-multi-thread", "macros"] }
|
||||||
sqlx = { workspace = true, features = [
|
sqlx = { workspace = true, features = [
|
||||||
"runtime-tokio-rustls",
|
"runtime-tokio-rustls",
|
||||||
|
|||||||
@@ -1,22 +1,29 @@
|
|||||||
// Copyright 2024 - Nym Technologies SA <contact@nymtech.net>
|
// Copyright 2024 - Nym Technologies SA <contact@nymtech.net>
|
||||||
// SPDX-License-Identifier: GPL-3.0-only
|
// SPDX-License-Identifier: GPL-3.0-only
|
||||||
|
|
||||||
|
use anyhow::Context;
|
||||||
use sqlx::{Connection, SqliteConnection};
|
use sqlx::{Connection, SqliteConnection};
|
||||||
use std::env;
|
use std::env;
|
||||||
|
|
||||||
#[tokio::main]
|
#[tokio::main]
|
||||||
async fn main() {
|
async fn main() -> anyhow::Result<()> {
|
||||||
let out_dir = env::var("OUT_DIR").unwrap();
|
let out_dir = env::var("OUT_DIR")?;
|
||||||
let database_path = format!("{out_dir}/gateway-stats-example.sqlite");
|
let database_path = format!("{out_dir}/gateway-stats-example.sqlite");
|
||||||
|
|
||||||
|
// remove the db file if it already existed from previous build
|
||||||
|
// in case it was from a different branch
|
||||||
|
if std::fs::exists(&database_path)? {
|
||||||
|
std::fs::remove_file(&database_path)?;
|
||||||
|
}
|
||||||
|
|
||||||
let mut conn = SqliteConnection::connect(&format!("sqlite://{database_path}?mode=rwc"))
|
let mut conn = SqliteConnection::connect(&format!("sqlite://{database_path}?mode=rwc"))
|
||||||
.await
|
.await
|
||||||
.expect("Failed to create SQLx database connection");
|
.context("Failed to create SQLx database connection")?;
|
||||||
|
|
||||||
sqlx::migrate!("./migrations")
|
sqlx::migrate!("./migrations")
|
||||||
.run(&mut conn)
|
.run(&mut conn)
|
||||||
.await
|
.await
|
||||||
.expect("Failed to perform SQLx migrations");
|
.context("Failed to perform SQLx migrations")?;
|
||||||
|
|
||||||
#[cfg(target_family = "unix")]
|
#[cfg(target_family = "unix")]
|
||||||
println!("cargo:rustc-env=DATABASE_URL=sqlite://{}", &database_path);
|
println!("cargo:rustc-env=DATABASE_URL=sqlite://{}", &database_path);
|
||||||
@@ -25,4 +32,6 @@ async fn main() {
|
|||||||
// for some strange reason we need to add a leading `/` to the windows path even though it's
|
// for some strange reason we need to add a leading `/` to the windows path even though it's
|
||||||
// not a valid windows path... but hey, it works...
|
// not a valid windows path... but hey, it works...
|
||||||
println!("cargo:rustc-env=DATABASE_URL=sqlite:///{}", &database_path);
|
println!("cargo:rustc-env=DATABASE_URL=sqlite:///{}", &database_path);
|
||||||
|
|
||||||
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user